Pourquoi * pas * analyser `ls` (et que faire à la place)?


204

Je vois toujours des réponses citant ce lien indiquant définitivement "Ne pas analyser ls!" Cela me dérange pour deux raisons:

  1. Il semble que l’information contenue dans ce lien ait été acceptée en gros avec peu de questions, bien que je puisse au moins relever quelques erreurs de lecture occasionnelle.

  2. Il semble également que les problèmes énoncés dans ce lien n’ont suscité aucun désir de trouver une solution.

Du premier paragraphe:

... lorsque vous demandez [ls]une liste de fichiers, le problème est énorme: Unix autorise presque tous les caractères d'un nom de fichier, y compris les espaces, les nouvelles lignes, les virgules, les symboles de conduite et à peu près tout ce que vous voudriez utiliser auparavant. délimiteur sauf NUL. ... lssépare les noms de fichiers avec des nouvelles lignes. C'est bien jusqu'à ce que vous ayez un fichier avec une nouvelle ligne dans son nom. Et comme je ne connais aucune implémentation de lscela qui vous permette de terminer les noms de fichiers avec des caractères NUL au lieu de sauts de ligne, nous ne pouvons pas obtenir une liste de noms de fichiers en toute sécurité ls.

Bummer, non? Comment jamais peut - on gérer un saut de ligne fin ensemble de données pour les données répertorié peuvent contenir des sauts de ligne? Eh bien, si les personnes qui répondent aux questions sur ce site Web ne font pas ce genre de choses tous les jours, je penserais peut-être que nous avions des problèmes.

En réalité, la plupart des lsimplémentations fournissent en réalité une API très simple pour analyser leurs résultats et nous le faisons tous depuis le début sans même nous en rendre compte. Non seulement vous pouvez terminer un nom de fichier par null, vous pouvez également commencer par un null ou par toute autre chaîne arbitraire de votre choix. De plus, vous pouvez affecter ces chaînes arbitraires par type de fichier . Veuillez considérer:

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^@^@^@$
^@

Voir cela pour plus.

Maintenant, c’est la partie suivante de cet article qui m’amène vraiment:

$ 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

Le problème est que, à la sortie de ls, ni vous ni l'ordinateur ne pouvez déterminer quelles parties de celui-ci constituent un nom de fichier. Est-ce chaque mot? Est-ce chaque ligne? Non. Il n'y a pas de réponse correcte à cette question si ce n'est: vous ne pouvez pas le dire.

