¿Usar un ciclo while para procesar texto generalmente se considera una mala práctica en shells POSIX?
Como señaló Stéphane Chazelas , algunas de las razones para no usar shell loop son conceptuales , confiabilidad , legibilidad , rendimiento y seguridad .
Esta respuesta explica los aspectos de confiabilidad y legibilidad :
while IFS= read -r line <&3; do
printf '%s\n' "$line"
done 3< "$InputFile"
Para el rendimiento , el while
bucle y la lectura son tremendamente lentos cuando se lee desde un archivo o una tubería, porque el shell de lectura incorporado lee un carácter a la vez.
¿Qué hay de los aspectos conceptuales y de seguridad ?
shell
text-processing
Cuonglm
fuente
fuente
yes
escribe para archivar tan rápido?bash
, lee un tamaño de búfer a la vez, intente,dash
por ejemplo. Ver también unix.stackexchange.com/q/209123/38906Respuestas:
Sí, vemos varias cosas como:
O peor:
(No te rías, he visto muchos de esos).
Generalmente de principiantes de scripts de shell Esas son traducciones literales ingenuas de lo que harías en lenguajes imperativos como C o python, pero no es así como haces las cosas en shells, y esos ejemplos son muy ineficientes, completamente poco confiables (potencialmente conducen a problemas de seguridad), y si alguna vez logras Para corregir la mayoría de los errores, su código se vuelve ilegible.
Conceptualmente
En C o en la mayoría de los otros lenguajes, los bloques de construcción están solo un nivel por encima de las instrucciones de la computadora. Le dice a su procesador qué hacer y luego qué hacer a continuación. Toma su procesador de la mano y lo microgestiona: abre ese archivo, lee tantos bytes, hace esto, lo hace con él.
Los shells son un lenguaje de nivel superior. Se puede decir que ni siquiera es un idioma. Están ante todos los intérpretes de línea de comandos. El trabajo lo realizan los comandos que ejecuta y el shell solo está destinado a orquestarlos.
Una de las mejores cosas que introdujo Unix fue la tubería y las secuencias stdin / stdout / stderr predeterminadas que todos los comandos manejan de manera predeterminada.
En 45 años, no hemos encontrado una API mejor que esa para aprovechar el poder de los comandos y hacer que cooperen en una tarea. Esa es probablemente la razón principal por la cual las personas todavía usan conchas hoy en día.
Tiene una herramienta de corte y una herramienta de transliteración, y simplemente puede hacer:
El shell solo está haciendo la plomería (abre los archivos, configura las tuberías, invoca los comandos) y cuando todo está listo, simplemente fluye sin que el shell haga nada. Las herramientas hacen su trabajo al mismo tiempo, de manera eficiente a su propio ritmo con suficiente almacenamiento en búfer para que ninguno bloquee al otro, es simplemente hermoso y, sin embargo, muy simple.
Sin embargo, invocar una herramienta tiene un costo (y lo desarrollaremos en el punto de rendimiento). Esas herramientas pueden escribirse con miles de instrucciones en C. Debe crearse un proceso, la herramienta debe cargarse, inicializarse, luego limpiarse, destruirse el proceso y esperar.
Invocar
cut
es como abrir el cajón de la cocina, tomar el cuchillo, usarlo, lavarlo, secarlo y volver a colocarlo en el cajón. Cuando tu lo hagas:Es como para cada línea del archivo, obtener la
read
herramienta del cajón de la cocina (una muy torpe porque no ha sido diseñada para eso ), leer una línea, lavar la herramienta de lectura, volver a colocarla en el cajón. Luego programe una reunión para la herramientaecho
ycut
, sáquelos del cajón, invoquelos, lávelos, séquelos, vuelva a colocarlos en el cajón, etc.Algunas de esas herramientas (
read
yecho
) están construidas en la mayoría de los shells, pero eso apenas hace una diferencia aquí desde entoncesecho
ycut
aún deben ejecutarse en procesos separados.Es como cortar una cebolla pero lavar el cuchillo y volver a colocarlo en el cajón de la cocina entre cada rebanada.
Aquí, la forma obvia es sacar su
cut
herramienta del cajón, cortar toda la cebolla y volver a colocarla en el cajón una vez que haya terminado todo el trabajo.IOW, en shells, especialmente para procesar texto, invocas la menor cantidad de utilidades posible y haces que cooperen en la tarea, no ejecutan miles de herramientas en secuencia esperando que cada una comience, se ejecute y se limpie antes de ejecutar la siguiente.
Lectura adicional en la buena respuesta de Bruce . Las herramientas internas de procesamiento de texto de bajo nivel en shells (excepto quizás para
zsh
) son limitadas, engorrosas y, en general, no son aptas para el procesamiento de texto general.Actuación
Como se dijo anteriormente, ejecutar un comando tiene un costo. Un costo enorme si ese comando no está integrado, pero incluso si están integrados, el costo es grande.
Y los shells no han sido diseñados para ejecutarse así, no pretenden ser lenguajes de programación eficaces. No lo son, solo son intérpretes de línea de comandos. Entonces, se ha hecho poca optimización en este frente.
Además, los shells ejecutan comandos en procesos separados. Esos bloques de construcción no comparten una memoria o estado común. Cuando haces una
fgets()
ofputs()
en C, esa es una función en stdio. stdio mantiene buffers internos para entrada y salida para todas las funciones stdio, para evitar hacer costosas llamadas al sistema con demasiada frecuencia.Los correspondientes incluso utilidades de shell incorporadas (
read
,echo
,printf
) pueden no hacerlo.read
está destinado a leer una línea. Si se lee más allá del carácter de nueva línea, eso significa que el siguiente comando que ejecute lo perderá. Por lo tanto,read
tiene que leer la entrada un byte a la vez (algunas implementaciones tienen una optimización si la entrada es un archivo normal en el sentido de que leen fragmentos y buscan, pero eso solo funciona para archivos regulares y,bash
por ejemplo, solo lee fragmentos de 128 bytes, lo cual es todavía mucho menos de lo que harán las utilidades de texto).Lo mismo en el lado de la salida,
echo
no puede simplemente almacenar su salida en el búfer, sino que debe enviarla de inmediato porque el siguiente comando que ejecute no compartirá ese búfer.Obviamente, ejecutar comandos secuencialmente significa que debe esperarlos, es un pequeño baile de planificación que le da el control desde el shell y las herramientas y viceversa. Eso también significa (en lugar de usar instancias de herramientas de larga ejecución en una tubería) que no puede aprovechar varios procesadores al mismo tiempo cuando estén disponibles.
Entre ese
while read
ciclo y el (supuestamente) equivalentecut -c3 < file
, en mi prueba rápida, hay una relación de tiempo de CPU de alrededor de 40000 en mis pruebas (un segundo versus medio día). Pero incluso si usa solo cartuchos incorporados:(aquí con
bash
), eso sigue siendo alrededor de 1: 600 (un segundo frente a 10 minutos).Fiabilidad / legibilidad
Es muy difícil obtener ese código correcto. Los ejemplos que di se ven con demasiada frecuencia en la naturaleza, pero tienen muchos errores.
read
es una herramienta útil que puede hacer muchas cosas diferentes. Puede leer la entrada del usuario, dividirla en palabras para almacenar en diferentes variables.read line
no no leer una línea de entrada, o tal vez se lee una línea de una manera muy especial. En realidad, lee palabras de la entrada de esas palabras separadas por$IFS
y donde la barra invertida se puede utilizar para escapar de los separadores o el carácter de nueva línea.Con el valor predeterminado de
$IFS
, en una entrada como:read line
se almacenará"foo/bar baz"
en$line
, no" foo\/bar \"
como es de esperar.Para leer una línea, en realidad necesita:
Eso no es muy intuitivo, pero así es, recuerde que los proyectiles no estaban destinados a ser utilizados de esa manera.
Lo mismo para
echo
.echo
Expande secuencias. No puede usarlo para contenidos arbitrarios como el contenido de un archivo aleatorio. Necesitasprintf
aquí en su lugar.Y, por supuesto, existe el típico olvido de citar su variable en la que todos caen. Entonces es más:
Ahora, algunas advertencias más:
zsh
, eso no funciona si la entrada contiene caracteres NUL mientras que al menos las utilidades de texto GNU no tendrían el problema.Si queremos abordar algunos de los problemas anteriores, se convierte en:
Eso se está volviendo cada vez menos legible.
Existen otros problemas al pasar datos a los comandos a través de los argumentos o al recuperar su salida en variables:
-
(o a+
veces)expr
,test
...Consideraciones de Seguridad
Cuando comienzas a trabajar con variables de shell y argumentos para comandos , estás ingresando un campo de minas.
Si olvida citar sus variables , olvide el final del marcador de opción , trabaje en entornos locales con caracteres de varios bytes (la norma en estos días), seguramente introducirá errores que tarde o temprano se convertirán en vulnerabilidades.
Cuando quieras usar bucles.
a ser determinado
fuente
cut
por ejemplo, es eficiente.cut -f1 < a-very-big-file
es eficiente, tan eficiente como lo obtendría si lo escribiera en C. Lo que es terriblemente ineficiente y propenso a errores es invocarcut
para cada línea de una-very-big-file
bucle de shell, que es el punto que se señala en esta respuesta. Eso coincide con su última declaración sobre la escritura de código innecesario que me hace pensar que tal vez no entiendo su comentario.En cuanto a lo conceptual y la legibilidad, los shells generalmente están interesados en los archivos. Su "unidad direccionable" es el archivo, y la "dirección" es el nombre del archivo. Los shells tienen todo tipo de métodos para probar la existencia del archivo, el tipo de archivo, el formato del nombre del archivo (comenzando con globbing). Los shells tienen muy pocas primitivas para tratar con el contenido del archivo. Los programadores de Shell deben invocar otro programa para tratar con el contenido del archivo.
Debido a la orientación del archivo y el nombre del archivo, la manipulación de texto en el shell es realmente lenta, como ya lo ha notado, pero también requiere un estilo de programación confuso y poco claro.
fuente
Hay algunas respuestas complicadas, que brindan muchos detalles interesantes para los geeks entre nosotros, pero es realmente bastante simple: procesar un archivo grande en un bucle de shell es demasiado lento.
Creo que el interrogador es interesante en un tipo típico de script de shell, que puede comenzar con un análisis de línea de comandos, configuración del entorno, comprobación de archivos y directorios, y un poco más de inicialización, antes de pasar a su trabajo principal: pasar por un gran archivo de texto orientado a líneas.
Para las primeras partes (
initialization
), generalmente no importa que los comandos de shell sean lentos; solo ejecuta unas pocas docenas de comandos, tal vez con un par de bucles cortos. Incluso si escribimos esa parte de manera ineficiente, generalmente tomará menos de un segundo hacer toda esa inicialización, y eso está bien, solo sucede una vez.Pero cuando pasamos a procesar el archivo grande, que podría tener miles o millones de líneas, no está bien que el script de shell tome una fracción significativa de segundo (incluso si solo son unas pocas docenas de milisegundos) para cada línea, ya que eso podría sumar horas.
Es entonces cuando necesitamos usar otras herramientas, y la belleza de los scripts de shell de Unix es que nos facilitan hacerlo.
En lugar de usar un bucle para mirar cada línea, necesitamos pasar todo el archivo a través de una tubería de comandos . Esto significa que, en lugar de llamar a los comandos miles o millones de veces, el shell los llama una sola vez. Es cierto que esos comandos tendrán bucles para procesar el archivo línea por línea, pero no son scripts de shell y están diseñados para ser rápidos y eficientes.
Unix tiene muchas herramientas maravillosas integradas, que van desde lo simple a lo complejo, que podemos usar para construir nuestras tuberías. Por lo general, comenzaría con los simples y solo usaría los más complejos cuando sea necesario.
También trataría de mantener las herramientas estándar que están disponibles en la mayoría de los sistemas e intentar mantener mi uso portátil, aunque eso no siempre es posible. Y si su idioma favorito es Python o Ruby, quizás no le importe el esfuerzo adicional de asegurarse de que esté instalado en todas las plataformas en las que su software necesita ejecutarse :-)
Herramientas simples incluyen
head
,tail
,grep
,sort
,cut
,tr
,sed
,join
(cuando la fusión de 2 archivos), yawk
de una sola línea, entre muchos otros. Es sorprendente lo que algunas personas pueden hacer con la coincidencia de patrones y lossed
comandos.Cuando se vuelve más complejo, y realmente tiene que aplicar un poco de lógica a cada línea,
awk
es una buena opción, ya sea una línea (algunas personas ponen scripts awk completos en 'una línea', aunque eso no es muy legible) o en un guión externo corto.Como
awk
es un lenguaje interpretado (como su shell), es sorprendente que pueda hacer un procesamiento línea por línea de manera tan eficiente, pero está especialmente diseñado para esto y es realmente muy rápido.Y luego hay
Perl
una gran cantidad de otros lenguajes de secuencias de comandos que son muy buenos para procesar archivos de texto y también vienen con muchas bibliotecas útiles.Y finalmente, hay una buena C antigua, si necesita la máxima velocidad y alta flexibilidad (aunque el procesamiento de texto es un poco tedioso). Pero probablemente sea un mal uso de su tiempo escribir un nuevo programa en C para cada tarea de procesamiento de archivos diferente que encuentre. Trabajo mucho con archivos CSV, así que he escrito varias utilidades genéricas en C que puedo reutilizar en muchos proyectos diferentes. En efecto, esto amplía el rango de 'herramientas Unix rápidas y simples' a las que puedo llamar desde mis scripts de shell, por lo que puedo manejar la mayoría de los proyectos solo escribiendo scripts, ¡lo cual es mucho más rápido que escribir y depurar código C personalizado cada vez!
Algunas sugerencias finales:
export LANG=C
, o muchas herramientas tratarán sus archivos ASCII simples como Unicode, haciéndolos mucho más lentosexport LC_ALL=C
si deseasort
producir pedidos consistentes, independientemente del entorno!sort
sus datos, eso probablemente tomará más tiempo (y recursos: CPU, memoria, disco) que todo lo demás, así que trate de minimizar la cantidad desort
comandos y el tamaño de los archivos que están ordenandofuente
Sí, pero...
La respuesta correcta de Stéphane Chazelas se basa en la cáscara concepto de delegación de cada operación de texto para los binarios específicos, como
grep
,awk
,sed
y otros.Como bash es capaz de hacer muchas cosas por sí mismo, soltar los tenedores puede ser más rápido (incluso que ejecutar otro intérprete para hacer todo el trabajo).
Por ejemplo, eche un vistazo a esta publicación:
https://stackoverflow.com/a/38790442/1765658
y
https://stackoverflow.com/a/7180078/1765658
probar y comparar ...
Por supuesto
¡No hay consideración sobre la entrada del usuario y la seguridad !
¡No escriba aplicaciones web bajo bash !
Pero para muchas tareas de administración del servidor, donde bash podría usarse en lugar de shell , el uso de basil integrado podría ser muy eficiente.
Mi significado:
Escribir herramientas como bin utils no es el mismo tipo de trabajo que la administración del sistema.
¡Entonces no la misma gente!
Donde los administradores de sistemas tienen que saber
shell
, podrían escribir prototipos utilizando su herramienta preferida (y más conocida).Si esta nueva utilidad (prototipo) es realmente útil, otras personas podrían desarrollar una herramienta dedicada utilizando un lenguaje más apropiado.
fuente
bash
. (más de 3 veces más rápido con ksh93 en mi prueba en mi sistema).bash
es generalmente el caparazón más lento. Evenzsh
es el doble de rápido en ese script. También tiene algunos problemas con las variables no citadas y el uso deread
. Así que en realidad estás ilustrando muchos de mis puntos aquí.sh
, AWK , sed ,grep
,ed
,ex
,cut
,sort
,join
... todo ello con más fiabilidad que Bash o Perl.bash
instalados por defecto.bash
se encuentra principalmente sólo en Apple MacOS y sistemas GNU (supongo que eso es lo que se llama las principales distribuciones ), aunque muchos sistemas también lo tienen como un paquete opcional (comozsh
,tcl
,python
...)