¿Por qué * no * analiza `ls` (y qué hacer en su lugar)?


205

Constantemente veo respuestas que citan este enlace que dice definitivamente "¡No analizar ls!" Esto me molesta por un par de razones:

  1. Parece que la información en ese enlace ha sido aceptada al por mayor con pocas preguntas, aunque puedo detectar al menos algunos errores en la lectura informal.

  2. También parece que los problemas establecidos en ese enlace no han provocado el deseo de encontrar una solución.

Del primer párrafo:

... cuando solicita [ls]una lista de archivos, hay un gran problema: Unix permite casi cualquier carácter en un nombre de archivo, incluidos espacios en blanco, líneas nuevas, comas, símbolos de barra y prácticamente cualquier otra cosa que alguna vez intente usar como delimitador excepto NUL. ... lssepara los nombres de archivo con nuevas líneas. Esto está bien hasta que tenga un archivo con una nueva línea en su nombre. Y dado que no conozco ninguna implementación lsque le permita terminar los nombres de archivo con caracteres NUL en lugar de líneas nuevas, esto no nos permite obtener una lista de nombres de archivo de forma segura ls.

Bummer, ¿verdad? Como siempre podemos manejar una línea nueva terminada conjunto de datos que figuran para los datos que puedan contener saltos de línea? Bueno, si las personas que responden preguntas en este sitio web no hacen este tipo de cosas a diario, podría pensar que estamos en problemas.

Sin embargo, la verdad es que la mayoría de las lsimplementaciones en realidad proporcionan una API muy simple para analizar su salida y todos lo hemos estado haciendo todo el tiempo sin siquiera darnos cuenta. No solo puede finalizar un nombre de archivo con nulo, también puede comenzar uno con nulo o con cualquier otra cadena arbitraria que desee. Además, puede asignar estas cadenas arbitrarias por 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^@^@^@$
^@

Mira esto para más.

Ahora es la siguiente parte de este artículo lo 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 que no sea: no se puede saber.

Observe también cómo a lsveces confunde 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 llama nombres de archivo confusos cuando lsdevuelve una lista de nombres de archivo que contienen globos de shell y luego recomienda usar un globo de shell 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 define los operandos -1y -q lsasí:

-q- Forzar que cada instancia de caracteres de nombre de archivo no imprimibles ys <tab>se escriban como el signo de interrogación ( '?'). Las implementaciones pueden proporcionar esta opción por defecto si la salida es a un dispositivo terminal.

-1- (El dígito numérico uno.) Fuerza la salida a ser una entrada por línea.

Globbing no está exento de problemas: ?coincide con cualquier carácter, por lo que múltiples ?resultados coincidentes en una lista coincidirán con el mismo archivo varias veces. Eso es fácil de manejar.

Aunque la forma de hacer esto no es el punto, después de todo, no se necesita mucho para hacer y se demuestra a continuación, estaba interesado en por qué no . Según lo considero, la mejor respuesta a esa pregunta ha sido aceptada. Te sugiero que trates de concentrarte más a menudo en decirle a la gente lo que pueden hacer que en lo que no pueden. Es mucho menos probable, como creo, que se demuestre que está equivocado al menos.

Pero, ¿por qué intentarlo? Es cierto que mi motivación principal era que otros me decían que no podía. Sé muy bien que la lssalida es tan regular y predecible como podrías desear, siempre y cuando sepas qué buscar. La información errónea 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ía de la información en las respuestas aquí es en su mayoría correcta: un globo de concha es más fácil de usar y generalmente 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, razones suficientes 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 analizarls " .

Tenga en cuenta que los resultados inconsistentes de la respuesta de Patrick son principalmente el resultado de su uso en zshese momento bash. zsh- por defecto - el $(comando de división de palabras no sustituye los )resultados de manera portátil. Entonces, cuando pregunta a dónde se fue el resto de los archivos. la respuesta a esa pregunta es que tu caparazón se los comió. Esta es la razón por la que necesita establecer la SH_WORD_SPLITvariable al usar zshy tratar con código shell portátil. Considero que no haber notado esto en su respuesta es terriblemente engañoso.

La respuesta de Wumpus no calcula para mí: en un contexto de lista, el ?personaje es un globo de shell. No sé cómo decir eso.

Para manejar un caso de resultados múltiples, debe restringir la codicia del globo. Lo siguiente solo creará una base de prueba de nombres de archivos horribles y lo mostrará por usted:

{ 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" $# )
}

SALIDA

`ls` ?QUOTED `-m` COMMA,SEP
??\, ??^, ??`, ??b, [?\, [?\, ]?^, ]?^, _?`, _?`, a?b, a?b

NOW LITERAL - COMMA,SEP
?
 \, ?
     ^, ?
         `, ?
             b, [       \, [
\, ]    ^, ]
^, _    `, _
`, a    b, a
b