Vous remarquerez également que lsparfois, les données de votre nom de fichier sont corrompues (dans notre cas, le \ncaractère entre les mots "a" et "nouvelle ligne" est transformé en ? Point d' interrogation ...

...

Si vous voulez juste parcourir tous les fichiers du répertoire courant, utilisez une forboucle et un glob:

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

L'auteur l'appelle en altérant les noms de fichiers lorsqu'il lsrenvoie une liste de noms de fichiers contenant des globs de shell , puis recommande l'utilisation d'un glob de shell pour récupérer une liste de fichiers!

Considérer ce qui suit:

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 définit les opérandes -1et -q lsdonc:

-q- Forcer chaque instance de caractères de nom de fichier non imprimables et <tab>s à être écrite en tant que caractère de point d'interrogation ( '?'). Les implémentations peuvent fournir cette option par défaut si la sortie est destinée à un terminal.

-1- (Le chiffre un.) Force la sortie à une entrée par ligne.

Globbing n'est pas sans problèmes: la ?correspondance avec n'importe quel caractère permet à plusieurs ?résultats dans une liste de faire correspondre le même fichier plusieurs fois. C'est facilement manipulé.

Bien que la manière de procéder ne soit pas l’essentiel - cela ne prend pas grand chose après tout, comme le montre l’illustration ci-dessous - je me demandais pourquoi pas . Selon moi, la meilleure réponse à cette question a été acceptée. Je vous suggérerais d'essayer de vous concentrer plus souvent sur le fait de dire aux gens ce qu'ils peuvent faire plutôt que ce qu'ils ne peuvent pas. Je pense que vous êtes beaucoup moins susceptible de vous tromper du moins.

Mais pourquoi même essayer? Certes, ma principale motivation était que les autres n'arrêtaient pas de me dire que je ne pouvais pas. Je sais très bien que la lssortie est aussi régulière et prévisible que vous le souhaiteriez, à condition de savoir quoi chercher. La désinformation me dérange plus que la plupart des choses.

La vérité est que, à l'exception notable des réponses de Patrick et de Wumpus Q. Wumbley (malgré le formidable traitement de ce dernier) , je considère que la plupart des informations des réponses ici sont globalement correctes: un shell glob est à la fois plus simple à utiliser. et généralement plus efficace pour la recherche dans le répertoire actuel que l'analyse syntaxique ls. Cependant, ils ne constituent pas, du moins à mon égard, une raison suffisante pour justifier la propagation de la désinformation citée dans l'article ci-dessus, ni une justification acceptable pour " ne jamais analyserls " .

Veuillez noter que les résultats contradictoires de la réponse de Patrick résultent principalement de son utilisation à ce moment- zshbash. zsh- par défaut - la $(commande de fractionnement de mots substituée ne produit pas les )résultats de manière portable. Alors, quand il demande où est allé le reste des fichiers? la réponse à cette question est que votre coquille les a mangés. C'est pourquoi vous devez définir la SH_WORD_SPLITvariable lorsque vous utilisez zshet manipulez du code shell portable. Je considère que son omission de noter cela dans sa réponse est terriblement trompeuse.

La réponse de Wumpus ne calcule pas pour moi - dans un contexte de liste, le ?caractère est un glob shell. Je ne sais pas comment dire autrement.

Afin de gérer un cas de résultats multiples, vous devez limiter la gourmandise du glob. Ce qui suit va juste créer une base de test de noms de fichiers affreux et l'afficher pour vous:

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

SORTIE

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

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

FILE COUNT: 12

Maintenant , je vais en sécurité tous les caractères qui n'est pas /slash, -dash, :colonou caractère alphanumérique dans un glob shell alors sort -ula liste des résultats uniques. Ceci est sûr car nous lsavons déjà sauvegardé tous les caractères non imprimables. Regarder:

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

SORTIE:

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'

Ci-dessous, j'aborde à nouveau le problème mais j'utilise une méthodologie différente. Rappelez-vous que, outre \0null, le /caractère ASCII est le seul octet interdit dans un chemin d'accès. Je mets globs de côté ici et combine à la place l' -doption spécifiée par POSIX pour lset la -exec $cmd {} +construction également spécifiée par POSIX pour find. Etant donné findqu’il n’en émettra naturellement que /successivement, les éléments suivants permettent d’obtenir facilement une liste de fichiers récursive et délimitée de manière fiable, y compris toutes les informations de répertoire pour chaque entrée. Imaginez ce que vous pourriez faire avec quelque chose comme ceci:

#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 peut être très utile - surtout lorsque l'unicité des résultats est en cause.

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

Ce ne sont que les moyens les plus portables que je puisse penser. Avec GNU, lsvous pouvez faire:

ls --quoting-style=WORD

Enfin, voici une méthode d' analysels beaucoup plus simple que j'utilise souvent lorsque j'ai besoin de numéros d'inode:

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

Cela ne fait que renvoyer des numéros d'inode - ce qui est une autre option pratique spécifiée par POSIX.


12
@ mikeserv Ok je l'ai fait. Shell glob est 2,48 fois plus rapide. 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 ce qui concerne votre dernière mise à jour, arrêtez de vous fier à la sortie visuelle pour déterminer si votre code fonctionne. Passez votre sortie à un programme réel et demandez au programme d’essayer d’effectuer une opération sur le fichier. C'est pourquoi j'utilisais statma réponse, car elle vérifie en réalité que chaque fichier existe. Votre part au bas avec la sedchose ne fonctionne pas.
Patrick

57
Tu ne peux pas être sérieux. En quoi toutes les étapes décrites dans votre question peuvent-elles être plus faciles, plus simples ou mieux en tous cas que de ne pas tout analyser lsen premier lieu? Ce que vous décrivez est très difficile. Je vais devoir le déconstruire pour tout comprendre et je suis un utilisateur relativement compétent. Vous ne pouvez pas vous attendre à ce que votre Joe moyen soit capable de gérer quelque chose comme ça.
terdon

46
-1 pour avoir utilisé une question pour choisir un argument. Toutes les raisons pour lesquelles l'analyse de la lssortie est erronée ont été bien couvertes dans le lien d'origine (et dans de nombreux autres endroits). Cette question aurait été raisonnable si OP demandait de l'aide pour le comprendre, mais au lieu de cela, OP essaie simplement de prouver que son utilisation incorrecte est correcte.
R ..

14
@ mikeserv Ce n'est pas que ça parsing ls is bad. Compter for something in $(command)sur les mots pour obtenir des résultats précis est une mauvaise command'schose pour la grande majorité de ceux-ci qui ne disposent pas d'une sortie simple.
BroSlow

Réponses:


184

Je ne suis pas du tout convaincu de cela, mais supposons, aux fins d'argument, que vous pourriez , si vous êtes prêt à faire un effort suffisant, analyser le résultat de lsmanière fiable, même face à un "adversaire" - quelqu'un qui connaît le code que vous avez écrit et choisit délibérément des noms de fichiers conçus pour le casser.

Même si vous pouviez le faire, ce serait quand même une mauvaise idée .

Bourne shell n'est pas une bonne langue. Il ne devrait pas être utilisé pour quelque chose de compliqué, à moins que la portabilité extrême soit plus importante que tout autre facteur (par exemple autoconf).

J'affirme que si vous rencontrez un problème où l'analyse de la sortie de lssemble être le chemin de moindre résistance pour un script shell, c'est une forte indication que tout ce que vous faites est trop compliqué pour shell et vous devez le réécrire en entier. Perl ou Python. Voici votre dernier programme 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))

Cela n’a aucun problème avec les caractères inhabituels dans les noms de fichiers - le résultat est ambigu de la même manière que le résultat lsest ambigu, mais cela n’aurait pas d’importance dans un "vrai" programme (par opposition à une démo comme celle-ci), ce qui utiliser le résultat de os.path.join(subdir, f)directement.

Tout aussi important, et à la différence de ce que vous avez écrit, cela aura encore un sens dans six mois et il sera facile de le modifier lorsque vous en aurez besoin pour faire quelque chose de légèrement différent. A titre d'illustration, supposons que vous découvriez la nécessité d'exclure les sauvegardes de fichiers de point et les éditeurs, et de tout traiter dans l'ordre alphabétique par nom de 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))

5
C'est bon. Est-ce que ça for in | for inparle de récursion? Je ne suis pas sûr. Même si c'est le cas, il ne peut y avoir plus d'un, non? C’est la seule réponse qui me semble logique jusqu’à présent.
mikeserv

10
Pas de récursion, juste des forboucles imbriquées . os.walkfait de gros efforts dans les coulisses, mais vous n’avez pas à vous soucier de cela, pas plus que de votre façon de travailler lsou de findtravailler en interne.
dimanche

6
Techniquement, os.walkretourne un objet générateur . Les générateurs sont la version Python des listes paresseuses. Chaque fois que la boucle for externe itère, le générateur est appelé et "renvoie" le contenu d'un autre sous-répertoire. Les fonctionnalités équivalentes en Perl sont File::Find, si cela aide.
dimanche

6
Vous devez savoir que je suis à 100% d’accord avec le document que vous critiquez et avec les réponses de Patrick et Terdon. Ma réponse visait à fournir une raison supplémentaire et indépendante d’éviter d’analyser la lssortie.
dimanche

19
C'est très trompeur. Shell n'est pas un bon langage de programmation, mais uniquement parce que ce n'est pas un langage de programmation. C'est un langage de script. Et c'est un bon langage de script.
Miles Rout

178

Ce lien est souvent référencé car les informations sont tout à fait exactes et existent depuis très longtemps.


lsremplace les caractères non imprimables par des caractères globaux oui, mais ces caractères ne figurent pas dans le nom de fichier réel. Pourquoi est-ce important? 2 raisons:

  1. Si vous transmettez ce nom de fichier à un programme, ce nom de fichier n'existe pas. Il faudrait élargir le glob pour obtenir le vrai nom de fichier.
  2. Le fichier glob peut correspondre à plus d'un fichier.

Par exemple:

$ touch a$'\t'b
$ touch a$'\n'b
$ ls -1
a?b
a?b

Remarquez comment nous avons 2 fichiers qui se ressemblent exactement. Comment allez-vous les distinguer s'ils sont tous deux représentés a?b?


L'auteur l'appelle en altérant les noms de fichiers lorsque ls renvoie une liste de noms de fichiers contenant des globs de shell, puis recommande l'utilisation d'un shell glob pour récupérer une liste de fichiers!

Il y a une différence ici. Comme vous pouvez le constater, lorsque vous obtenez un fichier glob, celui-ci peut correspondre à plusieurs fichiers. Toutefois, lorsque vous parcourez les résultats correspondant à un glob, vous récupérez le fichier exact, pas un glob.

Par exemple:

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

Remarquez comment la xxdsortie montre qu’elle $filecontenait les caractères bruts \tet \nnon ?.

Si vous utilisez ls, vous obtenez ceci à la place:

for file in $(ls -1q); do printf '%s' "$file" | xxd; done
0000000: 613f 62                                  a?b
0000000: 613f 62                                  a?b

"Je vais quand même itérer, pourquoi ne pas utiliser ls?"

Votre exemple que vous avez donné ne fonctionne pas réellement. On dirait que ça marche, mais ça ne marche pas.

Je parle de ceci:

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

J'ai créé un répertoire avec un tas de noms de fichiers:

$ 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

Quand je lance votre code, je reçois ceci:

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

Où est passé le reste des fichiers?

Essayons ceci à la place:

$ 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

Utilisons maintenant un glob réel:

$ for f in *; do stat --format='%n' "./$f"; done
./a b
./a  b
./ab
./ab
./a b
./a
b

Avec bash

L'exemple ci-dessus était avec mon shell normal, zsh. Lorsque je répète la procédure avec bash, vous obtenez un autre ensemble de résultats complètement différent avec votre exemple:

Même ensemble de fichiers:

$ 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

Des résultats radicalement différents avec votre code:

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

Avec un shell glob, cela fonctionne parfaitement bien:

$ for f in *; do stat --format='%n' "./$f"; done
./a b
./a  b
./ab
./ab
./a b
./a
b

La raison pour laquelle bash se comporte de cette manière remonte à l'un des points que j'ai mentionnés au début de la réponse: "Le fichier glob peut correspondre à plus d'un fichier".

lsrenvoie le même glob ( a?b) pour plusieurs fichiers, donc chaque fois que nous développons ce glob, nous obtenons chaque fichier correspondant.


Comment recréer la liste des fichiers que j'utilisais:

touch 'a b' 'a  b' a$'\xe2\x80\x82'b a$'\xe2\x80\x83'b a$'\t'b a$'\n'b

Les codes hexadécimaux sont des caractères UTF-8 NBSP.


5
@ mikeserv en fait, sa solution ne renvoie pas de glob. Je viens de mettre à jour ma réponse pour clarifier ce point.
Patrick

18
"Pas le reste"? C'est un comportement incohérent et des résultats inattendus, en quoi ce n'est pas une raison?
Patrick

11
@mikeserv N'avez-vous pas vu mon commentaire sur votre question? Shell globbing est 2,5 fois plus rapide que ls. J'ai également demandé que vous testiez votre code car il ne fonctionne pas. Qu'est-ce que zsh a à voir avec tout ça?
Patrick

27
@mikeserv Non, tout s'applique encore, même à bash. Bien que j'en ai fini avec cette question, vous n'écoutez pas ce que je dis.
Patrick

7
Vous savez quoi, je pense que je vais upvoter cette réponse et clarifier dans la mienne que je suis d'accord avec tout ce qu'il dit. ;-)
zwol

