Bash: canaliza a saída 'find' para 'readarray'

Bash: canaliza a saída 'find' para 'readarray'

Estou tentando procurar arquivos usando finde colocar esses arquivos em um array Bash para que eu possa fazer outras operações neles (por exemplo, lsou grepneles). Mas não consigo entender por que readarraya saída não está sendo lida findquando ela é canalizada para ela.

Digamos que eu tenha dois arquivos no diretório atual file1.txte file2.txt. Então a findsaída é a seguinte:

$ find . -name "file*"
./file1.txt
./file2.txt

Então, quero canalizar isso para um array cujos dois elementos são as strings "./file1.txt"e "./file2.txt"(sem aspas, obviamente).

Eu tentei isso, entre algumas outras coisas:

$ declare -a FILES
$ find . -name "file*" | readarray FILES
$ echo "${FILES[@]}"; echo "${#FILES[@]}"

0

Como você pode ver na echosaída, meu array está vazio.

Então, o que exatamente estou fazendo de errado aqui? Por que não está readarraylendo finda saída de como entrada padrão e colocando essas strings no array?

Responder1

Ao usar um pipeline, o bash executa os comandos em subshells¹. Portanto, o array é preenchido, mas em um subshell, portanto o shell pai não tem acesso a ele. Você provavelmente também deseja a -topção de não armazenar os delimitadores de linha nos membros da matriz, pois eles não fazem parte dos nomes dos arquivos.

Use substituição de processo:

readarray -t FILES < <(find .)

Observe que isso não funciona para arquivos com novas linhas em seus caminhos. A menos que você possa garantir que não será o caso, convém usar registros delimitados por NUL em vez de registros delimitados por nova linha:

readarray -td '' < <(find . -print0)

(a -dopção foi adicionada no bash 4.4)


¹ exceto para o último componente de pipe ao usar a lastpipeopção, mas isso é apenas para invocações não interativas de bash.

Responder2

A solução correta é:

unset a; declare -a a
while IFS= read -r -u3 -d $'\0' file; do
    a+=( "$file" )        # or however you want to process each file
done 3< <(find /tmp -type f -print0)

Isso é semelhante ao queBashFAQ 020 de Gregexplica detalhadamente eesta resposta cobre.

Não tem problemas com arquivos com nomes estranhos (que não contêm NUL no nome), com espaços ou novas linhas. E o resultado é definido em uma matriz, o que o torna útil para processamento posterior.

Responder3

readarraytambém pode ler de stdin

readarray FILES <<< "$(find . -name "file*")"; echo "${#FILES[@]}"

Responder4

Com shopt -s lastpipea solução mostrada na pergunta original funciona, pois a parte após a última |é executada no mesmo processo da maior parte do script:

shopt -s lastpipe

# This is just the code form the original question.
# Of course it could be done with -print0, etc.
declare -a FILES
find . -name "name*" | readarray -t FILES
echo find and readarray exit status: "${PIPESTATUS[@]}"
echo "${FILES[@]}"
echo "${#FILES[@]}"

No entanto, isso só é prático se lastpipepuder ser transformado em um padrão para todo o projeto (talvez você já tenha uma configuração comum para set -u, etc.). Saiba também que lastpipefunciona apenas para shells não interativos.

A outra substituição típica de pipe < <(...)(substituição de processo) tem o problema de ser muito difícil verificar se o processo filho foi bem-sucedido.

informação relacionada