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?


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

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

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
  • ...

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


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

10

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


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.


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 .)


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 

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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.