FILE COUNT: 12

Ahora voy a salvo cada carácter que no es un /slash, -dash, :colon, o carácter alfanumérico en un pegote cáscara continuación sort -ula lista de resultados únicos. Esto es seguro porque lsya nos ha guardado los caracteres no imprimibles. Reloj:

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

SALIDA:

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, vuelvo a abordar el problema pero utilizo una metodología diferente. Recuerde que, además de \0nulo, el /carácter ASCII es el único byte prohibido en un nombre de ruta. Puse los globos a un lado aquí y en su lugar combiné la -dopción especificada POSIX para lsy la -exec $cmd {} +construcción también POSIX especificada para find. Debido a findque solo emitirá naturalmente 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 rechazo para cada entrada. Solo imagine lo que podría 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 la unicidad del resultado está en duda.

ls -1iq | 
sed '/ .*/s///;s/^/-inum /;$!s/$/ -o /' | 
tr -d '\n' | 
xargs find

Estos son solo 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 de análisisls que uso con bastante frecuencia cuando necesito números de inodo:

ls -1iq | grep -o '^ *[0-9]*'

Eso solo devuelve números de inodo, que es otra práctica opción especificada por POSIX.


12
@mikeserv Ok, lo hice. Shell glob es 2.48 veces más rápido. time bash -c 'for i in {1..1000}; do ls -R &>/dev/null; done'= 3.18s vs time bash -c 'for i in {1..1000}; do echo **/* >/dev/null; done'= 1.28s
Patrick

28
En cuanto a su actualización más reciente, deje de confiar en la salida visual para determinar que su código funciona. Pase su salida a un programa real y haga que el programa intente y realice una operación en el archivo. Es por eso que estaba usando staten mi respuesta, ya que realmente comprueba que cada archivo existe. Su parte inferior con la sedcosa no funciona.
Patrick

57
No puedes hablar en serio. ¿Cómo puede saltar por todos los aros que describe su pregunta ser más fácil o más simple o mejor que simplemente no analizarlo lsen primer lugar? Lo que estás describiendo es muy difícil. Necesitaré deconstruirlo para comprenderlo todo y soy un usuario relativamente competente. No puedes esperar que tu Joe promedio pueda lidiar con algo como esto.
terdon

46
-1 por usar una pregunta para elegir un argumento. Todas las razones por las que el lsresultado del análisis es incorrecto se cubrieron bien en el enlace original (y en muchos otros lugares). Esta pregunta habría sido razonable si OP estuviera pidiendo ayuda para entenderla, pero en cambio OP simplemente está tratando de demostrar que su uso incorrecto está bien.
R ..

14
@mikeserv No es solo eso parsing ls is bad. Hacer for something in $(command)y confiar en la división de palabras para obtener resultados precisos es malo para la gran mayoría de los command'scuales no tienen una salida simple.
BroSlow

Respuestas:


184

No estoy del todo convencido de esto, pero supongamos por el argumento de que , si está preparado para hacer un esfuerzo suficiente, podría analizar el resultado de lsmanera confiable, incluso frente a un "adversario": alguien que conoce el código que escribió y elige deliberadamente nombres de archivo diseñados para romperlo.

Incluso si pudieras hacer eso, sería una mala idea .

Bourne Shell no es un buen lenguaje. No debe usarse para nada complicado, a menos que la portabilidad extrema sea más importante que cualquier otro factor (p autoconf. Ej .).

Afirmo que si se enfrenta 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 lo que sea que esté haciendo es demasiado complicado para shell y debe volver a escribir todo en Perl o Python. 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 los caracteres inusuales en los nombres de archivo: la salida es ambigua de la misma manera que la salida de lses ambigua, pero eso no importaría en un programa "real" (a diferencia de una demostración como esta), que usar el resultado de os.path.join(subdir, f)directamente.

Igualmente importante, y en marcado contraste con lo que escribiste, aún tendrá sentido dentro de seis meses, y será fácil de modificar cuando lo necesites para hacer algo ligeramente diferente. A modo de ilustración, suponga 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))

55
Esto es bueno. ¿ for in | for inEso habla de recursión? No estoy seguro. Incluso si es así, no puede ser más de uno, ¿verdad? Esta es la única respuesta que tiene sentido para mí hasta ahora.
mikeserv

10
Sin recursión, solo forbucles anidados . os.walkestá haciendo un trabajo muy pesado detrás de escena, pero no tiene que preocuparse más de lo que tiene que preocuparse por cómo lso findtrabajar internamente.
zwol

