Por que * não * analisa `ls` (e o que fazer)?

204

Eu sempre vejo respostas citando este link dizendo definitivamente "Não analise ls!" Isso me incomoda por alguns motivos:

  1. Parece que as informações nesse link foram aceitas no atacado com poucas perguntas, embora eu possa detectar pelo menos alguns erros na leitura casual.

  2. Também parece que os problemas mencionados nesse link não provocaram desejo de encontrar uma solução.

Do primeiro parágrafo:

... quando você solicita [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 pipe e praticamente qualquer outra coisa que você já tentou usar como um arquivo. delimitador, exceto NUL. ... lssepara nomes de arquivos com novas linhas. Tudo bem até você ter um arquivo com uma nova linha em seu nome. E como eu não conheço nenhuma implementação lsque permita que você encerre nomes de arquivos com caracteres NUL em vez de novas linhas, isso nos deixa incapazes de obter uma lista de nomes de arquivos com segurança ls.

Que chatice, certo? Como nunca podemos lidar com uma nova linha terminada conjunto de dados coletados para os dados que podem conter novas linhas? Bem, se as pessoas que respondem às perguntas neste site não faziam esse tipo de coisa diariamente, acho que estávamos com algum problema.

A verdade é que a maioria das lsimplementações realmente fornece uma API muito simples para analisar sua saída e todos nós fazemos isso o tempo todo, mesmo sem perceber. Não apenas você pode terminar um nome de arquivo com nulo, como também pode começar um com nulo ou com qualquer outra sequência arbitrária que desejar. Além disso, você pode atribuir essas seqüências arbitrárias por 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^@^@^@$
^@

Veja isso para mais.

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

$ 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. São cada palavra? Não. São cada linha? Não. Não há resposta correta para essa pergunta além de: você não pode dizer.

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

...

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

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

O autor chama de nomes de arquivos ilegíveis quando lsretorna uma lista de nomes de arquivos que contêm globs de shell e recomenda o uso de 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'

O POSIX define os operandos -1e -q lsassim:

-q- Força cada instância de caracteres de nome de arquivo não imprimível <tab>es a serem gravados como o caractere de ponto de interrogação ( '?'). As implementações podem fornecer essa 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 não tem seus próprios problemas - ?corresponde a qualquer caractere, portanto vários ?resultados correspondentes em uma lista correspondem ao mesmo arquivo várias vezes. Isso é facilmente manipulado.

Embora como fazer isso não seja o ponto - afinal, não é preciso muito para fazer e é demonstrado abaixo -, eu estava interessado em saber por que não . Na minha opinião, a melhor resposta para essa pergunta foi aceita. Eu sugiro que você tente se concentrar mais em dizer às pessoas o que elas podem fazer do que o que elas não podem. Você tem muito menos probabilidade, pelo que penso, de se provar errado, pelo menos.

Mas por que tentar? É certo que minha principal motivação era que outros continuavam me dizendo que eu não podia. Sei muito bem que o lsresultado é tão regular e previsível quanto você deseja, desde que saiba o que procurar. A desinformação me incomoda mais do que a maioria das coisas.

Porém, a verdade é que, com a exceção notável das respostas de Patrick e Wumpus Q. Wumbley (apesar do incrível manuseio deste último) , considero a maioria das informações nas respostas aqui quase sempre corretas - um shell shell é mais simples de usar e geralmente mais eficaz quando se trata de pesquisar o diretório atual do que está analisando ls. No entanto, elas não são, pelo menos no meu ponto de vista, razão suficiente para justificar a propagação das informações erradas citadas no artigo acima, nem são justificativas aceitáveis ​​para " nunca analisarls " .

Por favor, note que os resultados inconsistentes da resposta de Patrick são principalmente o resultado dele usando zshentão 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 pergunta para onde foi o restante dos arquivos? a resposta para essa pergunta é que sua concha os comeu. É por isso que você precisa definir a SH_WORD_SPLITvariável ao usar zshe lidar com o código do shell portátil. Considero sua falha em notar isso em sua resposta como terrivelmente enganosa.

A resposta de Wumpus não computa para mim - em um contexto de lista, o ?personagem é uma bola de fogo. Não sei mais o que dizer.

Para lidar com um caso de vários resultados, você precisa restringir a ganância do globo. A seguir, basta criar uma base de teste com 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" $# )
}

