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

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

si ejecuto el fragmento de código anterior en Bash shell, lo que obtengo es una cadena que contiene varios nombres de archivos separados por espacios 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 findcomando?

Haiyuan Zhang
fuente
3
La mejor manera de recorrer los nombres de los archivos depende bastante de lo que realmente quieras hacer con él, pero a menos que puedas garantizar que ningún archivo tenga espacios en blanco en su nombre, esta no es una excelente manera de hacerlo. Entonces, ¿qué quieres hacer para recorrer los archivos?
Kevin
1
Con respecto a la recompensa : la idea principal aquí es obtener una respuesta canónica que cubra todos los casos posibles (nombres de archivo con nuevas líneas, caracteres problemáticos ...). La idea es usar estos nombres de archivo para hacer algunas cosas (llamar a otro comando, cambiar el nombre ...). ¡Gracias!
Fedorqui 'así que deja de dañar'
No olvide que un nombre de archivo o carpeta puede contener ".txt" seguido de espacio y otra cadena, ejemplo "something.txt something" o "something.txt"
Yahya Yahyaoui
Use array, no var. x=( $(find . -name "*.txt") ); echo "${x[@]}"Entonces puede recorrerlofor item in "${x[@]}"; { echo "$item"; }
Ivan

Respuestas:

392

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


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 un espacio en blanco en su nombre, simplemente puede recorrer los archivos:

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

Marginalmente mejor, recorte 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 puedas. 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 globstaropción, puede glob 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 archivo ya están en un archivo, es posible que deba 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

readse puede usar de forma segura en combinación findestableciendo el delimitador adecuadamente:

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

Para búsquedas más complejas, es probable que desee utilizar find, ya sea con su -execopción o con -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

findtambién puede cd en el directorio de cada archivo antes de ejecutar un comando mediante el uso de en -execdirlugar de -exec, y se puede hacer interactivo (solicitar antes de ejecutar el comando para cada archivo) utilizando en -oklugar de -exec(o en -okdirlugar de -execdir).

*: Técnicamente, ambos findy xargs(por defecto) ejecutarán el comando con tantos argumentos como puedan caber en la línea de comando, tantas veces como sea necesario para pasar por 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 todos en la misma línea de comando, SOL encontrará una forma diferente.

Kevin
fuente
44
Vale la pena señalar que en el caso con done < filenamey el siguiente con la tubería, el stdin ya no se puede usar (→ no más cosas interactivas dentro del bucle), pero en los casos en que es necesario, se puede usar en 3<lugar de <y agregar <&3o -u3para la readparte, básicamente usando un descriptor de archivo separado. Además, creo que read -d ''es lo mismo read -d $'\0'pero no puedo encontrar ninguna documentación oficial sobre eso en este momento.
phk
1
para i en * .txt; no funciona, si no hay archivos que coincidan. Se necesita una prueba xtra, por ejemplo, [[-e $ i]]
Michael Brux
2
Estoy perdido con esta parte: -exec process {} \;y supongo que esa es otra pregunta: ¿qué significa eso y cómo lo manipulo? ¿Dónde hay un buen Q / A o doc. ¿en eso?
Alex Hall
1
@AlexHall siempre puedes mirar las páginas man ( man find). En este caso, -execle indica findque ejecute el siguiente comando, terminado por ;(o +), en donde {}será reemplazado por el nombre del archivo que está procesando (o, si +se usa, todos los archivos que han llegado a esa condición).
Kevin
3
@phk -d ''es mejor que -d $'\0'. Este último no solo es más largo, sino que también sugiere que podría pasar argumentos que contienen bytes nulos, pero no puede. El primer byte nulo marca el final de la cadena. En bash $'a\0bc'es lo mismo que ay $'\0'es lo mismo $'\0abc'o solo la cadena vacía ''. help readestablece que " El primer carácter de delim se usa para terminar la entrada ", por lo que usarlo ''como delimitador es un poco hack. El primer carácter en la cadena vacía es el byte nulo que siempre marca el final de la cadena (incluso si no lo escribe explícitamente).
Socowi
114

Hagas lo que hagas, no uses un forbucle :

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

Tres razones:

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

Siempre use una while readconstrucción:

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

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

El -print0usará el NULL como un separador de archivo en lugar de una nueva línea y la -d $'\0'va a usar NULL como el separador durante la lectura.