66
Técnicamente, os.walkdevuelve un objeto generador . Los generadores son la versión de Python de las listas perezosas. Cada vez que el bucle for externo se repite, se invoca el generador y "produce" el contenido de otro subdirectorio. Funcionalidad equivalente en Perl es File::Find, si eso ayuda.
zwol

66
Debe tener en cuenta que estoy 100% de acuerdo con el documento que está criticando y con las respuestas de Patrick y Terdon. Mi respuesta tenía la intención de proporcionar una razón adicional e independiente para evitar el análisis de lssalida.
zwol

19
Esto es muy engañoso. Shell no es un buen lenguaje de programación, sino solo porque no es un lenguaje de programación. Es un lenguaje de script. Y es un buen lenguaje de script.
Miles Rout

178

Se hace mucha referencia a ese enlace porque la información es completamente precisa y ha estado allí durante mucho tiempo.


lsreemplaza los caracteres no imprimibles con caracteres globales sí, pero esos caracteres no están en el nombre de archivo real. ¿Por qué importa esto? 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 archivo glob 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 los distinguirá si ambos están representados como a?b?


¡El autor lo llama nombres de archivo confusos cuando ls devuelve una lista de nombres de archivo que contienen globs de shell y luego recomienda usar un glob de shell para recuperar una lista de archivos!

Hay una diferencia aquí. Cuando recuperas un globo, como se muestra, ese globo puede coincidir con más de un archivo. Sin embargo, cuando itera por los resultados que coinciden con un glob, obtiene el archivo exacto, no un glob.

Por ejemplo:

$ for file in *; do printf '%s' "$file" | xxd; done
0000000: 6109 62                                  a.b
0000000: 610a 62                                  a.b

Observe cómo la xxdsalida muestra que $filecontenía los caracteres sin formato \ty \nno ?.

Si usas ls, obtienes 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 repetir de todos modos, ¿por qué no usar ls?"

Tu ejemplo que diste en realidad no funciona. Parece que funciona, pero no funciona.

Me refiero a esto:

 for f in $(ls -1q | tr " " "?") ; do [ -f "$f" ] && echo "./$f" ; done

He creado un directorio con un montón de nombres de archivo:

$ 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 su código, obtengo esto:

$ for f in $(ls -1q | tr " " "?") ; do [ -f "$f" ] && echo "./$f" ; done
./ab
./ab

¿A dónde fue el resto de los archivos?

Probemos 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
./ab
./ab
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
./ab
./ab
./a b
./a
b

Con bash

El ejemplo anterior fue con mi shell normal, zsh. Cuando repito el procedimiento con bash, obtengo otro conjunto de resultados completamente diferente con su 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
./ab
./ab
./a b
./a
b
./a  b
./ab
./ab
./a b
./ab
./ab
./a b
./a
b
./a b
./ab
./ab
./a b
./a
b

Con un globo de concha, funciona perfectamente bien:

$ for f in *; do stat --format='%n' "./$f"; done
./a b
./a  b
./ab
./ab
./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 principio de la respuesta: "El archivo glob podría coincidir con más de un archivo".

lsestá devolviendo el mismo glob ( a?b) para varios archivos, por lo que cada vez que expandimos este glob, 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 códigos hexadecimales son caracteres UTF-8 NBSP.


55
@mikeserv en realidad su solución no devuelve nada. Acabo de actualizar mi respuesta para aclarar ese punto.
Patrick

18
"No el resto"? Es un comportamiento inconsistente y resultados inesperados, ¿cómo es que no es una razón?
Patrick

11
@mikeserv ¿No viste mi comentario sobre tu pregunta? El engrosamiento de la carcasa es 2.5 veces más rápido que ls. También solicité que probaras tu código ya que no funciona. ¿Qué tiene que ver zsh con todo esto?
Patrick

27
@mikeserv No, todo se aplica incluso a bash. Aunque terminé con esta pregunta, ya que no estás escuchando lo que digo.
Patrick

77
Sabes qué, creo que votaré esta respuesta y aclararé en la mía que estoy de acuerdo con todo lo que dice. ;-)
zwol

54

Probemos y simplifiquemos un poco:

$ touch a$'\n'b a$'\t'b 'a b'
$ ls
a b  a?b  a?b
$ IFS="
"
$ set -- $(ls -1q | uniq)
$ echo "Total files in shell array: $#"
Total files in shell array: 4

¿Ver? Eso ya está mal allí. Hay 3 archivos pero bash está informando 4. Esto se debe a que setse le están dando los globos generados por los lscuales el shell expande antes de pasarlos set. Por eso obtienes:

$ for x ; do
>     printf 'File #%d: %s\n' $((i=$i+1)) "$x"
> done
File #1: a b
File #2: a b
File #3: a    b
File #4: a
b

