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 find
un 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
?
Respuestas:
El problema
combina dos cosas incompatibles.
find
imprime 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 inzsh
)) y realiza globing en cada palabra resultante (excepto inzsh
) (¡e incluso expansión de llaves en ksh93 o derivados de pdksh!).Incluso si lo haces:
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 -print
simplemente 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
find
completo, 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 cmd
tiene problemas similares (hay espacios en blanco, nueva línea, comillas simples, comillas dobles y barra diagonal inversa (y con algunasxarg
implementaciones, los bytes que no forman parte de caracteres válidos) son un problema)Alternativas más correctas
La única forma de usar un
for
bucle en la salida defind
sería usarzsh
ese soporteIFS=$'\0'
y:(sustituir
-print0
con-exec printf '%s\0' {} +
defind
implementaciones 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
:O si
something
puede tomar más de un argumento:Si necesita que esa lista de archivos sea manejada por un shell:
(cuidado, puede comenzar más de uno
sh
).En algunos sistemas, puede usar:
aunque eso tiene poca ventaja sobre la sintaxis estándar y significa
something
'sstdin
es la tubería o/dev/null
.Una razón por la que es posible que desee utilizar esa podría ser la
-P
opción de GNUxargs
para el procesamiento paralelo. Elstdin
problema también se puede solucionar con GNUxargs
con la-a
opción con shells que admiten la sustitución del proceso:por ejemplo, ejecutar hasta 4 invocaciones concurrentes de
something
cada una tomando 20 argumentos de archivo.Con
zsh
obash
, otra forma de recorrer la salida defind -print0
es con:read -d ''
lee registros delimitados por NUL en lugar de registros delimitados por nueva línea.bash-4.4
y superior también puede almacenar archivos devueltos porfind -print0
en una matriz con:El
zsh
equivalente (que tiene la ventaja de preservarfind
el estado de salida):Con
zsh
, puede traducir la mayoría de lasfind
expresiones a una combinación de globbing recursivo con calificadores glob. Por ejemplo, recorrerfind . -name '*.txt' -type f -mtime -1
sería:O
(tenga cuidado con la necesidad de
--
como con**/*
, las rutas de archivos no comienzan con./
, por lo que pueden comenzar con,-
por ejemplo).ksh93
ybash
finalmente 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 quebash
antes 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
/zsh
encuentra 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
(GNUfind
al menos) lo hace abriendo los directoriosopenat()
con losO_NOFOLLOW
indicadores correctos (donde sea compatible) y manteniendo abierto un descriptor de archivo para cada directorio,zsh
/bash
/ksh
no 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
find
desciende el directorio correctamente, con-exec cmd {} \;
y aún más con-exec cmd {} +
, una vez quecmd
se ejecuta, por ejemplo,cmd ./foo/bar
ocmd ./foo/bar ./foo/bar/baz
cuandocmd
se utiliza./foo/bar
, los atributos debar
ya no pueden cumplir con los criterios coincidentesfind
, pero aún peor,./foo
pueden 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 {} +
dondefind
espera tener suficientes archivos para llamarcmd
).Algunas
find
implementaciones tienen un-execdir
predicado (aún no estándar) para aliviar el segundo problema.Con:
find
chdir()
s en el directorio principal del archivo antes de ejecutarlocmd
. En lugar de llamarcmd -- ./foo/bar
, llamacmd -- ./bar
(cmd -- bar
con algunas implementaciones, de ahí la--
), por lo que./foo
se evita el problema de cambiar a un enlace simbólico. Eso hace que el uso de comandos searm
má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 GNUfind
, 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:
el tamaño de la ruta dada
cmd
crecerá con la profundidad del directorio en el que se encuentra el archivo. Si ese tamaño se hace mayor quePATH_MAX
(algo así como 4k en Linux), cualquier llamada al sistema que locmd
haga en esa ruta fallará con unENAMETOOLONG
error.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
) quePATH_MAX
, por lo queENAMETOOLONG
es menos probable que se encuentre el error.Bytes vs caracteres
Además, a menudo se pasa por alto al considerar la seguridad
find
y, 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 74
seríacôté.txt
para una aplicación que interpreta ese nombre de archivo en una configuración regional donde el conjunto de caracteres es ISO-8859-1, ycєtщ.txt
en 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.
find
es una de esas aplicaciones que considera los nombres de archivo como texto para sus-name
/-path
predicados (y más, como-iname
o-regex
con algunas implementaciones).Lo que eso significa es que, por ejemplo, con varias
find
implementaciones (incluida GNUfind
).no encontraría nuestro
63 f4 74 e9 2e 74 78 74
archivo 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:
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, enfind
encontrará con éxito los archivos cuyo nombre consta de 4 caracteres codificados en UTF-8, perodash
informará longitudes que oscilan entre 4 y 24.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.en un entorno local UTF-8 fallará en nuestro ISO-8859-1
côté.txt
de antes, por ejemplo.Aquellos como
bash
ozsh
donde 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) )Aquellos como el
sh
de FreeBSD (al menos 11) omksh -o utf8-mode
que admiten múltiples bytes, pero solo para UTF-8.Notas
1 Para completar, podríamos mencionar una forma hacky
zsh
de recorrer los archivos usando el engrosamiento recursivo sin almacenar toda la lista en la memoria:+cmd
es un calificador global que llamacmd
(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$REPLY
o devolver varios archivos en una$reply
matriz). Aquí hacemos el procesamiento en esa función y devolvemos false para que el archivo no esté seleccionado.fuente
find
para comportarse de manera segura. Globbing es seguro por defecto mientras que find es inseguro por defecto.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
find
y 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
find
incluye un-print0
primario que usará un byte nulo para delimitar los nombres de archivo que imprime, GNUfind
puede usarse de manera segura con GNUxargs
y su-0
bandera (y-r
bandera) para manejar la salida defind
:Sin embargo, no hay una buena razón para usar este formulario porque:
find
está diseñado para poder ejecutar comandos en los archivos que encuentra.Además, GNU
xargs
requiere-0
y-r
, mientras que FreeBSDxargs
solo requiere-0
(y no tiene-r
opción), y algunosxargs
no son compatibles-0
en absoluto. Por lo tanto, es mejor atenerse a las características POSIX defind
(consulte la siguiente sección) y omitirxargs
.En cuanto al punto 2
find
, la capacidad de ejecutar comandos en los archivos que encuentra, creo que Mike Loukides lo dijo mejor:POSIX usos especificados de
find
Para ejecutar un solo comando para cada archivo encontrado, use:
Para ejecutar varios comandos en secuencia para cada archivo encontrado, donde el segundo comando solo debe ejecutarse si el primer comando tiene éxito, use:
Para ejecutar un solo comando en varios archivos a la vez:
find
en combinación consh
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 -c
construcción. Debes saber algunas cosas sobre esto:Nunca incrustar
{}
directamente en elsh
có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 para
find
:Los argumentos que siguen a la cadena de comando del shell que se pasa a la
-c
opció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
$0
valor "ficticio" , comofind-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$0
significaría que el primer archivo pasado se establecería$0
y, por lo tanto, no se incluiría en él"$@"
.Para ejecutar un solo comando de shell por archivo, use:
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:
(Tenga en cuenta que
for f do
es equivalentefor f in "$@"; do
y maneja cada uno de los parámetros posicionales a su vez; en otras palabras, utiliza cada uno de los archivos encontradosfind
, independientemente de los caracteres especiales en sus nombres).Otros ejemplos de
find
uso correcto :(Nota: siéntase libre de ampliar esta lista).
fuente
find
la 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-u3
y3<
está allí en caso de que algo dentro del bucle intente leer stdin.find ... -exec
llamada. O simplemente use un globo de concha, si manejará su caso de uso.filelist=(); while ... do filelist+=("$file"); done ...
).find
salida o incluso peorls
. 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.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
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
find
y luego analizadafor
antes de ejecutar su primerasmth
.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
find
que se ejecute en paralelo a susmth
, y solo mantenga el nombre del archivo actual en la RAM mientras se lo entregasmth
.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 unsmth
proceso por archivo. Sismth
solo puede funcionar en un archivo, así es como debe ser.Si es posible, la solución óptima sería
find -print0 | smth
, consmth
ser capaz de procesar los nombres de archivo en su STDIN. Entonces solo tiene unsmth
proceso, 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 sismth
es 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 smth
es, probablemente, una de las mejores soluciones. Como @ dave_thompson_085 mencionó en los comentarios,xargs
divide los argumentos en varias ejecuciones desmth
cuándo se alcanzan los límites del sistema (por defecto, en el rango de 128 KB o cualquier límite impuesto porexec
el sistema), y tiene opciones para influir en cuántos los archivos se entregan a una llamada desmth
, por lo tanto, encontrar un equilibrio entre el número desmth
procesos y el retraso inicial.EDITAR: eliminó las nociones de "mejor"; es difícil decir si surgirá algo mejor. ;)
fuente
find ... -exec smth {} +
Es la solución.find -print0 | xargs smth
no funciona en absoluto, perofind -print0 | xargs -0 smth
(nota-0
) ofind | xargs smth
si los nombres de archivo no tienen comillas en espacios en blanco o la barra invertida se ejecuta unosmth
con tantos nombres de archivos como estén disponibles y caben en una lista de argumentos ; Si excede los maxargs, se ejecutasmth
tantas 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
.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'.
Funciona bien si se usa -exec en su lugar
fuente
find
que haya una opción para ejecutar un comando en cada archivo, es fácilmente la mejor opción.-exec ... {} \;
versus-exec ... {} +
for file in "$(find . -type f)"
, yecho "${file}"
entonces funciona incluso con espacios en blanco, otros caracteres especiales, supongo causa más problemas, aunquefor file in "$(find . -type f)";do printf '%s %s\n' name: "${file}";done
cuál debe (según usted) imprimir cada nombre de archivo en una línea separada precedida porname:
. No lo hace.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:
Lo mismo en pescado:
Si necesita las características de
find
, asegúrese de dividir solo en NUL (como elfind -print0 | xargs -r0
idioma).Fish puede iterar la salida delimitada por NUL. Entonces este no es realmente malo:
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
fuente
zsh
principios de los 90 (aunque lo necesitaría**/*
allí).fish
Al 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.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
fuente