desarrollo-web-br-bd.com

¿Recorrer archivos con espacios en los nombres?

Escribí el siguiente script para diferenciar las salidas de dos directores con todos los mismos archivos en ellos 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. Curiosamente, este script falla cuando los archivos tienen espacios en ellos. ¿Cómo puedo lidiar con esto?

Ejemplo de salida de find:

./zQuery - abc - Do Not Prompt for Date.csv
160
Amir Afghani

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 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
' {} ';'

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
' sh {} +

Respuesta larga

Tienes tres problemas:

  1. Por defecto, el Shell divide la salida de un comando en espacios, pestañas y líneas nuevas.
  2. Los nombres de archivo podrían contener caracteres comodín que se expandirían
  3. ¿Qué pasa si hay un directorio cuyo nombre termina en *.csv?

1. División solo en líneas nuevas

Para determinar en qué establecer file, el Shell debe tomar la salida de find e interpretarlo de alguna manera, de lo contrario file 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 algún 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 ciclo, etc., hasta que llega al final de la salida.

Así que efectivamente está haciendo esto:

for file in "zquery" "-" "abc" ...

Para indicarle que solo divida la entrada en las nuevas líneas, debe hacer

IFS=$'\n'

antes de su comando for ... find.

Eso establece IFS en una nueva línea nueva, por lo que solo se divide en nuevas líneas, y no en espacios y pestañas 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 sea suficiente para que su script funcione, pero si está interesado en manejar otros casos de esquina correctamente, siga leyendo ...

2. Expandiendo $file Sin comodines

Dentro del bucle donde haces

diff $file /some/other/path/$file

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 decirle al Shell que no expanda los caracteres comodín, coloque la variable entre comillas dobles, p.

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)

Sería como si hubieras corrido

for file in file1.csv file2.csv *.csv

que se ampliará a

for file in file1.csv file2.csv *.csv file1.csv file2.csv

haciendo que file1.csv y file2.csv se procesen dos veces.

En cambio, 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 variables que especifique.

Aquí, le estamos diciendo 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 bucle, la entrada estándar proviene de find a través de la tubería.

Si solo hiciéramos read, estaría consumiendo parte o la totalidad de un nombre de archivo, y algunos archivos serían omitidos.

/dev/tty Es el terminal desde donde el usuario ejecuta el script. Tenga en cuenta que esto causará 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 una tubería:

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 no permitidos en los nombres de archivo, por lo que esto debería manejar todos los nombres de archivo posibles, sin importar cuán extraño sea.

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 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 incorporados), pero bash entiende -d '' Como significado NUL delimitado . Entonces usamos -d '' Para hacer que 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 las barras invertidas en los 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 recordar todas las reglas anteriores sobre bytes nulos (de nuevo, gracias a Gilles):

find . -name '*.csv' -exec sh -c '
  file="$0"
  echo "$file"
  diff "$file" "/some/other/path/$file"
  read char </dev/tty
' {} ';'

3. Omitir directorios cuyos nombres terminan en * .csv

find . -name "*.csv"

también coincidirá con los 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
' {} ';'

Como señala glenn jackman , en estos dos ejemplos, los comandos a ejecutar para cada archivo se ejecutan en una subshell, por lo que si cambia cualquier variable dentro del bucle, se olvidarán.

Si necesita establecer variables y tenerlas configuradas al final del ciclo, puede reescribirlas para usar la sustitución de procesos de esta manera:

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, puede eliminar read line </dev/tty Y enviar el resultado a un localizador como less.


