Como ir para cada diretório e executar um comando?

143

Como escrevo um script bash que percorre cada diretório dentro de um diretório parent_executor e executa um comando em cada diretório .

A estrutura de diretórios é a seguinte:

parent_directory (o nome pode ser qualquer coisa - não segue um padrão)

  • 001 (os nomes de diretório seguem esse padrão)
    • 0001.txt (os nomes dos arquivos seguem esse padrão)
    • 0002.txt
    • 0003.txt
  • 002
    • 0001.txt
    • 0002.txt
    • 0003.txt
    • 0004.txt
  • 003
    • 0001.txt

o número de diretórios é desconhecido.

Van de Graff
fonte

Respostas:

108

Você pode fazer o seguinte, quando o diretório atual for parent_directory:

for d in [0-9][0-9][0-9]
do
    ( cd "$d" && your-command-here )
done

O (e )criar uma subshell, de modo que o diretório atual não é alterado no script principal.

Mark Longair
fonte
5
Não importa aqui porque o curinga não corresponde a nada além de números, mas no caso geral, você basicamente sempre deseja colocar o nome do diretório entre aspas duplas dentro do loop. cd "$d"seria melhor, pois ele é transferido para situações em que o curinga corresponde a arquivos cujos nomes contêm metacaracteres de espaço em branco e / ou shell.
Tripleee
1
(Todas as respostas aqui parecem ter a mesma falha, mas que mais importa no mais votados resposta.)
tripleee
1
Além disso, a grande maioria dos comandos não se importa em qual diretório você os executa. for f in foo bar; do cat "$f"/*.txt; done >outputé funcionalmente equivalente a cat foo/*.txt bar/*.txt >output. No entanto, lsé um comando que produz resultados ligeiramente diferentes, dependendo de como você passa os argumentos; da mesma forma, um grepou wcque emita um nome de arquivo relativo será diferente se você o executar em um subdiretório (mas frequentemente, você deseja evitar entrar em um subdiretório precisamente por esse motivo).
Tripleee
158

Esta resposta postada por Todd me ajudou.

find . -maxdepth 1 -type d \( ! -name . \) -exec bash -c "cd '{}' && pwd" \;

Os \( ! -name . \) evita a execução do comando no diretório atual.

Christian Vielma
fonte
4
E se você deseja apenas executar o comando em determinadas pastas:find FOLDER* -maxdepth 0 -type d \( ! -name . \) -exec bash -c "cd '{}' && pwd" \;
Martin K
3
Eu tive que fazer, -exec bash -c 'cd "$0" && pwd' {} \;já que alguns diretórios continham aspas simples e duplas.
Charlie Gorichanaz
12
Você também pode omitir o diretório atual adicionando-mindepth 1
mdziob
Como mudar isso para uma função aceitar um comando como "git remoto obter-url origin` em vez de pwddiretamente?
roachsinai
disd(){find . -maxdepth 1 -type d \( ! -name . \) -exec bash -c 'cd "{}" && "$@"' \;}não funciona.
21719 roachsinai
48

Se você estiver usando o GNU find, pode tentar o -execdirparâmetro, por exemplo:

find . -type d -execdir realpath "{}" ';'

ou (conforme comentário do @gniourf_gniourf ):

find . -type d -execdir sh -c 'printf "%s/%s\n" "$PWD" "$0"' {} \;

Nota: Você pode usar em ${0#./}vez de $0corrigir ./na frente.

ou exemplo mais prático:

find . -name .git -type d -execdir git pull -v ';'

Se você deseja incluir o diretório atual, é ainda mais simples usando -exec:

find . -type d -exec sh -c 'cd -P -- "{}" && pwd -P' \;

ou usando xargs:

find . -type d -print0 | xargs -0 -L1 sh -c 'cd "$0" && pwd && echo Do stuff'

Ou exemplo semelhante sugerido por @gniourf_gniourf :

find . -type d -print0 | while IFS= read -r -d '' file; do
# ...
done

Os exemplos acima suportam diretórios com espaços em seus nomes.


Ou atribuindo ao array bash:

dirs=($(find . -type d))
for dir in "${dirs[@]}"; do
  cd "$dir"
  echo $PWD
done

Mude .para o nome da sua pasta específica. Se você não precisa executar de forma recursiva, você pode usar: dirs=(*)em vez. O exemplo acima não suporta diretórios com espaços no nome.

Portanto, como sugeriu @gniourf_gniourf , a única maneira adequada de colocar a saída de find em uma matriz sem usar um loop explícito estará disponível no Bash 4.4 com:

mapfile -t -d '' dirs < <(find . -type d -print0)

Ou não é uma maneira recomendada (que envolve a análise dels ):

ls -d */ | awk '{print $NF}' | xargs -n1 sh -c 'cd $0 && pwd && echo Do stuff'

O exemplo acima ignoraria o diretório atual (conforme solicitado pelo OP), mas quebraria os nomes com os espaços.

Veja também:

kenorb
fonte
1
A única maneira adequada de colocar a saída findem uma matriz sem usar um loop explícito estará disponível no Bash 4.4 with mapfile -t -d '' dirs < <(find . -type d -print0). Até lá, sua única opção é usar um loop (ou adiar a operação para find).
Gniourf_gniourf #
1
De fato, você realmente não precisa da dirsmatriz; você poderia loop na findsaída 's (com segurança) assim: find . -type d -print0 | while IFS= read -r -d '' file; do ...; done.
Gniourf_gniourf #
@gniourf_gniourf Sou visual e estou sempre tentando encontrar / fazer as coisas que parecem simples. Por exemplo, eu tenho esse script e, usando o array bash, é fácil e legível para mim (separando a lógica em pedaços menores, em vez de usar pipes). Apresentando tubos adicionais, IFS, read, dá a impressão de complexidade adicional. Vou ter que pensar sobre isso, talvez haja alguma maneira mais fácil de fazer isso.
Kenorb # 4/15
Se por mais fácil você quer dizer quebrado, então sim, existem maneiras mais fáceis de fazer. Agora não se preocupe, tudo isso IFSe read(como você diz) se tornam uma segunda natureza à medida que você se acostuma com eles ... eles fazem parte das formas canônicas e idiomáticas de escrever scripts.
gniourf_gniourf
By the way, seu primeiro comando find . -type d -execdir echo $(pwd)/{} ';'não faz o que você quer (o $(pwd)é expandida antes findsequer é executado) ...
gniourf_gniourf
29

Você pode conseguir isso canalizando e usando xargs. O problema é que você precisa usar o -Isinalizador que substituirá a substring em seu comando bash pela substring passada por cada um dos xargs.

ls -d */ | xargs -I {} bash -c "cd '{}' && pwd"

Você pode substituir pwdpor qualquer comando que deseja executar em cada diretório.

Piyush Singh
fonte
2
Não sei por que isso não foi votado, mas o script mais simples que encontrei até agora. Obrigado
Linvi
20

Se a pasta de nível superior for conhecida, basta escrever algo como isto:

for dir in `ls $YOUR_TOP_LEVEL_FOLDER`;
do
    for subdir in `ls $YOUR_TOP_LEVEL_FOLDER/$dir`;
    do
      $(PLAY AS MUCH AS YOU WANT);
    done
done

No $ (JOGUE QUANTO QUERER); você pode colocar o código que quiser.

Note que eu não "cd" em nenhum diretório.

Felicidades,

gforcada
fonte
3
Existem alguns exemplos de "Uso inútil de ls" aqui - você pode simplesmente fazer isso for dir in $YOUR_TOP_LEVEL_FOLDER/*.
Mark Longair
1
Bem, talvez seja inútil em um sentido geral, mas permite filtrar diretamente no ls (ou seja, todos os diretórios terminados em .tmp). É por isso que eu usei a ls $direxpressão
gforcada
13
for dir in PARENT/*
do
  test -d "$dir" || continue
  # Do something with $dir...
done
Idelic
fonte
E isso é muito fácil de entender!
Rbjz
6

Enquanto um liners é bom para uso rápido e sujo, eu prefiro a versão mais detalhada abaixo para escrever scripts. Este é o modelo que eu uso, que cuida de muitos casos extremos e permite que você escreva um código mais complexo para executar em uma pasta. Você pode escrever seu código bash na função dir_command. Abaixo, dir_coomand implementa a marcação de cada repositório no git como um exemplo. O restante do script chama dir_command para cada pasta no diretório O exemplo de iteração através de apenas um determinado conjunto de pastas também é incluído.

#!/bin/bash

#Use set -x if you want to echo each command while getting executed
#set -x

#Save current directory so we can restore it later
cur=$PWD
#Save command line arguments so functions can access it
args=("$@")

#Put your code in this function
#To access command line arguments use syntax ${args[1]} etc
function dir_command {
    #This example command implements doing git status for folder
    cd $1
    echo "$(tput setaf 2)$1$(tput sgr 0)"
    git tag -a ${args[0]} -m "${args[1]}"
    git push --tags
    cd ..
}

#This loop will go to each immediate child and execute dir_command
find . -maxdepth 1 -type d \( ! -name . \) | while read dir; do
   dir_command "$dir/"
done

#This example loop only loops through give set of folders    
declare -a dirs=("dir1" "dir2" "dir3")
for dir in "${dirs[@]}"; do
    dir_command "$dir/"
done

#Restore the folder
cd "$cur"
Shital Shah
fonte
5

Não entendi o que é a formatação do arquivo, já que você só deseja percorrer as pastas ... Você está procurando algo assim?

cd parent
find . -type d | while read d; do
   ls $d/
done
Aif
fonte
4

você pode usar

find .

procurar todos os arquivos / diretórios no diretório atual recorrente

Do que você pode canalizar a saída, o comando xargs está assim

find . | xargs 'command here'
Dan Bizdadea
fonte
3
Não foi isso que foi perguntado.
kenorb
0
for p in [0-9][0-9][0-9];do
    (
        cd $p
        for f in [0-9][0-9][0-9][0-9]*.txt;do
            ls $f; # Your operands
        done
    )
done
Fedir RYKHTIK
fonte
Você precisaria alterar novamente o diretório ou fazer isso cdem uma subshell.
Mark Longair
Downvote: a edição perdeu o, cdpara que este código não faça nada útil.
Tripleee