Sí, vemos varias cosas como:
while read line; do
echo $line | cut -c3
done
O peor:
for line in `cat file`; do
foo=`echo $line | awk '{print $2}'`
echo whatever $foo
done
(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:
cut -c4-5 < in | tr a b > out
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:
while read line; do
echo $line | cut -c3
done < file
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 herramienta echo
y cut
, sáquelos del cajón, invoquelos, lávelos, séquelos, vuelva a colocarlos en el cajón, etc.
Algunas de esas herramientas ( read
y echo
) están construidas en la mayoría de los shells, pero eso apenas hace una diferencia aquí desde entonces echo
y cut
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()
o fputs()
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) equivalente cut -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:
while read line; do
echo ${line:2:1}
done
(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:
foo\/bar \
baz
biz
read line
se almacenará "foo/bar baz"
en $line
, no " foo\/bar \"
como es de esperar.
Para leer una línea, en realidad necesita:
IFS= read -r line
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. Necesitas printf
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:
while IFS= read -r line; do
printf '%s\n' "$line" | cut -c3
done < file
Ahora, algunas advertencias más:
- a excepción de
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 hay datos después de la última línea nueva, se omitirá
- dentro del bucle, stdin se redirige, por lo que debe prestar atención para que los comandos que contiene no lean desde stdin.
- para los comandos dentro de los bucles, no estamos prestando atención a si tienen éxito o no. Por lo general, las condiciones de error (disco lleno, errores de lectura ...) se manejarán mal, generalmente más mal que con el equivalente correcto .
Si queremos abordar algunos de los problemas anteriores, se convierte en:
while IFS= read -r line <&3; do
{
printf '%s\n' "$line" | cut -c3 || exit
} 3<&-
done 3< file
if [ -n "$line" ]; then
printf '%s' "$line" | cut -c3 || exit
fi
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:
- la limitación en el tamaño de los argumentos (algunas implementaciones de utilidades de texto también tienen un límite allí, aunque el efecto de los alcanzados son generalmente menos problemáticos)
- el carácter NUL (también un problema con las utilidades de texto).
- argumentos tomados como opciones cuando comienzan con
-
(o a +
veces)
- varias peculiaridades de los diversos comandos que se usan típicamente en esos bucles como
expr
, test
...
- Los operadores de manipulación de texto (limitado) de varios shells que manejan caracteres de varios bytes de manera inconsistente.
- ...
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
yes
escribe para archivar tan rápido?