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:
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.
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.
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) )
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.