[~ # ~] notas [~ # ~]

Eliminé los punto y coma (;) Dentro del bucle. Puede volver a colocarlos 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 personaje. Lee una línea completa, así que lo cambié a read line.

218
Mikel

Este script falla si el nombre de un archivo contiene espacios o caracteres globales 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:

  1. Ejecute el comando find, tome su salida.
  2. Divida la salida find en palabras separadas. Cualquier carácter de espacio en blanco es un separador de Word.
  3. Para cada palabra, si es un patrón global, amplíelo a la lista de archivos que coincida.

Por ejemplo, suponga que hay tres archivos en el directorio actual, llamados `foo* bar.csv, foo 1.txt Y foo 2.txt.

  1. El comando find devuelve ./foo* bar.csv.
  2. El Shell divide esta cadena en el espacio, produciendo dos palabras: ./foo* Y bar.csv.
  3. Dado que ./foo* Contiene un metacarácter global, se expande a la lista de archivos coincidentes: ./foo 1.txt Y ./foo 2.txt.
  4. Por lo tanto, el bucle 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 atenuando la división de Word y desactivando el glob. Para atenuar la división de Word, configure la variable IFS en un solo carácter de nueva línea; de esta manera, la salida de find solo se dividirá en las nuevas líneas y los espacios permanecerán. 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 le recomiendo usar $(…) over `…`. Tienen el mismo significado, pero la versión de backquote 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 la sustitución de comando anterior. Si debe recordar una cosa acerca de la programación de Shell, recuerde esto: se siempre comillas dobles alrededor de expansiones variables ($foo) Y sustituciones de comandos ($(bar)), a menos que Sé que quieres dividir. (Arriba, sabíamos que queríamos dividir la salida find en líneas).

Una forma confiable de llamar a find es diciéndole 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 excluir explícitamente todos los archivos "aburridos".

diff -r -x '*.txt' -x '*.ods' -x '*.pdf' … . /some/other/path

Me sorprende no ver a readarray mencionado. Esto lo hace muy fácil cuando se usa en combinación con <<< operador:

$ touch oneword "two words"

$ readarray -t files <<<"$(ls)"

$ for file in "${files[@]}"; do echo "|$file|"; done
|oneword|
|two words|

Utilizando el <<<"$expansion" construct también le permite dividir variables que contienen nuevas líneas en matrices, como:

$ string=$(dmesg)
$ readarray -t lines <<<"$string"
$ echo "${lines[0]}"
[    0.000000] Initializing cgroup subsys cpuset

readarray ha estado en Bash durante años, por lo que probablemente esta debería ser la forma canónica de hacerlo en Bash.

6
blujay

Recorra cualquier archivo ( cualquier carácter especial incluido) con búsqueda completamente segura (consulte el enlace para obtener documentación):

exec 9< <( find "$absolute_dir_path" -type f -print0 )
while IFS= read -r -d '' -u 9
do
    file_path="$(readlink -fn -- "$REPLY"; echo x)"
    file_path="${file_path%x}"
    echo "START${file_path}END"
done
6
l0b0

Afaik find tiene todo lo que necesitas.

find . -okdir diff {} /some/other/path/{} ";"

find se encarga de llamar a los programas de manera segura. -okdir le preguntará antes de la diferencia (¿está seguro de sí/no).

Sin Shell involucrado, sin problemas, bromistas, pi, pa, po.

Como nota al margen: si combina find con for/while/do/xargs, en la mayoría de los casos, lo está haciendo mal. :)

4
user unknown

Me sorprende que nadie haya mencionado la solución obvia zsh aquí todavía:

for file (**/*.csv(ND.)) {
  do-something-with $file
}

((D) para incluir también archivos ocultos, (N) para evitar el error si no hay coincidencia, (.) para restringir a regular archivos.)

bash4.3 y superior ahora también lo admite parcialmente:

shopt -s globstar nullglob dotglob
for file in **/*.csv; do
  [ -f "$file" ] || continue
  [ -L "$file" ] && continue
  do-something-with "$file"
done
4

Los nombres de archivo con espacios en ellos se ven como nombres múltiples en la línea de comando si no se citan. Si su archivo se llama "Hello World.txt", la línea de diferencia se expande a:

diff Hello World.txt /some/other/path/Hello World.txt

que se parece a cuatro nombres de archivo. Simplemente ponga citas alrededor de los argumentos:

diff "$file" "/some/other/path/$file"
2
Ross Smith

La cita doble es tu amiga.

diff "$file" "/some/other/path/$file"

De lo contrario, el contenido de la variable se divide en Word.

1
geekosaur

Con bash4, también puede usar la función de archivo de mapa incorporado para establecer una matriz que contenga cada línea e iterar en esta matriz.

$ tree 
.
├── a
│   ├── a 1
│   └── a 2
├── b
│   ├── b 1
│   └── b 2
└── c
    ├── c 1
    └── c 2

3 directories, 6 files
$ mapfile -t files < <(find -type f)
$ for file in "${files[@]}"; do
> echo "file: $file"
> done
file: ./a/a 2
file: ./a/a 1
file: ./b/b 2
file: ./b/b 1
file: ./c/c 2
file: ./c/c 1
1
kitekat75