Executar comando em todos os arquivos em um diretório

290

Alguém poderia, por favor, fornecer o código para fazer o seguinte: Suponha que exista um diretório de arquivos, todos os quais precisam ser executados através de um programa. O programa gera os resultados para a saída padrão. Eu preciso de um script que entre em um diretório, execute o comando em cada arquivo e concatize a saída em um grande arquivo de saída.

Por exemplo, para executar o comando em 1 arquivo:

$ cmd [option] [filename] > results.out
O maestro
fonte
3
Eu gostaria de acrescentar à pergunta. Isso pode ser feito usando xargs? por exemplo, ls <directory> | xargs cmd [options] {filenames put in here automatically by xargs} [more arguments] > results.out
Ozair Kafray
2
Pode, mas você provavelmente não quer usarls para dirigir xargs. Se cmdestá escrito com competência, talvez você possa simplesmente fazer cmd <wildcard>.
tripleee

Respostas:

425

O seguinte código bash passará $ file para o comando em que $ file representará todos os arquivos em / dir

for file in /dir/*
do
  cmd [option] "$file" >> results.out
done

Exemplo

el@defiant ~/foo $ touch foo.txt bar.txt baz.txt
el@defiant ~/foo $ for i in *.txt; do echo "hello $i"; done
hello bar.txt
hello baz.txt
hello foo.txt
Andrew Logvinov
fonte
23
Se não houver arquivos /dir/, o loop ainda será executado uma vez com o valor '*' para $file, o que pode ser indesejável. Para evitar isso, ative o nullglob pela duração do loop. Adicione esta linha antes do loop shopt -s nullglobe esta linha após o loop shopt -u nullglob #revert nullglob back to it's normal default state.
Stew-au
43
+1, e isso me custou toda a minha coleção de papéis de parede. todos depois de mim, use aspas duplas. "$ file"
Behrooz 12/09
Se o arquivo de saída for o mesmo dentro do loop, é muito mais eficiente redirecionar para fora do loop done >results.out(e provavelmente você poderá sobrescrever em vez de acrescentar, como eu assumi aqui).
Tripleee 31/10
Como você obtém arquivos de resultados individuais com nomes personalizados para seus arquivos de entrada?
Timothy Swan
1
tenha cuidado usando este comando para uma enorme quantidade de arquivos no diretório Use find -exec.
22419 kolisko
181

Que tal agora:

find /some/directory -maxdepth 1 -type f -exec cmd option {} \; > results.out
  • -maxdepth 1O argumento impede que a localização descida recursivamente em qualquer subdiretório. (Se você deseja que esses diretórios aninhados sejam processados, você pode omitir isso.)
  • -type -f especifica que apenas arquivos simples serão processados.
  • -exec cmd option {}diz para executar cmdcom o especificado optionpara cada arquivo encontrado, com o nome do arquivo substituído por{}
  • \; denota o fim do comando.
  • Finalmente, a saída de todas as cmdexecuções individuais é redirecionada para results.out

No entanto, se você se importa com a ordem em que os arquivos são processados, é melhor escrever um loop. Acho que findprocessa os arquivos em ordem de inode (embora eu possa estar errado sobre isso), o que pode não ser o que você deseja.

Jim Lewis
fonte
1
Esta é a maneira correta de processar arquivos. O uso de um loop for é suscetível a erros devido a vários motivos. A classificação também pode ser feita usando outros comandos como state sort, que obviamente dependem do que é o critério de classificação.
Tuxdna
1
se eu quisesse executar dois comandos, como os vincularia após a -execopção? Preciso envolvê-los em aspas simples ou algo assim?
frei
findé sempre a melhor opção, -namepois você pode filtrar por padrão de nome de arquivo com a opção e pode fazê-lo em um único comando.
João Pimentel Ferreira
3
@frei a resposta à sua pergunta é aqui: stackoverflow.com/a/6043896/1243247 mas basicamente basta adicionar -execopções:find . -name "*.txt" -exec echo {} \; -exec grep banana {} \;
João Pimentel Ferreira
2
como você pode fazer referência ao nome do arquivo como opção?
Toskan
54

Estou fazendo isso no meu raspberry pi na linha de comando executando:

for i in *;do omxplayer "$i";done
robgraves
fonte
7

As respostas aceitas / com alta votação são ótimas, mas faltam alguns detalhes minuciosos. Esta postagem aborda os casos de como lidar melhor quando a expansão do nome do caminho do shell (glob) falha, quando os nomes de arquivos contêm linhas de linha / símbolos de traço incorporados e movendo a direção de saída do comando para fora do loop for ao gravar os resultados em um Arquivo.

Ao executar a expansão glob do shell usando *, é possível que a expansão falhe se não houver arquivos presentes no diretório e uma cadeia glob não expandida for passada para o comando a ser executado, o que pode ter resultados indesejáveis. O bashshell fornece uma opção de shell estendida para esse uso nullglob. Portanto, o loop se torna basicamente o seguinte dentro do diretório que contém seus arquivos

 shopt -s nullglob

 for file in ./*; do
     cmdToRun [option] -- "$file"
 done

Isso permite que você saia com segurança do loop for quando a expressão ./*não retornar nenhum arquivo (se o diretório estiver vazio)

ou de maneira compatível com POSIX ( nullglobé bashespecífico)

 for file in ./*; do
     [ -f "$file" ] || continue
     cmdToRun [option] -- "$file"
 done

Isso permite que você entre no loop quando a expressão falhar pela primeira vez e a condição [ -f "$file" ]verificar se a cadeia não expandida ./*é um nome de arquivo válido nesse diretório, o que não seria. Portanto, nessa falha de condição, usando continueretomamos o forloop que não será executado posteriormente.

Observe também o uso de --pouco antes de passar o argumento do nome do arquivo. Isso é necessário porque, como observado anteriormente, os nomes de arquivos do shell podem conter traços em qualquer lugar do nome do arquivo. Alguns dos comandos do shell interpretam isso e os tratam como uma opção de comando quando o nome não é citado corretamente e executa o comando pensando se o sinalizador for fornecido.

Os --sinais indicam as opções de final de linha de comando nesse caso, o que significa que o comando não deve analisar nenhuma sequência além deste ponto como sinalizadores de comando, mas apenas como nomes de arquivos.


A citação dupla dos nomes de arquivos resolve adequadamente os casos em que os nomes contêm caracteres glob ou espaços em branco. Mas os nomes de arquivos * nix também podem conter novas linhas. Portanto, limitamos os nomes de arquivos com o único caractere que não pode fazer parte de um nome de arquivo válido - o byte nulo ( \0). Como bashinternamente usa Ccadeias de estilo nas quais os bytes nulos são usados ​​para indicar o final da cadeia, é o candidato certo para isso.

Portanto, usando a printfopção do shell para delimitar arquivos com esse byte NULL usando a -dopção de readcomando, podemos fazer abaixo

( shopt -s nullglob; printf '%s\0' ./* ) | while read -rd '' file; do
    cmdToRun [option] -- "$file"
done

O nullglobe o printfsão agrupados, o (..)que significa que eles são basicamente executados em um sub-shell (shell filho), porque, para evitar a nullglobopção de refletir no shell pai, uma vez que o comando é encerrado. A -d ''opção de readcomando não é compatível com POSIX, portanto, é necessário um bashshell para que isso seja feito. Usando o findcomando, isso pode ser feito como

while IFS= read -r -d '' file; do
    cmdToRun [option] -- "$file"
done < <(find -maxdepth 1 -type f -print0)

Para findimplementações que não suportam -print0(além das implementações GNU e FreeBSD), isso pode ser emulado usandoprintf

find . -maxdepth 1 -type f -exec printf '%s\0' {} \; | xargs -0 cmdToRun [option] --

Outra correção importante é mover a redirecionamento para fora do loop for para reduzir um alto número de E / S de arquivo. Quando usado dentro do loop, o shell deve executar chamadas do sistema duas vezes para cada iteração do loop for, uma vez para abrir e outra para fechar o descritor de arquivo associado ao arquivo. Isso se tornará um gargalo no desempenho para executar iterações grandes. A sugestão recomendada seria movê-lo para fora do loop.

Estendendo o código acima com essas correções, você pode fazer

( shopt -s nullglob; printf '%s\0' ./* ) | while read -rd '' file; do
    cmdToRun [option] -- "$file"
done > results.out

que basicamente coloca o conteúdo do seu comando para cada iteração da entrada do arquivo em stdout e, quando o loop terminar, abra o arquivo de destino uma vez para gravar o conteúdo do stdout e salvá-lo. A findversão equivalente do mesmo seria

while IFS= read -r -d '' file; do
    cmdToRun [option] -- "$file"
done < <(find -maxdepth 1 -type f -print0) > results.out
Inian
fonte
1
+1 para verificar se o arquivo existe. Se pesquisar em um diretório inexistente, $ file contenha a sequência de caracteres regex "/ invald_dir / *" que não seja um nome de arquivo válido.
cdalxndr 19/02
3

Uma maneira rápida e suja de realizar o trabalho às vezes é:

find directory/ | xargs  Command 

Por exemplo, para encontrar o número de linhas em todos os arquivos no diretório atual, você pode:

find . | xargs wc -l
Rahul
fonte
8
@Hubert Por que você tem novas linhas em seus nomes de arquivos ?!
musicin3d
2
não é uma questão de "por que", é uma questão de correção - os nomes de arquivos não precisam incluir caracteres imprimíveis, nem precisam ser sequências UTF-8 válidas. Além disso, o que é uma nova linha depende muito da codificação, uma codificação ♀ é a nova linha da outra. Veja a página de código 437
Hubert Kario
2
vamos, sério? isso não funciona 99,9% do tempo, e ele disse "rápida e suja"
Edoardo
Eu não sou fã de scripts Bash "rápidos e sujos" (também conhecidos como "quebrados"). Mais cedo ou mais tarde, termina em coisas como o famoso "Movido ~/.local/share/steam. Funcionou. Excluiu tudo no sistema de propriedade do usuário". relatório de erro.
reduzindo a atividade
Isso também não funcionará com arquivos que possuem espaços no nome.
Shamas S - Restabelece Monica
2

Eu precisava copiar todos os arquivos .md de um diretório para outro, então aqui está o que eu fiz.

for i in **/*.md;do mkdir -p ../docs/"$i" && rm -r ../docs/"$i" && cp "$i" "../docs/$i" && echo "$i -> ../docs/$i"; done