O, si lo prefieres:

$ printf ./%s\\0 "$@" |
> od -A n -c -w1 |
> sed -n '/ \{1,3\}/s///;H
> /\\0/{g;s///;s/\n//gp;s/.*//;h}'
./a b
./a b
./a\tb
./a\nb

Lo anterior se ejecutó bash 4.2.45.


2
Yo voté por esto. Es bueno ver que tu propio código te muerde. Pero solo porque me equivoqué no significa que no se pueda hacer bien. Te mostré una forma muy simple de hacerlo esta mañana ls -1qRi | grep -o '^ *[0-9]*': esa es la lssalida de análisis , hombre, y es la mejor y más rápida forma de obtener una lista de números de inodo.
mikeserv

38
@mikeserv: Se podría hacer bien, si tienes tiempo y paciencia. Pero el hecho es que es inherentemente propenso a errores. Usted mismo se equivocó. mientras discutía sobre sus méritos! Es un gran golpe contra él, incluso si la persona que lucha por él no lo hace correctamente. Y lo más probable es que probablemente pases aún más tiempo equivocándote antes de hacerlo bien. No sé sobre ti, pero la mayoría de las personas tienen mejor que ver con su tiempo que jugar por años con la misma línea de código.
cHao

@ cHao - no discutí sus méritos - protesté por su propaganda.
mikeserv

16
@mikeserv: Los argumentos en su contra están bien fundados y bien merecidos. Incluso tú les has demostrado que son verdad.
cHao

1
@ cHao: no estoy de acuerdo. Hay una línea no muy fina entre un mantra y una sabiduría.
mikeserv

50

La salida de ls -qno es un problema en absoluto. Suele ?significar "Aquí hay un personaje que no se puede mostrar directamente". Los globos suelen ?significar "Cualquier personaje está permitido aquí".

Los globos tienen otros caracteres especiales ( *y []al menos, y dentro del []par hay más). Ninguno de ellos se 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 se perderá por [x]completo. Como glob, no se combina como una cadena.

ls -q está destinado a salvar sus ojos y / o terminal de personajes locos, no para producir algo que pueda alimentar al shell.


42

La respuesta es simple: los casos especiales lsque tiene que manejar superan cualquier beneficio posible. Estos casos especiales se pueden evitar si no analiza la lssalida.

El mantra aquí es nunca confiar en el sistema de archivos del usuario (el equivalente a nunca confiar en la entrada del usuario ). Si hay un método que funcionará siempre, con 100% de certeza, debería ser el método que prefiera, incluso si lshace lo mismo pero con menos certeza. No voy a entrar en detalles técnicos, ya que fueron cubiertos por terdon y Patrick ampliamente. Sé que debido a los riesgos de usar 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 prefieren cierto riesgo sobre la certeza , pero he presentado un informe de error .


33

La razón por la que la gente dice que nunca hagas algo no es necesariamente porque no se puede hacer de manera absolutamente positiva. Es posible que podamos hacerlo, pero puede ser más complicado, menos eficiente tanto en el espacio como en el tiempo. Por ejemplo, estaría perfectamente bien decir "Nunca construyas un backend de comercio electrónico grande en ensamblado x86".

Ahora, al tema en cuestión: como ha demostrado, puede crear una solución que analice ls y proporcione el resultado correcto, por lo que la corrección no es un problema.

¿Es más complicado? Sí, pero podemos ocultar eso detrás de una función auxiliar.

Así que ahora a la eficiencia:

Eficiencia espacial: su solución se basa en uniqfiltrar duplicados, por lo tanto, no podemos generar los resultados de manera perezosa. Entonces, o O(1)vs. O(n)o ambos tienen O(n).

Eficiencia en el tiempo: Best case uniqutiliza un enfoque de hashmap, por lo que todavía tenemos un O(n)algoritmo en la cantidad de elementos adquiridos , aunque probablemente lo sea O(n log n).

Ahora el verdadero problema: si bien su algoritmo todavía no se ve tan mal, tuve mucho cuidado de usar elementos adquiridos y no elementos para n. Porque eso hace una gran diferencia. Supongamos que tiene un archivo \n\nque resultará en un globo para ??que coincida con cada archivo de 2 caracteres en la lista. Curiosamente, si tiene otro archivo \n\rque también dará como resultado ??y también devolverá los 2 archivos de caracteres ... ¿ve a dónde va esto? El comportamiento exponencial en lugar de lineal ciertamente califica como "peor comportamiento en tiempo de ejecución" ... es la diferencia entre un algoritmo práctico y uno sobre el que escribes artículos en revistas teóricas de CS.

Todo el mundo ama los ejemplos, ¿verdad? Aquí vamos. Haga una carpeta llamada "prueba" y use este script de Python en el mismo directorio donde está la carpeta.

#!/usr/bin/env python3
import itertools
dir = "test/"
filename_length = 3
options = "\a\b\t\n\v\f\r"

for filename in itertools.product(options, repeat=filename_length):
        open(dir + ''.join(filename), "a").close()

Lo único que hace es generar todos los productos de longitud 3 para 7 caracteres. Las matemáticas de la secundaria nos dicen que deberían ser 343 archivos. Bueno, eso debería ser realmente rápido de imprimir, así que veamos:

time for f in *; do stat --format='%n' "./$f" >/dev/null; done
real    0m0.508s
user    0m0.051s
sys 0m0.480s

Ahora intentemos su primera solución, porque realmente no puedo entender esto

eval set -- $(ls -1qrR ././ | tr ' ' '?' |
sed -e '\|^\(\.\{,1\}\)/\.\(/.*\):|{' -e \
        's//\1\2/;\|/$|!s|.*|&/|;h;s/.*//;b}' -e \
        '/..*/!d;G;s/\(.*\)\n\(.*\)/\2\1/' -e \
        "s/'/'\\\''/g;s/.*/'&'/;s/?/'[\"?\$IFS\"]'/g" |
uniq)

aquí para trabajar en Linux mint 16 (que creo que dice mucho sobre la usabilidad de este método).

De todos modos, ya que lo anterior solo filtra el resultado una vez que lo obtiene, la solución anterior debe ser al menos tan rápida como la posterior (no hay trucos de inodo en ese, pero no son confiables, por lo que renunciarías a la corrección).

Entonces, ¿cuánto tiempo dura

time for f in $(ls -1q | tr " " "?") ; do stat --format='%n' "./$f" >/dev/null; done

¿tomar? Bueno, realmente no lo sé, lleva un tiempo comprobar los nombres de los archivos 343 ^ 343: te lo diré después de la muerte por el calor del universo.


66
Por supuesto, como se menciona en los comentarios bajo otra respuesta , la afirmación "... ha demostrado que puede crear una solución que analiza ls y da el resultado correcto ..." en realidad no es cierto.
Comodín el

26

Intención declarada de OP dirigida

Prefacio y justificación de la respuesta original actualizado el 18/05/2015

mikeserv (OP) declaró en la última actualización de su pregunta: "Sin embargo, considero una pena que primero hice esta pregunta para señalar una fuente de información errónea y, desafortunadamente, la respuesta más votada aquí es en gran parte engañosa. "

Bueno esta bien; Creo que fue más bien una vergüenza que pasé mucho tiempo tratando de encontrar la manera de explicar lo que quiero decir sólo para encontrar que ya puedo volver a leer la pregunta. Esta pregunta terminó "[generando] discusión en lugar de respuestas" y terminó con un peso de ~ 18K de texto (solo para la pregunta, para ser claros), lo que sería largo incluso para una publicación de blog.

Pero StackExchange no es su caja de jabón, y no es su blog. Sin embargo, en efecto, lo ha usado como al menos un poco de ambos. Las personas terminaron pasando mucho tiempo respondiendo a su "To-Point-Out" en lugar de responder las preguntas reales de las personas. En este punto, marcaré la pregunta como no adecuada para nuestro formato, dado que el OP ha declarado explícitamente que ni siquiera tenía la intención de ser una pregunta.

En este punto, no estoy seguro de si mi respuesta fue correcta o no; probablemente no, pero se dirigió a algunas de sus preguntas, y tal vez pueda ser una respuesta útil para otra persona; los principiantes se animan, algunos de esos "no" se convierten en "hacer a veces" una vez que tenga más experiencia. :)