RESULTADO

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

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

FILE COUNT: 12

Agora vou segura cada caractere que não é um /slash, -dash, :colon, ou caractere alfa-numérico em uma glob de shell, em seguida, sort -ua lista de resultados únicos. Isso é seguro porque lsjá protegemos quaisquer caracteres não imprimíveis para nós. Ver:

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

RESULTADO:

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 uso uma metodologia diferente. Lembre-se de que, além de \0nulo, o /caractere ASCII é o único byte proibido em um nome de caminho. Coloquei globs de lado aqui e, em vez disso, combine a -dopção especificada POSIX para lse também a -exec $cmd {} +construção especificada POSIX para find. Como findapenas um emitirá naturalmente /em sequência, o item a seguir obtém facilmente uma lista de arquivos recursiva e delimitada de forma confiável, incluindo todas as informações de dentista para cada entrada. Imagine o que você pode 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 -i pode 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

Estes são apenas os meios mais portáteis em que consigo pensar. Com o GNU lsvocê pode fazer:

ls --quoting-style=WORD

E por último, aqui está um método muito mais simples de analisarls que, por acaso, 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.

mikeserv
fonte
12
@mikeserv Ok, eu fiz. O globo da casca é 2,48 vezes mais 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
Em relação à sua atualização mais recente, pare de confiar na saída visual para determinar se seu código funciona. Passe sua saída para um programa real e faça com que o programa tente executar uma operação no arquivo. É por isso que eu estava usando statminha resposta, pois ela realmente verifica se cada arquivo existe. Sua parte no fundo com a sedcoisa não funciona.
1212 Patrick
57
Você não pode estar falando sério. Como saltar por todos os obstáculos que sua pergunta descreve pode ser mais fácil ou mais simples ou de alguma maneira melhor do que simplesmente não analisar lsem primeiro lugar? O que você está descrevendo é muito difícil. Vou precisar desconstruí-lo para entender tudo e sou um usuário relativamente competente. Você não pode esperar que seu Joe médio seja capaz de lidar com algo assim.
terdon
46
-1 para usar uma pergunta para escolher um argumento. Todos os motivos pelos quais a análise da lssaída está incorreta foram abordados bem no link original (e em muitos outros lugares). Essa pergunta teria sido razoável se o OP estivesse pedindo ajuda para entendê-lo, mas, em vez disso, o OP está simplesmente tentando provar que seu uso incorreto está correto.
R ..
14
@mikeserv Não é só isso parsing ls is bad. Fazer for something in $(command)e confiar na divisão de palavras para obter resultados precisos é ruim para a grande maioria dos command'squais não possui saída simples.
BroSlow

Respostas:

184

Não estou absolutamente convencido disso, mas vamos supor, por uma questão de argumento, que você poderia , se estiver preparado para fazer um esforço suficiente, analisar 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 criados para quebrá-lo.

Mesmo se você pudesse fazer isso, ainda seria uma má idéia .

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

Eu afirmo que se você se deparar com um problema em que analisar a saída lsparece ser o caminho de menor resistência para um script de shell, isso é uma forte indicação de que o que você está fazendo é muito complicado para o shell e deve reescrever tudo Perl ou Python. 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 nos nomes de arquivos - a saída é ambígua da mesma maneira que a saída lsé ambígua, mas isso não importaria em um programa "real" (em oposição a uma demonstração como essa), o que seria use o resultado de os.path.join(subdir, f)diretamente.

