¿Cómo recorrer los nombres de archivo devueltos por find?

x=$(find . -name "*.txt") echo $x 

si ejecuto el fragmento de código anterior en el shell Bash, lo que obtengo es una cadena que contiene varios nombres de archivos separados por un espacio en blanco, no una lista.

Por supuesto, puedo separarlos aún más en blanco para obtener una lista, pero estoy seguro de que hay una mejor manera de hacerlo.

Entonces, ¿cuál es la mejor manera de recorrer los resultados de un comando de find ?

TL; DR: Si solo está aquí para obtener la respuesta más correcta, es probable que desee mi preferencia personal, find . -name '*.txt' -exec process {} \; find . -name '*.txt' -exec process {} \; (mira la parte inferior de esta publicación). Si tiene tiempo, lea el rest para ver varias formas diferentes y los problemas con la mayoría de ellos.


La respuesta completa:

La mejor manera depende de lo que quieras hacer, pero aquí hay algunas opciones. Siempre que ningún archivo o carpeta en el subárbol tenga espacios en blanco en su nombre, puede simplemente recorrer los archivos:

 for i in $x; do # Not recommended, will break on whitespace process "$i" done 

Marginalmente mejor, corte la variable temporal x :

 for i in $(find -name \*.txt); do # Not recommended, will break on whitespace process "$i" done 

Es mucho mejor pegarse cuando puedes. Espacio en blanco seguro, para archivos en el directorio actual:

 for i in *.txt; do # Whitespace-safe but not recursive. process "$i" done 

Al habilitar la opción globstar , puede agrupar todos los archivos coincidentes en este directorio y todos los subdirectorios:

 # Make sure globstar is enabled shopt -s globstar for i in **/*.txt; do # Whitespace-safe and recursive process "$i" done 

En algunos casos, por ejemplo, si los nombres de los archivos ya están en un archivo, es posible que necesite usar read :

 # IFS= makes sure it doesn't trim leading and trailing whitespace # -r prevents interpretation of \ escapes. while IFS= read -r line; do # Whitespace-safe EXCEPT newlines process "$line" done < filename 

read se puede usar de forma segura en combinación con find configurando el delimitador de forma apropiada:

 find . -name '*.txt' -print0 | while IFS= read -r -d $'\0' line; do process $line done 

Para búsquedas más complejas, es probable que desee utilizar find , ya sea con su opción -exec o con -print0 | xargs -0 -print0 | xargs -0 :

 # execute `process` once for each file find . -name \*.txt -exec process {} \; # execute `process` once with all the files as arguments*: find . -name \*.txt -exec process {} + # using xargs* find . -name \*.txt -print0 | xargs -0 process # using xargs with arguments after each filename (implies one run per filename) find . -name \*.txt -print0 | xargs -0 -I{} process {} argument 

find también puede -execdir en el directorio de cada archivo antes de ejecutar un comando usando -execdir lugar de -exec , y puede hacerse interactivo (preguntar antes de ejecutar el comando para cada archivo) usando -ok lugar de -exec (o -okdir lugar de -execdir ).

*: Técnicamente, tanto find como xargs (de forma predeterminada) ejecutarán el comando con tantos argumentos como quepan en la línea de comando, tantas veces como sea necesario para recorrer todos los archivos. En la práctica, a menos que tenga una gran cantidad de archivos, no importará, y si excede la longitud pero los necesita a todos en la misma línea de comando, SOL encontrará una manera diferente.

 find . -name "*.txt"|while read fname; do echo "$fname" done 

Nota: este método y el (segundo) método mostrado por bmargulies son seguros de usar con espacios en blanco en los nombres de archivo / carpeta.

Para tener también el caso, algo exótico, de nuevas líneas en los nombres de archivo / carpeta cubiertos, tendrá que recurrir al predicado -exec de find esta manera:

 find . -name '*.txt' -exec echo "{}" \; 

{} Es el marcador de posición para el elemento encontrado y el \; se usa para terminar el predicado -exec .

Y para completar, permítanme agregar otra variante: deben amar las formas * nix por su versatilidad:

 find . -name '*.txt' -print0|xargs -0 -n 1 echo 

Esto separaría los elementos impresos con un carácter \0 que no está permitido en ninguno de los sistemas de archivos en los nombres de archivos o carpetas, que yo sepa, y por lo tanto debería abarcar todas las bases. xargs recoge uno por uno, entonces …

