Как мне разобрать необязательные аргументы в bash-скрипте, если не указан порядок?

13

Я запутался, как включить необязательные аргументы / флаги при написании сценария bash для следующей программы:

Программа требует двух аргументов:

run_program --flag1 <value> --flag2 <value>

Однако есть несколько необязательных флагов:

run_program --flag1 <value> --flag2 <value> --optflag1 <value> --optflag2 <value> --optflag3 <value> --optflag4 <value> --optflag5 <value> 

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

#!/bin/sh

run_program --flag1 $1 --flag2 $2

Но что, если какие-либо необязательные аргументы включены? Я думаю, что это будет

if [ --optflag1 "$3" ]; then
    run_program --flag1 $1 --flag2 $2 --optflag1 $3
fi

Но что, если дается 4 доллара, а не 3?

ShanZhengYang
источник
getoptsэто то, что вы хотите. Без этого вы можете использовать цикл с оператором switch для определения каждого флага, необязательного или нет.
Орион
1
Дубликат stackoverflow.com/questions/16483119/…
Стив
@orion С getopts, мне нужно будет указать каждую комбинацию аргументов? 3 и 4, 3 и 5, 3 и 4 и 5 и т. Д.?
ShanZhengYang
Нет, вы просто устанавливаете их, если получаете, в противном случае он сообщает, что они не были найдены, поэтому в основном вы просто «получаете» каждый параметр в любом порядке и указываете их в любом порядке, если вообще. Но просто прочитайте справочную страницу bash, там все есть.
Орион
@orion Извините, но я все еще не совсем понимаю getopts. Допустим, я заставляю пользователей запускать скрипт со всеми аргументами: run_program.sh VAL VAL FALSE FALSE FALSE FALSE FALSEкоторый запускает программу как program --flag1 VAL --flag2 VAL. Если вы запустили run_program.sh VAL VAL FALSE 10 FALSE FALSE FALSE, программа запустится как program --flag1 VAL --flag2 VAL --optflag2 10. Как вы можете получить такое поведение с getopts?
ShanZhengYang

Ответы:

25

В этой статье показаны два разных способа - shiftи getopts(и обсуждаются преимущества и недостатки двух подходов).

С shiftвашим сценарием смотрит $1, решает, какое действие предпринять, а затем выполняет shift, перемещается $2в $1, $3в $2и т. Д.

Например:

while :; do
    case $1 in
        -a|--flag1) flag1="SET"            
        ;;
        -b|--flag2) flag2="SET"            
        ;;
        -c|--optflag1) optflag1="SET"            
        ;;
        -d|--optflag2) optflag2="SET"            
        ;;
        -e|--optflag3) optflag3="SET"            
        ;;
        *) break
    esac
    shift
done

При этом getoptsвы определяете (короткие) опции в whileвыражении:

while getopts abcde opt; do
    case $opt in
        a) flag1="SET"
        ;;
        b) flag2="SET"
        ;;
        c) optflag1="SET"
        ;;
        d) optflag2="SET"
        ;;
        e) optflag3="SET"
        ;;
    esac
done

Очевидно, что это всего лишь фрагменты кода, и я пропустил проверку - проверку, что установлены обязательные аргументы flag1 и flag2, и т. Д.

Какой подход вы используете, в некоторой степени зависит от вкуса: насколько вы хотите, чтобы ваш сценарий был переносимым, можете ли вы жить только с короткими (POSIX) опциями или хотите длинные (GNU) опции и т. Д.