Igualmente importante, e em forte contraste com o que você escreveu, ainda fará sentido daqui a seis meses e será fácil modificar quando você precisar fazer algo ligeiramente diferente. A título de ilustração, suponha que você descubra a necessidade de excluir arquivos de ponto e backups de editor e 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))
zwol
fonte
5
Isso é bom. Isso for in | for infala de recursão? Não tenho certeza. Mesmo que seja, não pode ser mais do que um, certo? Esta é a única resposta que faz sentido para mim até agora.
Mikeerv #
10
Sem recursão, apenas aninhados- forloops. os.walkestá fazendo um trabalho pesado nos bastidores, mas você não precisa se preocupar com isso mais do que se preocupar com como lsou findtrabalhar internamente.
Zwol
6
Tecnicamente, os.walkretorna um objeto gerador . Geradores são a versão das listas lentas do Python. Sempre que o loop for externo itera, o gerador é chamado e "produz" o conteúdo de outro subdiretório. Funcionalidade equivalente no Perl é File::Find, se isso ajudar.
Zwol
6
Você deve estar ciente de que concordo 100% com o documento que está criticando e com as respostas de Patrick e Terdon. Minha resposta pretendia fornecer um motivo adicional e independente para evitar a análise da lssaída.
Zwol
19
Isso é muito enganador. O Shell não é uma boa linguagem de programação, mas apenas porque não é uma linguagem de programação. É uma linguagem de script. E é uma boa linguagem de script.
Route de milhas
178

Esse link é muito referenciado porque as informações são completamente precisas e existem 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 existe realmente. Teria que expandir a glob para obter o nome do arquivo real.
  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 com a mesma aparência. Como você vai distingui-los se ambos são representados como a?b?


O autor o chama de nomes de arquivos ilegíveis quando ls retorna uma lista de nomes de arquivos contendo globs de shell e recomenda o uso de um shell glob para recuperar uma lista de arquivos!

Há uma diferença aqui. Quando você recebe um glob de volta, como mostrado, esse glob pode corresponder a mais de um arquivo. No entanto, quando você itera pelos resultados correspondentes a um globo, você recebe de volta o arquivo exato, não um globo.

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ê obtém isso:

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

"Eu vou repetir de qualquer maneira, por que não usar ls?"

Seu exemplo que você deu não funciona realmente. 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 o seguinte:

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

Para onde foram os outros 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
./ab
./ab
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
./ab
./ab
./a b
./a
b

With bash

O exemplo acima foi com o meu shell normal, zsh. Quando repito o procedimento com o bash, recebo 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 o seu 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

Com uma concha glob, funciona perfeitamente bem:

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

O motivo pelo qual o bash se comporta dessa maneira remonta a um dos pontos que fiz 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; portanto, toda vez que expandimos esse glob, obtemos todos os arquivos correspondentes.


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 de código hexadecimal são caracteres UTF-8 NBSP.

Patrick
fonte
5
@mikeserv, na verdade, sua solução não retorna um globo. Acabei de atualizar minha resposta para esclarecer esse ponto.
12124 Patrick
18
"Não é o resto"? É um comportamento inconsistente e resultados inesperados, como isso não é uma razão?
12124 Patrick
11
@mikeserv Você não viu meu comentário sobre sua pergunta? O globbing da casca é 2,5 vezes mais rápido que ls. Também solicitei que você teste seu código, pois ele não funciona. O que o zsh tem a ver com isso?
1226 Patrick
27
@ mikeserv Não, tudo ainda se aplica até ao bash. Embora eu tenha acabado com essa pergunta, você não está ouvindo o que estou dizendo.
1212 Patrick
7
Quer saber, acho que vou aprovar esta resposta e esclarecer na minha que concordo com tudo o que diz. ;-)
zwol 13/05
54

Vamos tentar simplificar um pouco:

$ 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

Vejo? Isso já está errado aqui. Existem 3 arquivos, mas o bash está relatando 4. Isso ocorre porque setos globs gerados lssão expandidos pelo shell antes de serem passados ​​para set. É por isso que você obtém:

$ 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, se você preferir:

$ 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

O acima foi executado bash 4.2.45.

terdon
fonte
2
Eu votei nisso. É bom ver seu próprio código morder você. Mas só porque eu entendi errado, não significa que não possa ser feito corretamente. Eu mostrei a você uma maneira muito simples de fazer isso esta manhã com ls -1qRi | grep -o '^ *[0-9]*'- isso é analisar a lssaída, cara, e é a maneira mais rápida e melhor da qual eu conheço para obter uma lista de números de inodes.
mikeserv
38
@ MikeServ: Isso poderia ser feito corretamente, se você tiver tempo e paciência. Mas o fato é que é inerentemente propenso a erros. Você mesmo entendeu errado. enquanto discutia sobre seus méritos! É uma greve enorme contra isso, se mesmo a única pessoa que luta por ela falha em fazê-lo corretamente. E as chances são de que você provavelmente gastará ainda mais tempo errando antes de acertar. Não sei quanto a você, mas a maioria das pessoas tem mais a ver com seu tempo do que mexer por idades com a mesma linha de código.
cHao 13/05
@cHao - não discuti seus méritos - protestei sua propaganda.
mikeserv
16
@ mikeserv: Os argumentos contra ele são bem fundamentados e merecidos. Até você mostrou que eles são verdadeiros.
cHao 13/05
1
@cHao - eu discordo. Existe uma linha não tão fina entre um mantra e uma sabedoria.
mikeserv
50

