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 find
comando?
x=( $(find . -name "*.txt") ); echo "${x[@]}"
Então você pode fazer um loopfor item in "${x[@]}"; { echo "$item"; }
Respostas:
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:
Marginalmente melhor, corte a variável temporária
x
:É muito melhor glob quando puder. Cofre em espaço em branco, para arquivos no diretório atual:
Ao ativar a
globstar
opção, você pode enviar todos os arquivos correspondentes neste diretório e todos os subdiretórios:Em alguns casos, por exemplo, se os nomes dos arquivos já estiverem em um arquivo, você pode precisar usar
read
:read
pode ser usado com segurança em combinação com afind
configuração apropriada do delimitador:Para pesquisas mais complexas, você provavelmente desejará usar
find
, com sua-exec
opção ou com-print0 | xargs -0
:find
também pode cd no diretório de cada arquivo antes de executar um comando usando em-execdir
vez de-exec
e pode ser interativo (prompt antes de executar o comando para cada arquivo) usando em-ok
vez de-exec
(ou em-okdir
vez de-execdir
).*: Tecnicamente, ambos
find
exargs
(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 SOLencontrará uma maneira diferente.fonte
done < filename
ea 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 usar3<
em vez de<
e adicionar<&3
ou-u3
a aread
parte, basicamente usando um descritor de arquivo separado. Além disso, acredito queread -d ''
é o mesmo,read -d $'\0'
mas não consigo encontrar nenhuma documentação oficial sobre isso no momento.-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?man find
). Nesse caso,-exec
dizfind
para 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).-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 quea
e$'\0'
é o mesmo que$'\0abc'
ou apenas a sequência vazia''
.help read
afirma 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).O que quer que você faça, não use um
for
loop :Três razões:
find
necessário executar até a conclusão.for
loop retornar 40 KB de texto. Os últimos 8 KB serão eliminados do seufor
loop e você nunca saberá.Sempre use uma
while read
construção:O loop será executado enquanto o
find
comando 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
-print0
usará o NULL como um separador de arquivos em vez de uma nova linha e o-d $'\0'
NULL como o separador durante a leitura.fonte
-exec
vez disso.-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 umfor
loop recomendado por muitos pôsteres.for file $(find)
devido aos problemas associados a isso.-r
opção pararead
:-r raw input - disables interpretion of backslash escapes and line-continuation in the read data
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
-exec
predicadofind
como este:O
{}
é o espaço reservado para o item encontrado e\;
é usado para finalizar o-exec
predicado.E por uma questão de perfeição, deixe-me acrescentar outra variante - você precisa amar as maneiras * nix por sua versatilidade:
Isso separaria os itens impressos com um
\0
caractere 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.xargs
pega-os um por um, então ...fonte
find -print0
exargs -0
são argumentos da extensão GNU e não portáveis (POSIX). Incrivelmente útil nos sistemas que os possuem!read -r
seriam corrigidas) ou nomes de arquivos terminados em espaço em branco (queIFS= read
seria corrigido). Daí a sugestão do BashFAQ # 1 #while IFS= read -r filename; do ...
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.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ê processarx
em um loop. Você pode melhorar isso alterando delimitador (bashIFS
Variable), 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:
2. Use a busca / leitura / enquanto substitui o processo
Observações
no padrão 1:
nullglob
pode ser usada para evitar essa linha extra.failglob
opçã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)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 Builtinextglob
,nocaseglob
,dotglob
& variável shellGLOBIGNORE
no padrão 2:
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,
find
sendo-print0
utilizados: 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.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:
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:
Manual do Gnu Bash, correspondência de padrões
Nomes de arquivos e nomes de caminhos no Shell: Como fazer isso corretamente, David A. Wheeler
Por que você não lê linhas com "for", Greg's Wiki
Por que você não deve analisar a saída de ls (1), Greg's Wiki
Gnu Bash Manual, Substituição de Processo
fonte
(Atualizado para incluir a excelente melhoria de velocidade da @ Socowi)
Com qualquer um
$SHELL
que o suporte (dash / zsh / bash ...):Feito.
Resposta original (mais curta, mas mais lenta):
fonte
\;
você pode usar+
para passar quantos arquivos possíveis para um únicoexec
. Em seguida, use"$@"
dentro do script de shell para processar todos esses parâmetros.$@
ocorre porque o omite, pois normalmente é o nome do script. Nós só precisa adicionardummy
entre'
e{}
para que ele possa tomar o lugar do nome do script, assegurando todas as partidas são processados pelo loop.OTHERVAR=foo find . -na.....
deve permitir o acesso a$OTHERVAR
partir desse shell recém-criado.fonte
for x in $(find ...)
quebrará para qualquer nome de arquivo com espaço em branco. Mesmo comfind ... | xargs
menos que você use-print0
e-0
find . -name "*.txt -exec process_one {} ";"
vez disso. Por que devemos usar xargs para coletar resultados, já temos?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 seprocess_one
for uma função definida pelo usuário, seu código não funcionará.Você pode armazenar sua
find
saída em array se desejar usá-la posteriormente como:Agora, para imprimir cada elemento na nova linha, você pode usar
for
a iteração de loop para todos os elementos da matriz ou usar a instrução printf.ou
Você também pode usar:
Isso imprimirá cada nome de arquivo em nova linha
Para imprimir apenas a
find
saída no formato de lista, você pode usar um dos seguintes:ou
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
.fonte
Se você pode assumir que os nomes dos arquivos não contêm novas linhas, você pode ler a saída
find
em uma matriz Bash usando o seguinte comando:Nota:
-t
causasreadarray
para descartar novas linhas.readarray
estiver 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
-d
parâ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
também pode ser chamado comomapfile
com as mesmas opções.Referência: https://mywiki.wooledge.org/BashFAQ/005#Loading_lines_from_a_file_or_stream
fonte
exit
ao fazer um loop nos resultadosreadarray -d '' x < <(find . -name '*.txt' -print0)
Eu gosto de usar o find, que é atribuído primeiro à variável e o IFS mudou para a nova linha da seguinte maneira:
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)
fonte
Você pode colocar os nomes de arquivos retornados por
find
em uma matriz como esta: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.
fonte
mapfile -t -d '' array < <(find ...)
. A configuraçãoIFS
não é necessária paramapfile
.baseado em outras respostas e comentários do @phk, usando o fd # 3:
(que ainda permite usar stdin dentro do loop)
fonte
find <path> -xdev -type f -name *.txt -exec ls -l {} \;
Isso listará os arquivos e fornecerá detalhes sobre os atributos.
fonte
Que tal se você usar grep em vez de encontrar?
Agora você pode ler este arquivo e os nomes dos arquivos estão na forma de uma lista.
fonte