Respuesta corta (más cercana a su respuesta, pero maneja espacios)
OIFS="$IFS" IFS=$"\n" for file in `find . -type f -name "*.csv"` do echo "file = $file" diff "$file" "/some/other/path/$file" read line done IFS="$OIFS"
Mejor respuesta (también maneja comodines y nuevas líneas en los nombres de archivo)
find . -type f -name "*.csv" -print0 | while IFS= read -r -d "" file; do echo "file = $file" diff "$file" "/some/other/path/$file" read line </dev/tty done
Mejor respuesta (basada en Gilles » answer )
find . -type f -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty " exec-sh {} ";"
O incluso mejor, para evitar ejecutar uno sh
por archivo:
find . -type f -name "*.csv" -exec sh -c " for file do echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty done " exec-sh {} +
Respuesta larga
Tiene tres problemas:
- De forma predeterminada, el shell divide la salida de un comando en espacios, pestañas y nuevas líneas
- Los nombres de archivo pueden contener caracteres comodín que se expandiría
- ¿Qué pasa si hay un directorio cuyo nombre termina en
*.csv
?
1. Dividiendo solo en líneas nuevas
Para averiguar en qué establecer file
, el shell tiene que tomar la salida de find
e interpretarlo de alguna manera; de lo contrario, file
solo sería la salida completa de find
.
El shell lee la variable IFS
, que se establece en <space><tab><newline>
de forma predeterminada.
Luego mira cada carácter en la salida de find
. Tan pronto como ve un carácter que «está en IFS
, cree que marca el final del nombre del archivo, por lo que establece file
a los caracteres que vio hasta ahora y ejecuta el ciclo. Luego comienza donde lo dejó para obtener el siguiente nombre de archivo, y ejecuta el siguiente ciclo, etc., hasta que llega al final de la salida.
Así que está haciendo esto de manera efectiva:
for file in "zquery" "-" "abc" ...
Para decirle que solo divida la entrada en nuevas líneas, debe hacerlo
IFS=$"\n"
antes de su for ... find
comando.
Eso establece IFS
en un línea nueva única, por lo que solo se divide en líneas nuevas, y no en espacios ni tabulaciones también.
Si está utilizando sh
o dash
en lugar de ksh93
, bash
o zsh
, debes escribir IFS=$"\n"
así en su lugar:
IFS=" "
Eso probablemente sea suficiente para que su script funcione, pero si está interesado en manejar otros casos de esquina correctamente, siga leyendo …
aña 2. Expandiendo $file
sin comodines
Dentro del bucle donde lo haces
diff $file /some/other/path/$file
el shell intenta expandir $file
(¡de nuevo!).
Podría contener espacios, pero como ya configuramos IFS
arriba, eso no será un problema aquí.
Pero también podría contener caracteres comodín como *
o ?
, lo que provocaría un comportamiento impredecible. (Gracias a Gilles por señalar esto.)
Para decirle al shell que no expanda los caracteres comodín, coloque la variable entre comillas dobles, por ejemplo
diff "$file" "/some/other/path/$file"
El mismo problema también podría afectarnos
for file in `find . -name "*.csv"`
Por ejemplo, si tuviera estos tres archivos
file1.csv file2.csv *.csv
(muy poco probable, pero aún posible)
Sería como si hubiera ejecutado
for file in file1.csv file2.csv *.csv
que se expandirá a
for file in file1.csv file2.csv *.csv file1.csv file2.csv
causando file1.csv
y file2.csv
para ser procesado dos veces.
En su lugar, tenemos que hacer
find . -name "*.csv" -print | while IFS= read -r file; do echo "file = $file" diff "$file" "/some/other/path/$file" read line </dev/tty done
read
lee líneas de la entrada estándar, divide la línea en palabras de acuerdo con IFS
y las almacena en los nombres de variable que usted especifique.
Aquí, «lo estamos diciendo no dividir la línea en palabras y almacenar la línea en $file
.
También tenga en cuenta que ha cambiado a read line </dev/tty
.
Esto se debe a que dentro del ciclo, la entrada estándar proviene de find
a través de la canalización.
Si hiciéramos read
, consumiría parte o todo el nombre de un archivo, y algunos archivos se omitirían .
/dev/tty
es el terminal desde donde el usuario está ejecutando el script. Tenga en cuenta que esto provocará un error si el script se ejecuta a través de cron, pero supongo que esto no es importante en este caso.
Entonces, ¿qué pasa si un nombre de archivo contiene nuevas líneas?
Podemos manejar eso cambiando -print
a -print0
y usando read -d ""
al final de un pipeline:
find . -name "*.csv" -print0 | while IFS= read -r -d "" file; do echo "file = $file" diff "$file" "/some/other/path/$file" read char </dev/tty done
Esto hace que find
ponga un byte nulo al final de cada nombre de archivo. Los bytes nulos son los únicos caracteres que no se permiten en los nombres de archivo, por lo que esto debería manejar todos los nombres de archivo posibles, sin importar cuán extraños sean.
Para obtener el nombre del archivo en el otro lado, usamos IFS= read -r -d ""
.
Donde usamos read
arriba, usamos el delimitador de línea predeterminado de nueva línea, pero ahora, find
utiliza nulo como delimitador de línea. En bash
, no puede «pasar un carácter NUL en un argumento a un comando (incluso los incorporados), pero bash
entiende -d ""
en el sentido de delimitado por NUL . Por lo tanto, usamos -d ""
para hacer read
use el mismo delimitador de línea que find
. Tenga en cuenta que -d $"\0"
, por cierto, también funciona, porque bash
que no admite bytes NUL lo trata como una cadena vacía.
Para ser correctos, también agregamos -r
, que dice que no maneje barras invertidas en nombres de archivo especialmente. Por ejemplo, sin -r
, \<newline>
se eliminan y \n
se convierte en n
.
Una forma más portátil de escribir esto que no «requiere bash
o zsh
o recordando todas las reglas anteriores sobre bytes nulos (nuevamente, gracias a Gilles):
find . -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read char </dev/tty " exec-sh {} ";"
* 3. Omitir directorios cuyo los nombres terminan en .csv
find . -name "*.csv"
también coincidirán con directorios que se llaman something.csv
.
Para evitar esto, agregue -type f
al comando find
.
find . -type f -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty " exec-sh {} ";"
Como glenn jackman señala, en ambos ejemplos, los comandos a ejecutar para cada archivo son se ejecuta en una subcapa, por lo que si cambia alguna variable dentro del bucle, se olvidará.
Si necesita establecer variables y tenerlas aún configuradas al final del ciclo, puede reescribirlo para usar la sustitución de procesos como esta:
i=0 while IFS= read -r -d "" file; do echo "file = $file" diff "$file" "/some/other/path/$file" read line </dev/tty i=$((i+1)) done < <(find . -type f -name "*.csv" -print0) echo "$i files processed"
Tenga en cuenta que si intenta copiar y pegar esto en la línea de comando , read line
consumirá echo "$i files processed"
, por lo que ese comando no se ejecutará.
Para evitar esto, podría eliminar read line </dev/tty
y enviar el resultado a un localizador como less
.
NOTAS
Eliminé el punto y coma (;
) dentro del lazo. Puede devolverlos si lo desea, pero no son necesarios.
En estos días, $(command)
es más común que `command`
. Esto se debe principalmente a que es más fácil escribir $(command1 $(command2))
que `command1 \`command2\``
.
read char
realmente no lee un carácter.Lee una línea completa, así que la cambié a read line
.
Comentarios
Este script falla si algún nombre de archivo contiene espacios o caracteres globbing de shell \[?*
. El comando find
genera un nombre de archivo por línea. Luego, el shell evalúa la sustitución del comando `find …`
de la siguiente manera:
- Ejecute el comando
find
, tome su salida.
- Divida la salida
find
en palabras separadas. Cualquier carácter de espacio en blanco es un separador de palabras.
- Para cada palabra, si es un patrón globular, amplíelo a la lista de archivos con los que coincide.
Por ejemplo, supongamos que hay tres archivos en el directorio actual, llamados `foo* bar.csv
, foo 1.txt
y foo 2.txt
.
- El comando
find
devuelve ./foo* bar.csv
.
- El shell divide esta cadena en el espacio, produciendo dos palabras:
./foo*
y bar.csv
.
- Desde
./foo*
contiene un metacarácter globbing, se expande a la lista de archivos coincidentes: ./foo 1.txt
y ./foo 2.txt
.
- Por tanto, el ciclo
for
se ejecuta sucesivamente con ./foo 1.txt
, ./foo 2.txt
y bar.csv
.
Puede evitar la mayoría de los problemas en esta etapa si atenúa la división de palabras y ing off globbing. Para atenuar la división de palabras, establece la variable IFS
en un solo carácter de nueva línea; de esta manera la salida de find
sólo se dividirá en líneas nuevas y quedarán espacios. Para desactivar el globbing, ejecute set -f
. Entonces, esta parte del código funcionará siempre que ningún nombre de archivo contenga un carácter de nueva línea.
IFS=" " set -f for file in $(find . -name "*.csv"); do …
(Esto no es parte de su problema, pero recomiendo usar $(…)
sobre `…`
. Tienen el mismo significado, pero la versión de comillas inversas tiene reglas de comillas extrañas.)
Hay otro problema a continuación: diff $file /some/other/path/$file
debería ser
diff "$file" "/some/other/path/$file"
De lo contrario, el valor de $file
se divide en palabras y las palabras se tratan como patrones globales, como con el comando substitutio anterior. Si debe recordar algo acerca de la programación de shell, recuerde esto: use siempre comillas dobles alrededor de las expansiones de variables ($foo
) y sustituciones de comandos ( $(bar)
) , a menos que sepa que desea dividir. (Arriba, sabíamos que queríamos dividir la salida find
en líneas).
Una forma confiable de llamar a find
le está diciendo que ejecute un comando para cada archivo que encuentre:
find . -name "*.csv" -exec sh -c " echo "$0" diff "$0" "/some/other/path/$0" " {} ";"
En este caso, otro enfoque es comparar los dos directorios, aunque debe excluya explícitamente todos los archivos «aburridos».
diff -r -x "*.txt" -x "*.ods" -x "*.pdf" … . /some/other/path
Comentarios