"Vou iterar de qualquer maneira, por que não usar ls?"

"Vou iterar de qualquer maneira, por que não usar ls?"

Eu sempre vejo respostas citandoesse linkafirmando definitivamente"Não analise ls!"Isso me incomoda por alguns motivos:

  1. Parece que as informações nesse link foram aceitas no atacado sem dúvidas, embora eu possa identificar pelo menos alguns erros na leitura casual.

  2. Parece também que os problemas indicados nesse link não despertaram nenhum desejo de encontrar uma solução.

Do primeiro parágrafo:

...quando você pede [ls]uma lista de arquivos, há um grande problema: o Unix permite quase qualquer caractere em um nome de arquivo, incluindo espaços em branco, novas linhas, vírgulas, símbolos de barra vertical e praticamente qualquer outra coisa que você tente usar como um delimitador exceto NUL. ... lssepara nomes de arquivos com novas linhas. Isso é bom até que você tenha um arquivo com uma nova linha no nome. E como não conheço nenhuma implementação lsque permita encerrar nomes de arquivos com caracteres NUL em vez de novas linhas, isso nos impede de obter uma lista de nomes de arquivos com segurança com ls.

Que chatice, certo? Comosemprepodemos lidar com um conjunto de dados listado terminado por nova linha para dados que podem conter novas linhas? Bem, se as pessoas que respondem às perguntas neste site não fizessem esse tipo de coisa diariamente, eu poderia pensar que estávamos em apuros.

A verdade é que a maioria lsdas implementações fornece uma API muito simples para analisar sua saída e todos nós temos feito isso o tempo todo, mesmo sem perceber. Você não apenas pode terminar um nome de arquivo com null, mas também pode começar um com null ou com qualquer outra string arbitrária que desejar. Além do mais, você pode atribuir essas strings arbitráriaspor tipo de arquivo. Por favor considere:

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

Veressepara mais.

Agora é a próxima parte deste artigo que realmente me impressiona:

$ 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

O problema é que, a partir da saída de ls, nem você nem o computador podem dizer quais partes dele constituem um nome de arquivo. É cada palavra? Não. É cada linha? Não. Não há resposta correta para esta pergunta além de: você não pode dizer.

