Capturando la salida de find. -print0 en una matriz bash

76

El uso find . -print0parece ser la única forma segura de obtener una lista de archivos en bash debido a la posibilidad de que los nombres de archivo contengan espacios, nuevas líneas, comillas, etc.

Sin embargo, estoy teniendo dificultades para hacer que la salida de find sea útil dentro de bash o con otras utilidades de línea de comando. La única forma en que he logrado hacer uso de la salida es canalizándola a perl y cambiando el IFS de perl a nulo:

find . -print0 | perl -e '$/="\0"; @files=<>; print $#files;'

Este ejemplo imprime la cantidad de archivos encontrados, evitando el peligro de que las nuevas líneas en los nombres de archivo corrompan el recuento, como ocurriría con:

find . | wc -l

Como la mayoría de los programas de línea de comando no admiten la entrada delimitada por nulos, me imagino que lo mejor sería capturar la salida de find . -print0en una matriz bash, como he hecho en el fragmento de Perl anterior, y luego continuar con la tarea, sea lo que sea. ser.

¿Cómo puedo hacer esto?

Esto no funciona:

find . -print0 | ( IFS=$'\0' ; array=( $( cat ) ) ; echo ${#array[@]} )

Una pregunta mucho más general podría ser: ¿Cómo puedo hacer cosas útiles con listas de archivos en bash?

Idris
fuente
¿A qué te refieres con hacer cosas útiles?
Balázs Pozsár
4
Oh, ya sabes, las cosas habituales para las que son útiles las matrices: averiguar su tamaño; iterando sobre su contenido; imprimiéndolos al revés; clasificándolos. Ese tipo de cosas. Hay una gran cantidad de utilidades en Unix para hacer estas cosas con datos: wc, bucles for de bash, tac y sort respectivamente; pero todos estos parecen inútiles cuando se trata de listas que pueden tener espacios o nuevas líneas. Es decir, nombres de archivo. La canalización de datos con separadores de campo de entrada de valor nulo parece ser la solución, pero muy pocas empresas de servicios públicos pueden manejar esto.
Idris
1
Aquí hay un ensayo sobre cómo manejar correctamente los nombres de archivo en shell, con muchos detalles: http://www.dwheeler.com/essays/filenames-in-shell.html
David A. Wheeler

Respuestas:

103

Robado descaradamente de BashFAQ de Greg :

unset a i
while IFS= read -r -d $'\0' file; do
    a[i++]="$file"        # or however you want to process each file
done < <(find /tmp -type f -print0)

Tenga en cuenta que la construcción de redirección que se usa aquí ( cmd1 < <(cmd2)) es similar, pero no igual que la tubería más habitual ( cmd2 | cmd1): si los comandos son elementos integrados de shell (p while. Ej. ), La versión de la tubería los ejecuta en subcapas y cualquier variable que establezcan. (por ejemplo, la matriz a) se pierden cuando salen. cmd1 < <(cmd2)solo ejecuta cmd2 en una subcapa, por lo que la matriz vive más allá de su construcción. Advertencia: esta forma de redirección solo está disponible en bash, ni siquiera bash en el modo de emulación sh; debe comenzar su guión con #!/bin/bash.

Además, debido a que el paso de procesamiento del archivo (en este caso, simplemente a[i++]="$file", pero es posible que desee hacer algo más elegante directamente en el ciclo) tiene su entrada redirigida, no puede usar ningún comando que pueda leer desde stdin. Para evitar esta limitación, tiendo a usar:

unset a i
while IFS= read -r -u3 -d $'\0' file; do
    a[i++]="$file"        # or however you want to process each file
done 3< <(find /tmp -type f -print0)

... que pasa la lista de archivos a través de la unidad 3, en lugar de stdin.

Gordon Davisson
fuente
Ahhh casi ahí ... esta es la mejor respuesta hasta ahora. Sin embargo, acabo de probarlo en un directorio que contiene un archivo con una nueva línea en su nombre, y al inspeccionar ese elemento usando echo $ {a [1]}, la nueva línea parece haberse convertido en un espacio (0x20). ¿Alguna idea de por qué está pasando esto?
Idris
¿Qué versión de bash estás ejecutando? He tenido problemas con versiones anteriores (desafortunadamente no recuerdo exactamente cuál) no lidiar con nuevas líneas y eliminaciones ( \177) en cadenas. IIRC, incluso x = "$ y" no siempre funcionaría bien con estos caracteres. Acabo de probar con bash 2.05b.0 y 3.2.17 (el más antiguo y el más nuevo que tengo a mano); ambos manejaron las nuevas líneas correctamente, pero v2.05b.0 se comió el carácter de eliminación.
Gordon Davisson
Lo he probado en 3.2.17 en osx, 3.2.39 en linux y 3.2.48 en netBSD; todos convierten la nueva línea en espacio.
Idris
12
-d ''es equivalente a -d $'\0'.
l0b0
15
Una forma más fácil de agregar un elemento al final de una matriz es:arr+=("$file")
dogbane
7

Quizás estés buscando xargs:

find . -print0 | xargs -r0 do_something_useful

La opción -L 1 también podría serle útil, lo que hace que xargs exec haga_something_useful con solo 1 argumento de archivo.

Balázs Pozsár
fuente
3
Esto no es exactamente lo que buscaba, porque no hay oportunidad de hacer cosas parecidas a matrices con la lista, como ordenar: debe usar cada elemento como y cuando aparece fuera del comando de búsqueda. Si pudiera ampliar este ejemplo, con la parte "do_something_useful" siendo una operación bash array-push, entonces esto podría ser lo que estoy buscando.
Idris
6

Desde Bash 4.4, la función incorporada mapfiletiene el -dmodificador (para especificar un delimitador, similar al -dcambio de la readinstrucción), y el delimitador puede ser el byte nulo. Por lo tanto, una buena respuesta a la pregunta del título.

Capturando la salida de find . -print0en una matriz bash

es:

mapfile -d '' ary < <(find . -print0)
gniourf_gniourf
fuente
5

El principal problema es que el delimitador NUL (\ 0) es inútil aquí, porque no es posible asignar a IFS un valor NUL. Entonces, como buenos programadores, nos ocupamos de que la entrada de nuestro programa sea algo que pueda manejar.

Primero creamos un pequeño programa, que hace esta parte por nosotros:

#!/bin/bash
printf "%s" "$@" | base64

... y llámalo base64str (no olvides chmod + x)

En segundo lugar, ahora podemos usar un bucle for simple y directo:

for i in `find -type f -exec base64str '{}' \;`
do 
  file="`echo -n "$i" | base64 -d`"
  # do something with file
done

Entonces, el truco es que una cadena base64 no tiene ningún signo que cause problemas para bash; por supuesto, un xxd o algo similar también puede hacer el trabajo.

zstegi
fuente
1
Uno debe asegurarse de que la parte del sistema de archivos que find está procesando no cambie desde que se invoca a find hasta que se completa el script. Si este no es el caso, se produce una condición de carrera, que se puede aprovechar para invocar comandos en los archivos incorrectos. Por ejemplo, un directorio que se eliminará (digamos / tmp / junk) podría ser reemplazado por un enlace simbólico a / home por un usuario sin privilegios. Si el comando find se ejecutaba como root y era find -type d -exec rm -rf '{}' \;, esto eliminaría las carpetas de inicio de todos los usuarios.
Demi
2
read -r -d ''leerá todo hasta el próximo NUL en "$REPLY". No hay necesidad de preocuparse IFS.
Charles Duffy
4

Otra forma más de contar archivos:

find /DIR -type f -print0 | tr -dc '\0' | wc -c 

fuente
2

Puede hacer el recuento con seguridad con esto:

find . -exec echo ';' | wc -l

(Imprime una nueva línea para cada archivo / directorio encontrado, y luego cuenta las nuevas líneas impresas ...)

Balázs Pozsár
fuente
Es mucho más rápido usar la -printfopción en lugar de -execpara cada archivo:find . -printf "\n" | wc -l
Oliver I
1

Creo que existen soluciones más elegantes, pero agregaré esta. Esto también funcionará para nombres de archivo con espacios y / o líneas nuevas:

i=0;
for f in *; do
  array[$i]="$f"
  ((i++))
done

A continuación, puede, por ejemplo, enumerar los archivos uno por uno (en este caso en orden inverso):

for ((i = $i - 1; i >= 0; i--)); do
  ls -al "${array[$i]}"
done

Esta página ofrece un buen ejemplo y, para obtener más información, consulte el Capítulo 26 de la Guía avanzada de secuencias de comandos Bash .

Stephan202
fuente
Esto (y otros ejemplos similares a continuación) es casi lo que busco, pero con un gran problema: solo funciona para globs del directorio actual. Me gustaría poder manipular listas de archivos completamente arbitrarias; la salida de "buscar", por ejemplo, que enumera directorios de forma recursiva o cualquier otra lista. ¿Y si mi lista fuera: (/tmp/foo.jpg | /home/alice/bar.jpg | / home / bob / my holiday / baz.jpg | /tmp/new\nline/grault.jpg), o cualquier otro lista de archivos totalmente arbitraria (por supuesto, potencialmente con espacios y nuevas líneas en ellos)?
Idris
1

Evite los xargs si puede:

man ruby | less -p 777 
IFS=$'\777' 
#array=( $(find ~ -maxdepth 1 -type f -exec printf "%s\777" '{}' \; 2>/dev/null) ) 
array=( $(find ~ -maxdepth 1 -type f -exec printf "%s\777" '{}' + 2>/dev/null) ) 
echo ${#array[@]} 
printf "%s\n" "${array[@]}" | nl 
echo "${array[0]}" 
IFS=$' \t\n' 

fuente
¿Por qué configura IFS en \777?
sschober
1

Soy nuevo pero creo que esta es una respuesta; espero que ayude a alguien:

STYLE="$HOME/.fluxbox/styles/"

declare -a array1

LISTING=`find $HOME/.fluxbox/styles/ -print0 -maxdepth 1 -type f`


echo $LISTING
array1=( `echo $LISTING`)
TAR_SOURCE=`echo ${array1[@]}`

#tar czvf ~/FluxieStyles.tgz $TAR_SOURCE

fuente
0

Esto es similar a la versión de Stephan202, pero los archivos (y directorios) se colocan en una matriz todos a la vez. El forciclo aquí es solo para "hacer cosas útiles":

files=(*)                        # put files in current directory into an array
i=0
for file in "${files[@]}"
do
    echo "File ${i}: ${file}"    # do something useful 
    let i++
done

Para obtener un recuento:

echo ${#files[@]}
Dennis Williamson
fuente
0

Pregunta vieja, pero nadie sugirió este método simple, así que pensé que lo haría. Por supuesto, si sus nombres de archivo tienen un ETX, esto no resuelve su problema, pero sospecho que sirve para cualquier escenario del mundo real. Intentar usar null parece contradecir las reglas de manejo de IFS predeterminadas. Sazone a su gusto con opciones de búsqueda y manejo de errores.

savedFS="$IFS"
IFS=$'\x3'
filenames=(`find wherever -printf %p$'\x3'`)
IFS="$savedFS"
Dennis Simpson
fuente
1
¿Qué significa ETX ? Tal vez nombre de archivo EXT ension o tal vez Fin del texto ...
oHo
0

La respuesta de Gordon Davisson es genial para bash. Sin embargo, existe un atajo útil para los usuarios de zsh:

Primero, coloque su cadena en una variable:

A="$(find /tmp -type f -print0)"

A continuación, divida esta variable y guárdela en una matriz:

B=( ${(s/^@/)A} )

Hay un truco: ^@es el carácter NUL. Para hacerlo, debe escribir Ctrl + V seguido de Ctrl + @.

Puede verificar que cada entrada de $ B contenga el valor correcto:

for i in "$B[@]"; echo \"$i\"

Los lectores cuidadosos pueden notar que la llamada al findcomando puede evitarse en la mayoría de los casos usando **sintaxis. Por ejemplo:

B=( /tmp/** )
Jérôme Pouiller
fuente
-1

Bash nunca ha sido bueno manejando nombres de archivos (o cualquier texto en realidad) porque usa espacios como delimitadores de listas.

Recomiendo usar Python con la biblioteca sh en su lugar.

Timmmm
fuente