¿Por qué es un bucle sobre la salida de find una mala práctica?

170

Esta pregunta está inspirada en

¿Por qué usar un bucle de shell para procesar texto se considera una mala práctica?

Veo estas construcciones

for file in `find . -type f -name ...`; do smth with ${file}; done

y

for dir in $(find . -type d -name ...); do smth with ${dir}; done

se usa aquí casi a diario, incluso si algunas personas se toman el tiempo para comentar esas publicaciones explicando por qué se debe evitar este tipo de cosas ...
Ver el número de publicaciones (y el hecho de que a veces esos comentarios simplemente se ignoran) Pensé que bien podría hacer una pregunta:

¿Por qué es findun mal bucle la salida de la práctica incorrecta y cuál es la forma correcta de ejecutar uno o más comandos para cada nombre de archivo / ruta devuelta find?

don_crissti
fuente
12
Creo que esto es algo así como "¡Nunca analice la salida de ls!" - Ciertamente puede hacer cualquiera de ellos de forma individual, pero son más un truco rápido que la calidad de producción. O, más generalmente, definitivamente nunca sea dogmático.
Bruce Ediger el
Esto debería convertirse en una respuesta canónica
Zaid
66
Porque el punto de encontrar es recorrer lo que encuentra.
OrangeDog
2
Un punto auxiliar: es posible que desee enviar la salida a un archivo y luego procesarlo más adelante en el script. De esta manera, la lista de archivos está disponible para su revisión si necesita depurar el script.
user117529

Respuestas:

87

El problema

for f in $(find .)

combina dos cosas incompatibles.

findimprime una lista de rutas de archivo delimitadas por caracteres de nueva línea. Mientras que el operador split + glob que se invoca cuando se deja sin $(find .)comillas en ese contexto de lista lo divide en los caracteres de $IFS(por defecto incluye nueva línea, pero también espacio y tabulación (y NUL in zsh)) y realiza globing en cada palabra resultante (excepto in zsh) (¡e incluso expansión de llaves en ksh93 o derivados de pdksh!).

Incluso si lo haces:

IFS='
' # split on newline only
set -o noglob # disable glob (also disables brace expansion in pdksh
              # but not ksh93)
for f in $(find .) # invoke split+glob

Eso sigue siendo incorrecto ya que el carácter de nueva línea es tan válido como cualquiera en una ruta de archivo. La salida de find -printsimplemente no es procesable de manera confiable (excepto mediante el uso de algún truco complicado, como se muestra aquí ).

Eso también significa que el shell necesita almacenar la salida por findcompleto, y luego dividirlo + glob (lo que implica almacenar esa salida por segunda vez en la memoria) antes de comenzar a recorrer los archivos.

Tenga en cuenta que find . | xargs cmdtiene problemas similares (hay espacios en blanco, nueva línea, comillas simples, comillas dobles y barra diagonal inversa (y con algunas xargimplementaciones, los bytes que no forman parte de caracteres válidos) son un problema)

Alternativas más correctas

La única forma de usar un forbucle en la salida de findsería usar zshese soporte IFS=$'\0'y:

IFS=$'\0'
for f in $(find . -print0)

(sustituir -print0con -exec printf '%s\0' {} +de findimplementaciones que no soportan el no estándar (pero bastante común hoy en día) -print0).

Aquí, la forma correcta y portátil es usar -exec:

find . -exec something with {} \;

O si somethingpuede tomar más de un argumento:

find . -exec something with {} +

Si necesita que esa lista de archivos sea manejada por un shell:

find . -exec sh -c '
  for file do
    something < "$file"
  done' find-sh {} +

(cuidado, puede comenzar más de uno sh).

En algunos sistemas, puede usar:

find . -print0 | xargs -r0 something with

aunque eso tiene poca ventaja sobre la sintaxis estándar y significa something's stdines la tubería o /dev/null.

Una razón por la que es posible que desee utilizar esa podría ser la -Popción de GNU xargspara el procesamiento paralelo. El stdinproblema también se puede solucionar con GNU xargscon la -aopción con shells que admiten la sustitución del proceso:

xargs -r0n 20 -P 4 -a <(find . -print0) something