A saída de ls -qnão é nada global. Usa-se ?para significar "Há um personagem aqui que não pode ser exibido diretamente". Globs costumam ?significar "Qualquer caractere é permitido aqui".

Os globos têm outros caracteres especiais ( *e []pelo menos, e dentro do []par, existem mais). Nenhum deles é escapado por ls -q.

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

Se você tratar a ls -1qsaída, há um conjunto de globs e os expande, além de receber xduas vezes, você perderá [x]completamente. Como um globo, ele não corresponde a si mesmo como uma corda.

ls -q serve para salvar seus olhos e / ou terminal de personagens malucos, não para produzir algo que você possa alimentar de volta para o shell.


fonte
42

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

O mantra aqui nunca é confiar no sistema de arquivos do usuário (o equivalente a nunca confiar na entrada do usuário ). Se houver um método que funcione sempre, com 100% de certeza, deve ser o método que você preferir, mesmo que lsfaça o mesmo, mas com menos certeza. Não vou entrar em detalhes técnicos, uma vez que foram extensivamente abordados por Terdon e Patrick . Sei que, devido aos riscos de usar lsem uma transação importante (e talvez cara) em que meu trabalho / prestígio esteja em jogo, prefiro qualquer solução que não tenha um grau de incerteza, se puder ser evitada.

Sei que algumas pessoas preferem algum risco à certeza , mas enviei um relatório de erro .

Braiam
fonte
33

A razão pela qual as pessoas dizem que nunca fazem algo não é necessariamente porque absolutamente positivamente não pode ser feito corretamente. Podemos fazê-lo, mas pode ser mais complicado, menos eficiente, tanto no espaço quanto no tempo. Por exemplo, seria perfeitamente correto dizer "Nunca crie um back-end grande para comércio eletrônico no assembly x86".

Agora, agora, ao problema em questão: como você demonstrou, pode criar uma solução que analise sl e forneça o resultado certo - portanto, a correção não é um problema.

É mais complicado? Sim, mas podemos esconder isso atrás de uma função auxiliar.

Então agora para eficiência:

Eficiência de espaço: sua solução depende da uniqfiltragem de duplicatas, consequentemente, não podemos gerar os resultados preguiçosamente. Então, O(1)vs O(n)ou ambos têm O(n).

Eficiência de tempo: o melhor caso uniqusa uma abordagem de hashmap, portanto ainda temos um O(n)algoritmo no número de elementos adquiridos , provavelmente por ser O(n log n).

Agora, o verdadeiro problema: embora seu algoritmo ainda não pareça tão ruim, tomei muito cuidado ao usar elementos adquiridos e não elementos para n. Porque isso faz uma grande diferença. Digamos que você tenha um arquivo \n\nque resultará em um globo para ??corresponder a cada arquivo de 2 caracteres na lista. O engraçado é que se você tiver outro arquivo \n\rque também resultará ??e também retorne todos os arquivos de 2 caracteres, veja para onde isso está indo? O comportamento exponencial, em vez do linear, certamente se qualifica como "pior comportamento em tempo de execução". É a diferença entre um algoritmo prático e um sobre o qual você escreve artigos em periódicos teóricos da CS.

Todo mundo adora exemplos, certo? Aqui vamos nós. Crie uma pasta chamada "test" e use este script python no mesmo diretório em que a pasta está.

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

A única coisa que isso faz é gerar todos os produtos de comprimento 3 para 7 caracteres. A matemática do ensino médio nos diz que deveriam ser 343 arquivos. Bem, isso deve ser muito rápido para imprimir, então vamos ver:

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

Agora vamos tentar sua primeira solução, porque realmente não consigo

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)

coisa aqui para trabalhar no Linux mint 16 (que eu acho que fala muito pela usabilidade desse método).