Como regla general...

por favor, perdona los bordes ásperos restantes; Ya he pasado demasiado tiempo en esto ... en lugar de citar el OP directamente (como se pretendía originalmente), intentaré resumir y parafrasear.

[revisado en gran medida desde mi respuesta original]
después de considerarlo, creo que leí mal el énfasis que el OP estaba poniendo en las preguntas que respondí; Sin embargo, los puntos tratados fueron criados, y he dejado las respuestas en gran parte intacto, ya que creo que fueran a-la-punto y para abordar los problemas que he visto criado en otros contextos también en relación con consejos a los principiantes.

La publicación original preguntaba, de varias maneras, por qué varios artículos daban consejos como "No analizar la lssalida" o "Nunca debe analizar la lssalida", y así sucesivamente.

Mi resolución sugerida para el problema es que las instancias de este tipo de afirmación son simplemente ejemplos de una expresión idiomática, redactada de formas ligeramente diferentes, en las que un cuantificador absoluto se combina con un imperativo [por ejemplo, «no [nunca] X», «[Usted debe] siempre Y», «[uno debe] nunca Z»] para formar declaraciones destinadas a ser utilizadas como reglas o pautas generales, especialmente cuando se les da a los nuevos en un tema, en lugar de ser como verdades absolutas, el no obstante la forma aparente de esas declaraciones.