por ejemplo, ejecutar hasta 4 invocaciones concurrentes de somethingcada una tomando 20 argumentos de archivo.

Con zsho bash, otra forma de recorrer la salida de find -print0es con:

while IFS= read -rd '' file <&3; do
  something "$file" 3<&-
done 3< <(find . -print0)

read -d '' lee registros delimitados por NUL en lugar de registros delimitados por nueva línea.

bash-4.4y superior también puede almacenar archivos devueltos por find -print0en una matriz con:

readarray -td '' files < <(find . -print0)

El zshequivalente (que tiene la ventaja de preservar findel estado de salida):

files=(${(0)"$(find . -print0)"})

Con zsh, puede traducir la mayoría de las findexpresiones a una combinación de globbing recursivo con calificadores glob. Por ejemplo, recorrer find . -name '*.txt' -type f -mtime -1sería:

for file (./**/*.txt(ND.m-1)) cmd $file

O

for file (**/*.txt(ND.m-1)) cmd -- $file

(tenga cuidado con la necesidad de --como con **/*, las rutas de archivos no comienzan con ./, por lo que pueden comenzar con, -por ejemplo).

ksh93y bashfinalmente agregó soporte para **/(aunque no más formas avanzadas de globbing recursivo), pero aún no los calificadores glob que hacen que el uso de **muy limitado allí. También tenga en cuenta que bashantes de 4.3 sigue los enlaces simbólicos al descender el árbol de directorios.

Al igual que para recorrer $(find .), eso también significa almacenar toda la lista de archivos en la memoria 1 . Sin embargo, puede ser deseable en algunos casos cuando no desea que sus acciones en los archivos influyan en la búsqueda de archivos (como cuando agrega más archivos que podrían terminar siendo encontrados).

Otras consideraciones de fiabilidad / seguridad

Condiciones de carrera

Ahora, si hablamos de confiabilidad, tenemos que mencionar las condiciones de carrera entre el tiempo find/ zshencuentra un archivo y verifica que cumpla con los criterios y el tiempo que se está utilizando ( carrera TOCTOU ).

Incluso al descender un árbol de directorios, uno debe asegurarse de no seguir enlaces simbólicos y hacerlo sin la carrera TOCTOU. find(GNU findal menos) lo hace abriendo los directorios openat()con los O_NOFOLLOWindicadores correctos (donde sea compatible) y manteniendo abierto un descriptor de archivo para cada directorio, zsh/ bash/ kshno lo haga. Entonces, ante la posibilidad de que un atacante pueda reemplazar un directorio con un enlace simbólico en el momento adecuado, podría terminar descendiendo el directorio incorrecto.

Incluso si finddesciende el directorio correctamente, con -exec cmd {} \;y aún más con -exec cmd {} +, una vez que cmdse ejecuta, por ejemplo, cmd ./foo/baro cmd ./foo/bar ./foo/bar/bazcuando cmdse utiliza ./foo/bar, los atributos de barya no pueden cumplir con los criterios coincidentes find, pero aún peor, ./foopueden haber sido reemplazado por un enlace simbólico a otro lugar (y la ventana de la carrera se hace mucho más grande con -exec {} +donde findespera tener suficientes archivos para llamar cmd).

Algunas findimplementaciones tienen un -execdirpredicado (aún no estándar) para aliviar el segundo problema.

Con:

find . -execdir cmd -- {} \;

find chdir()s en el directorio principal del archivo antes de ejecutarlo cmd. En lugar de llamar cmd -- ./foo/bar, llama cmd -- ./bar( cmd -- barcon algunas implementaciones, de ahí la --), por lo que ./foose evita el problema de cambiar a un enlace simbólico. Eso hace que el uso de comandos sea rmmás seguro (aún podría eliminar un archivo diferente, pero no un archivo en un directorio diferente), pero no comandos que pueden modificar los archivos a menos que hayan sido diseñados para no seguir enlaces simbólicos.

-execdir cmd -- {} +a veces también funciona, pero con varias implementaciones, incluidas algunas versiones de GNU find, es equivalente a -execdir cmd -- {} \;.