54

Essayons de simplifier un peu:

$ 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

Voir? C'est déjà faux juste là. Il y a 3 fichiers mais bash en signale 4. C'est parce setqu'on lui donne les globs générés lsqui sont développés par le shell avant d'être passés à set. C'est pourquoi vous obtenez:

$ 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

Ou, si vous préférez:

$ 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

Ce qui précède a été exécuté bash 4.2.45.


2
J'ai voté pour cela. C'est bien de voir votre propre code vous piquer. Mais ce n'est pas parce que je me suis trompé que cela ne peut pas être fait correctement. Je vous ai montré un moyen très simple de le faire ce matin avec ls -1qRi | grep -o '^ *[0-9]*'- cela consiste à analyser la lssortie, mec, et c’est le moyen le plus rapide et le meilleur que je connaisse pour obtenir une liste de numéros d’inodes.
mikeserv

38
@ mikeserv: Cela pourrait être fait correctement, si vous avez le temps et la patience. Mais le fait est que c'est intrinsèquement sujet aux erreurs. Vous vous êtes mal compris. en discutant de ses mérites! C'est une énorme frappe contre elle, si même la seule personne qui se bat pour elle ne le fait pas correctement. Et les chances sont, vous passerez probablement encore plus de temps à se tromper avant de faire les choses correctement. Je ne sais pas pour vous, mais la plupart des gens ont plus à faire avec leur temps que de tripoter pendant des siècles avec la même ligne de code.
cHao

