Bash y nombres de archivo con espacios

La siguiente es una línea de comando Bash simple:

grep -li 'regex' "filename with spaces" "filename" 

No hay problemas. También lo siguiente funciona bien:

 grep -li 'regex' $(<listOfFiles.txt) 

donde listOfFiles.txt contiene una lista de nombres de archivos para grepped, un nombre de archivo por línea.

El problema ocurre cuando listOfFiles.txt contiene nombres de archivos con espacios integrados. En todos los casos que he intentado (ver más abajo), Bash divide los nombres de los archivos en los espacios, así que, por ejemplo, una línea en listOfFiles.txt contenga un nombre como ./this is a file.xml termina tratando de ejecutar grep en cada uno pieza ( ./this , is , a y file.xml ).

Pensé que era un usuario de Bash relativamente avanzado, pero no puedo encontrar un hechizo mágico simple para hacer que esto funcione. Estas son las cosas que he intentado.

 grep -li 'regex' `cat listOfFiles.txt` 

Falla como se describió anteriormente (realmente no esperaba que esto funcionara), así que pensé en poner comillas alrededor de cada nombre de archivo:

 grep -li 'regex' `sed -e 's/.*/"&"/' listOfFiles.txt` 

Bash interpreta las citas como parte del nombre de archivo y da “No such archivo o directorio” para cada archivo (y aún divide los nombres de archivo con espacios en blanco)

 for i in $(<listOfFiles.txt); do grep -li 'regex' "$i"; done 

Esto falla en cuanto al bash original (es decir, se comporta como si se omitieran las comillas) y es muy lento, ya que tiene que iniciar un proceso ‘grep’ por archivo en lugar de procesar todos los archivos en una invocación.

Lo siguiente funciona, pero requiere un cuidadoso doble escape si la expresión regular contiene metacaracteres del shell:

 eval grep -li 'regex' `sed -e 's/.*/"&"/' listOfFiles.txt` 

¿Es esta la única forma de construir la línea de comando para que maneje correctamente los nombres de archivo con espacios?

Prueba esto:

 (IFS=$'\n'; grep -li 'regex' $( 

IFS es el separador de campo interno. Establecerlo en $'\n' le dice a Bash que use el carácter de nueva línea para delimitar los nombres de los archivos. Su valor predeterminado es $' \t\n' y se puede imprimir utilizando cat -etv <<<"$IFS" .

Al incluir el script entre paréntesis, se inicia una subcadena para que solo los comandos dentro del paréntesis se vean afectados por el valor IFS personalizado.

 cat listOfFiles.txt |tr '\n' '\0' |xargs -0 grep -li 'regex' 

La opción -0 en xargs le dice a xargs que use un carácter nulo en lugar de un espacio en blanco como un terminador de nombre de archivo. El comando tr convierte las nuevas líneas entrantes en un carácter nulo.

Esto cumple con el requisito del OP de que grep no se invoque varias veces. Según mi experiencia, para una gran cantidad de archivos evitar las múltiples invocaciones de grep mejora considerablemente el rendimiento.

Este esquema también evita un error en el método original de OP porque su esquema se romperá donde listOfFiles.txt contiene una cantidad de archivos que excedería el tamaño del búfer para los comandos. xargs conoce el tamaño máximo de comando e invocará grep varias veces para evitar ese problema.

Un problema relacionado con el uso de xargs y grep es que grep prefija el resultado con el nombre del archivo cuando se invoca con varios archivos. Debido a que xargs invoca grep con varios archivos, uno recibirá resultados con el nombre de archivo prefijado, pero no para el caso de un archivo en listOfFiles.txt o el caso de múltiples invocaciones donde la última invocación contiene un nombre de archivo. Para lograr un resultado consistente, agregue / dev / null al comando grep:

 cat listOfFiles.txt |tr '\n' '\0' |xargs -0 grep -i 'regex' /dev/null 

Tenga en cuenta que no era un problema para el OP porque estaba usando la opción -l en grep; sin embargo, es probable que sea un problema para otros.

Esto funciona:

 while read file; do grep -li dtw "$file"; done < listOfFiles.txt 

Aunque puede que no coincida, esta es mi solución favorita:

 grep -i 'regex' $(cat listOfFiles.txt | sed -e "s/ /?/g") 

Tenga en cuenta que si de alguna manera terminó con una lista en un archivo que tiene terminaciones de línea de Windows, \r\n , NINGUNA de las notas anteriores sobre el separador de archivos de entrada $IFS (y citando el argumento) funcionará; así que asegúrese de que las terminaciones de línea sean correctas \n (utilizo scite para mostrar los finales de línea, y los cambio fácilmente de uno a otro).

También se conectó cat while file read ... parece funcionar (aparentemente sin necesidad de establecer separadores):

 cat <(echo -e "AA AA\nBB BB") | while read file; do echo $file; done 

... aunque para mí fue más relevante para un "grep" a través de un directorio con espacios en los nombres de archivo:

 grep -rlI 'search' "My Dir"/ | while read file; do echo $file; grep 'search\|else' "$ix"; done 

Con Bash 4, también puede usar la función mapfile incorporada 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