Execute uma ação em cada subdiretório usando o Bash

194

Estou trabalhando em um script que precisa executar uma ação em todos os subdiretórios de uma pasta específica.

Qual é a maneira mais eficiente de escrever isso?

mikewilliamson
fonte
1
Por favor, considere voltar e reavaliar as respostas para correção - você tem uma resposta aceita obtendo muitas visualizações, apesar dos principais erros (f / e, executá-la em um diretório em que alguém anteriormente executou mkdir 'foo * bar'causará fooe barserá repetida mesmo que eles não existem e *serão substituídos por uma lista de todos os nomes de arquivos, mesmo os que não sejam do diretório).
Charles Duffy
1
... ainda pior é se alguém executou mkdir -p '/tmp/ /etc/passwd /'- se alguém executa um script seguindo esta prática /tmppara, por exemplo, encontrar diretórios para excluir, eles podem acabar excluindo /etc/passwd.
Charles Duffy

Respostas:

177
for D in `find . -type d`
do
    //Do whatever you need with D
done
Mike Clark
fonte
81
isso vai quebrar em espaços em branco
ghostdog74
2
Além disso, observe que você precisa ajustar os findparâmetros se desejar um comportamento recursivo ou não recursivo.
precisa
32
A resposta acima me deu o diretório auto, bem, então o seguinte trabalhou um pouco melhor para mim: find . -mindepth 1 -type d
jzheaux
1
@ JoshC, ambas as variantes dividirão o diretório criado por mkdir 'directory name with spaces'em quatro palavras separadas.
Charles Duffy
4
Eu precisava adicionar -mindepth 1 -maxdepth 1ou foi muito profundo.
ScrappyDev 01/05/19
282

Uma versão que evita a criação de um subprocesso:

for D in *; do
    if [ -d "${D}" ]; then
        echo "${D}"   # your processing here
    fi
done

Ou, se sua ação for um único comando, isso é mais conciso:

for D in *; do [ -d "${D}" ] && my_command; done

Ou uma versão ainda mais concisa ( obrigado @enzotib ). Observe que nesta versão cada valor de Dterá uma barra final:

for D in */; do my_command; done
kanaka
fonte
48
Você pode evitar o ifou [com:for D in */; do
enzotib 23/10/10
2
+1 porque os nomes dos diretórios não começam com ./, em vez da resposta aceita
Hayri Uğur Koltuk
3
Este está correto até mesmo nos espaços nos nomes dos diretórios +1
Alex Reinking 4/14
5
Há um problema com o último comando: se você estiver em um diretório sem subdiretórios; retornará "* /". Então é melhor usar o segundo comando for D in *; do [ -d "${D}" ] && my_command; doneou uma combinação dos dois mais recentes:for D in */; do [ -d $D ] && my_command; done
Chris Maes
5
Observe que esta resposta ignora diretórios ocultos. Para incluir diretórios ocultos usar for D in .* *; dovez for D in *; do.
Patryk.beza
105

A maneira mais simples e não recursiva é:

for d in */; do
    echo "$d"
done

No /final, use apenas diretórios.

Não há necessidade de

  • encontrar
  • awk
  • ...
d0x
fonte
11
Nota: isso não inclui pontos dirs (o que pode ser uma coisa boa, mas é importante saber).
wisbucky
Útil observar que você pode usar shopt -s dotglobpara incluir arquivos de pontos / pontos de ponto ao expandir curingas. Veja também: gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html
Steen Schütt
Eu acho que você quis dizer /*, em vez de */com o /que representa o caminho que você deseja usar.
Brōtsyorfuzthrāx
2
@Shule /*seria para caminho absoluto enquanto que */incluiria os subdiretórios do local atual
Dan G
3
dica útil: se você precisar cortar o '/' à direita de $ d, use${d%/*}
Hafthor 5/10/19
18

Usar find comando

No GNU find, você pode usar o -execdirparâmetro:

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

ou usando o -execparâmetro:

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

ou com o xargscomando:

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

Ou usando o loop for :

for d in */; { echo "$d"; }

Para recursividade, tente estender o globbing ( **/) em vez disso (ativar por :) shopt -s extglob.


Para mais exemplos, consulte: Como ir para cada diretório e executar um comando? na SO

kenorb
fonte
1
-exec {} +é especificado para POSIX, -exec sh -c 'owd=$PWD; for arg; do cd -- "$arg" && pwd -P; cd -- "$owd"; done' _ {} +é outra opção legal e chama menos shells que -exec sh -c '...' {} \;.
Charles Duffy
13

Handy one-liners

for D in *; do echo "$D"; done
for D in *; do find "$D" -type d; done ### Option A

find * -type d ### Option B

A opção A está correta para pastas com espaços no meio. Além disso, geralmente mais rápido, pois não imprime cada palavra no nome de uma pasta como uma entidade separada.

# Option A
$ time for D in ./big_dir/*; do find "$D" -type d > /dev/null; done
real    0m0.327s
user    0m0.084s
sys     0m0.236s

# Option B
$ time for D in `find ./big_dir/* -type d`; do echo "$D" > /dev/null; done
real    0m0.787s
user    0m0.484s
sys     0m0.308s
Sriram Murali
fonte
10

find . -type d -print0 | xargs -0 -n 1 my_command

Paul Tomblin
fonte
posso alimentar o valor de retorno desse achado em um loop for? Isso faz parte de um script maior ...
mikewilliamson
@ Mike, improvável. $? provavelmente você obterá o status do comando find ou xargs, em vez de my_command.
Paul Tomblin
7

Isso criará um subshell (o que significa que os valores das variáveis ​​serão perdidos quando o whileloop sair):

find . -type d | while read -r dir
do
    something
done

Isso não vai:

while read -r dir
do
    something
done < <(find . -type d)

Qualquer um funcionará se houver espaços nos nomes de diretório.

Pausado até novo aviso.
fonte
Para uma manipulação ainda melhor de nomes de arquivos estranhos (incluindo nomes que terminam em espaço em branco e / ou incluem alimentações de linha), use find ... -print0ewhile IFS="" read -r -d $'\000' dir
Gordon Davisson
@GordonDavisson, ... de fato, eu diria até que isso -d ''é menos enganador sobre sintaxe e recursos do bash, pois -d $'\000'implica (falsamente) que $'\000'é de alguma forma diferente de ''- de fato, alguém poderia facilmente (e novamente, falsamente) inferir de o bash suporta seqüências de caracteres no estilo Pascal (especificadas em comprimento, capazes de conter literais NUL) em vez de seqüências de caracteres C (delimitadas por NUL, incapazes de conter NULs).
Charles Duffy
5

Você poderia tentar:

#!/bin/bash
### $1 == the first args to this script
### usage: script.sh /path/to/dir/

for f in `find . -maxdepth 1 -mindepth 1 -type d`; do
  cd "$f"
  <your job here>
done

ou similar...

Explicação:

find . -maxdepth 1 -mindepth 1 -type d: Encontre apenas diretórios com profundidade recursiva máxima de 1 (apenas os subdiretórios $ 1) e profundidade mínima de 1 (excluindo a pasta atual .)

Henry Dobson
fonte
Isso é buggy - tente com um nome de diretório com espaços. Consulte BashPitfalls # 1 e DontReadLinesWithFor .
Charles Duffy
O nome do diretório com espaços está entre aspas e, portanto, funciona e o OP não está tentando ler as linhas do arquivo.
Henry Dobson
funciona no cd "$f". Ele não funciona quando a saída de findé dividida em seqüências de caracteres, portanto, você terá as partes separadas do nome como valores separados $f, tornando o quão bem você faz ou não cita $fo debate de expansão.
Charles Duffy
Eu não disse que eles estavam tentando ler linhas de um arquivo. findA saída de é orientada a linhas (um nome para uma linha, em teoria - mas veja abaixo) com a -printação padrão .
Charles Duffy
A saída orientada a linhas, a partir de, find -printnão é uma maneira segura de passar nomes de arquivos arbitrários, pois é possível executar algo mkdir -p $'foo\n/etc/passwd\nbar'e obter um diretório que possui /etc/passwduma linha separada em seu nome. O tratamento de nomes de arquivos /uploadou /tmpdiretórios sem cuidado é uma ótima maneira de obter ataques de escalonamento de privilégios.
Charles Duffy
4

a resposta aceita será interrompida em espaços em branco se os nomes de diretório os tiverem, e a sintaxe preferida é $()para bash / ksh. Use a GNU find -exec opção com +;por exemplo

find .... -exec mycommand +; #this is same as passing to xargs

ou use um loop while

find .... |  while read -r D
do
  ...
done 
ghostdog74
fonte
qual parâmetro conterá o nome do diretório? por exemplo, chmod + x $ DIR_NAME (sim, eu sei que existe uma opção chmod apenas para diretórios) #
Mike Graf
Há uma diferença sutil entre find -exece a passagem para xargs: findignorará o valor de saída do comando que está sendo executado, enquanto xargsfalhará em uma saída diferente de zero. Pode estar correto, dependendo de suas necessidades.
Jason Wodicka
find ... -print0 | while IFS= read -r dé mais seguro - suporta nomes que começam ou terminam em espaço em branco e nomes que contêm literais de nova linha.
Charles Duffy