De qualquer forma, como o acima mencionado apenas filtra o resultado depois que o obtém, a solução anterior deve ser pelo menos tão rápida quanto a posterior (não há truques de inode nesse caso - mas esses não são confiáveis, então você desistiria da correção).

Então agora quanto tempo

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

toma? Bem, eu realmente não sei, leva um tempo para verificar os nomes dos arquivos 343 ^ 343 - depois da morte por calor do universo.

Voo
fonte
6
Obviamente, como mencionado nos comentários em outra resposta , a declaração "... você demonstrou que pode criar uma solução que analisa ls e fornece o resultado certo ..." na verdade não é verdadeira.
Curinga
26

A intenção declarada do OP abordada

prefácio e justificativa original da resposta atualizado em 18/05/2015

mikeserv (o OP) afirmou na última atualização de sua pergunta: " Considero uma pena, embora eu tenha feito essa pergunta pela primeira vez, uma fonte de informações erradas e, infelizmente, a resposta mais votada aqui é em grande parte enganosa. "

Bem, tudo bem; Eu sinto que era uma pena que eu gastei tanto tempo tentando descobrir como explicar meu significado apenas para descobrir isso enquanto relia a pergunta. Essa pergunta acabou "[gerando] discussão em vez de respostas" e acabou pesando cerca de 18K de texto (apenas para a pergunta, apenas para esclarecer), o que seria longo até para um post do blog.

Mas o StackExchange não é sua caixa de sabão e não é seu blog. No entanto, na verdade, você o usou como pelo menos um pouco de ambos. As pessoas acabavam gastando muito tempo respondendo ao seu "To-Point-Out" em vez de responder às perguntas reais das pessoas. Nesse ponto, sinalizarei a pergunta como não adequada para o nosso formato, já que o OP declarou explicitamente que nem sequer pretendia ser uma pergunta.

Neste ponto, não tenho certeza se minha resposta foi direta ou não; provavelmente não, mas foi direcionado para algumas de suas perguntas, e talvez possa ser uma resposta útil para outra pessoa; iniciantes se animam, alguns desses "não" se transformam em "às vezes" quando você é mais experiente. :)

Como uma regra geral...

perdoe as arestas restantes; Como já gastei muito tempo com isso ... em vez de citar o OP diretamente (como originalmente pretendido), tentarei resumir e parafrasear.

[em grande parte reformulado a partir da minha resposta original],
após consideração, acredito que li mal a ênfase que o OP estava colocando nas perguntas que respondi; no entanto, os pontos abordados foram levantados, e eu deixei as respostas em grande parte intactas, pois acredito que elas são objetivas e tratam de questões que eu já vi levantadas em outros contextos, além de conselhos aos iniciantes.

A postagem original perguntava, de várias maneiras, por que vários artigos deram conselhos como "Não analise a lssaída" ou "Você nunca deve analisar a lssaída" e assim por diante.

Minha resolução sugerida para a questão é que instâncias desse tipo de afirmação são simplesmente exemplos de um idioma, redigido de maneiras ligeiramente diferentes, nas quais um quantificador absoluto é associado a um imperativo [por exemplo, «não [nunca] X», «[Você deve] sempre Y», «[nunca deve] Z»] para formar declarações destinadas a serem usadas como regras ou diretrizes gerais, especialmente quando dadas a pessoas novas em um assunto, em vez de serem verdades absolutas. forma aparente dessas declarações, não obstante.

Quando você está começando a aprender um novo assunto, e a menos que tenha uma boa compreensão do porquê de fazer algo diferente, é uma boa idéia simplesmente seguir as regras gerais aceitas sem exceção - a menos que sob orientação de alguém mais experiente você mesmo. Com o aumento da habilidade e experiência, você se torna mais capaz de determinar quando e se uma regra se aplica a qualquer situação específica. Depois de atingir um nível significativo de experiência, você provavelmente entenderá o raciocínio por trás da regra geral e, nesse ponto, poderá começar a usar seu julgamento para determinar se e em que nível as razões por trás da regra se aplicam. nessa situação, e também sobre se há talvez preocupações preponderantes.

E é aí que um especialista, talvez, pode optar por fazer as coisas violando as "Regras". Mas isso não os tornaria menos "As Regras".

