bash - функцию - shell скрипты




Выполнять команду из командной строки внутри каталогов из glob в скрипте оболочки bash (6)

В сценарии оболочки bash do-for.sh Я хочу выполнить команду во всех каталогах, названных в glob, используя bash. На это были даны ответы, но я хочу предоставить команду в командной строке. Другими словами, предполагая, что у меня есть каталоги:

  • foo
  • bar

Я хочу войти

do-for * pwd

и bash напечатать рабочий каталог внутри foo и затем внутри bar .

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

for dir in $1; do
  pushd ${dir}
  $2 $3 $4 $5 $6 $7 $8 $9
  popd
done

По-видимому, хотя glob * расширяется в другую переменную аргументов командной строки! Итак, первый раз через цикл, за $2 $3 $4 $5 $6 $7 $8 $9 Я ожидал foo pwd но вместо этого он появляется, я получаю foo bar !

Как я могу заставить glob в командной строке быть расширенным в другие параметры? Или есть лучший способ приблизиться к этому?

Чтобы сделать это более ясным, вот как я хочу использовать пакетный файл. (Это отлично работает в версии пакетного файла Windows, кстати.)

./do-for.sh repo-* git commit -a -m "Added new files."

В bash вы можете выполнить «set -o noglob», который запретит оболочке расширять имена путей (globs). Но это должно быть установлено на запущенной оболочке, прежде чем вы выполните скрипт, иначе вы должны указать любой метасимвол, который вы указываете в аргументах.


В этом случае проблема заключается не в расширении метасимвола, а в том, что ваш скрипт имеет неопределенное количество аргументов, последним из которых является команда для выполнения всех предыдущих аргументов.

#!/bin/bash
CMND=$(eval echo "\${$#}")        # get the command as last argument without arguments or
while [[ $# -gt 1 ]]; do          # execute loop for each argument except last one
     ( cd "$1" && eval "$CMND" )  # switch to each directory received and execute the command
     shift                    # throw away 1st arg and move to the next one in line
done

Использование: ./script.sh * pwd или ./script.sh * "ls -l"

Чтобы иметь команду, следующую за аргументами (пример ./script.sh * ls -l), сценарий должен быть длиннее, потому что каждый аргумент должен быть проверен, если он является каталогом, пока команда не будет идентифицирована (или назад, пока не будет идентифицирован каталог ).

Вот альтернативный скрипт, который бы принял синтаксис: ./script.sh <dirs...> <command> <arguments...> Например: ./script.sh * ls -la

# Move all dirs from args to DIRS array
typeset -i COUNT=0
while [[ $# -gt 1 ]]; do
    [[ -d "$1" ]] && DIRS[COUNT++]="$1" && shift || break
done

# Validate that the command received is valid
which "$1" >/dev/null 2>&1 || { echo "invalid command: $1"; exit 1; }

# Execute the command + it's arguments for each dir from array
for D in "${DIRS[@]}"; do 
     ( cd "$D" && eval "[email protected]" )
done

Вот как я это сделаю:

#!/bin/bash

# Read directory arguments into dirs array
for arg in "[email protected]"; do
    if [[ -d $arg ]]; then
        dirs+=("$arg")
    else
        break
    fi
done

# Remove directories from arguments
shift ${#dirs[@]}

cur_dir=$PWD

# Loop through directories and execute command
for dir in "${dirs[@]}"; do
    cd "$dir"
    "[email protected]"
    cd "$cur_dir"
done

Это зацикливается на аргументах, которые видны после расширения, и пока они являются каталогами, они добавляются в массив dirs . Как только встретится первый аргумент без каталога, мы предполагаем, что теперь начинается команда.

Затем каталоги удаляются из аргументов со shift , и мы сохраняем текущий каталог в cur_dir .

Последний цикл посещает каждый каталог и выполняет команду, состоящую из остальных аргументов.

Это работает для вашего

./do-for.sh repo-* git commit -a -m "Added new files."

пример - но если repo-* расширяется до чего-либо, кроме каталогов, сценарий прерывается, потому что он попытается выполнить имя файла как часть команды.

Это можно сделать более стабильным, если, например, glob и команда были разделены индикатором, таким как -- , но если вы знаете, что glob всегда будет просто каталогом, это должно работать.


Я предполагаю, что вы открыты для пользователей, которые должны предоставить какой-то разделитель, например

./do-for.sh repo-* -- git commit -a -m "Added new files."

Ваш скрипт мог бы что-то сделать (это просто объяснение концепции, я не тестировал фактический код):

CURRENT_DIR="$PWD"

declare -a FILES=()

for ARG in "[email protected]"
do
  [[ "$ARG" != "--" ]] || break
  FILES+=("$ARG")
  shift
done 

if
  [[ "${1-}" = "--" ]]
then
  shift
else
  echo "You must terminate the file list with -- to separate it from the command"
  (return, exit, whatever you prefer to stop the script/function)
fi

На данный момент у вас есть все целевые файлы в массиве, а «$ @» содержит только команду для выполнения. Остается только:

for FILE in "${FILES[@]-}"
do
  cd "$FILE"
  "[email protected]"
  cd "$CURRENT_DIR"
done

Обратите внимание, что это решение имеет то преимущество, что если ваш пользователь забывает разделитель «-», она будет уведомлена (в отличие от отказа из-за цитирования).


Почему бы не сохранить его простым и создать функцию оболочки, которая использует find но облегчает нагрузку для ваших пользователей на ввод команд, например:

do_for() { find . -type d \( ! -name . \) -not -path '*/\.*' -name $1 -exec bash -c "cd '{}' && "${@:2}" " \;  }

Поэтому они могут ввести что-то вроде do_for repo-* git commit -a -m "Added new files." Обратите внимание: если вы хотите использовать * самостоятельно, вам придется сбежать от него:

do_for \* pwd 

Никто не предлагает решение, использующее find ? Почему бы не попробовать что-то вроде этого:

find . -type d \( -wholename 'YOURPATTERN' \) -print0 | xargs -0 YOURCOMMAND

Посмотрите на man find дополнительные опции.





glob