@cHao - je n'ai pas discuté de ses mérites - j'ai protesté contre sa propagande.
mikeserv

16
@mikeserv: Les arguments à son encontre sont fondés et mérités. Même vous leur avez montré pour être vrai.
cHao

1
@ cHao - je ne suis pas d'accord. Il y a une frontière pas si fine entre un mantra et une sagesse.
mikeserv

50

La sortie de ls -qn'est pas un globe du tout. Cela ?veut dire "Il y a un caractère ici qui ne peut pas être affiché directement". Globs ?voulait dire "n'importe quel caractère est autorisé ici".

Les globes ont d'autres caractères spéciaux ( *et []au moins, et à l'intérieur de la []paire, il y en a plus). Aucun de ceux-ci n'est échappé ls -q.

$ touch x '[x]'
$ ls -1q
[x]
x

Si vous traitez la ls -1qsortie, il y a un ensemble de globs et les développez, non seulement vous obtiendrez xdeux fois, mais vous manquerez [x]complètement. En tant que glob, il ne correspond pas à une chaîne.

ls -q est conçu pour protéger vos yeux et / ou votre terminal des personnages loufoques, et non pour produire quelque chose que vous pouvez renvoyer à la coque.


42

La réponse est simple: les cas particuliers que lsvous devez gérer l'emportent sur les avantages éventuels. Ces cas particuliers peuvent être évités si vous n'analysez pas la lssortie.

Le mantra ici est de ne jamais faire confiance au système de fichiers de l'utilisateur (l'équivalent de ne jamais faire confiance aux entrées de l'utilisateur ). S'il existe une méthode qui fonctionnera toujours, avec une certitude de 100%, ce devrait être la méthode que vous préférez, même si c'est lsla même chose mais avec moins de certitude. Je n'entrerai pas dans les détails techniques, ceux-ci ayant été largement couverts par terdon et Patrick . Je sais qu’en raison des risques liés à l’utilisation lsd’une transaction importante (et peut-être coûteuse) dans laquelle mon travail / mon prestige est en jeu, je préférerai toute solution ne comportant pas un degré d’incertitude si elle peut être évitée.

Je sais que certaines personnes préfèrent certains risques à la certitude , mais j'ai déposé un rapport de bogue .


33

La raison pour laquelle les gens disent de ne jamais faire quelque chose n'est pas nécessairement parce que cela ne peut absolument pas être fait correctement. Nous pourrons peut-être le faire, mais ce sera peut-être plus compliqué, moins efficace, que ce soit en termes d'espace ou de temps. Par exemple, il serait parfaitement correct de dire "Ne construisez jamais un backend e-commerce volumineux dans un assemblage x86".

Passons maintenant au problème: comme vous l’avez démontré, vous pouvez créer une solution qui analyse et donne le résultat voulu. L’exactitude n’est donc pas un problème.