David W.
fuente
3
No funcionará con nuevas líneas en los nombres de archivo. Use find's -execen su lugar.
usuario desconocido
2
@userunknown: tienes razón en eso. -execes el más seguro ya que no usa el shell en absoluto. Sin embargo, NL en los nombres de archivo es bastante raro. Los espacios en los nombres de archivo son bastante comunes. El punto principal es no usar un forbucle que muchos carteles recomiendan.
David W.
1
@userunknown - Aquí. He solucionado esto, por lo que ahora se encargará de los archivos con nuevas líneas, pestañas y cualquier otro espacio en blanco. El objetivo de la publicación es decirle al OP que no use el for file $(find)debido a los problemas asociados con eso.
David W.
44
Si puede usar -exec, es mejor, pero hay momentos en los que realmente necesita que se le devuelva el nombre al shell. Por ejemplo, si desea eliminar extensiones de archivo.
Ben Reser
55
Debe usar la -ropción para read: -r raw input - disables interpretion of backslash escapes and line-continuation in the read data
Daira Hopwood
102
find . -name "*.txt"|while read fname; do
  echo "$fname"
done

Nota: este método y el (segundo) método mostrado por bmargulies son seguros para 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 -execpredicado de findesta manera:

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

El {}es el marcador de posición para el elemento encontrado y \;se utiliza para terminar el -execpredicado.

Y en aras de la integridad, 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 \0carácter 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 cubrir todas las bases. xargslos recoge uno por uno y luego ...

0xC0000022L
fuente
3
Falla si la nueva línea en el nombre del archivo.
usuario desconocido
2
@usuario desconocido: tienes razón, es un caso que no había considerado en absoluto y eso, creo, es muy exótico. Pero ajusté mi respuesta en consecuencia.
0xC0000022L
55
Probablemente valga la pena señalar eso find -print0y xargs -0son tanto extensiones GNU como argumentos no portátiles (POSIX). Sin embargo, ¡increíblemente útil en aquellos sistemas que los tienen!
Toby Speight el
1
Esto también falla con los nombres de archivo que contienen barras invertidas (lo read -rque solucionaría), o nombres de archivo que terminan en espacios en blanco (lo IFS= readque solucionaría). Por lo tanto, BashFAQ # 1 sugierewhile IFS= read -r filename; do ...
Charles Duffy
1
Otro problema con esto es que parece que el cuerpo del bucle se está ejecutando en el mismo shell, pero no es así, por ejemplo exit, no funcionará como se esperaba y las variables establecidas en el cuerpo del bucle no estarán disponibles después del bucle.
EM0
17

Los nombres de archivo pueden incluir espacios e incluso caracteres de control. Los espacios son delimitadores (predeterminados) para la expansión de shell en bash y, como resultado de eso, x=$(find . -name "*.txt")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 xen un bucle. Puede mejorar esto cambiando el delimitador ( IFSvariable bash ), por ejemplo \r\n, a , 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. Utilice find-read-while y sustitución de procesos

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 un 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 nullglobse puede utilizar para evitar esta línea adicional.
  3. "Si se establece la failglobopción de shell 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 se establece, el patrón '**' usado 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 un '/', solo los directorios y subdirectorios coinciden". ver Bash Manual, Shopt Builtin
  5. Otras opciones para la expansión de nombre de archivo: extglob, nocaseglob, dotgloby variable de shellGLOBIGNORE

en el Patrón 2:

  1. los nombres de archivo pueden contener espacios en blanco, tabulaciones, espacios, saltos de línea, ... a los nombres de archivo de proceso en una manera segura, findcon -print0se utiliza: nombre de archivo se imprime con todos los caracteres de control y termina con NUL. véase también Gnu Findutils Página de manual, Manejo inseguro Nombre de archivo , salvo Nombre de archivo Manipulación , caracteres inusuales en nombres de archivo . Ver 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_foundsiempre es "verdadero" y el código siempre repetirá "no se encontraron archivos". La razón es: cada comando de una tubería 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 la 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.
    Vea que configuro 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 adicionales y fuentes:

Michael Brux
fuente
8

(Actualizado para incluir la excelente mejora de velocidad de @Scowcowi)

Con cualquiera $SHELLque lo admita (dash / zsh / bash ...):

find . -name "*.txt" -exec $SHELL -c '
    for i in "$@" ; do
        echo "$i"
    done
' {} +

Hecho.


Respuesta original (más corta, pero más lenta):

