Como percorrer os nomes de arquivos retornados pelo find?

223
x=$(find . -name "*.txt")
echo $x

se eu executar o código acima no shell Bash, o que eu recebo é uma string contendo vários nomes de arquivos separados por branco, não uma lista.

Claro, posso separá-los ainda mais em branco para obter uma lista, mas tenho certeza de que há uma maneira melhor de fazê-lo.

Então, qual é a melhor maneira de percorrer os resultados de um findcomando?

Haiyuan Zhang
fonte
3
A melhor maneira de alternar nomes de arquivos depende bastante do que você realmente deseja fazer com ele, mas, a menos que você possa garantir que nenhum arquivo tenha espaço em branco no nome, essa não é uma ótima maneira de fazê-lo. Então, o que você quer fazer em loop sobre os arquivos?
Kevin
1
Em relação à recompensa : a principal idéia aqui é obter uma resposta canônica que cubra todos os casos possíveis (nomes de arquivos com novas linhas, caracteres problemáticos ...). A idéia é usar esses nomes de arquivos para fazer algumas coisas (chame outro comando, execute alguma renomeação ...). Obrigado!
fedorqui 'Então, pare de prejudicar'
Não esqueça que um nome de arquivo ou pasta pode conter ".txt" seguido de espaço e outra sequência, exemplo "something.txt something" ou "something.txt"
Yahya Yahyaoui
Use array, não var x=( $(find . -name "*.txt") ); echo "${x[@]}"Então você pode fazer um loopfor item in "${x[@]}"; { echo "$item"; }
Ivan

Respostas:

392

TL; DR: Se você está aqui apenas para obter a resposta mais correta, provavelmente deseja minha preferência pessoal find . -name '*.txt' -exec process {} \;(veja a parte inferior desta postagem). Se você tiver tempo, leia o restante para ver várias maneiras diferentes e os problemas com a maioria delas.


A resposta completa:

A melhor maneira depende do que você deseja fazer, mas aqui estão algumas opções. Contanto que nenhum arquivo ou pasta na subárvore tenha um espaço em branco em seu nome, você pode simplesmente fazer um loop sobre os arquivos:

for i in $x; do # Not recommended, will break on whitespace
    process "$i"
done

Marginalmente melhor, corte a variável temporária x:

for i in $(find -name \*.txt); do # Not recommended, will break on whitespace
    process "$i"
done

É muito melhor glob quando puder. Cofre em espaço em branco, para arquivos no diretório atual:

for i in *.txt; do # Whitespace-safe but not recursive.
    process "$i"
done

Ao ativar a globstaropção, você pode enviar todos os arquivos correspondentes neste diretório e todos os subdiretórios:

# Make sure globstar is enabled
shopt -s globstar
for i in **/*.txt; do # Whitespace-safe and recursive
    process "$i"
done

Em alguns casos, por exemplo, se os nomes dos arquivos já estiverem em um arquivo, você pode precisar usar read:

# IFS= makes sure it doesn't trim leading and trailing whitespace
# -r prevents interpretation of \ escapes.
while IFS= read -r line; do # Whitespace-safe EXCEPT newlines
    process "$line"
done < filename

readpode ser usado com segurança em combinação com a findconfiguração apropriada do delimitador:

find . -name '*.txt' -print0 | 
    while IFS= read -r -d '' line; do 
        process "$line"
    done

Para pesquisas mais complexas, você provavelmente desejará usar find, com sua -execopção ou com -print0 | xargs -0:

# execute `process` once for each file
find . -name \*.txt -exec process {} \;

# execute `process` once with all the files as arguments*:
find . -name \*.txt -exec process {} +

# using xargs*
find . -name \*.txt -print0 | xargs -0 process

# using xargs with arguments after each filename (implies one run per filename)
find . -name \*.txt -print0 | xargs -0 -I{} process {} argument

findtambém pode cd no diretório de cada arquivo antes de executar um comando usando em -execdirvez de -exece pode ser interativo (prompt antes de executar o comando para cada arquivo) usando em -okvez de -exec(ou em -okdirvez de -execdir).

*: Tecnicamente, ambos finde xargs(por padrão) executarão o comando com o maior número possível de argumentos na linha de comando, quantas vezes forem necessárias para passar por todos os arquivos. Na prática, a menos que você tenha um número muito grande de arquivos, isso não importará e, se você exceder o comprimento, mas precisar de todos na mesma linha de comando, o SOL encontrará uma maneira diferente.

Kevin
fonte
4
É importante notar que, no caso com done < filenameea seguinte com o tubo do stdin não pode ser mais usado (→ nenhum material mais interativo dentro do loop), mas nos casos em que é necessário um pode usar 3<em vez de <e adicionar <&3ou -u3a a readparte, basicamente usando um descritor de arquivo separado. Além disso, acredito que read -d ''é o mesmo, read -d $'\0'mas não consigo encontrar nenhuma documentação oficial sobre isso no momento.
PHK
1
para i em * .txt; não funciona, se não houver arquivos correspondentes. Um teste xtra, por exemplo, [[-e $ i]] é necessário
Michael Brux 13/16
2
Estou perdido com esta parte: -exec process {} \;e meu palpite é que é outra questão - o que isso significa e como eu o manipulo? Onde está um bom Q / A ou doc. nele?
Alex Hall
1
@AlexHall, você sempre pode consultar as páginas de manual ( man find). Nesse caso, -execdiz findpara executar o seguinte comando, terminado por ;(ou +), em que {}será substituído pelo nome do arquivo que está processando (ou, se +for usado, todos os arquivos que o fizeram nessa condição).
Kevin
3
@phk -d ''é melhor que -d $'\0'. O último não é apenas mais longo, mas também sugere que você pode passar argumentos contendo bytes nulos, mas não pode. O primeiro byte nulo marca o final da string. No bash $'a\0bc'é o mesmo que ae $'\0'é o mesmo que $'\0abc'ou apenas a sequência vazia ''. help readafirma que " O primeiro caractere de delim é usado para finalizar a entrada ", portanto, usar ''como delimitador é um pouco complicado. O primeiro caractere da sequência vazia é o byte nulo que sempre marca o final da sequência (mesmo que você não a escreva explicitamente).
Socowi 9/05/19
114

O que quer que você faça, não use um forloop :

# Don't do this
for file in $(find . -name "*.txt")
do
    code using "$file"
done

Três razões:

  • Para que o loop for seja iniciado, é findnecessário executar até a conclusão.
  • Se um nome de arquivo tiver algum espaço em branco (incluindo espaço, tabulação ou nova linha), ele será tratado como dois nomes separados.
  • Embora agora seja improvável, você pode exceder o buffer da linha de comando. Imagine se o buffer da linha de comando tiver 32 KB e o forloop retornar 40 KB de texto. Os últimos 8 KB serão eliminados do seu forloop e você nunca saberá.

Sempre use uma while readconstrução:

find . -name "*.txt" -print0 | while read -d $'\0' file
do
    code using "$file"
done

O loop será executado enquanto o findcomando estiver em execução. Além disso, este comando funcionará mesmo se um nome de arquivo for retornado com espaço em branco. E você não sobrecarregará seu buffer de linha de comando.

Ele -print0usará o NULL como um separador de arquivos em vez de uma nova linha e o -d $'\0'NULL como o separador durante a leitura.

David W.
fonte
3
Não funcionará com novas linhas nos nomes de arquivos. Use find's em -execvez disso.
desconhecido utilizador
2
@userunknown - Você está certo sobre isso. -execé o mais seguro, pois não usa o shell. No entanto, o NL nos nomes dos arquivos é bastante raro. Os espaços nos nomes dos arquivos são bastante comuns. O ponto principal é não usar um forloop recomendado por muitos pôsteres.
David W.
1
@userunknown - Aqui. Eu corrigi isso, então agora ele cuida de arquivos com novas linhas, guias e qualquer outro espaço em branco. O objetivo principal da postagem é dizer ao OP para não usá-lo for file $(find)devido aos problemas associados a isso.
11743 David W.
4
Se você pode usar -exec, é melhor, mas há momentos em que você realmente precisa do nome devolvido ao shell. Por exemplo, se você deseja remover extensões de arquivo.
Ben Reser
5
Você deve usar a -ropção para read: -r raw input - disables interpretion of backslash escapes and line-continuation in the read data
Daira Hopwood
102
find . -name "*.txt"|while read fname; do
  echo "$fname"
done

Nota: este método e o (segundo) método mostrado por bmargulies são seguros para uso com espaço em branco nos nomes de arquivos / pastas.

Para ter também o caso - um tanto exótico - de novas linhas nos nomes de arquivos / pastas, você precisará recorrer ao -execpredicado findcomo este:

find . -name '*.txt' -exec echo "{}" \;

O {}é o espaço reservado para o item encontrado e \;é usado para finalizar o -execpredicado.

E por uma questão de perfeição, deixe-me acrescentar outra variante - você precisa amar as maneiras * nix por sua versatilidade:

find . -name '*.txt' -print0|xargs -0 -n 1 echo

Isso separaria os itens impressos com um \0caractere que não é permitido em nenhum dos sistemas de arquivos nos nomes de arquivos ou pastas, que eu saiba e, portanto, deve cobrir todas as bases. xargspega-os um por um, então ...

0xC0000022L
fonte
3
Falha se nova linha no nome do arquivo.
desconhecido utilizador
2
@ usuário desconhecido: você está certo, é um caso que eu não havia considerado e que, eu acho, é muito exótico. Mas ajustei minha resposta de acordo.
0xC0000022L
5
Provavelmente vale ressaltar isso find -print0e xargs -0são argumentos da extensão GNU e não portáveis ​​(POSIX). Incrivelmente útil nos sistemas que os possuem!
precisa saber é o seguinte
1
Isso também falha com nomes de arquivos contendo barras invertidas (que read -rseriam corrigidas) ou nomes de arquivos terminados em espaço em branco (que IFS= readseria corrigido). Daí a sugestão do BashFAQ # 1 #while IFS= read -r filename; do ...
Charles Duffy
1
Outro problema é que parece que o corpo do loop está sendo executado no mesmo shell, mas não é, portanto, por exemplo exit, não funcionará conforme o esperado e as variáveis ​​definidas no corpo do loop não estarão disponíveis após o loop.
EM0 08/02
17

Os nomes de arquivos podem incluir espaços e até controlar caracteres. Os espaços são delimitadores (padrão) para expansão de shell no bash e, como resultado disso, x=$(find . -name "*.txt")a pergunta não é recomendada. Se find obtiver um nome de arquivo com espaços, por exemplo, "the file.txt"você receberá 2 strings separadas para processamento, se você processar xem um loop. Você pode melhorar isso alterando delimitador (bash IFSVariable), por exemplo \r\n, para , mas os nomes de arquivos podem incluir caracteres de controle - portanto, este não é um método (completamente) seguro.

Do meu ponto de vista, existem 2 padrões recomendados (e seguros) para o processamento de arquivos:

1. Use para expansão de loop e nome de arquivo:

for file in ./*.txt; do
    [[ ! -e $file ]] && continue  # continue, if file does not exist
    # single filename is in $file
    echo "$file"
    # your code here
done

2. Use a busca / leitura / enquanto substitui o processo

while IFS= read -r -d '' file; do
    # single filename is in $file
    echo "$file"
    # your code here
done < <(find . -name "*.txt" -print0)

Observações

no padrão 1:

  1. bash retorna o padrão de pesquisa ("* .txt") se nenhum arquivo correspondente for encontrado - portanto, a linha extra "continuar, se o arquivo não existir" será necessária. consulte Manual do Bash, Expansão do nome do arquivo
  2. A opção shell nullglobpode ser usada para evitar essa linha extra.
  3. "Se a failglobopção shell estiver definida e nenhuma correspondência for encontrada, uma mensagem de erro será impressa e o comando não será executado." (do Bash Manual acima)
  4. opção de shell globstar: "Se definido, o padrão '**' usado em um contexto de expansão de nome de arquivo corresponderá a todos os arquivos e zero ou mais diretórios e subdiretórios. Se o padrão for seguido por um '/', apenas diretórios e subdiretórios corresponderão." consulte o Manual do Bash, Shopt Builtin
  5. outras opções para a expansão filename: extglob, nocaseglob, dotglob& variável shellGLOBIGNORE

no padrão 2:

  1. os nomes de arquivos podem conter espaços em branco, tabulações, espaços, novas linhas, ... para processar os nomes de arquivos de maneira segura, findsendo -print0utilizados: o nome do arquivo é impresso com todos os caracteres de controle e finalizado com NUL. consulte também Página de manual do Gnu Findutils, tratamento inseguro de nomes de arquivos , tratamento seguro de nomes de arquivos , caracteres incomuns nos nomes de arquivos . Veja David A. Wheeler abaixo para uma discussão detalhada sobre este tópico.

  2. Existem alguns padrões possíveis para processar resultados de busca em um loop while. Outros (Kevin, David W.) mostraram como fazer isso usando pipes:

    files_found=1 find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do # single filename in $file echo "$file" files_found=0 # not working example # your code here done [[ $files_found -eq 0 ]] && echo "files found" || echo "no files found"
    Ao tentar esse trecho de código, você verá que ele não funciona: files_foundé sempre "verdadeiro" e o código sempre ecoará "nenhum arquivo encontrado". A razão é: cada comando de um pipeline é executado em um subshell separado, portanto, a variável alterada dentro do loop (subshell separado) não altera a variável no script de shell principal. É por isso que recomendo usar a substituição de processo como o padrão "melhor", mais útil e mais geral.
    Consulte Eu defino variáveis ​​em um loop que está em um pipeline. Por que eles desaparecem ... (nas Perguntas frequentes sobre o Greg's Bash) para uma discussão detalhada sobre esse tópico.

Referências e fontes adicionais:

Michael Brux
fonte
8

(Atualizado para incluir a excelente melhoria de velocidade da @ Socowi)

Com qualquer um $SHELLque o suporte (dash / zsh / bash ...):

find . -name "*.txt" -exec $SHELL -c '
    for i in "$@" ; do
        echo "$i"
    done
' {} +

Feito.


Resposta original (mais curta, mas mais lenta):

find . -name "*.txt" -exec $SHELL -c '
    echo "$0"
' {} \;
user569825
fonte
1
Lento como melaço (uma vez que lança um shell para cada arquivo), mas isso funciona. +1
dawg
1
Em vez de \;você pode usar +para passar quantos arquivos possíveis para um único exec. Em seguida, use "$@"dentro do script de shell para processar todos esses parâmetros.
Socowi 9/05/19
3
Há um erro neste código. O loop está ausente do primeiro resultado. Isso $@ocorre porque o omite, pois normalmente é o nome do script. Nós só precisa adicionar dummyentre 'e {}para que ele possa tomar o lugar do nome do script, assegurando todas as partidas são processados pelo loop.
BCartolo 5/08/19
E se eu precisar de outras variáveis ​​de fora do shell recém-criado?
Jodo
OTHERVAR=foo find . -na.....deve permitir o acesso a $OTHERVARpartir desse shell recém-criado.
user569825
6
# Doesn't handle whitespace
for x in `find . -name "*.txt" -print`; do
  process_one $x
done

or

# Handles whitespace and newlines
find . -name "*.txt" -print0 | xargs -0 -n 1 process_one
bmargulies
fonte
3
for x in $(find ...)quebrará para qualquer nome de arquivo com espaço em branco. Mesmo com find ... | xargsmenos que você use -print0e-0
Glenn Jackman
1
Use em find . -name "*.txt -exec process_one {} ";"vez disso. Por que devemos usar xargs para coletar resultados, já temos?
usuário desconhecido
@ userunknown Bem, tudo depende do que process_oneé. Se for um espaço reservado para um comando real , verifique se isso funcionaria (se você corrigir erro de digitação e adicionar aspas finais depois "*.txt). Mas se process_onefor uma função definida pelo usuário, seu código não funcionará.
toxalot
@ toxalot: Sim, mas não seria um problema escrever a função em um script para chamar.
desconhecido usuário
4

Você pode armazenar sua findsaída em array se desejar usá-la posteriormente como:

array=($(find . -name "*.txt"))

Agora, para imprimir cada elemento na nova linha, você pode usar fora iteração de loop para todos os elementos da matriz ou usar a instrução printf.

for i in ${array[@]};do echo $i; done

ou

printf '%s\n' "${array[@]}"

Você também pode usar:

for file in "`find . -name "*.txt"`"; do echo "$file"; done

Isso imprimirá cada nome de arquivo em nova linha

Para imprimir apenas a findsaída no formato de lista, você pode usar um dos seguintes:

find . -name "*.txt" -print 2>/dev/null

ou

find . -name "*.txt" -print | grep -v 'Permission denied'

Isso removerá as mensagens de erro e fornecerá apenas o nome do arquivo como saída em nova linha.

Se você deseja fazer algo com os nomes de arquivo, armazená-lo em matriz é bom; caso contrário, não há necessidade de consumir esse espaço e você pode imprimir diretamente a saída find.

Rakholiya Jenish
fonte
1
O loop da matriz falha com espaços nos nomes dos arquivos.
EM0 08/0218
Você deve excluir esta resposta. Não funciona com espaços em nomes de arquivos ou nomes de diretórios.
jww 25/08/19
4

Se você pode assumir que os nomes dos arquivos não contêm novas linhas, você pode ler a saída findem uma matriz Bash usando o seguinte comando:

readarray -t x < <(find . -name '*.txt')

Nota:

  • -tcausas readarraypara descartar novas linhas.
  • Não funcionará se readarrayestiver em um pipe, daí a substituição do processo.
  • readarray está disponível desde o Bash 4.

O Bash 4.4 ou superior também suporta o -dparâmetro para especificar o delimitador. O uso do caractere nulo, em vez de nova linha, para delimitar os nomes de arquivo também funciona nos raros casos em que os nomes de arquivo contêm novas linhas:

readarray -d '' x < <(find . -name '*.txt' -print0)

readarraytambém pode ser chamado como mapfilecom as mesmas opções.

Referência: https://mywiki.wooledge.org/BashFAQ/005#Loading_lines_from_a_file_or_stream

Seppo Enarvi
fonte
Esta é a melhor resposta! Funciona com: * Espaços nos nomes de arquivos * Nenhum arquivo correspondente * exitao fazer um loop nos resultados
EM0 8/18
Porém, não funciona com todos os nomes de arquivos possíveis - para isso, você deve usarreadarray -d '' x < <(find . -name '*.txt' -print0)
Charles Duffy
3

Eu gosto de usar o find, que é atribuído primeiro à variável e o IFS mudou para a nova linha da seguinte maneira:

FilesFound=$(find . -name "*.txt")

IFSbkp="$IFS"
IFS=$'\n'
counter=1;
for file in $FilesFound; do
    echo "${counter}: ${file}"
    let counter++;
done
IFS="$IFSbkp"

Apenas no caso de você desejar repetir mais ações no mesmo conjunto de dados e encontrar muito lento no seu servidor (alta utilização de I / 0)

Paco
fonte
2

Você pode colocar os nomes de arquivos retornados por findem uma matriz como esta:

array=()
while IFS=  read -r -d ''; do
    array+=("$REPLY")
done < <(find . -name '*.txt' -print0)

Agora você pode simplesmente percorrer a matriz para acessar itens individuais e fazer o que quiser com eles.

Nota: É um espaço em branco seguro.

Jahid
fonte
1
Com o bash 4.4 ou superior, você poderia usar um único comando em vez de um loop: mapfile -t -d '' array < <(find ...). A configuração IFSnão é necessária para mapfile.
Socowi
1

baseado em outras respostas e comentários do @phk, usando o fd # 3:
(que ainda permite usar stdin dentro do loop)

while IFS= read -r f <&3; do
    echo "$f"

done 3< <(find . -iname "*filename*")
Florian
fonte
-1

find <path> -xdev -type f -name *.txt -exec ls -l {} \;

Isso listará os arquivos e fornecerá detalhes sobre os atributos.

chetangb
fonte
-5

Que tal se você usar grep em vez de encontrar?

ls | grep .txt$ > out.txt

Agora você pode ler este arquivo e os nomes dos arquivos estão na forma de uma lista.

Dhruv Raj Singh Rathore
fonte
6
Não faça isso. Por que você não deve analisar a saída de ls . Isso é frágil, muito frágil.
fedorqui 'Então pare de prejudicar'