Observe também como lsàs vezes distorce os dados do nome do arquivo (no nosso caso, ele transformou o \ncaractere entre as palavras"a"e "nova linha"dentro de?ponto de interrogação...

...

Se você quiser apenas iterar todos os arquivos no diretório atual, use um forloop e um glob:

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

O autor chama issonomes de arquivos distorcidosquando lsretorna uma lista de nomes de arquivos contendo shell globse entãorecomenda usar um shell glob para recuperar uma lista de arquivos!

Considere o seguinte:

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 defineos operandos -1e -q lsassim:

-q- Força cada instância de caracteres de nome de arquivo não imprimíveis e s a serem escritos como o caractere <tab>de ponto de interrogação ( ). '?'As implementações podem fornecer esta opção por padrão se a saída for para um dispositivo terminal.

-1-(O dígito numérico um.)Força a saída a ser uma entrada por linha.

Globbing tem seus próprios problemas - os ?fósforosqualquercaractere para que vários ?resultados correspondentes em uma lista correspondam ao mesmo arquivo várias vezes. Isso é facilmente resolvido.

Embora como fazer isso não seja o ponto - afinal, não é preciso muito para fazer e é demonstrado abaixo - eu estava interessado empor que não. A meu ver, a melhor resposta a essa pergunta foi aceita. Eu sugiro que você tente se concentrar mais frequentemente em dizer às pessoas o que elaspodefazer do que no que elesnão pode.É muito menos provável, como eu penso, que você esteja errado, pelo menos.

Mas por que tentar? Reconheço que minha principal motivação foi que os outros continuavam me dizendo que eu não conseguiria. Eu sei muito bem que lsa saída é tão regular e previsível quanto você poderia desejar, desde que você saiba o que procurar. A desinformação me incomoda mais do que a maioria das coisas.

A verdade é que, com a notável exceção das respostas de Patrick e Wumpus Q. Wumbley(apesar do controle incrível deste último), considero a maioria das informações nas respostas aqui como corretas - um shell glob é mais simples de usar e geralmente mais eficaz quando se trata de pesquisar o diretório atual do que de análise ls. Contudo, não são, pelo menos a meu ver, razão suficiente para justificar a propagação da desinformação citada no artigo acima, nem são uma justificação aceitável para "nunca analise ls."

Observe que os resultados inconsistentes da resposta de Patrick são principalmente resultado do uso dele zshthen bash. zsh- por padrão - o comando de divisão de palavras não $(substitui )os resultados de maneira portátil. Então, quando ele perguntapara onde foi o resto dos arquivos?a resposta para essa pergunta ésua concha os comeu.É por isso que você precisa definir a SH_WORD_SPLITvariável ao usar zshe lidar com código shell portátil. Considero o fato de ele não ter notado isso em sua resposta como terrivelmente enganoso.

A resposta de Wumpus não vale para mim - em um contexto de lista, o ?personageméuma bola de concha. Não sei mais como dizer isso.

Para lidar com um caso de resultados múltiplos, você precisa restringir a ganância do globo. O seguinte irá apenas criar uma base de teste de nomes de arquivos horríveis e exibi-la para você:

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

SAÍDA

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

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

FILE COUNT: 12

Agora vou proteger todos os caracteres que não sejam /slash, -dash, :colonou caracteres alfanuméricos em um shell glob e depois sort -ua lista para resultados exclusivos. Isso é seguro porque lsjá guardou quaisquer caracteres não imprimíveis para nós. Assistir:

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

SAÍDA:

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'

Abaixo abordo o problema novamente, mas utilizo uma metodologia diferente. Lembre-se que - além de \0nulo - o /caractere ASCII é o único byte proibido em um nome de caminho. Deixei os globs de lado aqui e, em vez disso, combinei a -dopção especificada pelo POSIX para lse a construção também especificada pelo POSIX -exec $cmd {} +para find. Como findsó emitirá naturalmente um /em sequência, o seguinte obtém facilmente uma lista de arquivos recursiva e delimitada de forma confiável, incluindo todas as informações dentry para cada entrada. Imagine o que você poderia fazer com algo assim:

#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 -ipode ser muito útil - especialmente quando a exclusividade do resultado está em questão.

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

Esses são apenas os meios mais portáteis que consigo imaginar. Com o GNU lsvocê poderia fazer:

ls --quoting-style=WORD

E por último, aqui está um método muito mais simples deanáliselsque uso com frequência quando preciso de números de inode:

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

Isso apenas retorna números de inode - que é outra opção útil especificada pelo POSIX.

Responder1

Não estou nada convencido disso, mas vamos supor, para fins de argumentação, que vocêpoderia, se você estiver preparado para se esforçar o suficiente, analise a saída de lsmaneira confiável, mesmo diante de um "adversário" — alguém que conhece o código que você escreveu e está escolhendo deliberadamente nomes de arquivos projetados para quebrá-lo.

Mesmo se você pudesse fazer isso,ainda seria uma má ideia.

Bourne shell 1 é uma linguagem imprópria. Não deve ser usado para nada complicado, a menos que a portabilidade extrema seja mais importante do que qualquer outro fator (por exemplo autoconf).

Afirmo que se você se deparar com um problema em que a análise da saída de lsparece ser o caminho de menor resistência para um script de shell, isso é uma forte indicação de que tudo o que você está fazendo émuito complicado para ser um script de shelle você deve reescrever tudo em Perl, Python, Julia ou qualquer outrobomlinguagens de script que estão prontamente disponíveis. Como demonstração, aqui está seu último programa em 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))

Isso não tem nenhum problema com caracteres incomuns em nomes de arquivos - osaídaé ambíguo da mesma forma que a saída de lsé ambígua, mas isso não importaria em um programa "real" (ao contrário de uma demonstração como esta), que usaria o resultado os.path.join(subdir, f)diretamente.

Igualmente importante, e em total contraste com o que você escreveu, ainda fará sentido daqui a seis meses e será fácil de modificar quando você precisar fazer algo um pouco diferente. A título de ilustração, suponha que você descubra a necessidade de excluir dotfiles e backups de editores e de processar tudo em ordem alfabética por nome 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))

1 Sim, versões estendidas do shell Bourne estão disponíveis hoje em dia: bashe zshsão consideravelmente melhores que o original. As extensões GNU para os principais "utilitários shell" (find, grep, etc.) também ajudam muito. Mas mesmo com todas as extensões, o ambiente shell não foi melhoradosuficientepara competir com linguagens de script que são realmente boas, então meu conselho continua sendo "não use shell para nada complicado", independentemente de qual shell você esteja falando.

