bash - recursivo - comandos basicos de linux y ejemplos
¿Cómo recorrer los nombres de archivo devueltos por find? (14)
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
?
¿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.
Con cualquier $SHELL
que lo soporte (sh / bash / zsh / ...):
find . -name "*.txt" -exec $SHELL -c ''
echo "$0"
'' {} /;
Hecho.
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:
- 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
- la opción de shell
nullglob
se puede usar para evitar esta línea adicional. - "Si se
failglob
opción del shellfailglob
, y no se encuentran coincidencias, se imprime un mensaje de error y el comando no se ejecuta". (del manual de Bash arriba) - 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 - otras opciones para la expansión del nombre de archivo:
extglob
,nocaseglob
,dotglob
y variable de shellGLOBIGNORE
en el patrón 2:
los nombres de archivo pueden contener espacios en blanco, pestañas, 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.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"
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 fuentes adicionales:
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)
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
.
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.
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 -t x < <(find . -name ''*.txt'')
Nota:
-
-t
causareadarray
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.
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 resto 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.
basado en otras respuestas y comentarios de @phk, usando fd # 3:
(que aún permite usar stdin dentro del bucle)
while IFS= read -r f <&3; do
echo "$f"
done 3< <(find . -iname "*filename*")
find <path> -xdev -type f -name *.txt -exec ls -l {} /;
Esto listará los archivos y dará detalles sobre los atributos.
# 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
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 ...