Cuando comience a aprender un nuevo tema, y ​​a menos que comprenda bien por qué podría necesitar hacer otra cosa, es una buena idea simplemente seguir las reglas generales aceptadas sin excepción, a menos que esté bajo la guía de alguien más experimentado. que tu mismo Con el aumento de la habilidad y la experiencia, podrá determinar cuándo y si se aplica una regla en una situación particular. Una vez que alcance un nivel significativo de experiencia, es probable que entienda el razonamiento detrás de la regla general en primer lugar, y en ese punto puede comenzar a usar su juicio sobre si y en qué nivel se aplican las razones detrás de la regla. esa situación, y también en cuanto a si existen preocupaciones primordiales.

Y es entonces cuando un experto, quizás, podría optar por hacer cosas en violación de "Las Reglas". Pero eso no los haría menos "Las Reglas".

Y, por lo tanto, con el tema en cuestión: en mi opinión, solo porque un experto pueda violar esta regla sin ser completamente golpeado, no veo ninguna forma de justificar decirle a un principiante que "a veces" es Está bien analizar la lssalida, porque: no lo es . O, al menos, ciertamente no es correcto que un principiante lo haga.

Siempre pones tus peones en el centro; en la apertura de una pieza, un movimiento; castillo en la primera oportunidad; caballeros ante obispos; un caballero en el borde es sombrío; ¡y siempre asegúrese de poder ver su cálculo hasta el final! (Vaya, perdón, cansarse, eso es para el StackExchange de ajedrez).

¿Reglas que se deben romper?

Al leer un artículo sobre un tema dirigido a principiantes, o que pueda ser leído por ellos, a menudo verá cosas como esta:

  • "Usted no debe nunca hacer X."
  • "¡Nunca hagas Q!"
  • "No hagas Z."
  • "Uno siempre debe hacer Y!"
  • "C, pase lo que pase".

Si bien estas afirmaciones ciertamente parecen indicar reglas absolutas y atemporales, no lo son; en cambio, esta es una forma de establecer reglas generales [también conocidas como "pautas", "reglas generales", "lo básico", etc.] que es al menos una forma apropiada de enunciarlas para los principiantes que podrían estar leyendo esos artículos. Sin embargo, solo porque se expresan como absolutos, las reglas ciertamente no vinculan a profesionales y expertos [quienes probablemente fueron los que resumieron tales reglas en primer lugar, como una forma de registrar y transmitir el conocimiento adquirido mientras se enfrentaban a recurrentes problemas en su oficio particular.]

Esas reglas ciertamente no van a revelar cómo un experto trataría un problema complejo o matizado, en el cual, digamos, esas reglas entran en conflicto entre sí; o en el que las preocupaciones que llevaron a la regla en primer lugar simplemente no se aplican. Los expertos no tienen miedo (¡o no deberían tener miedo de hacerlo!) Simplemente romper las reglas que saben que no tienen sentido en una situación particular. Los expertos están constantemente tratando de equilibrar varios riesgos y preocupaciones en su oficio, y con frecuencia deben usar su criterio para elegir romper ese tipo de reglas, tener que equilibrar varios factores y no poder confiar en una tabla de reglas a seguir. Tomemos Gotocomo ejemplo: ha habido un largo y recurrente debate sobre si son perjudiciales. (Sí, nunca uses gotos.; D)

Una propuesta modal

Una característica extraña, al menos en inglés, y me imagino en muchos otros idiomas, de las reglas generales, es que se expresan en la misma forma que una propuesta modal, sin embargo, los expertos en un campo están dispuestos a dar una regla general para un situación, todo el tiempo sabiendo que romperán la regla cuando sea apropiado. Claramente, por lo tanto, estas declaraciones no están destinadas a ser equivalentes a las mismas declaraciones en lógica modal.

Es por eso que digo que simplemente deben ser idiomáticos. En lugar de ser realmente una situación de "nunca" o "siempre", estas reglas generalmente sirven para codificar pautas generales que tienden a ser apropiadas en una amplia gama de situaciones y que, cuando los principiantes las siguen ciegamente, es probable que resulten en mejores resultados que el principiante que elige ir en contra de ellos sin una buena razón. A veces codifican reglas que simplemente conducen a resultados deficientes en lugar de las fallas directas que acompañan a las elecciones incorrectas cuando van en contra de las reglas.