Est-ce plus compliqué? Oui, mais nous pouvons cacher cela derrière une fonction d'assistance.

Alors maintenant à l'efficacité:

Gain d'espace: votre solution repose sur uniqle filtrage des doublons. Par conséquent, nous ne pouvons pas générer les résultats paresseusement. Donc, soit O(1)contre O(n)ou les deux ont O(n).

Gain de temps: dans le meilleur des cas, on uniqutilise une approche de hachage, de sorte que nous avons toujours un O(n)algorithme dans le nombre d'éléments obtenus , probablement bien O(n log n).

Maintenant le vrai problème: bien que votre algorithme n’ait toujours pas l’air mauvais, j’ai vraiment fait attention à utiliser des éléments obtenus et non des éléments pour n. Parce que cela fait une grande différence. Supposons que vous ayez un fichier \n\nqui donnera un glob pour ??correspondre à tous les fichiers de 2 caractères de la liste. Bizarrement, si vous avez un autre fichier \n\rqui aboutira ??et renverra également tous les fichiers de 2 caractères .. voyez où cela se passe? Le comportement exponentiel au lieu du comportement linéaire peut certainement être qualifié de «pire comportement à l'exécution». C'est la différence entre un algorithme pratique et un algorithme sur lequel vous écrivez des articles dans des revues théoriques CS.

Tout le monde aime les exemples, non? Et c'est parti. Créez un dossier appelé "test" et utilisez ce script python dans le même répertoire que le dossier.

#!/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()

La seule chose à faire est de générer tous les produits de longueur 3 pour 7 caractères. Les mathématiques au lycée nous disent qu'il devrait y avoir 343 fichiers. Cela devrait être très rapide à imprimer, alors voyons:

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

Essayons maintenant votre première solution, car je ne peux vraiment pas obtenir ceci.

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)

chose ici pour travailler sur Linux mint 16 (ce qui en dit long sur la facilité d’utilisation de cette méthode).

