(8 respuestas)
Cerrado hace 3 años.
Escribí el siguiente script para diferenciar las salidas de dos directores con todos los mismos archivos como tales:
#!/bin/bash
for file in `find . -name "*.csv"`
do
echo "file = $file";
diff $file /some/other/path/$file;
read char;
done
Sé que hay otras formas de lograr esto. Sin embargo, curiosamente, este script falla cuando los archivos tienen espacios en ellos. ¿Cómo puedo lidiar con esto?
Ejemplo de resultado de encontrar:
./zQuery - abc - Do Not Prompt for Date.csv
Respuesta aceptada:
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 saltos de línea 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 la respuesta de Gilles)
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 un 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
Tienes tres problemas:
- De forma predeterminada, el shell divide la salida de un comando en espacios, tabulaciones y saltos de línea
- Los nombres de archivo podrían contener caracteres comodín que se expandirían
- ¿Qué pasa si hay un directorio cuyo nombre termina en
*.csv
? ?
Para averiguar qué configurar file
a, el shell tiene que tomar la salida de find
e interpretarlo de alguna manera, de lo contrario file
sería solo la salida completa de find
.
El shell lee el IFS
variable, que se establece en <space><tab><newline>
por defecto.
Luego mira cada carácter en la salida de find
. Tan pronto como vea cualquier 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 bucle. Luego comienza donde lo dejó para obtener el siguiente nombre de archivo y ejecuta el siguiente bucle, etc., hasta que llega al final de la salida.
Así que efectivamente está haciendo esto:
for file in "zquery" "-" "abc" ...
Para decirle que solo divida la entrada en líneas nuevas, debe hacerlo
IFS=$'n'
antes de su for ... find
comando.
Eso establece IFS
a una sola línea nueva, por lo que solo se divide en líneas nuevas, y no espacios y tabulaciones también.
Si está utilizando sh
o dash
en lugar de ksh93
, bash
o zsh
, debe escribir IFS=$'n'
así en su lugar:
IFS='
'
Probablemente eso sea suficiente para que su secuencia de comandos funcione, pero si está interesado en manejar otros casos de esquina correctamente, siga leyendo...
$file
sin comodines
Dentro del ciclo donde lo haces
diff $file /some/other/path/$file
el shell intenta expandir $file
(¡otra vez!).
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 conduciría a un comportamiento impredecible. (Gracias a Gilles por señalar esto).
Para indicarle 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 mordernos
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)
Relacionado:Si cambio los permisos en un archivo tar, ¿se aplicará eso a los archivos que contiene?Sería como si hubieras corrido
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 entrada estándar, divide la línea en palabras según IFS
y los almacena en los nombres de variables que especifique.
Aquí, le decimos que no divida la línea en palabras y que almacene la línea en $file
.
También tenga en cuenta que read line
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 solo hiciéramos read
, estaría consumiendo parte o todo el nombre de un archivo y algunos archivos se omitirían.
/dev/tty
es la 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é sucede si el nombre de un archivo contiene saltos de línea?
Podemos manejar eso cambiando -print
a -print0
y usando read -d ''
al final de una canalización:
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
coloque 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 raros sean.
Para obtener el nombre del archivo del 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
está usando nulo como delimitador de línea. En bash
, no puede pasar un carácter NUL en un argumento a un comando (incluso los integrados), pero bash
entiende -d ''
como significado NUL delimitado . Entonces usamos -d ''
para hacer read
use el mismo delimitador de línea que find
. Tenga en cuenta que -d $'