Джон Н
источник
Я обычно делаю while (( $# ))вместо этого while :;и часто ухожу с ошибкой в *случае
Jasen
это отвечает на заголовок, но не на вопрос.
Jasen
@Jasen Я посмотрел на прошлую ночь и не мог понять, почему. Этим утром все прояснилось (и я уже видел ответ Ориона). Я удалю этот ответ позже сегодня (сначала я хотел бы подтвердить ваш комментарий, и это казалось самым простым способом сделать это).
Джон Н
нет проблем, это все еще хороший ответ, просто вопрос уклонился от него.
Ясен
1
NB: в случае set -o nounsetпервого решения произойдет ошибка, если не указан параметр. Исправление:case ${1:-} in
Рафаэль
3

использовать массив.

#!/bin/bash

args=( --flag1 "$1" --flag2 "$2" )
[  "x$3" = xFALSE ] ||  args+=( --optflag1 "$3" )
[  "x$4" = xFALSE ] ||  args+=( --optflag2 "$4" )
[  "x$5" = xFALSE ] ||  args+=( --optflag3 "$5" )
[  "x$6" = xFALSE ] ||  args+=( --optflag4 "$6" )
[  "x$7" = xFALSE ] ||  args+=( --optflag5 "$7" )

program_name "${args[@]}"

это будет обрабатывать аргументы с пробелами в них правильно.

[править] Я использовал примерно эквивалентный синтаксис, args=( "${args[@]}" --optflag1 "$3" )но G-Man предложил лучший способ.

Jasen
источник
1
Вы можете немного упростить это, сказав args+=( --optflag1 "$3" ). Возможно, вы захотите увидеть мой ответ на нашу справочную работу « Последствия для безопасности: невозможность заключить в кавычки переменную в оболочках bash / POSIX» , где я обсуждаю этот метод (построения командной строки в массиве путем условного добавления необязательных аргументов).
G-Man говорит: «Восстанови Монику»
@ G-Man Спасибо, я не видел этого в документации, понимая, [@]что у меня есть достаточный инструмент для решения моей проблемы.
Jasen
1

В сценарии оболочки аргументы: «$ 1», «$ 2», «$ 3» и т. Д. Число аргументов - $ #.
Если ваш сценарий не распознает параметры, вы можете опустить обнаружение параметров и рассматривать все аргументы как операнды.
Для распознавания опций используйте встроенную функцию getopts

Майкл Даррант
источник
0

Если ваши параметры ввода являются позиционными (вы знаете, в каких местах они находятся) и не указаны с флагами, тогда вам нужно просто создать командную строку. Просто подготовьте аргументы команды для всех них:

FLAG1="--flag1 $1"
FLAG2="--flag2 $2"
OPTFLAG1=""
OPTFLAG2=""
OPTFLAG3=""
if [ xFALSE != x"$3" ]; then
   OPTFLAG1="--optflag1 $3"
fi
if [ xFALSE != x"$4" ]; then
   OPTFLAG2="--optflag2 $4"
fi
#the same for other flags

#now just run the program
runprogram $FLAG1 $FLAG2 $OPTFLAG1 $OPTFLAG2 $OPTFLAG3

Если параметры не указаны, то соответствующие строки пусты и расширяются в ничто. Обратите внимание, что в последней строке нет кавычек. Это потому, что вы хотите, чтобы оболочка разбивала параметры на слова (для предоставления --flag1и в $1качестве отдельных аргументов для вашей программы). Конечно, это пойдет не так, если ваши исходные параметры содержат пробелы. Если вы выполняете это, вы можете оставить его, но если это общий сценарий, он может иметь неожиданное поведение, если пользователь вводит что-то с пробелами. Чтобы справиться с этим, вам нужно сделать код немного страшнее.

xПрефикс в []тесте есть в случае , $3или $4пусто. В этом случае, баш будет расширяться [ FALSE != $3 ]в [ FALSE != ]котором ошибка синтаксиса, так что еще один произвольный символ есть , чтобы защититься от этого. Это очень распространенный способ, вы увидите это во многих сценариях.

Я установил OPTFLAG1и остальные из них ""в начале, чтобы быть уверенным (в случае, если они были установлены на что-то раньше), но если они не были фактически объявлены в среде, то вам не нужно делать это строго.

Пара дополнительных замечаний:

  • На самом деле вы можете просто получить параметры так же, как runprogramи с флагами. Вот о чем John Nидет речь. Вот где getoptsстановится полезным.
  • Использование FALSE для этой цели немного нестандартно и довольно долго. Обычно можно использовать один символ (возможно -) для обозначения пустого значения или просто передать пустую строку, например "", если это не является допустимым значением параметра. Пустая строка также делает тест короче, просто используйте if [ -n "$3" ].
  • Если есть только один необязательный флаг или если они зависимы, поэтому у вас никогда не будет OPTFLAG2, но не OPTFLAG1, тогда вы можете просто пропустить те, которые вы не хотите устанавливать. Если вы используете ""пустые параметры, как предложено выше, вы все равно можете пропустить все завершающие пустые области.

Этот метод в значительной степени используется для передачи параметров компиляторам в Makefiles.

И снова - если входные данные могут содержать пробелы, это становится ужасно.

Орион
источник
Нет, вы не хотите, чтобы оболочка разделяла параметры на слова. Вы всегда должны заключать в кавычки все ссылки на переменные оболочки, если у вас нет веских причин не делать этого, и вы уверены, что знаете, что делаете. Посмотрите, что для безопасности не удается процитировать переменную в оболочках bash / POSIX , если вы с ней не знакомы, и прокрутите вниз до моего ответа , где я решаю именно эту проблему (с помощью той же техники, которую использует Jasen ).
G-Man говорит: «Восстановите Монику»