E, portanto, para o tópico em questão: na minha opinião, apenas porque um especialista pode violar essa regra sem ser completamente derrubado, não vejo como justificar dizer a um iniciante que "às vezes" é Não há problema em analisar a lssaída, porque: não é . Ou, pelo menos, certamente não é certo para um iniciante fazê-lo.

Você sempre coloca seus peões no centro; na abertura de uma peça, um movimento; castelo na primeira oportunidade; cavaleiros diante dos bispos; um cavaleiro na beira é sombrio; e sempre verifique se você pode ver seu cálculo até o final! (Opa, desculpe, ficar cansado, isso é para o StackExchange de xadrez.)

Regras devem ser quebradas?

Ao ler um artigo sobre um assunto que é direcionado ou que provavelmente será lido por iniciantes, muitas vezes você verá coisas assim:

  • "Você nunca deve fazer o X."
  • "Nunca faça Q!"
  • "Não faça Z."
  • "Deve-se sempre fazer Y!"
  • "C, não importa o quê."

Embora essas declarações certamente pareçam estar estabelecendo regras absolutas e atemporais, elas não o são; em vez disso, é uma maneira de declarar regras gerais [aka "diretrizes", "regras básicas", "o básico" etc.) que é pelo menos uma maneira apropriada de indicá-las para os iniciantes que possam estar lendo esses artigos. No entanto, apenas por serem declaradas absolutas, as regras certamente não vinculam profissionais e especialistas [que provavelmente foram os que resumiram essas regras em primeiro lugar, como uma maneira de registrar e transmitir o conhecimento adquirido à medida que lidavam com questões recorrentes. questões em seu ofício particular.]

Essas regras certamente não vão revelar como um especialista lidaria com um problema complexo ou matizado, no qual, digamos, essas regras entram em conflito entre si; ou nas quais as preocupações que levaram à regra simplesmente não se aplicam. Os especialistas não têm medo de (ou não devem ter medo de!) Simplesmente violar regras que eles sabem que não fazem sentido em uma situação específica. Os especialistas estão constantemente lidando com o equilíbrio de vários riscos e preocupações em seu ofício, e devem frequentemente usar seu julgamento para optar por violar esse tipo de regra, tendo que equilibrar vários fatores e não poder confiar apenas em uma tabela de regras a seguir. Tomemos Gotocomo exemplo: houve um longo e recorrente debate sobre se eles são prejudiciais. (Sim, nunca use gotos.; D)

Uma proposição modal

Uma característica estranha, pelo menos em inglês, e imagino em muitas outras línguas, de regras gerais, é que elas são declaradas da mesma forma que uma proposição modal, mas os especialistas em um campo estão dispostos a dar uma regra geral para uma situação, sabendo o tempo todo que eles violarão a regra quando apropriado. Claramente, portanto, essas declarações não são equivalentes às mesmas declarações na lógica modal.

É por isso que digo que eles devem ser simplesmente idiomáticos. Em vez de realmente ser uma situação de "nunca" ou "sempre", essas regras geralmente servem para codificar diretrizes gerais que tendem a ser apropriadas para uma ampla gama de situações e que, quando os iniciantes as seguem cegamente, provavelmente resultam em melhores resultados do que o iniciante escolhendo ir contra eles sem uma boa razão. Às vezes, eles codificam regras que simplesmente resultam em resultados abaixo do padrão, em vez de falhas definitivas que acompanham escolhas incorretas ao ir contra as regras.

Portanto, regras gerais não são as proposições modais absolutas que parecem estar na superfície, mas são uma maneira abreviada de fornecer à regra um clichê padrão implícito, algo como o seguinte:

a menos que você tenha a capacidade de dizer que esta diretriz está incorreta em um caso específico e provar a si mesmo que está certo, então $ {RULE}

onde, é claro, você pode substituir "nunca analisar a lssaída" no lugar de $ {RULE}. :)

Oh sim! E quanto à análise de lssaída?

Bem, então, considerando tudo isso ... eu acho que é bem claro que essa regra é boa. Primeiro de tudo, a regra real deve ser entendida como idiomática, conforme explicado acima ...