Entonces, las reglas generales no son las proposiciones modales absolutas que parecen estar en la superficie, sino que son una forma abreviada de dar la regla con un estándar estándar implícito, algo como lo siguiente:

a menos que tenga la capacidad de decir que esta directriz es incorrecta en un caso particular y probarse a sí mismo que tiene razón, entonces $ {REGLA}

donde, por supuesto, podría sustituir "nunca analizar la lssalida" en lugar de $ {REGLA}. :)

¡Oh si! ¿Qué pasa con lals salida de análisis ?

Bueno, dado todo eso ... creo que está bastante claro que esta regla es buena. En primer lugar, la verdadera regla debe entenderse como idiomática, como se explicó anteriormente ...

Pero además, no es solo que tengas que ser muy bueno con las secuencias de comandos de shell para saber si se puede romper, en algún caso en particular. ¡También es que se necesita tanta habilidad para decir que te equivocaste cuando intentas romperlo en las pruebas! Y digo con confianza que una gran mayoría de la audiencia probable de tales artículos (dando consejos como «¡No analice la salida de ls!») No puede hacer esas cosas , y aquellos que tienen tal habilidad probablemente se darán cuenta de que ellos se dan cuenta solos e ignoran la regla de todos modos

Pero ... solo mira esta pregunta, y cómo incluso las personas que probablemente tienen la habilidad pensaron que era una mala decisión hacerlo; ¡y cuánto esfuerzo el autor de la pregunta gastó para llegar al punto del mejor ejemplo actual! Le garantizo que en un problema tan difícil, ¡el 99% de las personas se equivocarían y con resultados potencialmente muy malos! Incluso si el método que se decide resulta ser bueno; hasta que (u otra) lsidea de análisis sea adoptada por el personal de TI / desarrollador en su conjunto, resista muchas pruebas (especialmente la prueba del tiempo) y, finalmente, logre graduarse a un estado de 'técnica común', es probable que Mucha gente podría intentarlo y equivocarse ... con consecuencias desastrosas.

Por lo tanto, voy a reiterar una vez más .... que, especialmente en este caso , que es por eso que " no analizar lsla salida!" es decididamente la forma correcta de expresarlo.

[ACTUALIZACIÓN 2014-05-18: razonamiento aclarado para la respuesta (arriba) para responder a un comentario de OP; la siguiente adición es en respuesta a las adiciones del OP a la pregunta de ayer]

[ACTUALIZACIÓN 2014-11-10: encabezados agregados y contenido reorganizado / refactorizado; y también: reformatear, volver a redactar, aclarar y um ... "conciso" ... pretendía que esto fuera simplemente una limpieza, aunque se convirtió en un poco de un reproceso. Lo había dejado en un estado lamentable, así que traté principalmente de darle un poco de orden. sentí que era importante dejar en gran parte intacta la primera sección; entonces solo dos cambios menores allí, redundantes 'pero' eliminados, y 'eso' enfatizado.]

† Originalmente pretendía esto únicamente como una aclaración sobre mi original; pero decidió otras adiciones tras la reflexión

‡ consulte https://unix.stackexchange.com/tour para obtener instrucciones sobre publicaciones


2
Nunca es idiomático. Esta no es una respuesta a nada.
mikeserv

1
Hmm Bueno, no sabía si esta respuesta sería satisfactoria, pero no esperaba que fuera controvertida . Y no quise (pretender) argumentar que "nunca" era per se idiomático; pero que "¡Nunca hagas X!" Es un uso idiomático . Veo dos casos generales que pueden mostrar que '¡Nunca / no analices ls!' es un consejo correcto: 1. demuestre (para su satisfacción) que cada caso de uso en el que se puede analizar la lssalida tiene otra solución disponible, superior de alguna manera, sin hacerlo. 2. demuestre que, en los casos citados, la declaración no es literal.
shelleybutterfly

Al mirar su pregunta nuevamente, veo que primero menciona "no ..." en lugar de "nunca ...", lo cual está bien en su análisis, por lo que también aclararé ese punto. En este punto, ya hay una solución del primer tipo, que aparentemente se demuestra / explica a su satisfacción, por lo que no profundizaré mucho en eso. Pero intentaré aclarar un poco mi respuesta: como digo, no estaba tratando de ser controvertido (¡o de confrontación!), Sino de señalar cómo se pretende generalmente esas declaraciones.
shelleybutterfly

1
Debería limpiar esa publicación. Aún así, no es que no la manera correcta de expresarlo. Es un poco ridículo que las personas piensen que están calificadas para decirles a los demás que nunca o no , solo diles que no crees que funcionará y por qué, pero sí sabes qué funcionará y por qué. lses una utilidad informática: puede analizar la salida de la computadora.
mikeserv

