Получение нескольких аргументов для одного параметра с помощью getopts в Bash - PullRequest
43 голосов
/ 23 сентября 2011

Мне нужна помощь с getopts.

Я создал скрипт Bash, который выглядит так при запуске:

$ foo.sh -i env -d каталог -s подкаталог -f файл

Работает правильно при обработке одного аргумента от каждого флага. Но когда я вызываю несколько аргументов из каждого флага, я не уверен, как извлечь информацию о нескольких переменных из переменных в getopts.

while getopts ":i:d:s:f:" opt
   do
     case $opt in
        i ) initial=$OPTARG;;
        d ) dir=$OPTARG;;
        s ) sub=$OPTARG;;
        f ) files=$OPTARG;;

     esac
done

После получения параметров я хочу построить структуры каталогов из переменных

foo.sh -i test -d directory -s subdirectory -s subdirectory2 -f file1 file2 file3

Тогда структура каталогов будет

/test/directory/subdirectory/file1
/test/directory/subdirectory/file2
/test/directory/subdirectory/file3
/test/directory/subdirectory2/file1
/test/directory/subdirectory2/file2
/test/directory/subdirectory2/file3

Есть идеи?

Ответы [ 9 ]

62 голосов
/ 24 декабря 2013

Вы можете использовать одну и ту же опцию несколько раз и добавлять все значения в массив .

Для очень специфического исходного вопроса решение Райана mkdir -p, очевидно, является лучшим.

Однако, для более общего вопроса получения нескольких значений из одного и того же параметра с помощью getopts , вот оно:

#!/bin/bash

while getopts "m:" opt; do
    case $opt in
        m) multi+=("$OPTARG");;
        #...
    esac
done
shift $((OPTIND -1))

echo "The first value of the array 'multi' is '$multi'"
echo "The whole list of values is '${multi[@]}'"

echo "Or:"

for val in "${multi[@]}"; do
    echo " - $val"
done

Вывод будет:

$ /tmp/t
The first value of the array 'multi' is ''
The whole list of values is ''
Or:

$ /tmp/t -m "one arg with spaces"
The first value of the array 'multi' is 'one arg with spaces'
The whole list of values is 'one arg with spaces'
Or:
 - one arg with spaces

$ /tmp/t -m one -m "second argument" -m three
The first value of the array 'multi' is 'one'
The whole list of values is 'one second argument three'
Or:
 - one
 - second argument
 - three
22 голосов
/ 30 мая 2012

Я знаю, что этот вопрос старый, но я хотел бы добавить этот ответ на тот случай, если кто-то придет в поисках ответа.

Такие оболочки, как BASH, делают рекурсивные каталоги уже такими, поэтому скрипт на самом деле не нужен. Например, оригинальный плакат хотел что-то вроде:

$ foo.sh -i test -d directory -s subdirectory -s subdirectory2 -f file1 file2 file3
/test/directory/subdirectory/file1
/test/directory/subdirectory/file2
/test/directory/subdirectory/file3
/test/directory/subdirectory2/file1
/test/directory/subdirectory2/file2
/test/directory/subdirectory2/file3

Это легко сделать с помощью этой командной строки:

pong:~/tmp
[10] rmclean$ mkdir -pv test/directory/{subdirectory,subdirectory2}/{file1,file2,file3}
mkdir: created directory ‘test’
mkdir: created directory ‘test/directory’
mkdir: created directory ‘test/directory/subdirectory’
mkdir: created directory ‘test/directory/subdirectory/file1’
mkdir: created directory ‘test/directory/subdirectory/file2’
mkdir: created directory ‘test/directory/subdirectory/file3’
mkdir: created directory ‘test/directory/subdirectory2’
mkdir: created directory ‘test/directory/subdirectory2/file1’
mkdir: created directory ‘test/directory/subdirectory2/file2’
mkdir: created directory ‘test/directory/subdirectory2/file3’

Или даже немного короче:

pong:~/tmp
[12] rmclean$ mkdir -pv test/directory/{subdirectory,subdirectory2}/file{1,2,3}
mkdir: created directory ‘test’
mkdir: created directory ‘test/directory’
mkdir: created directory ‘test/directory/subdirectory’
mkdir: created directory ‘test/directory/subdirectory/file1’
mkdir: created directory ‘test/directory/subdirectory/file2’
mkdir: created directory ‘test/directory/subdirectory/file3’
mkdir: created directory ‘test/directory/subdirectory2’
mkdir: created directory ‘test/directory/subdirectory2/file1’
mkdir: created directory ‘test/directory/subdirectory2/file2’
mkdir: created directory ‘test/directory/subdirectory2/file3’

Или короче, с большим соответствием:

pong:~/tmp
[14] rmclean$ mkdir -pv test/directory/subdirectory{1,2}/file{1,2,3}
mkdir: created directory ‘test’
mkdir: created directory ‘test/directory’
mkdir: created directory ‘test/directory/subdirectory1’
mkdir: created directory ‘test/directory/subdirectory1/file1’
mkdir: created directory ‘test/directory/subdirectory1/file2’
mkdir: created directory ‘test/directory/subdirectory1/file3’
mkdir: created directory ‘test/directory/subdirectory2’
mkdir: created directory ‘test/directory/subdirectory2/file1’
mkdir: created directory ‘test/directory/subdirectory2/file2’
mkdir: created directory ‘test/directory/subdirectory2/file3’

Или, наконец, используя последовательности:

pong:~/tmp
[16] rmclean$ mkdir -pv test/directory/subdirectory{1..2}/file{1..3}
mkdir: created directory ‘test’
mkdir: created directory ‘test/directory’
mkdir: created directory ‘test/directory/subdirectory1’
mkdir: created directory ‘test/directory/subdirectory1/file1’
mkdir: created directory ‘test/directory/subdirectory1/file2’
mkdir: created directory ‘test/directory/subdirectory1/file3’
mkdir: created directory ‘test/directory/subdirectory2’
mkdir: created directory ‘test/directory/subdirectory2/file1’
mkdir: created directory ‘test/directory/subdirectory2/file2’
mkdir: created directory ‘test/directory/subdirectory2/file3’
19 голосов
/ 23 сентября 2011

Опции getopts могут принимать только ноль или один аргумент. Возможно, вы захотите изменить свой интерфейс, чтобы удалить опцию -f, и просто перебрать оставшиеся неопционные аргументы

usage: foo.sh -i end -d dir -s subdir file [...]

Итак,

while getopts ":i:d:s:" opt; do
  case "$opt" in
    i) initial=$OPTARG ;;
    d) dir=$OPTARG ;;
    s) sub=$OPTARG ;;
  esac
done
shift $(( OPTIND - 1 ))

path="/$initial/$dir/$sub"
mkdir -p "$path"

for file in "$@"; do
  touch "$path/$file"
done
9 голосов
/ 28 декабря 2011

Я исправил ту же проблему, что и у вас, вот так:

Вместо:

foo.sh -i test -d directory -s subdirectory -s subdirectory2 -f file1 file2 file3

Сделайте это:

foo.sh -i test -d directory -s "subdirectory subdirectory2" -f "file1 file2 file3"

С разделителем пространства вы можете простопробежаться по нему с помощью основного цикла.Вот код:

while getopts ":i:d:s:f:" opt
   do
     case $opt in
        i ) initial=$OPTARG;;
        d ) dir=$OPTARG;;
        s ) sub=$OPTARG;;
        f ) files=$OPTARG;;

     esac
done

for subdir in $sub;do
   for file in $files;do
      echo $subdir/$file
   done   
done

Вот пример вывода:

$ ./getopts.sh -s "testdir1 testdir2" -f "file1 file2 file3"
testdir1/file1
testdir1/file2
testdir1/file3
testdir2/file1
testdir2/file2
testdir2/file3
5 голосов
/ 16 марта 2017

Если вы хотите указать любое количество значений для опции, вы можете использовать простой цикл, чтобы найти их и поместить их в массив.Например, давайте изменим пример OP, чтобы разрешить любое количество параметров -s:

unset -v sub
while getopts ":i:d:s:f:" opt
   do
     case $opt in
        i ) initial=$OPTARG;;
        d ) dir=$OPTARG;;
        s ) sub=("$OPTARG")
            until [[ $(eval "echo \${$OPTIND}") =~ ^-.* ]] || [ -z $(eval "echo \${$OPTIND}") ]; do
                sub+=($(eval "echo \${$OPTIND}"))
                OPTIND=$((OPTIND + 1))
            done
            ;;
        f ) files=$OPTARG;;
     esac
done

Это принимает первый аргумент ($ OPTARG) и помещает его в массив $ sub.Затем он продолжит поиск по оставшимся параметрам, пока не достигнет другого пунктирного параметра ИЛИ больше не будет аргументов для оценки.Если он находит другие параметры, которые не являются пунктирными, он добавляет его в массив $ sub и увеличивает переменную $ OPTIND.

Так, в примере с OP можно выполнить следующее:

foo.sh -i test -d directory -s subdirectory1 subdirectory2 -f file1

Если мы добавим эти строки в скрипт для демонстрации:

echo ${sub[@]}
echo ${sub[1]}
echo $files

Вывод будет:

subdirectory1 subdirectory2
subdirectory2
file1
5 голосов
/ 27 марта 2014

На самом деле есть способ получить несколько аргументов, используя getopts, но он требует ручного взлома с помощью переменной getopts 'OPTIND.

См. Следующий сценарий (воспроизведенный ниже): https://gist.github.com/achalddave/290f7fcad89a0d7c3719. Возможно, есть более простой способ, но это был самый быстрый способ, который я смог найти.

#!/bin/sh

usage() {
cat << EOF
$0 -a <a1> <a2> <a3> [-b] <b1> [-c]
    -a      First flag; takes in 3 arguments
    -b      Second flag; takes in 1 argument
    -c      Third flag; takes in no arguments
EOF
}

is_flag() {
    # Check if $1 is a flag; e.g. "-b"
    [[ "$1" =~ -.* ]] && return 0 || return 1
}

# Note:
# For a, we fool getopts into thinking a doesn't take in an argument
# For b, we can just use getopts normal behavior to take in an argument
while getopts "ab:c" opt ; do
    case "${opt}" in
        a)
            # This is the tricky part.

            # $OPTIND has the index of the _next_ parameter; so "\${$((OPTIND))}"
            # will give us, e.g., ${2}. Use eval to get the value in ${2}.
            # The {} are needed in general for the possible case of multiple digits.

            eval "a1=\${$((OPTIND))}"
            eval "a2=\${$((OPTIND+1))}"
            eval "a3=\${$((OPTIND+2))}"

            # Note: We need to check that we're still in bounds, and that
            # a1,a2,a3 aren't flags. e.g.
            #   ./getopts-multiple.sh -a 1 2 -b
            # should error, and not set a3 to be -b.
            if [ $((OPTIND+2)) -gt $# ] || is_flag "$a1" || is_flag "$a2" || is_flag "$a3"
            then
                usage
                echo
                echo "-a requires 3 arguments!"
                exit
            fi

            echo "-a has arguments $a1, $a2, $a3"

            # "shift" getopts' index
            OPTIND=$((OPTIND+3))
            ;;
        b)
            # Can get the argument from getopts directly
            echo "-b has argument $OPTARG"
            ;;
        c)
            # No arguments, life goes on
            echo "-c"
            ;;
    esac
done
2 голосов
/ 28 сентября 2016

Исходный вопрос касается getopts, но есть другое решение, которое предоставляет более гибкую функциональность без getopts (возможно, это немного более многословно, но предоставляет гораздо более гибкий интерфейс командной строки). Вот пример:

while [[ $# > 0 ]]
do
    key="$1"
    case $key in
        -f|--foo)
            nextArg="$2"
            while ! [[ "$nextArg" =~ -.* ]] && [[ $# > 1 ]]; do
                case $nextArg in
                    bar)
                        echo "--foo bar found!"
                    ;;
                    baz)
                        echo "--foo baz found!"
                    ;;
                    *)
                        echo "$key $nextArg found!"
                    ;;
                esac
                if ! [[ "$2" =~ -.* ]]; then
                    shift
                    nextArg="$2"
                else
                    shift
                    break
                fi
            done
        ;;
        -b|--bar)
            nextArg="$2"
            while ! [[ "$nextArg" =~ -.* ]] && [[ $# > 1 ]]; do
                case $nextArg in
                    foo)
                        echo "--bar foo found!"
                    ;;
                    baz)
                        echo "--bar baz found!"
                    ;;
                    *)
                        echo "$key $nextArg found!"
                    ;;
                esac
                if ! [[ "$2" =~ -.* ]]; then
                    shift
                    nextArg="$2"
                else
                    shift
                    break
                fi
            done
        ;;
        -z|--baz)
            nextArg="$2"
            while ! [[ "$nextArg" =~ -.* ]] && [[ $# > 1 ]]; do

                echo "Doing some random task with $key $nextArg"

                if ! [[ "$2" =~ -.* ]]; then
                    shift
                    nextArg="$2"
                else
                    shift
                    break
                fi
            done
        ;;
        *)
            echo "Unknown flag $key"
        ;;
    esac
    shift
done

В этом примере мы перебираем все параметры командной строки, ища параметры, которые соответствуют принятым нами флагам командной строки (таким как -f или --foo). Как только мы находим флаг, мы перебираем каждый параметр, пока не исчерпаем параметры или не встретим другой флаг. Это возвращает нас обратно в наш внешний цикл, который обрабатывает только флаги.

При такой настройке следующие команды эквивалентны:

script -f foo bar baz
script -f foo -f bar -f baz
script --foo foo -f bar baz
script --foo foo bar -f baz

Вы также можете анализировать невероятно неорганизованные наборы параметров, такие как:

script -f baz derp --baz herp -z derp -b foo --foo bar -q llama --bar fight

Чтобы получить вывод:

--foo baz found!
-f derp found!
Doing some random task with --baz herp
Doing some random task with -z derp
--bar foo found!
--foo bar found!
Unknown flag -q
Unknown flag llama
--bar fight found!
0 голосов
/ 12 января 2019
#!/bin/bash
myname=$(basename "$0")

# help function
help () { cat <<EOP
   $myname: -c cluster [...] -a action [...] -i instance [...]
EOP
}

# parse sub options
get_opts () {
  rs='' && rc=0 # return string and return code
  while [[ $# -gt 0 ]]; do
    shift
    [[ "$1" =~ -.* ]] && break ||  rs="$rs $1" && rc=$((rc + 1))
  done
  echo "$rs"
}

#parse entire command-line
while [[ $# -gt 0 ]]; do
    case $1 in
        "-a") ACTS="$(get_opts $@)"
           ;;
        "-i") INSTS=$(get_opts $@)
           ;;
        "-c") CLUSTERS=$(get_opts $@)
           ;;
        "-h") help
           ;;
        ?) echo "sorry, I dont do $1"
           exit
           ;;
    esac
    shift
done
0 голосов
/ 23 сентября 2011

Поскольку вы не показываете, как вы надеетесь построить свой список

/test/directory/subdirectory/file1
. . .
test/directory/subdirectory2/file3

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

 case $opt in
    d ) dirList="${dirList} $OPTARG" ;;
 esac

Обратите внимание, что при первом проходе dir будет пустым, и в результате вы получите пробел, начинающийся с from вашего окончательного значения для ${dirList}.(Если вам действительно нужен код, который не содержит лишних пробелов, спереди или сзади, есть команда, которую я могу вам показать, но она будет трудна для понимания, и, похоже, она вам здесь не понадобится,но дайте мне знать)

Вы можете затем обернуть список переменных в циклы for для выдачи всех значений, т.е.

for dir in ${dirList} do
   for f in ${fileList} ; do
      echo $dir/$f
   done
done

Наконец, считается хорошей практикой "захватывать" любые неизвестныевходные данные для вашего дела, то есть

 case $opt in
    i ) initial=$OPTARG;;
    d ) dir=$OPTARG;;
    s ) sub=$OPTARG;;
    f ) files=$OPTARG;;
    * ) 
       printf "unknown flag supplied "${OPTARG}\nUsageMessageGoesHere\n"
       exit 1
    ;;

 esac 

Надеюсь, это поможет.

...