Além disso, não é apenas necessário que você seja muito bom com o script de shell para saber se ele pode ser quebrado, em alguns casos específicos. Também é preciso muita habilidade para dizer que você errou ao tentar quebrá-lo nos testes! E digo com confiança que uma grande maioria do público provável desses artigos (dando conselhos como "Não analise o resultado de ls!") Não pode fazer essas coisas , e aqueles que têm essa habilidade provavelmente perceberão que eles descobrem por conta própria e ignoram a regra de qualquer maneira.

Mas ... basta olhar para esta pergunta, e como até as pessoas que provavelmente possuem essa habilidade pensaram que era uma má decisão fazê-lo; e quanto esforço o autor da pergunta gastou apenas para chegar a um ponto do melhor exemplo atual! Garanto a você que, em um problema, 99% das pessoas errariam e com resultados potencialmente muito ruins! Mesmo que o método decidido seja bom; até que (ou outra) a lsideia de análise seja adotada pelo pessoal de TI / desenvolvedor como um todo, resista a muitos testes (especialmente o teste do tempo) e, finalmente, consiga passar para um status de 'técnica comum', é provável que um muitas pessoas podem tentar e errar ... com consequências desastrosas.

Então, vou reiterar mais uma vez .... que, especialmente neste caso , que é por isso que " não analisar lssaída!" é decididamente o caminho certo para expressá-lo.

[ATUALIZAÇÃO 18/05/2014: raciocínio esclarecido para a resposta (acima) para responder a um comentário do OP; a seguinte adição é uma resposta às adições do OP à pergunta de ontem]

[ATUALIZAÇÃO 10/11/2014: cabeçalhos adicionados e conteúdo reorganizado / refatorado; e também: reformatação, reformulação, esclarecimento e um ... "conciso-seying" ... eu pretendia que isso fosse simplesmente uma limpeza, embora tenha se transformado em um retrabalho. eu o tinha deixado em um estado lastimável, então tentei principalmente dar-lhe alguma ordem. eu senti que era importante deixar intacta a primeira seção; portanto, apenas duas pequenas alterações, redundantes 'mas' removidas e 'isso' enfatizadas.]

† Eu pretendia isso originalmente apenas como um esclarecimento sobre o meu original; mas decidiu outras adições após a reflexão

‡ consulte https://unix.stackexchange.com/tour para obter orientações sobre postagens

shelleybutterfly
fonte
2
Nunca não é idiomático. Esta não é uma resposta para nada.
mikeserv
1
Hmm. Bem, eu não sabia se essa resposta seria satisfatória, mas absolutamente não esperava que fosse controversa . E eu não pretendi argumentar que 'nunca' era por si só idiomático; mas que "Nunca faça X!" é um uso idiomático . Eu vejo dois casos gerais que podem mostrar que 'Nunca / não analise ls!' é o conselho correto: 1. demonstre (para sua satisfação) que todo caso de uso em que se pode analisar a lssaída tem outra solução disponível, superior de alguma forma, sem fazê-lo. 2. mostre que, nos casos citados, a afirmação não é literal.
Shelleybutterfly
Analisando sua pergunta novamente, vejo que você mencionou primeiro "não ..." em vez de "nunca ...", o que faz parte de sua análise, por isso vou esclarecer sobre esse ponto também. Neste ponto, já existe uma solução do primeiro tipo, que aparentemente é demonstrada / explicada para sua satisfação, então não vou me aprofundar muito nisso. Mas tentarei esclarecer um pouco a minha resposta: como eu disse, não estava tentando ser controverso (ou conflituoso!), Mas para mostrar como essas declarações geralmente se destinam.
Shelleybutterfly
1
Eu deveria limpar esse post. Ainda assim, não é não o caminho certo para expressá-lo. É um pouco ridículo que as pessoas pensem que estão qualificadas para dizer aos outros nunca ou não - apenas diga que você não acha que vai funcionar e por quê, mas você sabe o que vai funcionar e por que. lsé um utilitário de computador - você pode analisar a saída do computador.
mikeserv
1
Bem, eu invertei meu voto negativo, porque, no mínimo, você está certo sobre a questão da sinalização. Vou tentar limpá-lo hoje à noite ou amanhã. Meu pensamento é que vou mover a maioria dos exemplos de código para uma resposta, eu acho. Mas ainda assim, no que me diz respeito, não justifica as imprecisões nesse post de blog frequentemente citado. Eu gostaria que as pessoas parassem citando o manual do bash completamente - pelo menos não até depois theyve citou as especificações POSIX ...
mikeserv
16