O que é muito difícil de ler, então vamos detalhar.

primeiro CD no diretório com seus arquivos,

for i in **/*.md; para cada arquivo em seu padrão

mkdir -p ../docs/"$i"crie esse diretório em uma pasta docs fora da pasta que contém seus arquivos. O que cria uma pasta extra com o mesmo nome desse arquivo.

rm -r ../docs/"$i" remova a pasta extra criada como resultado de mkdir -p

cp "$i" "../docs/$i" Copie o arquivo real

echo "$i -> ../docs/$i" Eco o que você fez

; done Viva feliz para sempre

Eric Wooley
fonte
Nota: para **funcionar, a globstaropção shell precisa ser configurada:shopt -s globstar
Hubert Kario 12/11
2

Você pode usar xarg

ls | xargs -L 1 -d '\n' your-desired-command

-L 1 faz passar 1 item de cada vez

-d '\n'make output of lsé dividido com base na nova linha.

Al Mamun
fonte
1

Com base na abordagem de Jim Lewis:

Aqui está uma solução rápida, usando finde também classificando os arquivos pela data de modificação:

$ find  directory/ -maxdepth 1 -type f -print0 | \
  xargs -r0 stat -c "%y %n" | \
  sort | cut -d' ' -f4- | \
  xargs -d "\n" -I{} cmd -op1 {} 

Para classificação, consulte:

http://www.commandlinefu.com/commands/view/5720/find-files-and-list-them-sorted-by-modification-time

tuxdna
fonte
isso não vai funcionar se os arquivos têm novas linhas em seus nomes
Hubert Kario
1
@HubertKario Você pode querer ler mais sobre -print0para finde -0para o xargsqual usar caracteres nulos em vez de qualquer espaço em branco (incluindo novas linhas).
Tuxdna
sim, usando -print0é algo que ajuda, mas toda a necessidades de dutos para usar algo como isso, e sortnão é
Hubert Kario
1

Eu acho que a solução simples é:

sh /dir/* > ./result.txt
yovie
fonte
2
Você entendeu a pergunta corretamente? Isso apenas tentará executar cada arquivo no diretório através do shell - como se fosse um script.
Rd #
1

Profundidade máxima

Eu achei que funciona muito bem com a resposta de Jim Lewis, basta adicionar um pouco assim:

$ export DIR=/path/dir && cd $DIR && chmod -R +x *
$ find . -maxdepth 1 -type f -name '*.sh' -exec {} \; > results.out

Ordem de classificação

Se você deseja executar em ordem de classificação, modifique-o assim:

$ export DIR=/path/dir && cd $DIR && chmod -R +x *
find . -maxdepth 2 -type f -name '*.sh' | sort | bash > results.out

Apenas por exemplo, isso será executado na seguinte ordem:

bash: 1: ./assets/main.sh
bash: 2: ./builder/clean.sh
bash: 3: ./builder/concept/compose.sh
bash: 4: ./builder/concept/market.sh
bash: 5: ./builder/concept/services.sh
bash: 6: ./builder/curl.sh
bash: 7: ./builder/identity.sh
bash: 8: ./concept/compose.sh
bash: 9: ./concept/market.sh
bash: 10: ./concept/services.sh
bash: 11: ./product/compose.sh
bash: 12: ./product/market.sh
bash: 13: ./product/services.sh
bash: 14: ./xferlog.sh

Profundidade Ilimitada

Se você deseja executar em profundidade ilimitada por determinadas condições, pode usar o seguinte:

export DIR=/path/dir && cd $DIR && chmod -R +x *
find . -type f -name '*.sh' | sort | bash > results.out

em seguida, coloque em cima de cada arquivo nos diretórios filhos assim:

#!/bin/bash
[[ "$(dirname `pwd`)" == $DIR ]] && echo "Executing `realpath $0`.." || return

e em algum lugar no corpo do arquivo pai:

if <a condition is matched>
then
    #execute child files
    export DIR=`pwd`
fi
Chetabahana
fonte