"Como seria um bom shell interativo que também fosse uma boa linguagem de script?" é uma questão de pesquisa em tempo real, porque há uma tensão inerente entre as conveniências exigidas para uma CLI interativa (como ter permissão para digitar cc -c -g -O2 -o foo.o foo.cem vez de subprocess.run(["cc", "-c", "-g", "-O2", "-o", "foo.o", "foo.c"])) e as restrições necessárias para evitar erros sutis em scripts complexos (comonãointerpretando palavras sem aspas em locais aleatórios como literais de string). Se eu tentasse projetar algo assim, provavelmente começaria colocando IPython, PowerShell e Lua em um liquidificador, mas não tenho ideia de como seria o resultado.

Responder2

Esse link é muito referenciado porque a informação é totalmente precisa e já existe há muito tempo.


lssubstitui caracteres não imprimíveis por caracteres glob, sim, mas esses caracteres não estão no nome do arquivo real. Por que isso importa? 2 razões:

  1. Se você passar esse nome de arquivo para um programa, esse nome de arquivo não existirá de fato. Seria necessário expandir o globo para obter o nome real do arquivo.
  2. O arquivo glob pode corresponder a mais de um arquivo.

Por exemplo:

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

Observe como temos 2 arquivos que parecem exatamente iguais. Como você vai distingui-los se ambos são representados como a?b?


O autor chama isso de nomes de arquivos distorcidos quando ls retorna uma lista de nomes de arquivos contendo shell globs e então recomenda usar um shell glob para recuperar uma lista de arquivos!

Há uma diferença aqui. Quando você recupera um glob, como mostrado, esse glob pode corresponder a mais de um arquivo. No entanto, quando você itera pelos resultados que correspondem a um glob, você obtém o arquivo exato, não um glob.

Por exemplo:

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

Observe como a xxdsaída mostra que $filecontinha os caracteres brutos \te \n, não ?.

Se você usar ls, você obterá isto:

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

"Vou iterar de qualquer maneira, por que não usar ls?"

Seu exemplo que você deu realmente não funciona. Parece que funciona, mas não funciona.

Estou me referindo a isso:

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

Eu criei um diretório com vários nomes de arquivos:

$ 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

Quando executo seu código, recebo isto:

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

Para onde foi o resto dos arquivos?

Vamos tentar isso:

$ 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

Agora vamos usar um globo real:

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

Com festa

O exemplo acima foi com meu shell normal, zsh. Quando repito o procedimento com o bash, obtenho outro conjunto de resultados completamente diferente com o seu exemplo:

Mesmo conjunto de arquivos:

$ 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 com seu 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

Com um shell glob, funciona perfeitamente bem:

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

A razão pela qual o bash se comporta dessa maneira remonta a um dos pontos que mencionei no início da resposta: "O arquivo glob pode corresponder a mais de um arquivo".

lsestá retornando o mesmo glob ( a?b) para vários arquivos, então cada vez que expandimos esse glob, obtemos todos os arquivos que correspondem a ele.


Como recriar a lista de arquivos que eu estava usando:

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

Os códigos hexadecimais são caracteres UTF-8 NBSP.

Responder3

A saída de ls -qnão é um globo. Costuma ?significar "Há um caractere aqui que não pode ser exibido diretamente". Globs ?significam "Qualquer caractere é permitido aqui".

Globs têm outros caracteres especiais ( *e []pelo menos dentro do []par há mais). Nenhum deles escapa ls -q.

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

Se você tratar a ls -1qsaída, há um conjunto de globs e expandi-los, além de obter xo dobro, você perderá [x]completamente. Como um glob, ele não corresponde a uma string.

ls -qdestina-se a salvar seus olhos e/ou terminal de personagens malucos, não a produzir algo que você possa enviar de volta ao shell.

Responder4

A resposta é simples: os casos especiais com os quais lsvocê precisa lidar superam qualquer benefício possível. Esses casos especiais podem ser evitados se você não analisar lsa saída.

O mantra aqui énunca confie no sistema de arquivos do usuário(o equivalente anunca confie na entrada do usuário). Se existe um método que funcionará sempre, com 100% de certeza, deve ser o método que você prefere, mesmo que lsfaça o mesmo, mas com menos certeza. Não entrarei em detalhes técnicos, pois estes foram abordados porterdonePatrícioextensivamente. Sei que devido aos riscos de usar lsem uma transação importante (e talvez cara) onde meu trabalho/prestígio está em jogo, preferirei qualquer solução que não tenha grau de incerteza se puder ser evitada.

Eu sei que algumas pessoas preferemalgum risco sobre a certeza, masEu registrei um relatório de bug.

informação relacionada