Как перейти в каждый каталог и выполнить команду?

152

Как я пишу Баш скрипт , который проходит через каждый каталог внутри parent_directory и выполняет в команду в каждом каталоге .

Структура каталогов следующая:

parent_directory (имя может быть любым - не соответствует шаблону)

  • 001 (имена каталогов соответствуют этому шаблону)
    • 0001.txt (имена файлов следуют этому шаблону)
    • 0002.txt
    • 0003.txt
  • 002
    • 0001.txt
    • 0002.txt
    • 0003.txt
    • 0004.txt
  • 003
    • 0001.txt

количество каталогов неизвестно.

Ван де Графф
источник

Ответы:

110

Вы можете сделать следующее, если ваш текущий каталог parent_directory:

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

Команды (и )создают подоболочку, поэтому текущий каталог не изменяется в основном сценарии.

Марк Лонгэр
источник
5
Здесь это не имеет значения, потому что подстановочный знак не соответствует ничему, кроме чисел, но в общем случае вы всегда хотите поместить имя каталога в двойные кавычки внутри цикла. cd "$d"было бы лучше, если бы он переносился в ситуации, когда подстановочный знак действительно соответствует файлам, имена которых содержат пробелы и / или метасимволы оболочки.
Tripleee
1
(Кажется, что все ответы здесь имеют один и тот же недостаток, но он имеет наибольшее значение в ответе, получившем наибольшее количество голосов.)
tripleee
1
Кроме того, подавляющему большинству команд на самом деле все равно, в каком каталоге вы их выполняете. for f in foo bar; do cat "$f"/*.txt; done >outputфункционально эквивалентен cat foo/*.txt bar/*.txt >output. Тем не менее, lsэто одна команда, которая выводит немного разные результаты в зависимости от того, как вы передаете ей аргументы; аналогично, grepили, wcкоторый выводит относительное имя файла, будет другим, если вы запустите его из подкаталога (но часто вы хотите избежать входа в подкаталог именно по этой причине).
tripleee
168

Этот ответ, опубликованный Тоддом, мне помог.

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

В \( ! -name . \) избегает выполнения команды в текущем каталоге.

Кристиан Вьельма
источник
4
И если вы хотите запускать команду только в определенных папках:find FOLDER* -maxdepth 0 -type d \( ! -name . \) -exec bash -c "cd '{}' && pwd" \;
Мартин К.
3
Мне пришлось это сделать, -exec bash -c 'cd "$0" && pwd' {} \;поскольку некоторые каталоги содержали одинарные и некоторые двойные кавычки.
Чарли Горичаназ
13
Вы также можете опустить текущий каталог, добавив-mindepth 1
mdziob
Как превратить это в функцию, принимающую команду типа «git remote get-url origin», а не pwdнапрямую?
roachsinai
disd(){find . -maxdepth 1 -type d \( ! -name . \) -exec bash -c 'cd "{}" && "$@"' \;}не работает.
roachsinai
50

Если вы используете GNU find, вы можете попробовать -execdirпараметр, например:

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

или (согласно комментарию @gniourf_gniourf ):

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

Примечание: вы можете использовать ${0#./}вместо, $0чтобы исправить ./спереди.

или более практический пример:

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

Если вы хотите включить текущий каталог, это еще проще, используя -exec:

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

или используя xargs:

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

Или аналогичный пример, предложенный @gniourf_gniourf :

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

В приведенных выше примерах поддерживаются каталоги с пробелами в имени.


Или назначив в массив bash:

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

Измените .имя папки на свое. Если вам не нужно запускать рекурсивно, вы можете использовать dirs=(*)вместо этого :. В приведенном выше примере не поддерживаются каталоги с пробелами в имени.

Итак, как предложил @gniourf_gniourf , единственный правильный способ поместить вывод find в массив без использования явного цикла будет доступен в Bash 4.4 с:

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

Или не рекомендуемый способ (который включает в себя разборls ):

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

В приведенном выше примере будет игнорироваться текущий каталог (по запросу OP), но он будет разбит на имена с пробелами.

Смотрите также:

Kenorb
источник
1
Единственный правильный способ поместить вывод findв массив без использования явного цикла будет доступен в Bash 4.4 с mapfile -t -d '' dirs < <(find . -type d -print0). До тех пор единственный вариант - использовать цикл (или отложить операцию до find).
gniourf_gniourf
1
Фактически, вам действительно не нужен dirsмассив; вы могли бы петля на findвыходе «S (безопасно) , так как: find . -type d -print0 | while IFS= read -r -d '' file; do ...; done.
gniourf_gniourf
@gniourf_gniourf Я визуальный и всегда пытаюсь найти / сделать вещи, которые выглядят простыми. Например, у меня есть этот сценарий, и с помощью массива bash я легко и легко могу читать (разделив логику на более мелкие части вместо использования каналов). Вводя дополнительные трубы, IFS, read, это создает впечатление дополнительной сложности. Придется подумать, может есть способ попроще.
kenorb 04
Если под « проще» вы имеете в виду сломанный, то да, есть более простые способы сделать это. Теперь не волнуйтесь, все это IFSи read(как вы говорите) стать второй натурой , как вы привыкнете к ним ... они являются частью канонического и идиоматических способов написания сценариев.
gniourf_gniourf
Между прочим, ваша первая команда find . -type d -execdir echo $(pwd)/{} ';'не делает то, что вы хотите ( $(pwd)расширяется до того, findкак даже выполняется)…
gniourf_gniourf
33

Вы можете добиться этого, установив трубопровод, а затем используя xargs. Уловка заключается в том, что вам нужно использовать -Iфлаг, который заменит подстроку в вашей команде bash подстрокой, переданной каждым из xargs.

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

Вы можете заменить pwdлюбую команду, которую хотите выполнить в каждом каталоге.

Пиюш Сингх
источник
4
Не знаю, почему за него не проголосовали, но это самый простой сценарий, который я нашел до сих пор. Спасибо
Linvi
20

Если папка верхнего уровня известна, вы можете просто написать что-то вроде этого:

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

На $ (ИГРАЙТЕ СКОЛЬКО ХОЧЕТЕ); вы можете поместить столько кода, сколько захотите.

Обратите внимание, что я не «cd» ни в одном каталоге.

Привет,

Gforcada
источник
3
Здесь есть пара примеров «бесполезного использования ls» - вы можете просто сделать это for dir in $YOUR_TOP_LEVEL_FOLDER/*.
Марк Лонгэр
1
Что ж, может быть, это бесполезно в общем смысле, но позволяет делать фильтрацию прямо в ls (т.е. все каталоги, заканчивающиеся на .tmp). Вот почему я использовал ls $dirвыражение
gforcada
14
for dir in PARENT/*
do
  test -d "$dir" || continue
  # Do something with $dir...
done
Idelic
источник
И это очень легко понять!
Rbjz
6

Хотя один лайнер хорош для быстрого и грязного использования, я предпочитаю ниже более подробную версию для написания скриптов. Это шаблон, который я использую, он заботится о многих крайних случаях и позволяет вам писать более сложный код для выполнения в папке. Вы можете написать свой код на bash в функции dir_command. Ниже dir_coomand в качестве примера реализует пометку каждого репозитория в git. Остальная часть скрипта вызывает dir_command для каждой папки в каталоге. Также включен пример перебора только заданного набора папок.

#!/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"
Шитал Шах
источник
5

Я не понимаю, что такое форматирование файла, так как вы хотите только перебирать папки ... Вы ищете что-то вроде этого?

cd parent
find . -type d | while read d; do
   ls $d/
done
Аиф
источник
4

ты можешь использовать

find .

для рекурсивного поиска всех файлов / каталогов в текущем каталоге

Затем вы можете передать вывод команде xargs следующим образом

find . | xargs 'command here'
Дэн Биздадеа
источник
3
Это не то, о чем спрашивали.
kenorb 04
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
Федор РЫХТИК
источник
Вам нужно будет снова изменить резервную копию каталога или сделать это cdв подоболочке.
Марк Лонгэр
Голос против: редактирование потеряно, cdпоэтому этот код фактически не делает ничего полезного.
Tripleee