"Voy a iterar de todos modos, ¿por qué no usarlo ls?"

"Voy a iterar de todos modos, ¿por qué no usarlo ls?"

Constantemente veo respuestas que citaneste enlaceafirmando definitivamente"¡No analices ls!"Esto me molesta por un par de razones:

  1. 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.

  2. 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. ... lssepara 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ón lsque 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 con ls.

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 lslas 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 lsa veces se confunden los datos de su nombre de archivo (en nuestro caso, convirtió el \ncará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 forbucle y un globo:

for f in *; do
    [[ -e $f ]] || continue
    ...
done

El autor lo llamanombres de archivos confusoscuando lsdevuelve 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 -1y -q lsentonces:

-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 lsla 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ó zshthen 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_SPLITvariable cuando usa zshy 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, :colono alfanuméricos en un shell global y luego sort -uen la lista para obtener resultados únicos. Esto es seguro porque lsya 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 \0nulo, 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 -dopción especificada por POSIX para lsy la -exec $cmd {} +construcción también especificada por POSIX para find. Debido a que findnaturalmente 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 -ipuede 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 lspodrías hacer:

ls --quoting-style=WORD

Y por último, aquí hay un método mucho más simple deanalizandolsque 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 lsmanera 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 lsparece 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 lses 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: bashy zshambas 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.cen 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.


lsreemplaza 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:

  1. 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.
  2. 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 xxdresultado muestra que $filecontenía los caracteres sin formato \ty \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".

lsdevuelve 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 -qno 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 -1qsalida, hay un conjunto de globos y los expande, no solo obtendrá xdos veces, sino que los perderá [x]por completo. Como globo, no coincide consigo mismo como una cadena.

ls -qestá 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 lsusted tiene que manejar superan cualquier posible beneficio. Estos casos especiales se pueden evitar si no analiza lsla 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 lshace 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 lsen 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.

información relacionada