Quoi qu'il en soit, puisque ce qui précède ne filtre que le résultat après l'avoir obtenu, la solution précédente devrait être au moins aussi rapide que la dernière (aucune astuce d'inode dans celle-ci - mais celles-ci ne sont pas fiables, vous abandonneriez la correction).

Alors maintenant combien de temps

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

prendre? Eh bien, je ne sais vraiment pas, il faut un certain temps pour vérifier les noms de fichiers 343 ^ 343 - je vous le dirai après la mort de l'univers par la chaleur.


6
Bien sûr, comme mentionné dans les commentaires sous une autre réponse , l'affirmation "... vous avez démontré que vous pouvez créer une solution qui analyse ls et donne le bon résultat ..." est en réalité fausse.
Wildcard

26

Intention déclarée de l'OP adressée

Préface et justification de la réponse originale Mis à jour le 2015-05-18

Dans la dernière mise à jour de sa question, mikeserv (le PO) a déclaré: «J’estime dommage que j’ai d’abord posé cette question pour signaler une source de désinformation et, malheureusement, la réponse la plus votée est en grande partie trompeuse. "

Bien, OK; Je pense qu'il était un peu dommage que j'ai passé tellement de temps à essayer de comprendre comment expliquer mon sens pour constater que que je relis la question. Cette question a fini par "[générer] une discussion plutôt que des réponses" et a fini par peser à environ 18K de texte (pour la question seule, pour que tout soit clair), ce qui serait long même pour un article de blog.

Mais StackExchange n'est pas votre tribune et ce n'est pas votre blog. Cependant, en réalité, vous l'avez utilisé au moins un peu des deux. Les gens finissaient par passer beaucoup de temps à répondre à votre question au lieu de répondre à leurs questions. À ce stade, je signalerai que la question ne convient pas à notre format, étant donné que le PO a explicitement indiqué qu'il ne s'agissait même pas du tout d'une question.

À ce stade, je ne suis pas sûr si ma réponse était exacte ou non; probablement pas, mais cela visait certaines de vos questions et cela pourrait peut-être être une réponse utile à quelqu'un d'autre; les débutants prennent courage, certains de ces "ne" deviennent pas "faire" parfois une fois que vous avez plus d'expérience. :)

En règle générale...

veuillez pardonner les aspérités restantes; J'ai déjà passé beaucoup trop de temps là-dessus ... plutôt que de citer directement l'OP (comme prévu à l'origine), je vais essayer de résumer et de paraphraser.

[en grande partie retravaillée à partir de ma réponse initiale]
après examen, je crois avoir mal interprété l'accent mis par le PO sur les questions auxquelles j'ai répondu; cependant, les points abordés ont été soulevés et j'ai laissé les réponses en grande partie intactes, car je pense qu'elles sont claires et qu'elles abordent des problèmes que j'ai vus soulevés dans d'autres contextes, en ce qui concerne les conseils aux débutants.

Le message original demandait, de plusieurs manières, pourquoi différents articles donnaient des conseils tels que «Ne pas analyser la lssortie» ou «Vous ne devriez jamais analyser la lssortie», etc.

Ma solution suggérée au problème est que les exemples de ce type d’énoncé ne sont que des exemples d’idiomes, formulés de manière légèrement différente, dans lesquels un quantificateur absolu est associé à un impératif [par exemple, «ne jamais [jamais] X», «[Vous devriez] toujours Y», «[il ne faut jamais» Z »] pour former des énoncés destinés à être utilisés comme règles générales ou directives, en particulier lorsqu'ils sont donnés à des personnes novices, plutôt que comme des vérités absolues, forme apparente de ces déclarations nonobstant.

Lorsque vous commencez à apprendre de nouveaux sujets, et à moins de bien comprendre pourquoi vous devez faire autrement, il est judicieux de simplement suivre les règles générales acceptées sans exception - à moins que vous ne soyez guidé par quelqu'un de plus expérimenté. que vous-même. Avec de plus en plus de compétences et d'expérience, vous serez en mesure de déterminer quand et si une règle s'applique dans une situation donnée. Une fois que vous aurez atteint un niveau d’expérience significatif, vous comprendrez probablement le raisonnement qui sous-tend la règle générale et vous pourrez alors commencer à utiliser votre jugement pour déterminer si et à quel niveau les motifs de la règle s’appliquent cette situation et aussi s’il existe peut-être des préoccupations primordiales.

Et c'est à ce moment-là qu'un expert, peut-être, pourrait choisir de faire des choses en violation du "Règlement". Mais cela ne les rendrait pas moins "Les règles".

Et donc, pour le sujet à traiter: à mon avis, juste parce qu’un expert peut violer cette règle sans se faire complètement abattre, je ne vois aucune façon de justifier que vous disiez à un débutant que "parfois", c’est ok pour analyser la lssortie, parce que: ce n'est pas . Ou du moins, certainement, ce n'est pas correct pour un débutant.

Vous mettez toujours vos pions au centre; dans le premier morceau, un mouvement; château à la première occasion; les chevaliers avant les évêques; un chevalier sur le rebord est sinistre; et assurez-vous toujours que vous pouvez voir votre calcul jusqu'au bout! (Oups, désolé, je suis fatigué, c'est pour les échecs StackExchange.)

Règles, destinées à être brisées?

Lorsque vous lisez un article sur un sujet qui est destiné aux débutants ou sur lequel il est susceptible d'être lu, vous verrez souvent des choses comme ceci:

  • "Tu ne devrais jamais faire X."
  • "Ne fais jamais Q!"
  • "Ne fais pas Z."
  • "On devrait toujours faire Y!"
  • "C, peu importe quoi."

Bien que ces déclarations semblent certes énoncer des règles absolues et intemporelles, elles ne le sont pas; à la place, c’est une façon d’énoncer des règles générales [c.-à-d. «directives», «règles générales», «les bases», etc.] qui est du moins sans doute une façon appropriée de les énoncer pour les débutants qui pourraient lire ces articles. Cependant, juste parce qu’elles sont énoncées comme absolues, les règles ne lient certainement pas les professionnels et les experts [qui sont probablement ceux qui ont résumé ces règles au départ - comme moyen de consigner et de transmettre les connaissances acquises lorsqu’elles ont été traitées de manière récurrente. questions dans leur métier particulier.]

Ces règles ne vont certainement pas révéler la manière dont un expert traiterait un problème complexe ou nuancé dans lequel, par exemple, ces règles sont en conflit; ou dans lequel les préoccupations qui ont conduit à la règle en premier lieu ne s'appliquent tout simplement pas. Les experts n'ont pas peur (ou ne devraient pas avoir peur!) De simplement enfreindre les règles qu'ils savent ne pas avoir de sens dans une situation donnée. Les experts doivent constamment trouver un équilibre entre les risques et les préoccupations de leur métier et doivent fréquemment user de leur jugement pour choisir de ne pas respecter ce type de règles. Ils doivent équilibrer divers facteurs et ne pas être en mesure de se fier à un tableau de règles à suivre. Prenons Gotoun exemple: il y a eu un long débat récurrent sur leur nocivité. (Ouais, ne jamais utiliser gotos.; D)

Une proposition modale

Une caractéristique étrange, du moins en anglais, et j'imagine dans de nombreuses autres langues, de règles générales, est qu'elles sont énoncées sous la même forme qu'une proposition modale, alors que les experts dans un domaine sont disposés à donner une règle générale tout en sachant qu’ils enfreindront la règle le cas échéant. Il est donc clair que ces instructions ne sont pas censées être équivalentes aux mêmes instructions de la logique modale.

C'est pourquoi je dis qu'ils doivent simplement être idiomatiques. Plutôt que d'être véritablement une situation "jamais" ou "toujours", ces règles servent généralement à codifier des orientations générales qui ont tendance à être appropriées dans un large éventail de situations et qui, lorsque les débutants les suivent aveuglément, risquent de se traduire par des de meilleurs résultats que le débutant qui choisit de les affronter sans bonne raison. Parfois, ils codifient des règles menant simplement à des résultats inférieurs aux normes plutôt qu’aux échecs absolus qui accompagnent des choix incorrects lorsqu’on les enfreint.

Ainsi, les règles générales ne sont pas les propositions modales absolues qu’elles semblent être à la surface, mais plutôt un moyen simple de donner la règle avec un passe-partout standard, comme ce qui suit:

sauf si vous avez la possibilité de dire que cette directive est incorrecte dans un cas particulier et de vous prouver que vous avez raison, alors $ {RULE}

où, bien sûr, vous pouvez remplacer "jamais d'analyser la lssortie" à la place de $ {RULE}. :)

Oh oui! Qu'en est -il de lals sortie d' analyse ?

Eh bien, alors, vu tout cela ... je pense qu'il est assez clair que cette règle est bonne. Tout d'abord, il faut comprendre que la vraie règle est idiomatique, comme expliqué ci-dessus ...

Mais en plus, il n’est pas seulement nécessaire d’être très bon avec les scripts shell pour savoir s’il peut être cassé, dans certains cas. C'est aussi qu'il faut autant de talent pour dire que vous vous êtes trompé lorsque vous essayez de le casser en test! Et, je dis en toute confiance qu'une très grande majorité du public probable de tels articles (donnant des conseils tels que «Ne pas analyser la sortie de ls!») Ne peut pas faire ces choses , et ceux qui ont une telle compétence se rendront probablement compte que ils s'en rendent compte par eux-mêmes et ignorent la règle quand même.

Mais ... regardez juste cette question, et comment même les personnes qui ont probablement les compétences ont pensé que c'était un mauvais appel à le faire; et combien d'efforts l'auteur de la question a-t-il dépensé pour atteindre le meilleur exemple actuel! Je vous garantis que sur un problème difficile, 99% des gens se tromperaient et avec des résultats potentiellement très mauvais! Même si la méthode choisie se révèle bonne; tant que cette lsidée d'analyse (ou une autre) ne sera pas adoptée par l'ensemble des informaticiens / développeurs, résistera à de nombreux tests (en particulier à l'épreuve du temps) et, finalement, parviendra à obtenir le statut de «technique commune», il est probable qu'un Beaucoup de gens pourraient essayer, et se tromper ... avec des conséquences désastreuses.

