Faça iterações recursivas através de arquivos em um diretório

15

A iteração recursiva dos arquivos em um diretório pode ser facilmente realizada por:

find . -type f -exec bar {} \;

No entanto, o acima exposto não funciona para coisas mais complexas, onde muitas ramificações condicionais, repetições etc. precisam ser feitas. Eu costumava usar isso para o acima:

while read line; do [...]; done < <(find . -type f)

No entanto, parece que isso não funciona para arquivos que contêm caracteres obscuros:

$ touch $'a\nb'
$ find . -type f
./a?b

Existe uma alternativa que lida bem com caracteres tão obscuros?

user2064000
fonte
1
find ... -exec bash -c 'echo filename is in \$0: "$0"' {} \;é a melhor maneira de fazer isso.
jw013
Você pode resolver isso e manter o design original, alterando read linepara IFS= read -r line. O único caractere que o quebrará é uma nova linha.
Patrick
1
@ Patrick, mas os nomes de arquivos podem conter novas linhas. É por isso que -d $'\0'é preferível.
godlygeek

Respostas:

7

Mais um uso segurofind :

while IFS= read -r -d '' -u 9
do
    [Do something with "$REPLY"]
done 9< <( find . -type f -exec printf '%s\0' {} + )

(Isso funciona com qualquer POSIX find, mas a parte do shell requer bash. Com * BSD e GNU find, você pode usar em -print0vez de -exec printf '%s\0' {} +, será um pouco mais rápido.)

Isso torna possível usar a entrada padrão dentro do loop e funciona com qualquer caminho.

l0b0
fonte
1
Como eu tive que procurar: "leia ... Se nenhum nome for fornecido, a linha de leitura será atribuída à variável REPLY". Entãodo echo "Filename is '$REPLY'"
Andrew
9

Fazer isso é tão simples quanto:

find -exec sh -c 'inline script "$0"' {} \;

Ou...

find -exec executable_script {} \;
mikeserv
fonte
5

A abordagem mais simples (ainda que segura) é usar globbing de shell:

$ for f in *; do printf ":%s:\n" "$f"; done 
:a b:
:c
d:
:-e:
:e  f:
h:

Para fazer a recursão acima em subdiretórios (no bash), você pode usar a globstaropção; também definido dotglobpara corresponder aos arquivos cujo nome começa com .:

$ shopt -s globstar dotglob
$ for f in **/*; do printf ":%s:\n" "$f"; done 
:a b:
:c
d:
:-e:
:e  f:
:foo:
:foo/file1:
:foo/file two:
h:

Cuidado com o fato de que, até o bash 4.2, se **/repete em links simbólicos para diretórios. Desde o bash 4.3, ele se **/repete apenas em diretórios, como find.

Outra solução comum é usar find -print0com xargs -0:

$ touch -- 'a b' $'c\nd' $'e\tf' $'g\rh' '-e'
$ find . -type f -print0 | xargs -0 -I{} printf ":%s:\n" {}
h:/g
:./e    f:
:./a b:
:./-e:
:./c
d:

Observe que h:/gestá realmente correto, pois o nome do arquivo contém a \r.

terdon
fonte
4

É um pouco difícil de fazer seu loop de leitura portably, mas para a festança, em particular, você pode tentar algo como isso .

Parte relevante:

while IFS= read -d $'\0' -r file ; do
        printf 'File found: %s\n' "$file"
done < <(find . -iname 'foo*' -print0)

Isso instrui finda imprimir sua saída delimitada por caracteres NUL (0x00) e reada buscar linhas delimitadas por NUL ( -d $'\0') sem manipular barras invertidas como escapes para outros caracteres ( -r) e não fazer nenhuma palavra dividida nas linhas ( IFS=). Como 0x00 é um byte que não pode ocorrer em nomes de arquivo ou caminhos no Unix, isso deve lidar com todos os seus problemas estranhos de nome de arquivo.

godlygeek
fonte
1
-d ''é equivalente a -d $'\0'.
l0b0