-execdir También tiene la ventaja de solucionar algunos de los problemas asociados con los árboles de directorios demasiado profundos.

En:

find . -exec cmd {} \;

el tamaño de la ruta dada cmdcrecerá con la profundidad del directorio en el que se encuentra el archivo. Si ese tamaño se hace mayor que PATH_MAX(algo así como 4k en Linux), cualquier llamada al sistema que lo cmdhaga en esa ruta fallará con un ENAMETOOLONGerror.

Con -execdir, solo ./se pasa el nombre del archivo (posiblemente con el prefijo ) cmd. Los nombres de los archivos en la mayoría de los sistemas de archivos tienen un límite mucho menor ( NAME_MAX) que PATH_MAX, por lo que ENAMETOOLONGes menos probable que se encuentre el error.

Bytes vs caracteres

Además, a menudo se pasa por alto al considerar la seguridad findy, en general, al manejar los nombres de archivos en general, es el hecho de que en la mayoría de los sistemas tipo Unix, los nombres de archivos son secuencias de bytes (cualquier valor de byte pero 0 en una ruta de archivo, y en la mayoría de los sistemas ( Los basados ​​en ASCII, ignoraremos los raros basados ​​en EBCDIC por ahora) 0x2f es el delimitador de ruta).

Depende de las aplicaciones decidir si quieren considerar esos bytes como texto. Y generalmente lo hacen, pero generalmente la traducción de bytes a caracteres se realiza en función de la configuración regional del usuario, en función del entorno.

Lo que eso significa es que un nombre de archivo dado puede tener una representación de texto diferente según la configuración regional. Por ejemplo, la secuencia de bytes 63 f4 74 e9 2e 74 78 74sería côté.txtpara una aplicación que interpreta ese nombre de archivo en una configuración regional donde el conjunto de caracteres es ISO-8859-1, y cєtщ.txten una configuración regional donde el conjunto de caracteres es IS0-8859-5.

Peor. En una ubicación donde el juego de caracteres es UTF-8 (la norma hoy en día), 63 f4 74 e9 2e 74 78 74 simplemente no se pudo asignar a los personajes.

findes una de esas aplicaciones que considera los nombres de archivo como texto para sus -name/ -pathpredicados (y más, como -inameo -regexcon algunas implementaciones).

Lo que eso significa es que, por ejemplo, con varias findimplementaciones (incluida GNU find).

find . -name '*.txt'

no encontraría nuestro 63 f4 74 e9 2e 74 78 74archivo arriba cuando se llama en un entorno local UTF-8 ya *que (que coincide con 0 o más caracteres , no bytes) no podría coincidir con aquellos que no son caracteres.

LC_ALL=C find... solucionaría el problema ya que la configuración regional de C implica un byte por carácter y (en general) garantiza que todos los valores de byte se correlacionan con un carácter (aunque posiblemente sean indefinidos para algunos valores de byte).