Donc, je vais répéter une dernière fois ... que, en particulier dans ce cas , c’est pourquoi " ne jamais analyser la lssortie!" est décidément la bonne façon de le formuler.

[MISE À JOUR 2014-05-18: clarification du raisonnement de la réponse (ci-dessus) pour répondre à un commentaire de OP; l'ajout suivant est une réponse aux ajouts du PO à la question d'hier]

[MISE À JOUR 2014-11-10: ajout d'en-têtes et de contenu réorganisé / refactoré; et aussi: reformater, reformuler, clarifier, et euh ... "concis-ifying" ... ... je voulais que ce soit simplement un nettoyage, bien que cela se soit transformé en une retouche. Je l'avais laissée dans un état déplorable, j'ai donc surtout essayé de lui donner un ordre. j’ai pensé qu’il était important de laisser en grande partie la première section intacte; il n'y a donc que deux modifications mineures: redondant 'mais' supprimé et 'cela' souligné.]

† Au départ, je voulais utiliser ceci uniquement à titre de clarification de mon original; mais a décidé d'autres ajouts après réflexion

‡ voir https://unix.stackexchange.com/tour pour des instructions sur les publications


2
Jamais n'est pas idiomatique. Ce n'est une réponse à rien.
mikeserv

1
Hmm. Eh bien, je ne savais pas si cette réponse serait satisfaisante, mais je ne m'attendais absolument pas à une controverse . Et, je n'ai pas (eu l'intention de) dire que «jamais» était en soi idiomatique; mais que "Ne fais jamais X!" est une utilisation idiomatique . Je vois deux cas généraux qui peuvent montrer que "Ne jamais analyser / ne pas analyser ls!" Un conseil correct: 1. Démontrez (à votre satisfaction) que chaque cas d'utilisation où l'on pourrait analyser une lssortie dispose d'une autre solution disponible, supérieure d'une manière ou d'une autre, sans le faire. 2. montrer que, dans les cas cités, la déclaration n’est pas littérale.
Shelleybutterfly

En repensant à votre question, je constate que vous parlez d’abord de «ne pas…» plutôt que de «jamais…», ce qui entre bien dans votre analyse. Je vais donc clarifier ce point également. À ce stade, il existe déjà une solution du premier type, qui a apparemment été démontrée / expliquée à votre satisfaction, je ne vais donc pas y beaucoup approfondir. Mais je vais essayer de clarifier un peu ma réponse: comme je l'ai dit, je n'essayais pas d'être controversée (ni conflictuelle!), Mais de préciser comment ces déclarations sont généralement destinées.
Shelleybutterfly

1
Je devrais nettoyer ce post. Pourtant, jamais n'est pas la bonne façon de le formuler. C'est un peu ridicule que les gens pensent qu'ils sont qualifiés pour dire aux autres ce qu'ils ne font jamais ou ne leur disent pas - dites-leur simplement que vous ne pensez pas que cela fonctionnera et pourquoi, mais vous savez ce qui fonctionnera et pourquoi. lsest un utilitaire informatique - vous pouvez analyser la sortie de l'ordinateur.
mikeserv

1
Eh bien, j’ai inversé mon vote négatif parce que, à tout le moins, vous avez raison en ce qui concerne la question des drapeaux. Je vais essayer de tout nettoyer ce soir ou demain. Mon idée est que je vais déplacer la plupart des exemples de code vers une réponse, je suppose. Mais, dans la mesure où cela m’intéresse, il n’excuse toujours pas les inexactitudes de cet article de blog souvent cité. J'aimerais que les gens arrêtent de citer le manuel bash - du moins pas après avoir cité les spécifications POSIX ...
mikeserv

16

Est-il possible d'analyser la sortie de lsdans certains cas? Sûr. L'idée d'extraire une liste de numéros d'inodes à partir d' un répertoire est un bon exemple - si vous savez que votre de mise en œuvre des lssupports -q, et donc chaque fichier produira exactement une ligne de sortie, et tout ce dont vous avez besoin sont les numéros d'inodes, les analyse sur ls -Rai1qla sortie est certainement une solution possible. Bien sûr, si l'auteur n'avait pas vu un conseil du genre "Ne jamais analyser la sortie de ls" auparavant, il ne penserait probablement pas aux noms de fichiers contenant des nouvelles lignes, et laisserait probablement le "q" en conséquence, et le code serait subtilement cassé dans ce cas limite - ainsi, même dans les cas où lsla sortie de l' analyse syntaxique est raisonnable, ce conseil est toujours utile.

Le point le plus large est que, lorsqu'un débutant en scripts shell essaie de faire comprendre à un script quel est le fichier le plus volumineux d'un répertoire ou le fichier le plus récemment modifié dans un répertoire, son premier instinct est d'analyser lsle fichier . output - compréhensible, car lsest l’une des premières commandes apprises par les débutants.

Malheureusement, cet instinct est faux et cette approche est rompue. Encore plus malheureusement, il est subtilement cassé - cela fonctionnera la plupart du temps, mais échouera dans les cas extrêmes qui pourraient peut-être être exploités par une personne connaissant le code.

Le débutant pourrait penser ls -s | sort -n | tail -n 1 | awk '{print $2}'à un moyen d’obtenir le plus gros fichier d’un répertoire. Et ça marche, jusqu’à ce que vous ayez un fichier avec un espace dans le nom.

OK, alors ls -s | sort -n | tail -n 1 | sed 's/[^ ]* *[0-9]* *//'? Fonctionne bien jusqu'à ce que vous ayez un fichier avec une nouvelle ligne dans le nom.

L'ajout -qd' lsarguments à aide en cas de nouvelle ligne dans le nom du fichier. Cela peut ressembler à cela, jusqu'à ce que vous ayez 2 fichiers différents contenant un caractère non imprimable au même endroit du nom du fichier, et que lsla sortie ne vous permette pas de distinguer lequel de ceux-ci était le plus grand. Pire encore, pour élargir le "?", Il a probablement recours à celui de son shell, evalce qui posera problème s'il frappe un fichier nommé, par exemple:

foo`/tmp/malicious_script`bar

Est-ce que ça --quoting-style=shellaide (si votre lssoutien le soutient)? Non, affiche toujours? pour les caractères non imprimables, il est donc toujours ambigu de savoir lequel des multiples correspondances était le plus gros. --quoting-style=literal? Non, pareil. --quoting-style=localeou --quoting-style=cpeut aider si vous avez juste besoin d'imprimer le nom du plus gros fichier sans ambiguïté, mais probablement pas si vous devez faire quelque chose avec le fichier par la suite - ce serait un tas de code pour annuler la citation et revenir au vrai nom de fichier afin que vous pouvez le transmettre à, par exemple, gzip.

Et à la fin de tout ce travail, même si ce qu'il a est sûr et correct pour tous les noms de fichiers possibles, il est illisible et incontrôlable, et aurait pu être effectué beaucoup plus facilement, en toute sécurité et de manière lisible, en python, en perl ou en ruby.

Ou même en utilisant d’autres outils de coquillage, je pense que cela devrait suffire:

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

Et devrait être au moins aussi portable qu’il --quoting-styleest.


Oh vrai sur la taille - je pourrais probablement le faire si j'essayais - devrais-je? Je suis un peu fatigué ou tout ce genre de chose - j'aime votre réponse parce que vous ne dites pas ne pouvez pas ou ne faites jamais ou jamais mais donnez en fait des exemples de peut-être pourquoi pas et comparable sinon - merci.
mikeserv

Je pense que si vous essayiez, vous découvririez que c'est beaucoup plus difficile que vous ne le pensez. Donc, oui, je recommanderais d'essayer. Je serai heureux de continuer à donner des noms de fichiers qui se briseront pour vous aussi longtemps que je pourrai y penser. :)
godlygeek

Les commentaires ne sont pas pour une discussion prolongée; cette conversation a été déplacée pour discuter .
terdon

@ mikeserv et godlygeek, j'ai déplacé ce fil de commentaire pour discuter . Merci de ne pas avoir de longues discussions comme celle-ci dans les commentaires, c'est à cela que sert le chat.
terdon
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.