Lo que sea que hagas, no uses un ciclo for :

 # Don't do this for file in $(find . -name "*.txt") do …code using "$file" done 

Tres razones:

  • Para que el bucle for incluso comience, el find debe ejecutarse hasta su finalización.
  • Si un nombre de archivo tiene algún espacio en blanco (incluyendo espacio, pestaña o línea nueva), se tratará como dos nombres separados.
  • Aunque ahora es poco probable, puede sobrepasar el buffer de línea de comando. Imagínese si su buffer de línea de comando contiene 32KB, y su ciclo for devuelve 40KB de texto. Los últimos 8 KB se eliminarán de tu bucle y nunca lo sabrás.

Siempre use una construcción de while read simultánea:

 find . -name "*.txt" -print0 | while read -d $'\0' file do …code using "$file" done 

El ciclo se ejecutará mientras se ejecuta el comando find . Además, este comando funcionará incluso si se devuelve un nombre de archivo con espacios en blanco. Y no se desbordará el búfer de línea de comando.

El -print0 usará NULL como un separador de archivos en lugar de una nueva línea y -d $'\0' usará NULL como separador mientras lee.

Los nombres de archivo pueden incluir espacios e incluso caracteres de control. Los espacios son delimitadores (por defecto) para la expansión de shell en bash y como resultado de eso x=$(find . -name "*.txt") de la pregunta no se recomienda en absoluto. Si find obtiene un nombre de archivo con espacios, por ejemplo, "the file.txt" , obtendrá 2 cadenas separadas para el procesamiento, si procesa x en un bucle. Puede mejorar esto cambiando el delimitador (variable IFS bash), por ejemplo, a \r\n , pero los nombres de archivo pueden incluir caracteres de control, por lo que este no es un método (completamente) seguro.

Desde mi punto de vista, hay 2 patrones recomendados (y seguros) para procesar archivos:

