El problema
for f in $(find .)
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 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 -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 algunas xarg
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 de find
sería usar zsh
ese soporte IFS=$'\0'
y:
IFS=$'\0'
for f in $(find . -print0)
(sustituir -print0
con -exec printf '%s\0' {} +
de find
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
:
find . -exec something with {} \;
O si something
puede 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 stdin
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 GNU xargs
para el procesamiento paralelo. El stdin
problema también se puede solucionar con GNU xargs
con la -a
opció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 something
cada una tomando 20 argumentos de archivo.
Con zsh
o bash
, otra forma de recorrer la salida de find -print0
es 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.4
y superior también puede almacenar archivos devueltos por find -print0
en una matriz con:
readarray -td '' files < <(find . -print0)
El zsh
equivalente (que tiene la ventaja de preservar find
el estado de salida):
files=(${(0)"$(find . -print0)"})
Con zsh
, puede traducir la mayoría de las find
expresiones a una combinación de globbing recursivo con calificadores glob. Por ejemplo, recorrer find . -name '*.txt' -type f -mtime -1
serí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).
ksh93
y bash
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 que bash
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
(GNU find
al menos) lo hace abriendo los directorios openat()
con los O_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 que cmd
se ejecuta, por ejemplo, cmd ./foo/bar
o cmd ./foo/bar ./foo/bar/baz
cuando cmd
se utiliza ./foo/bar
, los atributos de bar
ya no pueden cumplir con los criterios coincidentes find
, 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 {} +
donde find
espera tener suficientes archivos para llamar cmd
).
Algunas find
implementaciones tienen un -execdir
predicado (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 -- 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 sea rm
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 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 cmd
crecerá 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 cmd
haga en esa ruta fallará con un ENAMETOOLONG
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
) que PATH_MAX
, por lo que ENAMETOOLONG
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ía cô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, y cє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 GNU find
).
find . -name '*.txt'
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, en
find . -name '????' -exec dash -c '
name=${1##*/}; echo "${#name}"' sh {} \;
find
encontrará con éxito los archivos cuyo nombre consta de 4 caracteres codificados en UTF-8, pero dash
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.
find . -exec yash -c 'echo "$1"' sh {} \;
en un entorno local UTF-8 fallará en nuestro ISO-8859-1 côté.txt
de antes, por ejemplo.
Aquellos como bash
o zsh
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) o mksh -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:
process() {
something with $REPLY
false
}
: **/*(ND.m-1+process)
+cmd
es 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 $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.