find . -name "*.txt" -exec $SHELL -c '
    echo "$0"
' {} \;
usuario569825
fuente
1
Lento como la melaza (ya que lanza un shell para cada archivo) pero esto funciona. +1
dawg
1
En lugar de \;usar, puede +pasar tantos archivos como sea posible a un solo exec. Luego use "$@"dentro del script de shell para procesar todos estos parámetros.
Socowi,
3
Hay un error en este código. Al bucle le falta el primer resultado. Esto se debe a que lo $@omite, ya que generalmente es el nombre del script. Sólo tenemos que añadir dummyen el medio 'y {}por lo que puede tomar el lugar del nombre del script, asegurando que todos los partidos son procesados por el bucle.
BCartolo
¿Qué sucede si necesito otras variables externas al shell recién creado?
Jodo
OTHERVAR=foo find . -na.....debería permitirle acceder $OTHERVARdesde ese shell recién creado.
user569825
6
# 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
bmargulies
fuente
3
for x in $(find ...)se romperá para cualquier nombre de archivo con espacios en blanco. Lo mismo con a find ... | xargsmenos que use -print0y-0
Glenn Jackman
1
Usar en su find . -name "*.txt -exec process_one {} ";"lugar. ¿Por qué deberíamos usar xargs para recopilar resultados, que ya tenemos?
usuario desconocido
@userunknown Bueno, todo depende de lo que process_onesea. Si es un marcador de posición para un comando real , asegúrese de que funcionaría (si corrige el error tipográfico y agrega comillas de cierre después "*.txt). Pero si process_onees una función definida por el usuario, su código no funcionará.
toxalot
@toxalot: Sí, pero no sería un problema escribir la función en un script para llamar.
Usuario desconocido
4

Puede almacenar su findsalida en una matriz si desea usar la salida más tarde como:

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

Ahora, para imprimir cada elemento en una nueva línea, puede usar foriteració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 el findresultado 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 resultado 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 desde find.

Rakholiya Jenish
fuente
1
El bucle sobre la matriz falla con espacios en los nombres de archivo.
EM0
Deberías eliminar esta respuesta. No funciona con espacios en nombres de archivo o nombres de directorio.
jww
4

Si puede suponer que los nombres de los archivos no contienen líneas nuevas, puede leer la salida finden una matriz Bash con el siguiente comando:

readarray -t x < <(find . -name '*.txt')

Nota:

  • -thace readarrayque se eliminen las nuevas líneas.
  • No funcionará si readarrayestá en una tubería, de ahí la sustitución del proceso.
  • readarray está disponible desde Bash 4.

Bash 4.4 y versiones posteriores también admiten el -dparámetro para especificar el delimitador. El uso del carácter nulo, en lugar de nueva línea, para delimitar los nombres de archivo funciona también en el raro caso de que los nombres de archivo contengan nuevas líneas:

readarray -d '' x < <(find . -name '*.txt' -print0)

readarrayTambién se puede invocar como mapfilecon las mismas opciones.

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

Seppo Enarvi
fuente
¡Esta es la mejor respuesta! Funciona con: * Espacios en los nombres de archivo * No hay archivos coincidentes * exital
recorrer
No funciona con todos los nombres de archivos posibles, sin embargo - para eso, se debe utilizarreadarray -d '' x < <(find . -name '*.txt' -print0)
Charles Duffy
3

Me gusta usar find, que primero se asigna a la variable e IFS cambió a una 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 desee repetir más acciones en el mismo conjunto de DATOS y la búsqueda es muy lenta en su servidor (I / 0 de alta utilización)

Paco
fuente
2

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

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

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

Nota: es un espacio en blanco seguro.

Jahid
fuente
1
Con fiesta de 4.4 o más alto se puede utilizar un solo comando en lugar de un bucle: mapfile -t -d '' array < <(find ...). La configuración IFSno es necesaria para mapfile.
Socowi
1

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

while IFS= read -r f <&3; do
    echo "$f"

done 3< <(find . -iname "*filename*")
Florian
fuente
-1

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

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

chetangb
fuente
-5

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

ls | grep .txt$ > out.txt

Ahora puede leer este archivo y los nombres de los archivos tienen la forma de una lista.

Dhruv Raj Singh Rathore
fuente
66
No, no hagas esto. Por qué no deberías analizar la salida de ls . Esto es frágil, muy frágil.
Fedorqui 'así que deja de dañar'