Usando un bucle como
for i in `find . -name \*.txt`
se romperá si algunos nombres de archivo tienen espacios en ellos.
¿Qué técnica puedo usar para evitar este problema?
Usando un bucle como
for i in `find . -name \*.txt`
se romperá si algunos nombres de archivo tienen espacios en ellos.
¿Qué técnica puedo usar para evitar este problema?
Respuestas:
Idealmente, no lo hace de esa manera, porque analizar los nombres de archivo correctamente en un script de shell siempre es difícil (arreglarlo para espacios, aún tendrá problemas con otros caracteres incrustados, en particular, nueva línea). Esto incluso aparece como la primera entrada en la página BashPitfalls.
Dicho esto, hay una manera de hacer casi lo que quieres:
oIFS=$IFS
IFS=$'\n'
find . -name '*.txt' | while read -r i; do
# use "$i" with whatever you're doing
done
IFS=$oIFS
Recuerde también citar $i
cuando lo use, para evitar otras cosas que interpreten los espacios más adelante. También recuerde $IFS
retroceder después de usarlo, porque no hacerlo causará errores desconcertantes más tarde.
Esto tiene otra advertencia adjunta: lo que sucede dentro del while
ciclo puede tener lugar en un subshell, dependiendo del shell exacto que esté utilizando, por lo que las configuraciones variables pueden no persistir. La for
versión de bucle evita eso, pero al precio que, incluso si aplica la $IFS
solución para evitar problemas con los espacios, se meterá en problemas si find
devuelve demasiados archivos.
En algún momento, la solución correcta para todo esto se hace en un lenguaje como Perl o Python en lugar de shell.
Úselo find -print0
y canalícelo xargs -0
, o escriba su propio pequeño programa C y canalícelo a su pequeño programa C. Esto es lo que -print0
y -0
se inventó para.
Los scripts de shell no son la mejor manera de manejar nombres de archivos con espacios en ellos: puede hacerlo, pero se vuelve torpe.
Puede establecer el "separador de campo interno" ( IFS
) en algo más que espacio para la división del argumento de bucle, p. Ej.
ORIGIFS=${IFS}
NL='
'
IFS=${NL}
for i in $(find . -name '*.txt'); do
IFS=${ORIGIFS}
#do stuff
done
IFS=${ORIGIFS}
Restablezco IFS
después de su uso en find, principalmente porque se ve bien, creo. No he visto ningún problema al configurarlo en nueva línea, pero creo que esto es "más limpio".
Otro método, dependiendo de lo que desee hacer con la salida find
, es usar directamente -exec
con el find
comando o usarlo -print0
y canalizarlo xargs -0
. En el primer caso find
se encarga del escape del nombre del archivo. En el -print0
caso, find
imprime su salida con un separador nulo y luego se xargs
divide en esto. Como ningún nombre de archivo puede contener ese carácter (lo que sé), esto también siempre es seguro. Esto es sobre todo útil en casos simples; y generalmente no es un gran sustituto para un for
ciclo completo .
find -print0
conxargs -0
El uso find -print0
combinado con xargs -0
es completamente robusto contra los nombres de archivos legales, y es uno de los métodos más extensibles disponibles. Por ejemplo, supongamos que desea una lista de cada archivo PDF dentro del directorio actual. Podrías escribir
$ find . -iname '*.pdf' -print0 | xargs -0 -n 1 echo
Esto encontrará cada PDF (vía -iname '*.pdf'
) en el directorio actual ( .
) y cualquier subdirectorio, y pasará cada uno de ellos como argumento al echo
comando. Como especificamos la -n 1
opción, xargs
solo pasaremos un argumento a la vez a echo
. Si hubiéramos omitido esa opción, xargs
habría pasado la mayor cantidad posible a echo
. (Puede echo short input | xargs --show-limits
ver cuántos bytes están permitidos en una línea de comando).
xargs
exactamente?Podemos ver claramente el efecto que xargs
tiene en su entrada, y el efecto de -n
en particular, mediante el uso de un script que hace eco de sus argumentos de una manera más precisa que echo
.
$ cat > echoArgs.sh <<'EOF'
#!/bin/bash
echo "Number of arguments: $#"
[[ $# -eq 0 ]] && exit
for i in $(seq 1 $#); do
echo "Arg $i: <$1>"
shift
done
EOF
$ find . -iname '*.pdf' -print0 | xargs -0 ./echoArgs.sh
$ find . -iname '*.pdf' -print0 | xargs -0 -n 1 ./echoArgs.sh
Tenga en cuenta que maneja espacios y líneas nuevas perfectamente bien,
$ touch 'A space-age
new line of vending machines.pdf'
$ find . -iname '*space*' -print0 | xargs -0 -n 1 ./echoArgs.sh
lo cual sería especialmente problemático con la siguiente solución común:
chmod +x ./echoArgs.sh
for file in $(ls *spacey*); do
./echoArgs.sh "$file"
done
Notas
No estoy de acuerdo con los bash
bashers, porque bash
, junto con el conjunto de herramientas * nix, es bastante experto en el manejo de archivos (incluidos aquellos cuyos nombres tienen espacios en blanco incrustados).
En realidad, find
le da un control de grano fino sobre la elección de qué archivos procesar ... En el lado bash, realmente solo necesita darse cuenta de que debe convertir sus cadenas bash words
; normalmente mediante el uso de "comillas dobles", o algún otro mecanismo como el uso de IFS, o los hallazgos{}
Tenga en cuenta que en la mayoría de las situaciones no es necesario configurar y restablecer IFS; simplemente use IFS localmente como se muestra en los ejemplos a continuación. Los tres manejan bien los espacios en blanco. Tampoco necesita una estructura de bucle "estándar", porque find's \;
es efectivamente un bucle; simplemente ponga su lógica de bucle en una función bash (si no está llamando a una herramienta estándar).
IFS=$'\n' find ~/ -name '*.txt' -exec function-or-util {} \;
Y dos ejemplos más
IFS=$'\n' find ~/ -name '*.txt' -exec printf 'Hello %s\n' {} \;
IFS=$'\n' find ~/ -name '*.txt' -exec echo {} \+ |sed 's/home//'
'encontrar also allows you to pass multiple filenames as args to you script ..(if it suits your need: use
+ instead
\; `)
find -print0
yxargs -0
.