Получение нескольких аргументов для одного параметра с помощью getopts в Bash

Мне нужна помощь с 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

Любые идеи?


person vegasbrianc    schedule 23.09.2011    source источник
comment
Не могли бы вы выбрать лучший ответ на этот вопрос :)   -  person lig    schedule 31.07.2019


Ответы (9)


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

Для очень конкретного исходного вопроса здесь решение Райана 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
person mivk    schedule 24.12.2013
comment
Я ценю это подробное объяснение. Одним из дополнений является то, что использование ${multi[@]} вместо ${multi[@]} предотвращает проблемы с аргументами, содержащими пробелы, для тех, кто не знал и был любопытен. - person jimh; 03.05.2017
comment
Это способ сделать то, что задал OP в bash. - person laur; 02.10.2020

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

Такие оболочки, как 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’
person Ryan    schedule 30.05.2012
comment
Да, за исключением того, что он создает каталоги вместо файлов в качестве листовых узлов. - person Zac Thompson; 24.11.2014

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

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

So,

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
person glenn jackman    schedule 23.09.2011
comment
Будет ли getopt лучшим вариантом для этого или вы будете использовать что-то другое? - person vegasbrianc; 23.09.2011
comment
Я бы использовал только то, что написал выше. Если вы хотите иметь возможность указать параметр -f с несколькими аргументами или предоставить -f один аргумент несколько раз, я знаю, что вы можете сделать это в Perl с помощью модуля Getopt::Long. - person glenn jackman; 23.09.2011
comment
Я согласен с Гленном, обычно это то, что я использую. Однако другой вариант - просто использовать другой разделитель, например. запятые, чтобы разделить несколько аргументов вместо пробелов, а затем разделить $OPTARG на запятую. Например -f файл1,файл2,файл3. Я склонен делать это только с командами, которые я планирую держать при себе, поскольку я не верю, что другие понимают, что они не должны ставить пробелы после запятых. - person frankc; 23.09.2011
comment
Также обратите внимание, что in "$@" можно удалить без изменения семантики. - person choroba; 28.04.2013
comment
getopts поддерживает несколько вариантов одной и той же опции для каждого решения @mivk ниже — usage: foo.sh -i end -d dir -s subdir1 -s subdir2 -s subdir3 file [...] - person cchamberlain; 16.06.2015
comment
@cole, да, и в ветке case вы бы добавили к массиву: subdirs+=( "$OPTARG" ) - person glenn jackman; 16.06.2015

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

Вместо:

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
person David Berkan    schedule 28.12.2011
comment
Я также изначально использовал этот подход, но вскоре столкнулся с проблемами, когда в одном из моих параметров были пробелы, требующие кавычек внутри кавычек. Если у вас есть несколько параметров, и некоторые из них нуждаются в кавычках, это может быстро запутаться. - person KoZm0kNoT; 19.11.2020

Если вы хотите указать любое количество значений для параметра, вы можете использовать простой цикл, чтобы найти их и поместить в массив. Например, давайте изменим пример 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
person KoZm0kNoT    schedule 16.03.2017

На самом деле есть способ получить несколько аргументов с помощью 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
person Achal Dave    schedule 27.03.2014

Исходный вопрос касается 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!
person Brendon Dugan    schedule 28.09.2016
comment
Спасибо за это - мне было интересно, есть ли изящный способ избежать getopts, и хотя ваш ответ показал мне, что его нет, он показал мне, что моя домашняя альтернатива находится на правильном пути. - person chrBrd; 26.11.2016

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

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

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

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

Обратите внимание, что при первом проходе dir будет пустым, и вы получите пробел перед вашим окончательным значением для ${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 

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

person shellter    schedule 23.09.2011
comment
Это имеет больше смысла, но еще не на 100%. Мой список файлов будет извлекать значения из флагов, чтобы построить путь к файлу. если второй построен, мне нужно восстановить новый путь ко второму каталогу/файлу - person vegasbrianc; 23.09.2011

Следующая ссылка должна быть общим решением для этого требования.

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

Аргументы с несколькими параметрами с использованием getopts (bash)

function getopts-extra () {
    declare i=1
    # if the next argument is not an option, then append it to array OPTARG
    while [[ ${OPTIND} -le $# && ${!OPTIND:0:1} != '-' ]]; do
        OPTARG[i]=${!OPTIND}
        let i++ OPTIND++
    done
}

# Use it within the context of `getopts`:
while getopts s: opt; do
    case $opt in
       s) getopts-extra "$@"
          args=( "${OPTARG[@]}" )
    esac
done
person alex    schedule 21.03.2021