variável IFS e resultado diferente com listagem de arquivos com loop

5

Eu quero obter lista de arquivos no diretório atual e seus subdiretórios (eu quero usar o script one-liner):

IFS=$(echo -en "\n\b");
for FILE in $(find -type f); do echo "$FILE"; done

Normalmente, funciona como esperado, mas recentemente, com minha lista de arquivos:

file_.doc
file_0.doc
file_[2006_02_25].doc
file_[2016_06_16].odt
file_[2016_06_16].pdf
file_[16-6-2006].doc
file_.pdf
file_ 4-4-2006.doc

a saída é:

./file_.doc                                                                                                                                                 
./file_0.doc                                                                                                                                                
./file_0.doc
./file_[2016_06_16].odt
./file_[2016_06_16].pdf
./file_0.doc
./file_.pdf
./file_ 4-4-2006.doc

Se eu alterar a variável IFS para:

IFS=$(echo -en "\n");

a saída será (corrigida):

./file_.doc
./file_0.doc
./file_[2006_02_25].doc
./file_[2016_06_16].odt
./file_[2016_06_16].pdf
./file_[16-6-2006].doc
./file_.pdf
./file_ 4-4-2006.doc

Eu li que '\b' é necessário , e encontrou uma solução que usando printf , em vez de echo .

Minhas perguntas são:

1) Você poderia explicar o que tornou esses resultados diferentes?

2) Uma solução usando printf acima poderia ser uma alternativa para echo -en "\n\b" ?

    
por duqu 28.02.2018 / 09:06

2 respostas

4

A saída de uma substituição de comando está sujeita à divisão de palavras (a qual você cuidou definindo IFS ), e globalização de nome de arquivo. A construção [abc] é "corresponde a qualquer um dos caracteres a , b , c " como de costume e [2006_02_25].doc corresponde a 0.doc .

No Bash / ksh / zsh, você pode usar a estrela dupla para obter todos os arquivos na árvore de diretórios (recursivamente, não apenas no diretório atual). Isso deve encontrar os mesmos arquivos do seu exemplo:

shopt -s globstar      # in Bash
# set -o globstar      # in ksh
for file in **/* ; do
    [[ -f $file ]] || continue     # check it's a regular file, like find -type f
    ...
done

Naturalmente, find é poderoso, então usá-lo pode ser mais fácil se você tiver muitas condições. Se fizer isso, você deve desabilitar o globbing de nomes de arquivo com set -f além de corrigir IFS :

set -f
IFS=$'\n'
for file in $(find -type f -some -other -conditions) ; do
    ...
done

ou use um loop while read com substituição de processo:

while IFS= read -r file ; do 
    ...
done < <(find -type f -some -other -conditions)

(O acima é semelhante a find ... | while ... , mas ignora o problema da última parte de um pipeline sendo executado em um subshell.)

Ambos supõem que os nomes de arquivos não contêm novas linhas, pois são usados como separador para a saída de find . (e porque $(..) come novas linhas finais.)

No Bash, pelo menos, há uma maneira de fazer com que novas linhas de nomes funcionem também. Configurar o read delimitador para a cadeia vazia faz com que ele use efetivamente o byte NUL como delimitador. Então:

while IFS= read -d '' -r file ; do 
    ...
done < <(find -type f -some -other -conditions -print0)

Embora isso esteja começando a ficar difícil de escrever, já que você precisa de todas essas opções para read para fazê-lo funcionar sem atrapalhar a entrada.

Quanto ao IFS ... Configurar IFS=$(echo -en "\n"); conjuntos IFS para a cadeia vazia (porque a substituição de comando come a nova linha à direita), resultando na divisão não . A saída, neste caso, parece correta, já que você obtém toda a saída de find de uma só vez, não linha por linha. Isso também mascara o problema com o globbing de nomes de arquivos, já que a string multilinha completa não corresponde a nenhum nome de arquivo e é passada como está.

Você pode ver a diferença se fizer qualquer outra coisa além de imprimir o valor do loop. Tente adicionar alguns separadores:

IFS=$(echo -en "\n")       # same as IFS=
for FILE in $(find -type f); do echo "<$FILE>"; done

De longe, a maneira mais fácil de definir IFS para uma nova linha em qualquer coisa, exceto nas conchas padrão mais básicas, é IFS=$'\n' .

    
por 28.02.2018 / 10:19
4

Não faça $( find ... ) . Ele invocará geração de nome de arquivo (globbing) e alguns dos seus nomes de arquivos serão interpretados como padrões de globbing que correspondem a outros nomes de arquivos. Por exemplo, o padrão file_[2006_02_25].doc e file_[16-6-2006].doc corresponde a file_0.doc , motivo pelo qual esse nome de arquivo ocorre em vez desses dois padrões.

Seu loop também não iniciará a iteração até que o comando find na substituição do comando tenha gerado todos os nomes de caminho, o que poderia, no caso geral, consumir uma grande quantidade de memória e não é realmente elegante .

Em vez disso, use apenas find (e não modifique IFS ):

find . type -f -print

Se você quiser fazer outras coisas com esses arquivos, faça isso em -exec :

find . -type f -exec sh -c 'printf "Found the file %s\n" "$@"' sh {} +

Se você quiser apenas processar os arquivos no diretório atual, você pode simplesmente

for name in *; do
    printf 'Found the name %s\n' "$name"
done

Relacionados:

por 28.02.2018 / 09:16