1. Use para la expansión de bucle y nombre de archivo:

 for file in ./*.txt; do [[ ! -e $file ]] && continue # continue, if file does not exist # single filename is in $file echo "$file" # your code here done 

2. Use la sustitución de buscar-leer-y-procesar

 while IFS= read -r -d '' file; do # single filename is in $file echo "$file" # your code here done < <(find . -name "*.txt" -print0) 

Observaciones

en el patrón 1:

  1. bash devuelve el patrón de búsqueda ("* .txt") si no se encuentra ningún archivo coincidente, por lo que se necesita la línea adicional "continuar, si el archivo no existe". ver Bash Manual, Filename Expansion
  2. la opción de shell nullglob se puede usar para evitar esta línea adicional.
  3. "Si se failglob opción del shell failglob , y no se encuentran coincidencias, se imprime un mensaje de error y el comando no se ejecuta". (del manual de Bash arriba)
  4. opción de shell globstar : "Si está establecido, el patrón '**' utilizado en un contexto de expansión de nombre de archivo coincidirá con todos los archivos y cero o más directorios y subdirectorios. Si el patrón es seguido por '/', solo los directorios y subdirectorios coinciden." ver Bash Manual, Shopt Builtin
  5. otras opciones para la expansión del nombre de archivo: extglob , nocaseglob , dotglob y variable de shell GLOBIGNORE

en el patrón 2:

  1. los nombres de archivo pueden contener espacios en blanco, tabs, espacios, líneas nuevas, ... para procesar nombres de archivos de forma segura, se find con -print0 : el nombre del archivo se imprime con todos los caracteres de control y termina con NUL. consulte también la página de manual de Gnu Findutils, Manejo inseguro de nombres de archivos , Manejo seguro de nombres de archivos , caracteres inusuales en los nombres de archivos . Consulte a David A. Wheeler a continuación para una discusión detallada de este tema.

  2. Hay algunos patrones posibles para procesar resultados de búsqueda en un ciclo while. Otros (Kevin, David W.) han mostrado cómo hacer esto usando tuberías:

    files_found=1 find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do # single filename in $file echo "$file" files_found=0 # not working example # your code here done [[ $files_found -eq 0 ]] && echo "files found" || echo "no files found"

    Cuando pruebe este fragmento de código, verá que no funciona: files_found siempre es "verdadero" y el código siempre mostrará "no se encontraron archivos". La razón es que cada comando de una canalización se ejecuta en una subshell separada, por lo que la variable modificada dentro del bucle (subshell separado) no cambia la variable en el script de shell principal. Es por eso que recomiendo usar la sustitución de procesos como el patrón "mejor", más útil y más general.
    Veo que establezco las variables en un bucle que está en una tubería. ¿Por qué desaparecen? (De las preguntas frecuentes de Greg's Bash) para una discusión detallada sobre este tema.

Referencias y fonts adicionales:

  • Manual de Gnu Bash, coincidencia de patrones

  • Nombres de archivos y rutas en Shell: cómo hacerlo correctamente, David A. Wheeler

  • Por qué no lees líneas con "para", Wiki de Greg

  • Por qué no deberías analizar el resultado de ls (1), Greg's Wiki

  • Manual de Gnu Bash, Sustitución de procesos

 # Doesn't handle whitespace for x in `find . -name "*.txt" -print`; do process_one $x done or # Handles whitespace and newlines find . -name "*.txt" -print0 | xargs -0 -n 1 process_one 

Puede almacenar su salida de find en matriz si desea utilizar la salida más tarde como:

 array=($(find . -name "*.txt")) 

Ahora, para imprimir cada elemento en una nueva línea, puede usar iteración de bucle para todos los elementos de la matriz, o puede usar la instrucción printf.

 for i in ${array[@]};do echo $i; done 

o

 printf '%s\n' "${array[@]}" 

También puedes usar:

 for file in "`find . -name "*.txt"`"; do echo "$file"; done 

Esto imprimirá cada nombre de archivo en nueva línea

Para imprimir solo la salida de find en forma de lista, puede usar cualquiera de los siguientes:

 find . -name "*.txt" -print 2>/dev/null 

o

 find . -name "*.txt" -print | grep -v 'Permission denied' 

Esto eliminará los mensajes de error y solo dará el nombre del archivo como salida en una nueva línea.

Si desea hacer algo con los nombres de archivo, almacenarlo en una matriz es bueno, de lo contrario no hay necesidad de consumir ese espacio y puede imprimir directamente la salida de find .

Con cualquier $SHELL que lo soporte (sh / bash / zsh / …):

 find . -name "*.txt" -exec $SHELL -c ' echo "$0" ' {} \; 

Hecho.

Si puede asumir que los nombres de los archivos no contienen líneas nuevas, puede leer la salida de find en una matriz Bash usando el comando readarray :

 readarray -tx < <(find . -name '*.txt') 

Nota:

  • -t causa readarray para quitar nuevas líneas.
  • No funcionará si readarray está en una tubería, de ahí la sustitución del proceso.
  • readarray está disponible desde Bash 4.

readarray también se puede invocar como mapfile con las mismas opciones.

Referencia: https://mywiki.wooledge.org/BashFAQ/005#Loading_lines_from_a_file_or_stream

Suponiendo que no tiene nombres de archivo con líneas nuevas incorporadas, puede obtener una lista como esta:

 list=($(find . -name '*.txt')) printf '%s\n' "${list[@]}" 

Como han señalado otras personas, si esto es útil depende del contexto.

find -xdev -type f -name *.txt -exec ls -l {} \;

Esto listará los archivos y dará detalles sobre los atributos.

basado en otras respuestas y comentarios de @phk, usando fd # 3:
(que aún permite usar stdin dentro del bucle)

 while IFS= read -rf <&3; do echo "$f" done 3< <(find . -iname "*filename*") 

Puede poner los nombres de archivo devueltos por find en una matriz como esta:

 array=() while IFS= read -r -d $'\0'; do array+=("$REPLY") done < <(find . -name '*.txt' -print0) 

Ahora puede recorrer el conjunto para acceder a elementos individuales y hacer lo que quiera con ellos.

Nota: es un espacio en blanco seguro.

Me gusta utilizar find que primero se asigna a variable e IFS cambia a nueva línea de la siguiente manera:

 FilesFound=$(find . -name "*.txt") IFSbkp="$IFS" IFS=$'\n' counter=1; for file in $FilesFound; do echo "${counter}: ${file}" let counter++; done IFS="$IFSbkp" 

En caso de que quiera repetir más acciones en el mismo conjunto de DATOS y encuentre que su servidor es muy lento (alta utilización I / 0)

¿Qué tal si usas grep en lugar de encontrar?

 ls | grep .txt$ > out.txt 

Ahora puede leer este archivo y los nombres de los archivos están en forma de lista.