1
Bueno, revirtí mi voto negativo porque, al menos, tienes razón sobre lo de marcar. Trataré de limpiarlo esta noche o mañana. Creo que moveré la mayoría de los ejemplos de código a una respuesta, supongo. Pero aún así, en lo que a mí respecta, no disculpa las inexactitudes en esa publicación de blog citada con frecuencia. Me gustaría que la gente deje de citar el manual de fiesta por completo - al menos no hasta después de theyve citaron las especificaciones POSIX ...
mikeserv

16

¿Es posible analizar la salida de lsen ciertos casos? Seguro. La idea de extraer una lista de números de inodo de un directorio es un buen ejemplo: si sabe que su implementación es lscompatible -qy, por lo tanto, cada archivo producirá exactamente una línea de salida, y todo lo que necesita son los números de inodo, analizándolos ls -Rai1qLa salida es sin duda una posible solución. Por supuesto, si el autor no hubiera visto consejos como "Nunca analizar el resultado de ls" antes, probablemente no pensaría en nombres de archivo con nuevas líneas en ellos, y probablemente dejaría la 'q' como resultado, y el el código se rompería sutilmente en ese caso límite, por lo que, incluso en los casos en que el resultado del análisis lses razonable, este consejo sigue siendo útil.

El punto más amplio es que, cuando un novato que shell scripting trata de tener una figura de la escritura a cabo (por ejemplo) lo que es el archivo más grande de un directorio, o lo que es el archivo modificado más recientemente en un directorio, su primer instinto es analizar ls's salida: comprensible, porque lses uno de los primeros comandos que aprende un novato.

Desafortunadamente, ese instinto está mal, y ese enfoque está roto. Aún más desafortunadamente, está sutilmente roto: funcionará la mayor parte del tiempo, pero fallará en casos extremos que tal vez podrían ser explotados por alguien con conocimiento del código.

El novato podría pensar ls -s | sort -n | tail -n 1 | awk '{print $2}'en una forma de obtener el archivo más grande en un directorio. Y funciona, hasta que tenga un archivo con un espacio en el nombre.

OK, ¿qué tal ls -s | sort -n | tail -n 1 | sed 's/[^ ]* *[0-9]* *//'? Funciona bien hasta que tenga un archivo con una nueva línea en el nombre.

No añadiendo -qa ls's argumentos ayuda cuando hay una nueva línea en el nombre del archivo? Puede parecer así, hasta que tenga 2 archivos diferentes que contengan un carácter no imprimible en el mismo lugar en el nombre del archivo, y luego lsla salida no le permite distinguir cuál de ellos era el más grande. Peor aún, para expandir el "?", Probablemente recurra a su caparazón, evallo que causará problemas si golpea un archivo llamado, por ejemplo,

foo`/tmp/malicious_script`bar

¿ --quoting-style=shellAyuda (si tu lsincluso lo soporta)? No, todavía se muestra? para caracteres no imprimibles, por lo que aún es ambiguo cuál de las múltiples coincidencias fue la más grande. --quoting-style=literal? No, lo mismo. --quoting-style=localeo --quoting-style=cpodría ayudar si solo necesita imprimir el nombre del archivo más grande sin ambigüedades, pero probablemente no si necesita hacer algo con el archivo después; sería un montón de código para deshacer la cita y volver al nombre de archivo real que puedes pasar a, por ejemplo, gzip.

Y al final de todo ese trabajo, incluso si lo que tiene es seguro y correcto para todos los nombres de archivo posibles, es ilegible e imposible de mantener, y podría haberse hecho de manera mucho más fácil, segura y legible en Python, Perl o Ruby.

O incluso usando otras herramientas de shell: desde la parte superior de mi cabeza, creo que esto debería hacer el truco:

find . -type f -printf "%s %f\0" | sort -nz | awk 'BEGIN{RS="\0"} END{sub(/[0-9]* /, "", $0); print}'

Y debería ser al menos tan portátil como --quoting-stylees.


Oh, cierto sobre el tamaño, probablemente podría hacerlo si lo intentara, ¿debería? Estoy un poco cansado o todo esto, me gusta su respuesta porque no dice que no puede o no, o nunca, pero en realidad da ejemplos de tal vez por qué no y comparable de qué otra manera , gracias.
mikeserv

Creo que si lo intentaras, descubrirías que es mucho más difícil de lo que piensas. Entonces, sí, recomendaría intentarlo. Estaré encantado de seguir dando nombres de archivo que se romperán para ti mientras pueda pensar en ellos. :)
godlygeek

Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
terdon

@mikeserv y godlygeek, he movido este hilo de comentarios al chat . Por favor, no tengas largas discusiones como esta en los comentarios, para eso está el chat.
terdon
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.