Ahora, cuando se trata de recorrer esos nombres de archivo desde un shell, ese byte vs carácter también puede convertirse en un problema. Por lo general, vemos 4 tipos principales de conchas en ese sentido:

  1. Los que todavía no son conscientes de varios bytes, como dash. Para ellos, un byte se asigna a un personaje. Por ejemplo, en UTF-8, côtétiene 4 caracteres, pero 6 bytes. En un entorno local donde UTF-8 es el juego de caracteres, en

    find . -name '????' -exec dash -c '
      name=${1##*/}; echo "${#name}"' sh {} \;
    

    findencontrará con éxito los archivos cuyo nombre consta de 4 caracteres codificados en UTF-8, pero dashinformará longitudes que oscilan entre 4 y 24.

  2. yash: lo contrario. Solo trata con personajes . Toda la entrada que toma se traduce internamente a caracteres. Es el shell más consistente, pero también significa que no puede hacer frente a secuencias de bytes arbitrarias (aquellas que no se traducen en caracteres válidos). Incluso en la configuración regional C, no puede hacer frente a los valores de bytes por encima de 0x7f.

    find . -exec yash -c 'echo "$1"' sh {} \;
    

    en un entorno local UTF-8 fallará en nuestro ISO-8859-1 côté.txtde antes, por ejemplo.

  3. Aquellos como basho zshdonde el soporte de múltiples bytes se ha agregado progresivamente. Esos volverán a considerar los bytes que no se pueden asignar a los caracteres como si fueran caracteres. Todavía tienen algunos errores aquí y allá, especialmente con caracteres de varios bytes menos comunes como GBK o BIG5-HKSCS (aquellos que son bastante desagradables ya que muchos de sus caracteres de varios bytes contienen bytes en el rango de 0-127 (como los caracteres ASCII) )

  4. Aquellos como el shde FreeBSD (al menos 11) o mksh -o utf8-modeque admiten múltiples bytes, pero solo para UTF-8.

Notas

1 Para completar, podríamos mencionar una forma hacky zshde recorrer los archivos usando el engrosamiento recursivo sin almacenar toda la lista en la memoria:

process() {
  something with $REPLY
  false
}
: **/*(ND.m-1+process)

+cmdes un calificador global que llama cmd(generalmente una función) con la ruta actual del archivo $REPLY. La función devuelve verdadero o falso para decidir si el archivo debe seleccionarse (y también puede modificar $REPLYo devolver varios archivos en una $replymatriz). Aquí hacemos el procesamiento en esa función y devolvemos false para que el archivo no esté seleccionado.

Stéphane Chazelas
fuente
Si zsh y bash están disponibles, puede que sea ​​mejor usar construcciones globles y shell en lugar de tratar de contorsionarse findpara comportarse de manera segura. Globbing es seguro por defecto mientras que find es inseguro por defecto.
Kevin
@ Kevin, ver edición.
Stéphane Chazelas
182

¿Por qué es findun mal bucle la salida de la práctica?

La respuesta simple es:

Porque los nombres de archivo pueden contener cualquier carácter.

Por lo tanto, no hay caracteres imprimibles que pueda usar de manera confiable para delimitar nombres de archivos.


Las líneas nuevas a menudo se usan (incorrectamente) para delimitar nombres de archivos, porque es inusual incluir caracteres de líneas nuevas en los nombres de archivos.

Sin embargo, si construye su software en base a suposiciones arbitrarias, en el mejor de los casos simplemente no puede manejar casos inusuales y, en el peor de los casos, se abre a exploits maliciosos que delatan el control de su sistema. Entonces es una cuestión de robustez y seguridad.

Si puede escribir software de dos maneras diferentes, y una de ellas maneja casos extremos (entradas inusuales) correctamente, pero la otra es más fácil de leer, podría argumentar que existe una compensación. (No lo haría. Prefiero el código correcto).

Sin embargo, si la versión correcta y robusta del código también es fácil de leer, no hay excusa para escribir código que falla en casos extremos. Este es el caso findy la necesidad de ejecutar un comando en cada archivo encontrado.


Seamos más específicos: en un sistema UNIX o Linux, los nombres de archivo pueden contener cualquier carácter excepto un /(que se usa como un separador de componentes de ruta), y no pueden contener un byte nulo.

Por lo tanto, un byte nulo es la única forma correcta de delimitar nombres de archivo.


Dado que GNU findincluye un -print0primario que usará un byte nulo para delimitar los nombres de archivo que imprime, GNU find puede usarse de manera segura con GNU xargsy su -0bandera (y -rbandera) para manejar la salida de find:

find ... -print0 | xargs -r0 ...

Sin embargo, no hay una buena razón para usar este formulario porque:

  1. Agrega una dependencia en findutils de GNU que no necesita estar allí, y
  2. findestá diseñado para poder ejecutar comandos en los archivos que encuentra.

Además, GNU xargsrequiere -0y -r, mientras que FreeBSD xargssolo requiere -0(y no tiene -ropción), y algunos xargsno son compatibles -0en absoluto. Por lo tanto, es mejor atenerse a las características POSIX de find(consulte la siguiente sección) y omitir xargs.

En cuanto al punto 2 find, la capacidad de ejecutar comandos en los archivos que encuentra, creo que Mike Loukides lo dijo mejor:

findEl negocio es evaluar expresiones, no localizar archivos. Sí, findciertamente localiza archivos; pero eso es realmente solo un efecto secundario.

--Unix Power Tools


POSIX usos especificados de find

¿Cuál es la forma correcta de ejecutar uno o más comandos para cada uno de findlos resultados?

Para ejecutar un solo comando para cada archivo encontrado, use:

find dirname ... -exec somecommand {} \;

Para ejecutar varios comandos en secuencia para cada archivo encontrado, donde el segundo comando solo debe ejecutarse si el primer comando tiene éxito, use:

find dirname ... -exec somecommand {} \; -exec someothercommand {} \;

Para ejecutar un solo comando en varios archivos a la vez:

find dirname ... -exec somecommand {} +

find en combinación con sh

Si necesita usar funciones de shell en el comando, como redirigir la salida o quitar una extensión del nombre del archivo o algo similar, puede hacer uso de la sh -cconstrucción. Debes saber algunas cosas sobre esto:

  • Nunca incrustar {}directamente en el shcódigo. Esto permite la ejecución de código arbitrario a partir de nombres de archivos creados con fines malintencionados. Además, POSIX ni siquiera especifica que funcionará en absoluto. (Ver siguiente punto)

  • No lo use {}varias veces, ni lo use como parte de un argumento más largo. Esto no es portátil. Por ejemplo, no hagas esto:

    find ... -exec cp {} somedir/{}.bak \;

    Para citar las especificaciones POSIX parafind :

    Si una cadena de argumentos o nombre de utilidad contiene los dos caracteres "{}", pero no solo los dos caracteres "{}", se define la implementación si find reemplaza esos dos caracteres o si usa la cadena sin cambios.

    ... Si hay más de un argumento que contiene los dos caracteres "{}", el comportamiento no está especificado.

  • Los argumentos que siguen a la cadena de comando del shell que se pasa a la -copción se establecen en los parámetros posicionales del shell, comenzando por$0 . Que no empiezan con $1.

    Por esta razón, es bueno incluir un $0valor "ficticio" , como find-sh, que se utilizará para informar errores desde el shell generado. Además, esto permite el uso de construcciones como "$@"cuando se pasan varios archivos al shell, mientras que omitir un valor $0significaría que el primer archivo pasado se establecería $0y, por lo tanto, no se incluiría en él "$@".


Para ejecutar un solo comando de shell por archivo, use:

find dirname ... -exec sh -c 'somecommandwith "$1"' find-sh {} \;

Sin embargo, generalmente dará un mejor rendimiento para manejar los archivos en un bucle de shell para que no genere un shell por cada archivo encontrado:

find dirname ... -exec sh -c 'for f do somecommandwith "$f"; done' find-sh {} +

(Tenga en cuenta que for f does equivalente for f in "$@"; doy maneja cada uno de los parámetros posicionales a su vez; en otras palabras, utiliza cada uno de los archivos encontrados find, independientemente de los caracteres especiales en sus nombres).


Otros ejemplos de finduso correcto :

(Nota: siéntase libre de ampliar esta lista).

Comodín
fuente
55
Hay un caso en el que no conozco una alternativa a findla salida del análisis , donde necesita ejecutar comandos en el shell actual (por ejemplo, porque desea establecer variables) para cada archivo. En este caso, while IFS= read -r -u3 -d '' file; do ... done 3< <(find ... -print0)es el mejor idioma que conozco. Notas: <( )no es portátil; use bash o zsh. Además, el -u3y 3<está allí en caso de que algo dentro del bucle intente leer stdin.
Gordon Davisson el
1
@GordonDavisson, tal vez, pero ¿qué tiene que establecer las variables de ? Yo diría que lo que sea se debe manejar dentro de la find ... -execllamada. O simplemente use un globo de concha, si manejará su caso de uso.
Comodín el
1
A menudo quiero imprimir un resumen después de procesar archivos ("2 convertidos, 3 omitidos, los siguientes archivos tenían errores: ..."), y esos recuentos / listas tienen que acumularse en las variables de shell. Además, hay situaciones en las que quiero crear una matriz de nombres de archivos para poder hacer cosas más complejas que iterar en orden (en ese caso, es filelist=(); while ... do filelist+=("$file"); done ...).
Gordon Davisson el
3
Tu respuesta es correcta. Sin embargo, no me gusta el dogma. Aunque lo sé mejor, hay muchos casos de uso (especialmente interactivos) en los que es seguro y más fácil escribir en bucle sobre la findsalida o incluso peor ls. Estoy haciendo esto a diario sin problemas. Conozco las opciones -print0, --null, -z o -0 de todo tipo de herramientas. Pero no perdería el tiempo para usarlos en mi indicador de shell interactivo a menos que realmente sea necesario. Esto también podría observarse en su respuesta.
rudimeier
16
@rudimeier, el argumento sobre el dogma contra las mejores prácticas ya se ha hecho hasta la muerte . No interesado. Si lo usa de forma interactiva y funciona, bien, bien por usted, pero no voy a promover que lo haga. El porcentaje de autores de guiones que se molestan en aprender qué es un código robusto y luego solo lo hacen al escribir guiones de producción, en lugar de simplemente hacer lo que están acostumbrados a hacer de manera interactiva, es extremadamente mínimo. El manejo es promover las mejores prácticas todo el tiempo. La gente necesita aprender que hay una forma correcta de hacer las cosas.
Comodín el
10

Esta respuesta es para conjuntos de resultados muy grandes y se refiere principalmente al rendimiento, por ejemplo, al obtener una lista de archivos en una red lenta. Para pequeñas cantidades de archivos (digamos unos 100 o quizás 1000 en un disco local), la mayor parte de esto es discutible.

Paralelismo y uso de memoria

Aparte de las otras respuestas dadas, relacionadas con problemas de separación y demás, hay otro problema con

for file in `find . -type f -name ...`; do smth with ${file}; done

La parte dentro de los backticks debe evaluarse completamente primero, antes de dividirse en los saltos de línea. Esto significa que si obtiene una gran cantidad de archivos, puede ahogarse en cualquier límite de tamaño que exista en los diversos componentes; puede quedarse sin memoria si no hay límites; y, en cualquier caso, debe esperar hasta que toda la lista haya sido generada findy luego analizada forantes de ejecutar su primera smth.

La forma preferida de Unix es trabajar con tuberías, que se ejecutan inherentemente en paralelo, y que en general no necesitan búferes arbitrariamente enormes. Eso significa: preferiría findque se ejecute en paralelo a su smth, y solo mantenga el nombre del archivo actual en la RAM mientras se lo entrega smth.

Una solución al menos parcialmente aceptable para eso es la mencionada anteriormente find -exec smth. Elimina la necesidad de mantener todos los nombres de archivo en la memoria y funciona bien en paralelo. Desafortunadamente, también inicia un smthproceso por archivo. Si smthsolo puede funcionar en un archivo, así es como debe ser.

Si es posible, la solución óptima sería find -print0 | smth, con smthser capaz de procesar los nombres de archivo en su STDIN. Entonces solo tiene un smthproceso, sin importar cuántos archivos haya, y necesita almacenar solo una pequeña cantidad de bytes (lo que sea que esté sucediendo en el almacenamiento intermedio de tubería intrínseca) entre los dos procesos. Por supuesto, esto es bastante poco realista si smthes un comando estándar de Unix / POSIX, pero podría ser un enfoque si lo está escribiendo usted mismo.

Si eso no es posible, entonces find -print0 | xargs -0 smthes, probablemente, una de las mejores soluciones. Como @ dave_thompson_085 mencionó en los comentarios, xargsdivide los argumentos en varias ejecuciones de smthcuándo se alcanzan los límites del sistema (por defecto, en el rango de 128 KB o cualquier límite impuesto por execel sistema), y tiene opciones para influir en cuántos los archivos se entregan a una llamada de smth, por lo tanto, encontrar un equilibrio entre el número de smthprocesos y el retraso inicial.

EDITAR: eliminó las nociones de "mejor"; es difícil decir si surgirá algo mejor. ;)

AnoE
fuente
find ... -exec smth {} +Es la solución.
Comodín el
find -print0 | xargs smthno funciona en absoluto, pero find -print0 | xargs -0 smth(nota -0) o find | xargs smthsi los nombres de archivo no tienen comillas en espacios en blanco o la barra invertida se ejecuta uno smthcon tantos nombres de archivos como estén disponibles y caben en una lista de argumentos ; Si excede los maxargs, se ejecuta smthtantas veces como sea necesario para manejar todos los argumentos dados (sin límite). Puede establecer 'fragmentos' más pequeños (por lo tanto, paralelismo algo anterior) con -L/--max-lines -n/--max-args -s/--max-chars.
dave_thompson_085
4

Una razón es que el espacio en blanco arroja una llave en las obras, haciendo que el archivo 'foo bar' sea evaluado como 'foo' y 'bar'.

$ ls -l
-rw-rw-r-- 1 ec2-user ec2-user 0 Nov  7 18:24 foo bar
$ for file in `find . -type f` ; do echo filename $file ; done
filename ./foo
filename bar
$

Funciona bien si se usa -exec en su lugar

$ find . -type f -exec echo filename {} \;
filename ./foo bar
$ find . -type f -exec stat {} \;
  File: ‘./foo bar’
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: ca01h/51713d    Inode: 9109        Links: 1
Access: (0664/-rw-rw-r--)  Uid: (  500/ec2-user)   Gid: (  500/ec2-user)
Access: 2016-11-07 18:24:42.027554752 +0000
Modify: 2016-11-07 18:24:42.027554752 +0000
Change: 2016-11-07 18:24:42.027554752 +0000
 Birth: -
$
Steve
fuente
Especialmente en el caso de findque haya una opción para ejecutar un comando en cada archivo, es fácilmente la mejor opción.
Centimane
1
Considere también -exec ... {} \;versus-exec ... {} +
thrig
1
si se utiliza for file in "$(find . -type f)" , y echo "${file}"entonces funciona incluso con espacios en blanco, otros caracteres especiales, supongo causa más problemas, aunque
mazs
99
@mazs: no, las citas no hacen lo que piensas. En un directorio con varios archivos, pruebe for file in "$(find . -type f)";do printf '%s %s\n' name: "${file}";donecuál debe (según usted) imprimir cada nombre de archivo en una línea separada precedida por name:. No lo hace.
don_crissti
2

Debido a que la salida de cualquier comando es una sola cadena, pero su bucle necesita una matriz de cadenas para recorrer. La razón por la que "funciona" es que los proyectiles dividen la cadena en el espacio en blanco.

En segundo lugar, a menos que necesite una característica particular de find, tenga en cuenta que su caparazón probablemente ya puede expandir un patrón de globo recursivo por sí mismo y, lo que es más importante, que se expandirá a una matriz adecuada.

Ejemplo de Bash:

shopt -s nullglob globstar
for i in **
do
    echo «"$i"»
done

Lo mismo en pescado:

for i in **
    echo «$i»
end

Si necesita las características de find, asegúrese de dividir solo en NUL (como el find -print0 | xargs -r0idioma).

Fish puede iterar la salida delimitada por NUL. Entonces este no es realmente malo:

find -print0 | while read -z i
    echo «$i»
end

Como último pequeño inconveniente, en muchos shells (no Fish, por supuesto), hacer un bucle sobre la salida del comando hará que el cuerpo del bucle sea un subshell (lo que significa que no puede establecer una variable de ninguna manera que sea visible después de que finalice el bucle), que es nunca lo que quieres

usuario2394284
fuente
@don_crissti Precisamente. No generalmente funciona. Estaba tratando de ser sarcástico al decir que "funciona" (con comillas).
user2394284
Tenga en cuenta que el globbing recursivo se originó a zshprincipios de los 90 (aunque lo necesitaría **/*allí). fishAl igual que las implementaciones anteriores de la característica equivalente de bash, sigue enlaces simbólicos al descender el árbol de directorios. Consulte El resultado de ls *, ls ** y ls *** para conocer las diferencias entre las implementaciones.
Stéphane Chazelas
1

Recorrer el resultado de find no es una mala práctica: lo que es una mala práctica (en esta y en todas las situaciones) es asumir que su entrada es un formato particular en lugar de saber (probar y confirmar) que es un formato particular.

tldr / cbf: find | parallel stuff

Jan Kyu Peblik
fuente