¿Cómo puedo almacenar los resultados del comando "buscar" como una matriz en Bash?

92

Estoy tratando de guardar el resultado findcomo matrices. Aquí está mi código:

#!/bin/bash

echo "input : "
read input

echo "searching file with this pattern '${input}' under present directory"
array=`find . -name ${input}`

len=${#array[*]}
echo "found : ${len}"

i=0

while [ $i -lt $len ]
do
echo ${array[$i]}
let i++
done

Obtengo 2 archivos .txt en el directorio actual. Así que espero '2' como resultado de ${len}. Sin embargo, imprime 1. La razón es que toma todos los resultados de findcomo un elemento. ¿Cómo puedo arreglar esto?

PD
: Encontré varias soluciones en StackOverFlow sobre un problema similar. Sin embargo, son un poco diferentes, por lo que no puedo aplicar en mi caso. Necesito almacenar los resultados en una variable antes del ciclo. Gracias de nuevo.

Juneyoung Oh
fuente

Respuestas:

133

Actualización 2020 para usuarios de Linux:

Si usted tiene una versión puesta al día de fiesta (4,4-alfa o mejor), ya que es probable que hacer si se encuentra en Linux, entonces usted debe estar usando la respuesta de Benjamin W. .

Si está en Mac OS, que —la última vez que lo comprobé— todavía usa bash 3.2, o está usando un bash anterior, continúe con la siguiente sección.

Respuesta para bash 4.3 o anterior

Aquí hay una solución para obtener la salida de finduna bashmatriz:

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

Esto es complicado porque, en general, los nombres de archivo pueden tener espacios, nuevas líneas y otros caracteres hostiles al script. La única forma de utilizar findy tener los nombres de los archivos separados de forma segura entre sí es utilizar -print0que imprima los nombres de los archivos separados con un carácter nulo. Esto no sería un gran inconveniente si las funciones readarray/ de bash admitieran mapfilecadenas separadas por nulos, pero no lo hacen. Bash's lo readhace y eso nos lleva al bucle anterior.

[Esta respuesta se escribió originalmente en 2014. Si tiene una versión reciente de bash, consulte la actualización a continuación].

Cómo funciona

  1. La primera línea crea una matriz vacía: array=()

  2. Cada vez que readse ejecuta la instrucción, se lee un nombre de archivo separado por nulos de la entrada estándar. La -ropción dice readque deje los caracteres de barra invertida solos. El -d $'\0'indica readque la entrada estará separada por nulos. Desde omitimos el nombre read, la cáscara pone la entrada en el nombre por defecto: REPLY.

  3. La array+=("$REPLY")declaración agrega el nuevo nombre de archivo a la matriz array.

  4. La línea final combina la redirección y la sustitución de comandos para proporcionar la salida finda la entrada estándar del whilebucle.

¿Por qué utilizar la sustitución de procesos?

Si no usamos la sustitución de procesos, el ciclo podría escribirse como:

array=()
find . -name "${input}" -print0 >tmpfile
while IFS=  read -r -d $'\0'; do
    array+=("$REPLY")
done <tmpfile
rm -f tmpfile

En lo anterior, la salida de findse almacena en un archivo temporal y ese archivo se usa como entrada estándar para el ciclo while. La idea de la sustitución de procesos es hacer innecesarios esos archivos temporales. Entonces, en lugar de que el whilebucle obtenga su stdin tmpfile, podemos hacer que obtenga su stdin <(find . -name ${input} -print0).

La sustitución de procesos es muy útil. En muchos lugares donde un comando quiere leer de un archivo, puede especificar la sustitución del proceso <(...), en lugar de un nombre de archivo. Existe una forma análoga >(...), que se puede usar en lugar de un nombre de archivo donde el comando quiere escribir en el archivo.

Al igual que las matrices, la sustitución de procesos es una característica de bash y otros shells avanzados. No forma parte del estándar POSIX.

Alternativa: lastpipe

Si lo desea, lastpipese puede utilizar en lugar de la sustitución del proceso (punta de sombrero: Caesar ):

set +m
shopt -s lastpipe
array=()
find . -name "${input}" -print0 | while IFS=  read -r -d $'\0'; do array+=("$REPLY"); done; declare -p array

shopt -s lastpipele dice a bash que ejecute el último comando en la tubería en el shell actual (no en el fondo). De esta manera, los arrayrestos existen después de que se completa la canalización. Porque lastpipesolo tiene efecto si el control de trabajos está desactivado, ejecutamos set +m. (En una secuencia de comandos, a diferencia de la línea de comando, el control de trabajos está desactivado de forma predeterminada).

Notas adicionales

El siguiente comando crea una variable de shell, no una matriz de shell:

array=`find . -name "${input}"`

Si quisiera crear una matriz, necesitaría poner parens alrededor de la salida de find. Entonces, ingenuamente, uno podría:

array=(`find . -name "${input}"`)  # don't do this

El problema es que el shell realiza la división de palabras en los resultados de, findpor lo que no se garantiza que los elementos de la matriz sean los que desea.

Actualización 2019

A partir de la versión 4.4-alpha, bash ahora admite una -dopción para que el ciclo anterior ya no sea necesario. En su lugar, se puede utilizar:

mapfile -d $'\0' array < <(find . -name "${input}" -print0)

Para más información sobre esto, por favor ver (y upvote) La respuesta de Benjamin W. .

Juan1024
fuente
1
@JuneyoungOh Me alegro de que haya ayudado. Agregué una sección de sustitución de procesos.
John1024
3
@Rockallite Esa es una buena observación pero incompleta. Si bien es cierto que no nos dividimos en varias palabras, debemos IFS=evitar eliminar los espacios en blanco del comienzo o el final de las líneas de entrada. Puede probar esto fácilmente comparando la salida de read var <<<' abc '; echo ">$var<"con la salida de IFS= read var <<<' abc '; echo ">$var<". En el primer caso, abcse eliminan los espacios antes y después . En este último, no lo son. Los nombres de archivo que comienzan o terminan con espacios en blanco pueden ser inusuales pero, si existen, queremos que se procesen correctamente.
John1024
1
Hola, después de ejecutar su código, obtengo un error de sintaxis del mensaje cerca de un token inesperado <' hecho <<(encuentre aaa / -not -newermt "$ last_build_timestamp_v" -type f -print0) '
Przemysław Sienkiewicz
1
Una nota: el más simple ''se puede usar en lugar de $'\0':n=0; while IFS= read -r -d '' line || [ "$line" ]; do echo "$((++n)):$line"; done < <(printf 'first\nstill first\0second\0third')
glenn jackman
1
@theeagle Supongo que tenías la intención de escribir BLAH=$(find . -name '*.php'). Como se discutió en la respuesta, ese enfoque funcionará en casos limitados, pero no funcionará en general con todos los nombres de archivo y no produce, como esperaba el OP, una matriz .
John1024
35

Bash 4.4 introdujo una -dopción para readarray/ mapfile, por lo que ahora se puede resolver con

readarray -d '' array < <(find . -name "$input" -print0)

para un método que funciona con nombres de archivo arbitrarios, incluidos espacios en blanco, nuevas líneas y caracteres globales. Esto requiere que su findsoporte -print0, como por ejemplo GNU find lo hace.

Del manual (omitiendo otras opciones):

mapfile [-d delim] [array]

-d
El primer carácter de delimse utiliza para terminar cada línea de entrada, en lugar de nueva línea. Si delimes la cadena vacía, mapfileterminará una línea cuando lea un carácter NUL.

Y readarrayes solo un sinónimo de mapfile.

Benjamin W.
fuente
18

Si está usando bash4 o posterior, puede reemplazar su uso de findcon

shopt -s globstar nullglob
array=( **/*"$input"* )

El **patrón habilitado por globstarcoincide con 0 o más directorios, lo que permite que el patrón coincida con una profundidad arbitraria en el directorio actual. Sin elnullglob opción, el patrón (después de la expansión de parámetros) se trata literalmente, por lo que sin coincidencias tendría una matriz con una sola cadena en lugar de una matriz vacía.

Agregue la dotglobopción a la primera línea también si desea recorrer directorios ocultos (como .ssh) y hacer coincidir archivos ocultos (como .bashrc) también.

chepner
fuente
4
Quizás nullglobtambién ...
kojiro
1
Sí, siempre lo olvido.
Chepner
5
Tenga en cuenta que esto no incluirá los archivos y directorios ocultos, a menos que dotglobse establezca (esto puede ser deseado o no, pero también vale la pena mencionarlo).
gniourf_gniourf
10

puedes probar algo como

array=(`find . -type f | sort -r | head -2`)
, y para imprimir los valores de la matriz, puede probar algo como echo "${array[*]}"

Ahmed Al-Haffar
fuente
7
Se interrumpe si hay nombres de archivo con espacios o caracteres globales.
gniourf_gniourf
1

Lo siguiente parece funcionar tanto para Bash como para Z Shell en macOS.

#! /bin/sh

IFS=$'\n'
paths=($(find . -name "foo"))
unset IFS

printf "%s\n" "${paths[@]}"
Sunknudsen
fuente
-1

En bash, $(<any_shell_cmd>)ayuda a ejecutar un comando y capturar la salida. Pasar esto a IFSwith \ncomo delimitador ayuda a convertir eso en una matriz.

IFS='\n' read -r -a txt_files <<< $(find /path/to/dir -name "*.txt")
rashok
fuente
3
Esto obtendrá solo el primer archivo de los resultados finden la matriz.
Benjamin W.
-2

Podrías hacer así:

#!/bin/bash
echo "input : "
read input

echo "searching file with this pattern '${input}' under present directory"
array=(`find . -name '*'${input}'*'`)

for i in "${array[@]}"
do :
    echo $i
done
usuario1357768
fuente
Gracias. mucho. Pero como señaló @anishsane, los espacios vacíos en el nombre del archivo deben considerarse en mi programa. ¡Gracias de todos modos!
Juneyoung Oh
-3

Para mí, esto funcionó bien en cygwin:

declare -a names=$(echo "("; find <path> <other options> -printf '"%p" '; echo ")")
for nm in "${names[@]}"
do
    echo "$nm"
done

Esto funciona con espacios, pero no con comillas dobles (") en los nombres de los directorios (que de todos modos no están permitidos en un entorno Windows).

Tenga cuidado con el espacio en la opción -printf.

R Risack
fuente
3
Roto y peligroso : no acepta comillas y está sujeto a inyección de código arbitrario. NO UTILICE.
gniourf_gniourf
2
Parece que alguien marcó esta publicación para eliminarla. "Está mal" no es una razón para la eliminación en SO. El usuario intentó responder, está en el tema y cumple con los criterios de respuesta. El botón de voto negativo se utiliza para medir la utilidad y la corrección, no el botón de eliminación.
Frambot
3
Como señaló gniourf, no es para entornos donde otros ingresan las opciones en su sistema, por ejemplo, páginas web. Pero no todo el mundo programa para ese entorno. Lo usé para cambiar el nombre de archivos en directorios.
R Risack