Constantemente veo respuestas que citaneste enlaceafirmando definitivamente"¡No analices ls
!"Esto me molesta por un par de razones:
Parece que la información en ese enlace ha sido aceptada en su totalidad sin muchas dudas, aunque puedo detectar al menos algunos errores en una lectura casual.
También parece que los problemas expuestos en ese enlace no han despertado ningún deseo de encontrar una solución.
Del primer párrafo:
...cuando pides
[ls]
una lista de archivos, hay un gran problema: Unix permite casi cualquier carácter en un nombre de archivo, incluidos espacios en blanco, nuevas líneas, comas, símbolos de canalización y prácticamente cualquier otra cosa que intentes usar como delimitador excepto NUL. ...ls
separa los nombres de archivos con nuevas líneas. Esto está bien hasta que tenga un archivo con una nueva línea en su nombre. Y como no conozco ninguna implementaciónls
que le permita terminar nombres de archivos con caracteres NUL en lugar de nuevas líneas, esto nos deja sin poder obtener una lista de nombres de archivos de forma segura conls
.
Qué fastidio, ¿verdad? Cómoalguna vez¿Podemos manejar un conjunto de datos listado terminado en nueva línea para datos que puedan contener nuevas líneas? Bueno, si las personas que responden preguntas en este sitio web no hicieran este tipo de cosas a diario, podría pensar que estábamos en algún problema.
Sin embargo, la verdad es que la mayoría de ls
las implementaciones proporcionan una API muy simple para analizar su salida y todos lo hemos estado haciendo todo el tiempo sin siquiera darnos cuenta. No sólo puede terminar un nombre de archivo con nulo, sino que también puede comenzar uno con nulo o con cualquier otra cadena arbitraria que desee. Es más, puedes asignar estas cadenas arbitrariaspor tipo de archivo. Por favor considera:
LS_COLORS='lc=\0:rc=:ec=\0\0\0:fi=:di=:' ls -l --color=always | cat -A
total 4$
drwxr-xr-x 1 mikeserv mikeserv 0 Jul 10 01:05 ^@^@^@^@dir^@^@^@/$
-rw-r--r-- 1 mikeserv mikeserv 4 Jul 10 02:18 ^@file1^@^@^@$
-rw-r--r-- 1 mikeserv mikeserv 0 Jul 10 01:08 ^@file2^@^@^@$
-rw-r--r-- 1 mikeserv mikeserv 0 Jul 10 02:27 ^@new$
line$
file^@^@^@$
^@
Verestepara más.
Ahora es la siguiente parte de este artículo la que realmente me atrapa:
$ ls -l
total 8
-rw-r----- 1 lhunath lhunath 19 Mar 27 10:47 a
-rw-r----- 1 lhunath lhunath 0 Mar 27 10:47 a?newline
-rw-r----- 1 lhunath lhunath 0 Mar 27 10:47 a space
El problema es que a partir de la salida de
ls
, ni usted ni la computadora pueden saber qué partes constituyen un nombre de archivo. ¿Es cada palabra? No. ¿Es cada línea? No. No hay una respuesta correcta a esta pregunta aparte de: no se puede saber.También observe cómo
ls
a veces se confunden los datos de su nombre de archivo (en nuestro caso, convirtió el\n
carácter entre las palabras"a"y "nueva línea"en un?signo de interrogación......
Si solo desea iterar sobre todos los archivos en el directorio actual, use un
for
bucle y un globo:
for f in *; do
[[ -e $f ]] || continue
...
done
El autor lo llamanombres de archivos confusoscuando ls
devuelve una lista de nombres de archivos que contienen globos de shelly luegorecomienda usar un shell global para recuperar una lista de archivos.
Considera lo siguiente:
printf 'touch ./"%b"\n' "file\nname" "f i l e n a m e" |
. /dev/stdin
ls -1q
f i l e n a m e
file?name
IFS="
" ; printf "'%s'\n" $(ls -1q)
'f i l e n a m e'
'file
name'
POSIX definelos operandos -1
y -q
ls
entonces:
-q
- Fuerza que cada instancia de caracteres de nombre de archivo no imprimibles<tab>
se escriba como el'?'
carácter de signo de interrogación ( ). Las implementaciones pueden proporcionar esta opción de forma predeterminada si la salida es a un dispositivo terminal.
-1
-(El dígito numérico uno.)Fuerza la salida para que sea una entrada por línea.
El globbing no está exento de problemas: los ?
partidoscualquiercarácter para que varios ?
resultados coincidentes en una lista coincidan con el mismo archivo varias veces. Eso se maneja fácilmente.
Aunque el punto no es cómo hacer esto (después de todo, no se necesita mucho y se demuestra a continuación), estaba interesado enpor qué no. A mi modo de ver, se ha aceptado la mejor respuesta a esa pregunta. Te sugiero que trates de concentrarte más a menudo en decirle a la gente lo quepoderhacer que en lo que ellosno poder.Creo que es mucho menos probable que al menos se demuestre que estás equivocado.
Pero ¿por qué intentarlo? Es cierto que mi motivación principal fue que los demás seguían diciéndome que no podía. Sé muy bien que ls
la producción es tan regular y predecible como uno podría desear, siempre que sepa qué buscar. La desinformación me molesta más que la mayoría de las cosas.
Sin embargo, la verdad es que, con la notable excepción de las respuestas de Patrick y Wumpus Q. Wumbley(a pesar del increíble manejo de este último)Considero que la mayor parte de la información contenida en las respuestas aquí es mayoritariamente correcta: un shell global es más sencillo de usar y, en general, más efectivo cuando se trata de buscar en el directorio actual que el análisis ls
. Sin embargo, no son, al menos en mi opinión, razón suficiente para justificar la propagación de la información errónea citada en el artículo anterior ni son una justificación aceptable para "nunca analizar ls
."
Tenga en cuenta que los resultados inconsistentes de la respuesta de Patrick se deben principalmente a que usó zsh
then bash
. zsh
- De forma predeterminada, el comando de división de palabras no $(
sustituye )
los resultados de forma portátil. Entonces cuando él pregunta¿A dónde fue el resto de los archivos?la respuesta a esa pregunta estu caparazón se los comió.Es por eso que necesita configurar la SH_WORD_SPLIT
variable cuando usa zsh
y trabaja con código de shell portátil. Considero que el hecho de que no haya señalado esto en su respuesta es tremendamente engañoso.
La respuesta de Wumpus no me funciona: en un contexto de lista, el ?
personajeesuna bola de concha. No sé de qué otra manera decir eso.
Para manejar un caso de resultados múltiples, es necesario restringir la avidez del globo. Lo siguiente simplemente creará una base de prueba de nombres de archivos horribles y la mostrará:
{ printf %b $(printf \\%04o `seq 0 127`) |
sed "/[^[-b]*/s///g
s/\(.\)\(.\)/touch '?\v\2' '\1\t\2' '\1\n\2'\n/g" |
. /dev/stdin
echo '`ls` ?QUOTED `-m` COMMA,SEP'
ls -qm
echo ; echo 'NOW LITERAL - COMMA,SEP'
ls -m | cat
( set -- * ; printf "\nFILE COUNT: %s\n" $# )
}
PRODUCCIÓN
`ls` ?QUOTED `-m` COMMA,SEP
??\, ??^, ??`, ??b, [?\, [?\, ]?^, ]?^, _?`, _?`, a?b, a?b
NOW LITERAL - COMMA,SEP
?
\, ?
^, ?
`, ?
b, [ \, [
\, ] ^, ]
^, _ `, _
`, a b, a
b
FILE COUNT: 12
Ahora protegeré todos los caracteres que no sean /slash
, -dash
, :colon
o alfanuméricos en un shell global y luego sort -u
en la lista para obtener resultados únicos. Esto es seguro porque ls
ya nos ha guardado todos los caracteres no imprimibles. Mirar:
for f in $(
ls -1q |
sed 's|[^-:/[:alnum:]]|[!-\\:[:alnum:]]|g' |
sort -u | {
echo 'PRE-GLOB:' >&2
tee /dev/fd/2
printf '\nPOST-GLOB:\n' >&2
}
) ; do
printf "FILE #$((i=i+1)): '%s'\n" "$f"
done
PRODUCCIÓN:
PRE-GLOB:
[!-\:[:alnum:]][!-\:[:alnum:]][!-\:[:alnum:]]
[!-\:[:alnum:]][!-\:[:alnum:]]b
a[!-\:[:alnum:]]b
POST-GLOB:
FILE #1: '?
\'
FILE #2: '?
^'
FILE #3: '?
`'
FILE #4: '[ \'
FILE #5: '[
\'
FILE #6: '] ^'
FILE #7: ']
^'
FILE #8: '_ `'
FILE #9: '_
`'
FILE #10: '?
b'
FILE #11: 'a b'
FILE #12: 'a
b'
A continuación abordo el problema nuevamente pero utilizo una metodología diferente. Recuerde que, además de \0
nulo, el /
carácter ASCII es el único byte prohibido en una ruta de acceso. Dejo a un lado los globos aquí y en su lugar combino la -d
opción especificada por POSIX para ls
y la -exec $cmd {} +
construcción también especificada por POSIX para find
. Debido a que find
naturalmente solo emitirá uno /
en secuencia, lo siguiente proporciona fácilmente una lista de archivos recursiva y delimitada de manera confiable que incluye toda la información de dentry para cada entrada. Imagínense lo que podrían hacer con algo como esto:
#v#note: to do this fully portably substitute an actual newline \#v#
#v#for 'n' for the first sed invocation#v#
cd ..
find ././ -exec ls -1ldin {} + |
sed -e '\| *\./\./|{s||\n.///|;i///' -e \} |
sed 'N;s|\(\n\)///|///\1|;$s|$|///|;P;D'
###OUTPUT
152398 drwxr-xr-x 1 1000 1000 72 Jun 24 14:49
.///testls///
152399 -rw-r--r-- 1 1000 1000 0 Jun 24 14:49
.///testls/?
\///
152402 -rw-r--r-- 1 1000 1000 0 Jun 24 14:49
.///testls/?
^///
152405 -rw-r--r-- 1 1000 1000 0 Jun 24 14:49
.///testls/?
`///
...
ls -i
puede ser muy útil, especialmente cuando se cuestiona la unicidad del resultado.
ls -1iq |
sed '/ .*/s///;s/^/-inum /;$!s/$/ -o /' |
tr -d '\n' |
xargs find
Estos son sólo los medios más portátiles que se me ocurren. Con GNU ls
podrías hacer:
ls --quoting-style=WORD
Y por último, aquí hay un método mucho más simple deanalizandols
que uso con bastante frecuencia cuando necesito números de inodo:
ls -1iq | grep -o '^ *[0-9]*'
Eso simplemente devuelve números de inodo, que es otra opción útil especificada por POSIX.
Respuesta1
No estoy del todo convencido de esto, pero supongamos, a modo de argumento, que ustedpodría, si está preparado para esforzarse lo suficiente, analice el resultado de ls
manera confiable, incluso frente a un "adversario", alguien que conoce el código que escribió y elige deliberadamente nombres de archivos diseñados para romperlo.
Incluso si pudieras hacer eso,todavía sería una mala idea.
Bourne shell 1 es un mal lenguaje. No debe usarse para nada complicado, a menos que la portabilidad extrema sea más importante que cualquier otro factor (p. ej. autoconf
).
Sostengo que si te enfrentas a un problema en el que analizar la salida de ls
parece ser el camino de menor resistencia para un script de shell, eso es una fuerte indicación de que cualquier cosa que estés haciendo esdemasiado complicado para ser un script de shelly deberías reescribir todo en Perl, Python, Julia o cualquiera de los otrosbienlenguajes de programación que están fácilmente disponibles. Como demostración, aquí está su último programa en Python:
import os, sys
for subdir, dirs, files in os.walk("."):
for f in dirs + files:
ino = os.lstat(os.path.join(subdir, f)).st_ino
sys.stdout.write("%d %s %s\n" % (ino, subdir, f))
Esto no tiene ningún problema con caracteres inusuales en los nombres de archivos: elproducciónes ambiguo de la misma manera que el resultado de ls
es ambiguo, pero eso no importaría en un programa "real" (a diferencia de una demostración como esta), que usaría el resultado de os.path.join(subdir, f)
directamente.
Lo que es igualmente importante, y en marcado contraste con lo que escribiste, seguirá teniendo sentido dentro de seis meses y será fácil de modificar cuando necesites hacer algo ligeramente diferente. A modo de ilustración, supongamos que descubre la necesidad de excluir archivos de puntos y copias de seguridad del editor, y procesar todo en orden alfabético por nombre base:
import os, sys
filelist = []
for subdir, dirs, files in os.walk("."):
for f in dirs + files:
if f[0] == '.' or f[-1] == '~': continue
lstat = os.lstat(os.path.join(subdir, f))
filelist.append((f, subdir, lstat.st_ino))
filelist.sort(key = lambda x: x[0])
for f, subdir, ino in filelist:
sys.stdout.write("%d %s %s\n" % (ino, subdir, f))
1 Sí, hoy en día se encuentran disponibles versiones ampliadas del shell Bourne: bash
y zsh
ambas son considerablemente mejores que el original. Las extensiones GNU a las "utilidades de shell" principales (find, grep, etc.) también ayudan mucho. Pero incluso con todas las extensiones, el entorno del shell no mejora.suficientepara competir con lenguajes de scripting que son realmente buenos, por lo que mi consejo sigue siendo "no uses shell para nada complicado", independientemente de qué shell estés hablando.
"¿Cómo sería un buen shell interactivo que también fuera un buen lenguaje de programación?" es una pregunta de investigación viva, porque existe una tensión inherente entre las comodidades requeridas para una CLI interactiva (como poder escribir cc -c -g -O2 -o foo.o foo.c
en lugar de subprocess.run(["cc", "-c", "-g", "-O2", "-o", "foo.o", "foo.c"])
) y las restricciones requeridas para evitar errores sutiles en scripts complejos (comonointerpretar palabras sin comillas en ubicaciones aleatorias como cadenas literales). Si intentara diseñar algo así, probablemente comenzaría poniendo IPython, PowerShell y Lua en una licuadora, pero no tengo idea de cómo se vería el resultado.
Respuesta2
Se hace mucha referencia a ese enlace porque la información es completamente precisa y ha estado ahí durante mucho tiempo.
ls
reemplaza los caracteres no imprimibles con caracteres globales, sí, pero esos caracteres no están en el nombre del archivo real. ¿Por qué esto importa? 2 razones:
- Si pasa ese nombre de archivo a un programa, ese nombre de archivo en realidad no existe. Tendría que expandir el globo para obtener el nombre real del archivo.
- El conjunto de archivos puede coincidir con más de un archivo.
Por ejemplo:
$ touch a$'\t'b
$ touch a$'\n'b
$ ls -1
a?b
a?b
Observe cómo tenemos 2 archivos que se ven exactamente iguales. ¿Cómo vas a distinguirlos si ambos están representados como a?b
?
El autor lo llama nombres de archivos confusos cuando ls devuelve una lista de nombres de archivos que contienen globos de shell y luego recomienda usar un globo de shell para recuperar una lista de archivos.
Hay una diferencia aquí. Cuando recupera un globo, como se muestra, ese globo puede coincidir con más de un archivo. Sin embargo, cuando recorre los resultados que coinciden con un globo, obtiene el archivo exacto, no un globo.
Por ejemplo:
$ for file in *; do printf '%s' "$file" | xxd; done
0000000: 6109 62 a.b
0000000: 610a 62 a.b
Observe cómo el xxd
resultado muestra que $file
contenía los caracteres sin formato \t
y \n
, no ?
.
Si usas ls
, obtendrás esto en su lugar:
for file in $(ls -1q); do printf '%s' "$file" | xxd; done
0000000: 613f 62 a?b
0000000: 613f 62 a?b
"Voy a iterar de todos modos, ¿por qué no usarlo ls
?"
El ejemplo que diste en realidad no funciona. Parece que funciona, pero no es así.
Me refiero a esto:
for f in $(ls -1q | tr " " "?") ; do [ -f "$f" ] && echo "./$f" ; done
Creé un directorio con varios nombres de archivos:
$ for file in *; do printf '%s' "$file" | xxd; done
0000000: 6120 62 a b
0000000: 6120 2062 a b
0000000: 61e2 8082 62 a...b
0000000: 61e2 8083 62 a...b
0000000: 6109 62 a.b
0000000: 610a 62 a.b
Cuando ejecuto tu código, me sale esto:
$ for f in $(ls -1q | tr " " "?") ; do [ -f "$f" ] && echo "./$f" ; done
./a b
./a b
¿A dónde fue el resto de los archivos?
Intentemos esto en su lugar:
$ for f in $(ls -1q | tr " " "?") ; do stat --format='%n' "./$f"; done
stat: cannot stat ‘./a?b’: No such file or directory
stat: cannot stat ‘./a??b’: No such file or directory
./a b
./a b
stat: cannot stat ‘./a?b’: No such file or directory
stat: cannot stat ‘./a?b’: No such file or directory
Ahora usemos un globo real:
$ for f in *; do stat --format='%n' "./$f"; done
./a b
./a b
./a b
./a b
./a b
./a
b
con fiesta
El ejemplo anterior fue con mi shell normal, zsh. Cuando repito el procedimiento con bash, obtengo otro conjunto de resultados completamente diferente a tu ejemplo:
Mismo conjunto de archivos:
$ for file in *; do printf '%s' "$file" | xxd; done
0000000: 6120 62 a b
0000000: 6120 2062 a b
0000000: 61e2 8082 62 a...b
0000000: 61e2 8083 62 a...b
0000000: 6109 62 a.b
0000000: 610a 62 a.b
Resultados radicalmente diferentes con su código:
for f in $(ls -1q | tr " " "?") ; do stat --format='%n' "./$f"; done
./a b
./a b
./a b
./a b
./a
b
./a b
./a b
./a b
./a b
./a b
./a b
./a b
./a
b
./a b
./a b
./a b
./a b
./a
b
Con un shell glob, funciona perfectamente bien:
$ for f in *; do stat --format='%n' "./$f"; done
./a b
./a b
./a b
./a b
./a b
./a
b
La razón por la que bash se comporta de esta manera se remonta a uno de los puntos que mencioné al comienzo de la respuesta: "El archivo global puede coincidir con más de un archivo".
ls
devuelve el mismo globo ( a?b
) para varios archivos, por lo que cada vez que expandimos este globo, obtenemos todos los archivos que coinciden.
Cómo recrear la lista de archivos que estaba usando:
touch 'a b' 'a b' a$'\xe2\x80\x82'b a$'\xe2\x80\x83'b a$'\t'b a$'\n'b
Los de código hexadecimal son caracteres UTF-8 NBSP.
Respuesta3
La salida de ls -q
no es una masa en absoluto. Solía ?
significar "Aquí hay un carácter que no se puede mostrar directamente". Los globos suelen ?
significar "Aquí se permite cualquier carácter".
Los globos tienen otros caracteres especiales ( *
y []
al menos, y dentro del []
par hay más). Ninguno de ellos escapa ls -q
.
$ touch x '[x]'
$ ls -1q
[x]
x
Si trata la ls -1q
salida, hay un conjunto de globos y los expande, no solo obtendrá x
dos veces, sino que los perderá [x]
por completo. Como globo, no coincide consigo mismo como una cadena.
ls -q
está destinado a salvar tus ojos y/o terminal de personajes locos, no a producir algo que puedas retroalimentar al shell.
Respuesta4
La respuesta es simple: los casos especiales que ls
usted tiene que manejar superan cualquier posible beneficio. Estos casos especiales se pueden evitar si no analiza ls
la salida.
El mantra aquí esnunca confíes en el sistema de archivos del usuario(el equivalente anunca confíes en la entrada del usuario). Si hay un método que funcionará siempre, con 100% de certeza, debería ser el método que prefieras incluso si ls
hace lo mismo pero con menos certeza. No entraré en detalles técnicos ya que estaban cubiertos porterdónyPatricioextensamente. Sé que debido a los riesgos de utilizarlo ls
en una transacción importante (y tal vez costosa) donde mi trabajo/prestigio está en juego, preferiré cualquier solución que no tenga un grado de incertidumbre si se puede evitar.
Sé que algunas personas prefierencierto riesgo sobre la certeza, peroHe presentado un informe de error.