É possível analisar a saída lsem certos casos? Certo. A idéia de extrair uma lista de números de inode de um diretório é um bom exemplo - se você souber que sua implementação é lscompatível -qe, portanto, cada arquivo produzirá exatamente uma linha de saída, e tudo que você precisa são os números de inode, analisando-os ls -Rai1qa saída é certamente uma solução possível. Obviamente, se o autor não tivesse visto conselhos como "Nunca analise a saída de ls" antes, provavelmente não pensaria em nomes de arquivos com novas linhas e provavelmente deixaria de fora o 'q' como resultado. o código seria sutilmente quebrado nesse caso extremo - portanto, mesmo nos casos em que lsa saída da análise for razoável, esse conselho ainda será útil.

O ponto mais amplo é que, quando um novato para shell script tenta ter uma figura roteiro out (por exemplo) o que é o maior arquivo em um diretório, ou o que é o arquivo modificado mais recentemente em um diretório, seu primeiro instinto é para analisar ls's saída - compreensível, porque lsé um dos primeiros comandos que um novato aprende.

Infelizmente, esse instinto está errado e essa abordagem está quebrada. Ainda mais infelizmente, ele está sutilmente quebrado - funcionará na maioria das vezes, mas falha em casos extremos que talvez possam ser explorados por alguém com conhecimento do código.

O novato pode pensar ls -s | sort -n | tail -n 1 | awk '{print $2}'em uma maneira de obter o maior arquivo em um diretório. E funciona, até que você tenha um arquivo com um espaço no nome.

OK, e daí ls -s | sort -n | tail -n 1 | sed 's/[^ ]* *[0-9]* *//'? Funciona bem até você ter um arquivo com uma nova linha no nome.

A adição -qde lsargumentos nos ajuda quando há uma nova linha no nome do arquivo? Pode parecer, até que você tenha 2 arquivos diferentes que contêm um caractere não imprimível no mesmo local no nome do arquivo e, em seguida ls, a saída não permite distinguir qual deles foi o maior. Pior, para expandir o "?", Ele provavelmente recorre ao de seu shell eval- o que causará problemas se ele acessar um arquivo chamado, por exemplo,

foo`/tmp/malicious_script`bar

Será que --quoting-style=shellajuda (se o lsmesmo suporta-lo)? Não, ainda exibe? para caracteres não imprimíveis, ainda é ambíguo qual das várias correspondências foi a maior. --quoting-style=literal? Não, o mesmo. --quoting-style=localeou --quoting-style=cpode ajudar se você apenas precisar imprimir o nome do arquivo maior sem ambiguidade, mas provavelmente não se precisar fazer algo com o arquivo posteriormente - seria um monte de código para desfazer a citação e voltar ao nome real do arquivo, que você pode passar para, digamos, gzip.

E no final de todo esse trabalho, mesmo que o que ele tenha seja seguro e correto para todos os nomes de arquivos possíveis, é ilegível e impossível de manter, e poderia ter sido feito com muito mais facilidade, segurança e legibilidade em python, perl ou ruby.

Ou mesmo usando outras ferramentas de shell - no topo da minha cabeça, acho que isso deve funcionar:

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

E deve ser pelo menos tão portátil quanto --quoting-styleé.

godlygeek
fonte
Oh verdade sobre o tamanho - eu provavelmente poderia fazer isso se eu tentasse - devo? Estou meio cansado ou essa coisa toda - eu gosto da sua resposta porque você não diz que não pode ou não ou nunca, mas na verdade dá exemplos de talvez por que não e comparável de que outra forma - obrigado.
Mikeerv #
Acho que se você tentasse, descobriria que é muito mais difícil do que pensa. Então, sim, eu recomendaria tentar. Ficarei feliz em continuar dando nomes de arquivos que serão quebrados por você enquanto eu puder pensar neles. :)
godlygeek 16/05
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
terdon
@mikeserv e godlygeek, mudei este tópico de comentário para o chat . Por favor, não tenha discussões longas como essa nos comentários, é para isso que serve o bate-papo.
terdon