Как мне разобрать аргументы командной строки в Bash?

1922

Скажем, у меня есть скрипт, который вызывается с этой строкой:

./myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile

или этот:

./myscript -v -f -d -o /fizz/someOtherFile ./foo/bar/someFile 

Что общепринятый способ разбора это таким образом, что в каждом конкретном случае (или некоторая комбинация из двух) $v, $fи $dвсе будет установлен trueи $outFileбудет равно /fizz/someOtherFile?

Лоуренс Джонстон
источник
1
Для пользователей zsh есть отличная встроенная функция под названием zparseopts, которая может сделать: zparseopts -D -E -M -- d=debug -debug=dИ иметь и то -dи другое --debugв $debugмассиве, echo $+debug[1]будет возвращать 0 или 1, если используется один из них. Ссылка: zsh.org/mla/users/2011/msg00350.html
dezza
1
Действительно хороший учебник: linuxcommand.org/lc3_wss0120.php . Мне особенно нравится пример «Параметры командной строки».
Габриэль Стейплс

Ответы:

2677

Способ № 1: Использование bash без getopt [s]

Два распространенных способа передачи аргументов пары ключ-значение:

Разделенный пробелами (например, --option argument) (без getopt [s])

Применение demo-space-separated.sh -e conf -s /etc -l /usr/lib /etc/hosts

cat >/tmp/demo-space-separated.sh <<'EOF'
#!/bin/bash

POSITIONAL=()
while [[ $# -gt 0 ]]
do
key="$1"

case $key in
    -e|--extension)
    EXTENSION="$2"
    shift # past argument
    shift # past value
    ;;
    -s|--searchpath)
    SEARCHPATH="$2"
    shift # past argument
    shift # past value
    ;;
    -l|--lib)
    LIBPATH="$2"
    shift # past argument
    shift # past value
    ;;
    --default)
    DEFAULT=YES
    shift # past argument
    ;;
    *)    # unknown option
    POSITIONAL+=("$1") # save it in an array for later
    shift # past argument
    ;;
esac
done
set -- "${POSITIONAL[@]}" # restore positional parameters

echo "FILE EXTENSION  = ${EXTENSION}"
echo "SEARCH PATH     = ${SEARCHPATH}"
echo "LIBRARY PATH    = ${LIBPATH}"
echo "DEFAULT         = ${DEFAULT}"
echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l)
if [[ -n $1 ]]; then
    echo "Last line of file specified as non-opt/last argument:"
    tail -1 "$1"
fi
EOF

chmod +x /tmp/demo-space-separated.sh

/tmp/demo-space-separated.sh -e conf -s /etc -l /usr/lib /etc/hosts

вывод от копирования-вставки блока выше:

FILE EXTENSION  = conf
SEARCH PATH     = /etc
LIBRARY PATH    = /usr/lib
DEFAULT         =
Number files in SEARCH PATH with EXTENSION: 14
Last line of file specified as non-opt/last argument:
#93.184.216.34    example.com

Разделенный Bash Равным (например, --option=argument) (без getopt [s])

Применение demo-equals-separated.sh -e=conf -s=/etc -l=/usr/lib /etc/hosts

cat >/tmp/demo-equals-separated.sh <<'EOF'
#!/bin/bash

for i in "$@"
do
case $i in
    -e=*|--extension=*)
    EXTENSION="${i#*=}"
    shift # past argument=value
    ;;
    -s=*|--searchpath=*)
    SEARCHPATH="${i#*=}"
    shift # past argument=value
    ;;
    -l=*|--lib=*)
    LIBPATH="${i#*=}"
    shift # past argument=value
    ;;
    --default)
    DEFAULT=YES
    shift # past argument with no value
    ;;
    *)
          # unknown option
    ;;
esac
done
echo "FILE EXTENSION  = ${EXTENSION}"
echo "SEARCH PATH     = ${SEARCHPATH}"
echo "LIBRARY PATH    = ${LIBPATH}"
echo "DEFAULT         = ${DEFAULT}"
echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l)
if [[ -n $1 ]]; then
    echo "Last line of file specified as non-opt/last argument:"
    tail -1 $1
fi
EOF

chmod +x /tmp/demo-equals-separated.sh

/tmp/demo-equals-separated.sh -e=conf -s=/etc -l=/usr/lib /etc/hosts

вывод от копирования-вставки блока выше:

FILE EXTENSION  = conf
SEARCH PATH     = /etc
LIBRARY PATH    = /usr/lib
DEFAULT         =
Number files in SEARCH PATH with EXTENSION: 14
Last line of file specified as non-opt/last argument:
#93.184.216.34    example.com

Чтобы лучше понять, ${i#*=}ищите «Удаление подстроки» в этом руководстве . Это функционально эквивалентно `sed 's/[^=]*=//' <<< "$i"`который вызывает излишний подпроцесс или `echo "$i" | sed 's/[^=]*=//'`который вызывает два ненужных подпроцессы.

Способ № 2: Использование bash с getopt [s]

от: http://mywiki.wooledge.org/BashFAQ/035#getopts

Ограничения getopt (1) (более старые, относительно недавние getoptверсии):

  • не может обрабатывать аргументы, которые являются пустыми строками
  • не может обрабатывать аргументы со встроенным пробелом

Более поздние getoptверсии не имеют этих ограничений.

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

Применение demo-getopts.sh -vf /etc/hosts foo bar

cat >/tmp/demo-getopts.sh <<'EOF'
#!/bin/sh

# A POSIX variable
OPTIND=1         # Reset in case getopts has been used previously in the shell.

# Initialize our own variables:
output_file=""
verbose=0

while getopts "h?vf:" opt; do
    case "$opt" in
    h|\?)
        show_help
        exit 0
        ;;
    v)  verbose=1
        ;;
    f)  output_file=$OPTARG
        ;;
    esac
done

shift $((OPTIND-1))

[ "${1:-}" = "--" ] && shift

echo "verbose=$verbose, output_file='$output_file', Leftovers: $@"
EOF

chmod +x /tmp/demo-getopts.sh

/tmp/demo-getopts.sh -vf /etc/hosts foo bar

вывод от копирования-вставки блока выше:

verbose=1, output_file='/etc/hosts', Leftovers: foo bar

Преимущества getopts:

  1. Это более портативно, и будет работать в других оболочках, как dash.
  2. Он может обрабатывать несколько отдельных параметров, как -vf filenameв обычном Unix-режиме, автоматически.

Недостатком getoptsявляется то, что он может обрабатывать только короткие опции (но -hне --help) без дополнительного кода.

Существует учебник getopts, который объясняет, что означает весь синтаксис и переменные. В bash также есть информация help getopts, которая может быть информативной.

Бруно Броноски
источник
44
Это правда? Согласно Википедии, есть более новая улучшенная версия GNU, getoptкоторая включает в себя все функциональные возможности, getoptsа затем и некоторые. man getoptв Ubuntu 13.04 выводится getopt - parse command options (enhanced)как имя, поэтому я полагаю, что эта расширенная версия является стандартной.
Ливвен
47
То, что что-то является определенным образом в вашей системе, является очень слабой предпосылкой для того, чтобы основывать предположения о «стандарте».
суббота
13
@Livven, это getoptне утилита GNU, она часть util-linux.
Стефан Шазелас
4
Если вы используете -gt 0, удалите ваш shiftпосле esac, увеличьте все на shift1 и добавьте этот случай: *) break;;вы можете обрабатывать не необязательные аргументы. Пример: pastebin.com/6DJ57HTc
Николас Лакомб,
2
Вы не эхо –default. В первом примере я замечаю, что если –defaultпоследний аргумент if , он не обрабатывается (считается не opt), если while [[ $# -gt 1 ]]не задан как while [[ $# -gt 0 ]]
kolydart
562

Нет ответа упоминает расширенный Getopt . И ответ с наибольшим количеством голосов вводит в заблуждение: он либо игнорирует -⁠vfdкороткие варианты стиля (запрошенные OP), либо опции после позиционных аргументов (также запрошенные OP); и он игнорирует ошибки синтаксического анализа. Вместо:

  • Используйте расширенный getoptот util-linux или ранее GNU glibc . 1
  • Работает с getopt_long()функцией C в GNU glibc.
  • Имеет все полезные отличительные особенности (у других их нет):
    • обрабатывает пробелы, заключенные в кавычки символы и даже двоичные в аргументах 2 (не расширенные getoptне могут этого сделать)
    • он может обрабатывать опции в конце: script.sh -o outFile file1 file2 -v( getoptsне делает этого)
    • позволяет =-стиль длинные параметры: script.sh --outfile=fileOut --infile fileIn(разрешение обоих длинно, если самостоятельно анализирует)
    • позволяет комбинированные короткие варианты, например -vfd(реальная работа, если сам анализирует)
    • позволяет трогать опционные аргументы, например, -oOutfileили-vfdoOutfile
  • Уже настолько 3 года, что ни в одной системе GNU его нет (например, в любом Linux).
  • Вы можете проверить его наличие с помощью: getopt --test→ возвращаемого значения 4.
  • Другие getoptили встроенные оболочки getoptsимеют ограниченное использование.

Следующие звонки

myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile
myscript -v -f -d -o/fizz/someOtherFile -- ./foo/bar/someFile
myscript --verbose --force --debug ./foo/bar/someFile -o/fizz/someOtherFile
myscript --output=/fizz/someOtherFile ./foo/bar/someFile -vfd
myscript ./foo/bar/someFile -df -v --output /fizz/someOtherFile

все возвращаются

verbose: y, force: y, debug: y, in: ./foo/bar/someFile, out: /fizz/someOtherFile

со следующим myscript

#!/bin/bash
# saner programming env: these switches turn some bugs into errors
set -o errexit -o pipefail -o noclobber -o nounset

# -allow a command to fail with !’s side effect on errexit
# -use return value from ${PIPESTATUS[0]}, because ! hosed $?
! getopt --test > /dev/null 
if [[ ${PIPESTATUS[0]} -ne 4 ]]; then
    echo 'I’m sorry, `getopt --test` failed in this environment.'
    exit 1
fi

OPTIONS=dfo:v
LONGOPTS=debug,force,output:,verbose

# -regarding ! and PIPESTATUS see above
# -temporarily store output to be able to check for errors
# -activate quoting/enhanced mode (e.g. by writing out “--options”)
# -pass arguments only via   -- "$@"   to separate them correctly
! PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$@")
if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
    # e.g. return value is 1
    #  then getopt has complained about wrong arguments to stdout
    exit 2
fi
# read getopt’s output this way to handle the quoting right:
eval set -- "$PARSED"

d=n f=n v=n outFile=-
# now enjoy the options in order and nicely split until we see --
while true; do
    case "$1" in
        -d|--debug)
            d=y
            shift
            ;;
        -f|--force)
            f=y
            shift
            ;;
        -v|--verbose)
            v=y
            shift
            ;;
        -o|--output)
            outFile="$2"
            shift 2
            ;;
        --)
            shift
            break
            ;;
        *)
            echo "Programming error"
            exit 3
            ;;
    esac
done

# handle non-option arguments
if [[ $# -ne 1 ]]; then
    echo "$0: A single input file is required."
    exit 4
fi

echo "verbose: $v, force: $f, debug: $d, in: $1, out: $outFile"

1 расширенный getopt доступен на большинстве «bash-систем», включая Cygwin; в OS X попробуйте brew install gnu-getopt илиsudo port install getopt
2,exec() соглашенияPOSIXне имеют надежного способа передачи двоичного NULL в аргументах командной строки; эти байты преждевременно заканчиваютпервую версиюаргумента
3, выпущенную в 1997 году или ранее (я отслеживал его только до 1997 года)

Роберт Симер
источник
4
Спасибо за это. Просто подтвердите из таблицы возможностей на en.wikipedia.org/wiki/Getopts , если вам нужна поддержка длинных опций, а вы не в Solaris, getoptэто путь.
johncip
4
Я полагаю, что единственное предостережение в getoptтом, что его нельзя удобно использовать в сценариях-оболочках, где можно иметь несколько параметров, специфичных для сценария-оболочки, и затем передавать параметры сценария, не являющегося оболочкой, неповрежденному упакованному исполняемому файлу. Допустим, у меня есть grepвызываемая оболочка, mygrepи у меня есть опция, --fooспецифичная для mygrep, тогда я не могу сделать mygrep --foo -A 2, и я -A 2передаю автоматически grep; Мне нужно сделать mygrep --foo -- -A 2. Вот моя реализация поверх вашего решения.
Каушал Моди
2
@bobpaul Ваше утверждение об util-linux также неверно и вводит в заблуждение: пакет помечен как «существенный» в Ubuntu / Debian. Как таковой, он всегда установлен. - О каких дистрибутивах вы говорите (где вы говорите, что их нужно устанавливать специально)?
Роберт Симер
3
Обратите внимание, что это не работает на Mac по крайней мере до текущего 10.14.3. Getopt, который отправляет это BSD getopt с 1999 года ...
JJJ
2
@transang Булево отрицание возвращаемого значения. И его побочный эффект: позволить команде завершиться с ошибкой (в противном случае errexit прервет программу при ошибке). - Комментарии в сценарии расскажут вам больше. В противном случае:man bash
Роберт Симер
145

Более лаконичный способ

script.sh

#!/bin/bash

while [[ "$#" -gt 0 ]]; do
    case $1 in
        -d|--deploy) deploy="$2"; shift ;;
        -u|--uglify) uglify=1 ;;
        *) echo "Unknown parameter passed: $1"; exit 1 ;;
    esac
    shift
done

echo "Should deploy? $deploy"
echo "Should uglify? $uglify"

Применение:

./script.sh -d dev -u

# OR:

./script.sh --deploy dev --uglify
Инан Гумус
источник
3
Это то, что я делаю. Должен, while [[ "$#" > 1 ]]если я хочу поддержать окончание строки логическим флагом ./script.sh --debug dev --uglify fast --verbose. Пример: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58
hfossli
12
Вот Это Да! Просто и чисто! Вот как я использую это: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58
hfossli
2
Это гораздо приятнее вставить в каждый сценарий, чем иметь дело с источником или задавать вопросы, где на самом деле начинается ваша функциональность.
RealHandy
Предупреждение: это допускает повторяющиеся аргументы, последний аргумент преобладает. например ./script.sh -d dev -d prod, приведет к deploy == 'prod'. Я использовал это в любом случае: P :): +1:
Яир
Я использую это (спасибо!), Но учтите, что он допускает пустое значение аргумента, например ./script.sh -d, не генерирует ошибку, а просто устанавливает $deployпустую строку.
EM0
137

от: digitalpeer.com с небольшими изменениями

Применение myscript.sh -p=my_prefix -s=dirname -l=libname

#!/bin/bash
for i in "$@"
do
case $i in
    -p=*|--prefix=*)
    PREFIX="${i#*=}"

    ;;
    -s=*|--searchpath=*)
    SEARCHPATH="${i#*=}"
    ;;
    -l=*|--lib=*)
    DIR="${i#*=}"
    ;;
    --default)
    DEFAULT=YES
    ;;
    *)
            # unknown option
    ;;
esac
done
echo PREFIX = ${PREFIX}
echo SEARCH PATH = ${SEARCHPATH}
echo DIRS = ${DIR}
echo DEFAULT = ${DEFAULT}

Чтобы лучше понять, ${i#*=}ищите «Удаление подстроки» в этом руководстве . Это функционально эквивалентно `sed 's/[^=]*=//' <<< "$i"`который вызывает излишний подпроцесс или `echo "$i" | sed 's/[^=]*=//'`который вызывает два ненужных подпроцессы.

guneysus
источник
4
Ухоженная! Хотя это не сработает для разделенных пробелами аргументов а-ля mount -t tempfs .... Вероятно, это можно исправить с помощью чего-то подобного и while [ $# -ge 1 ]; do param=$1; shift; case $param in; -p) prefix=$1; shift;;т. Д.
Тобиас Кинцлер
3
Это не может обрабатывать -vfdкомбинированные короткие варианты стиля.
Роберт Симер
105

getopt()/ getopts()это хороший вариант. Украдено отсюда :

Простое использование "getopt" показано в этом мини-скрипте:

#!/bin/bash
echo "Before getopt"
for i
do
  echo $i
done
args=`getopt abc:d $*`
set -- $args
echo "After getopt"
for i
do
  echo "-->$i"
done

Мы сказали, что любой из -a, -b, -c или -d будет разрешен, но за -c следует аргумент («c:» говорит это).

Если мы назовем это "g" и попробуем это:

bash-2.05a$ ./g -abc foo
Before getopt
-abc
foo
After getopt
-->-a
-->-b
-->-c
-->foo
-->--

Мы начинаем с двух аргументов, и «getopt» разбивает параметры и помещает каждый в свой аргумент. Также добавлено «-».

Мэтт Дж
источник
4
Использование $*является нарушением использования getopt. (Он содержит аргументы с пробелами.) Смотрите мой ответ для правильного использования.
Роберт Симер
Почему вы хотите сделать это более сложным?
SDsolar
@Matt J, первая часть скрипта (для i) сможет обрабатывать аргументы с пробелами в них, если вы используете «$ i» вместо $ i. Кажется, что getopts не в состоянии обрабатывать аргументы с пробелами. Каким будет преимущество использования getopt над циклом for i?
thebunnyrules
99

На риск добавления еще одного примера, чтобы проигнорировать, вот моя схема.

  • ручки -n argи--name=arg
  • позволяет аргументы в конце
  • показывает вменяемые ошибки, если что-то написано с ошибкой
  • совместимый, не использует bashisms
  • читаемый, не требует поддержания состояния в цикле

Надеюсь, это кому-нибудь пригодится.

while [ "$#" -gt 0 ]; do
  case "$1" in
    -n) name="$2"; shift 2;;
    -p) pidfile="$2"; shift 2;;
    -l) logfile="$2"; shift 2;;

    --name=*) name="${1#*=}"; shift 1;;
    --pidfile=*) pidfile="${1#*=}"; shift 1;;
    --logfile=*) logfile="${1#*=}"; shift 1;;
    --name|--pidfile|--logfile) echo "$1 requires an argument" >&2; exit 1;;

    -*) echo "unknown option: $1" >&2; exit 1;;
    *) handle_argument "$1"; shift 1;;
  esac
done
Bronson
источник
4
Извините за задержку. В моем сценарии функция handle_argument получает все неопционные аргументы. Вы можете заменить эту строку на что угодно, возможно, *) die "unrecognized argument: $1"или собрать аргументы в переменную *) args+="$1"; shift 1;;.
бронсон
Удивительно! Я протестировал пару ответов, но это единственный, который работал для всех случаев, включая многие позиционные параметры (как до, так и после флагов)
Гильерме Гарнье,
2
хороший лаконичный код, но использование -n и других аргументов приводит к бесконечному циклу из-за ошибки при shift 2выдаче shiftдважды вместо shift 2. Предложил редактировать.
Lauksas
42

Я на 4 года опоздал на этот вопрос, но хочу вернуть. Я использовал предыдущие ответы в качестве отправной точки, чтобы привести в порядок мой старый анализ adhoc param. Затем я переработал следующий код шаблона. Он обрабатывает как длинные, так и короткие параметры, используя = или разделенные пробелом аргументы, а также несколько коротких параметров, сгруппированных вместе. Наконец, он повторно вставляет все непарамальные аргументы обратно в переменные $ 1, $ 2 ... Я надеюсь, что это полезно.

#!/usr/bin/env bash

# NOTICE: Uncomment if your script depends on bashisms.
#if [ -z "$BASH_VERSION" ]; then bash $0 $@ ; exit $? ; fi

echo "Before"
for i ; do echo - $i ; done


# Code template for parsing command line parameters using only portable shell
# code, while handling both long and short params, handling '-f file' and
# '-f=file' style param data and also capturing non-parameters to be inserted
# back into the shell positional parameters.

while [ -n "$1" ]; do
        # Copy so we can modify it (can't modify $1)
        OPT="$1"
        # Detect argument termination
        if [ x"$OPT" = x"--" ]; then
                shift
                for OPT ; do
                        REMAINS="$REMAINS \"$OPT\""
                done
                break
        fi
        # Parse current opt
        while [ x"$OPT" != x"-" ] ; do
                case "$OPT" in
                        # Handle --flag=value opts like this
                        -c=* | --config=* )
                                CONFIGFILE="${OPT#*=}"
                                shift
                                ;;
                        # and --flag value opts like this
                        -c* | --config )
                                CONFIGFILE="$2"
                                shift
                                ;;
                        -f* | --force )
                                FORCE=true
                                ;;
                        -r* | --retry )
                                RETRY=true
                                ;;
                        # Anything unknown is recorded for later
                        * )
                                REMAINS="$REMAINS \"$OPT\""
                                break
                                ;;
                esac
                # Check for multiple short options
                # NOTICE: be sure to update this pattern to match valid options
                NEXTOPT="${OPT#-[cfr]}" # try removing single short opt
                if [ x"$OPT" != x"$NEXTOPT" ] ; then
                        OPT="-$NEXTOPT"  # multiple short opts, keep going
                else
                        break  # long form, exit inner loop
                fi
        done
        # Done with that param. move to next
        shift
done
# Set the non-parameters back into the positional parameters ($1 $2 ..)
eval set -- $REMAINS


echo -e "After: \n configfile='$CONFIGFILE' \n force='$FORCE' \n retry='$RETRY' \n remains='$REMAINS'"
for i ; do echo - $i ; done
Шейн Дэй
источник
Этот код не может обрабатывать параметры с аргументами , как это: -c1. И использование =для отделения коротких опций от их аргументов необычно ...
Роберт Симер
2
Я столкнулся с двумя проблемами с этим полезным фрагментом кода: 1) «shift» в случае «-c = foo» заканчивает тем, что съел следующий параметр; и 2) 'c' не следует включать в шаблон "[cfr]" для комбинируемых коротких опций.
sfnd
36

Я сочла нужным написать переносимый синтаксический анализ в сценариях, настолько разочаровывающий, что я написал Argbash - генератор кода FOSS, который может генерировать код синтаксического анализа аргументов для вашего сценария, плюс он имеет несколько приятных особенностей:

https://argbash.io

Bubla
источник
Спасибо за написание argbash, я просто использовал его и обнаружил, что он работает хорошо. В основном я использовал argbash, потому что это генератор кода, поддерживающий более старый bash 3.x, который есть в OS X 10.11 El Capitan. Единственным недостатком является то, что подход с генератором кода означает довольно много кода в вашем основном скрипте, по сравнению с вызовом модуля.
RichVel
На самом деле вы можете использовать Argbash таким образом, чтобы он создавал специализированную библиотеку синтаксического анализа именно для вас, которую вы можете включить в свой скрипт или вы можете поместить в отдельный файл и просто получить его исходный код. Я добавил пример, чтобы продемонстрировать это, и я также сделал это более явным в документации.
Бубла
Хорошо знать. Этот пример интересен, но все еще не совсем понятен - может быть, вы можете изменить имя сгенерированного скрипта на «parse_lib.sh» или аналогичное и показать, где его вызывает основной скрипт (как в разделе сценария упаковки, который является более сложным вариантом использования).
RichVel
Проблемы были устранены в последней версии argbash: улучшена документация, введен скрипт быстрого запуска argbash-init, и вы даже можете использовать argbash в Интернете по адресу argbash.io/generate
bubla
29

Мой ответ в значительной степени основан на ответе Бруно Броноски , но я как бы скомбинировал его две чистые реализации bash в одну, которую я использую довольно часто.

# As long as there is at least one more argument, keep looping
while [[ $# -gt 0 ]]; do
    key="$1"
    case "$key" in
        # This is a flag type option. Will catch either -f or --foo
        -f|--foo)
        FOO=1
        ;;
        # Also a flag type option. Will catch either -b or --bar
        -b|--bar)
        BAR=1
        ;;
        # This is an arg value type option. Will catch -o value or --output-file value
        -o|--output-file)
        shift # past the key and to the value
        OUTPUTFILE="$1"
        ;;
        # This is an arg=value type option. Will catch -o=value or --output-file=value
        -o=*|--output-file=*)
        # No need to shift here since the value is part of the same string
        OUTPUTFILE="${key#*=}"
        ;;
        *)
        # Do whatever you want with extra options
        echo "Unknown option '$key'"
        ;;
    esac
    # Shift after checking all the cases to get the next option
    shift
done

Это позволяет вам иметь как разделенные пробелами параметры / значения, так и равные определенные значения.

Таким образом, вы можете запустить свой скрипт, используя:

./myscript --foo -b -o /fizz/file.txt

а также:

./myscript -f --bar -o=/fizz/file.txt

и оба должны иметь одинаковый конечный результат.

ПЛЮСЫ:

  • Позволяет для обоих -arg = значение и -arg значение

  • Работает с любым именем arg, которое вы можете использовать в bash

    • Значение -a или -arg или --arg или -arg или что-то еще
  • Чистая Баш. Нет необходимости изучать / использовать getopt или getopts

МИНУСЫ:

  • Не могу объединить аргументы

    • Значение нет-abc. Вы должны сделать -a -b -c

Это единственные плюсы и минусы, которые я могу придумать

Ponyboy47
источник
15

Я думаю, что этот достаточно прост в использовании:

#!/bin/bash
#

readopt='getopts $opts opt;rc=$?;[ $rc$opt == 0? ]&&exit 1;[ $rc == 0 ]||{ shift $[OPTIND-1];false; }'

opts=vfdo:

# Enumerating options
while eval $readopt
do
    echo OPT:$opt ${OPTARG+OPTARG:$OPTARG}
done

# Enumerating arguments
for arg
do
    echo ARG:$arg
done

Пример вызова:

./myscript -v -do /fizz/someOtherFile -f ./foo/bar/someFile
OPT:v 
OPT:d 
OPT:o OPTARG:/fizz/someOtherFile
OPT:f 
ARG:./foo/bar/someFile
Alek
источник
1
Я прочитал все, и это мой любимый. Я не люблю использовать -a=1как стиль argc. Я предпочитаю ставить сначала основной вариант - опции, а затем специальные с одинарным интервалом -o option. Я ищу самый простой-лучший способ чтения argvs.
m3nda
Это работает очень хорошо, но если вы передадите аргумент опции non:: все следующие опции будут приняты в качестве аргументов. Вы можете проверить эту строку ./myscript -v -d fail -o /fizz/someOtherFile -f ./foo/bar/someFileс помощью собственного сценария. -d опция не установлена ​​как d:
m3nda
15

Расширяя превосходный ответ @guneysus, здесь есть твик, который позволяет пользователю использовать любой синтаксис, который он предпочитает, например

command -x=myfilename.ext --another_switch 

против

command -x myfilename.ext --another_switch

То есть, равные могут быть заменены пробелами.

Эта «нечеткая интерпретация» может не понравиться, но если вы создаете сценарии, которые взаимозаменяемы с другими утилитами (как в случае с моей, которая должна работать с ffmpeg), гибкость полезна.

STD_IN=0

prefix=""
key=""
value=""
for keyValue in "$@"
do
  case "${prefix}${keyValue}" in
    -i=*|--input_filename=*)  key="-i";     value="${keyValue#*=}";; 
    -ss=*|--seek_from=*)      key="-ss";    value="${keyValue#*=}";;
    -t=*|--play_seconds=*)    key="-t";     value="${keyValue#*=}";;
    -|--stdin)                key="-";      value=1;;
    *)                                      value=$keyValue;;
  esac
  case $key in
    -i) MOVIE=$(resolveMovie "${value}");  prefix=""; key="";;
    -ss) SEEK_FROM="${value}";          prefix=""; key="";;
    -t)  PLAY_SECONDS="${value}";           prefix=""; key="";;
    -)   STD_IN=${value};                   prefix=""; key="";; 
    *)   prefix="${keyValue}=";;
  esac
done
синхронизирован
источник
13

Этот пример показывает, как использовать getoptи evalи HEREDOCи shiftобрабатывать короткие и длинные параметры с и без требуемого значения, которое следует. Кроме того, инструкция switch / case является краткой и легкой для понимания.

#!/usr/bin/env bash

# usage function
function usage()
{
   cat << HEREDOC

   Usage: $progname [--num NUM] [--time TIME_STR] [--verbose] [--dry-run]

   optional arguments:
     -h, --help           show this help message and exit
     -n, --num NUM        pass in a number
     -t, --time TIME_STR  pass in a time string
     -v, --verbose        increase the verbosity of the bash script
     --dry-run            do a dry run, dont change any files

HEREDOC
}  

# initialize variables
progname=$(basename $0)
verbose=0
dryrun=0
num_str=
time_str=

# use getopt and store the output into $OPTS
# note the use of -o for the short options, --long for the long name options
# and a : for any option that takes a parameter
OPTS=$(getopt -o "hn:t:v" --long "help,num:,time:,verbose,dry-run" -n "$progname" -- "$@")
if [ $? != 0 ] ; then echo "Error in command line arguments." >&2 ; usage; exit 1 ; fi
eval set -- "$OPTS"

while true; do
  # uncomment the next line to see how shift is working
  # echo "\$1:\"$1\" \$2:\"$2\""
  case "$1" in
    -h | --help ) usage; exit; ;;
    -n | --num ) num_str="$2"; shift 2 ;;
    -t | --time ) time_str="$2"; shift 2 ;;
    --dry-run ) dryrun=1; shift ;;
    -v | --verbose ) verbose=$((verbose + 1)); shift ;;
    -- ) shift; break ;;
    * ) break ;;
  esac
done

if (( $verbose > 0 )); then

   # print out all the parameters we read in
   cat <<-EOM
   num=$num_str
   time=$time_str
   verbose=$verbose
   dryrun=$dryrun
EOM
fi

# The rest of your script below

Наиболее значимые строки сценария выше:

OPTS=$(getopt -o "hn:t:v" --long "help,num:,time:,verbose,dry-run" -n "$progname" -- "$@")
if [ $? != 0 ] ; then echo "Error in command line arguments." >&2 ; exit 1 ; fi
eval set -- "$OPTS"

while true; do
  case "$1" in
    -h | --help ) usage; exit; ;;
    -n | --num ) num_str="$2"; shift 2 ;;
    -t | --time ) time_str="$2"; shift 2 ;;
    --dry-run ) dryrun=1; shift ;;
    -v | --verbose ) verbose=$((verbose + 1)); shift ;;
    -- ) shift; break ;;
    * ) break ;;
  esac
done

Коротко, по сути, читабельно и обрабатывает практически все (ИМХО).

Надеюсь, что это помогает кому-то.

phyatt
источник
1
Это один из лучших ответов.
г-н Поливирл
11

Я даю вам функцию, parse_paramsкоторая будет анализировать параметры из командной строки.

  1. Это чисто решение Bash, никаких дополнительных утилит.
  2. Не загрязняет глобальный охват.
  3. Без усилий возвращает вам простые в использовании переменные, на которых вы могли бы построить дополнительную логику.
  4. Количество тире перед параметрами не имеет значения ( --allравно -allравно all=all)

Сценарий ниже является рабочей демонстрацией копирования-вставки. Смотрите show_useфункцию, чтобы понять, как использовать parse_params.

Ограничения:

  1. Не поддерживает разделенные пробелами params ( -d 1)
  2. Имена параметров потеряют тире так --any-paramи -anyparamэквивалентны
  3. eval $(parse_params "$@")должен использоваться внутри функции bash (она не будет работать в глобальной области видимости)

#!/bin/bash

# Universal Bash parameter parsing
# Parse equal sign separated params into named local variables
# Standalone named parameter value will equal its param name (--force creates variable $force=="force")
# Parses multi-valued named params into an array (--path=path1 --path=path2 creates ${path[*]} array)
# Puts un-named params as-is into ${ARGV[*]} array
# Additionally puts all named params as-is into ${ARGN[*]} array
# Additionally puts all standalone "option" params as-is into ${ARGO[*]} array
# @author Oleksii Chekulaiev
# @version v1.4.1 (Jul-27-2018)
parse_params ()
{
    local existing_named
    local ARGV=() # un-named params
    local ARGN=() # named params
    local ARGO=() # options (--params)
    echo "local ARGV=(); local ARGN=(); local ARGO=();"
    while [[ "$1" != "" ]]; do
        # Escape asterisk to prevent bash asterisk expansion, and quotes to prevent string breakage
        _escaped=${1/\*/\'\"*\"\'}
        _escaped=${_escaped//\'/\\\'}
        _escaped=${_escaped//\"/\\\"}
        # If equals delimited named parameter
        nonspace="[^[:space:]]"
        if [[ "$1" =~ ^${nonspace}${nonspace}*=..* ]]; then
            # Add to named parameters array
            echo "ARGN+=('$_escaped');"
            # key is part before first =
            local _key=$(echo "$1" | cut -d = -f 1)
            # Just add as non-named when key is empty or contains space
            if [[ "$_key" == "" || "$_key" =~ " " ]]; then
                echo "ARGV+=('$_escaped');"
                shift
                continue
            fi
            # val is everything after key and = (protect from param==value error)
            local _val="${1/$_key=}"
            # remove dashes from key name
            _key=${_key//\-}
            # skip when key is empty
            # search for existing parameter name
            if (echo "$existing_named" | grep "\b$_key\b" >/dev/null); then
                # if name already exists then it's a multi-value named parameter
                # re-declare it as an array if needed
                if ! (declare -p _key 2> /dev/null | grep -q 'declare \-a'); then
                    echo "$_key=(\"\$$_key\");"
                fi
                # append new value
                echo "$_key+=('$_val');"
            else
                # single-value named parameter
                echo "local $_key='$_val';"
                existing_named=" $_key"
            fi
        # If standalone named parameter
        elif [[ "$1" =~ ^\-${nonspace}+ ]]; then
            # remove dashes
            local _key=${1//\-}
            # Just add as non-named when key is empty or contains space
            if [[ "$_key" == "" || "$_key" =~ " " ]]; then
                echo "ARGV+=('$_escaped');"
                shift
                continue
            fi
            # Add to options array
            echo "ARGO+=('$_escaped');"
            echo "local $_key=\"$_key\";"
        # non-named parameter
        else
            # Escape asterisk to prevent bash asterisk expansion
            _escaped=${1/\*/\'\"*\"\'}
            echo "ARGV+=('$_escaped');"
        fi
        shift
    done
}

#--------------------------- DEMO OF THE USAGE -------------------------------

show_use ()
{
    eval $(parse_params "$@")
    # --
    echo "${ARGV[0]}" # print first unnamed param
    echo "${ARGV[1]}" # print second unnamed param
    echo "${ARGN[0]}" # print first named param
    echo "${ARG0[0]}" # print first option param (--force)
    echo "$anyparam"  # print --anyparam value
    echo "$k"         # print k=5 value
    echo "${multivalue[0]}" # print first value of multi-value
    echo "${multivalue[1]}" # print second value of multi-value
    [[ "$force" == "force" ]] && echo "\$force is set so let the force be with you"
}

show_use "param 1" --anyparam="my value" param2 k=5 --force --multi-value=test1 --multi-value=test2
Алексей Чекулаев
источник
Чтобы использовать демо-версию для разбора параметров, которые входят в ваш bash-скрипт, вы просто делаете этоshow_use "$@"
Алексей Чекулаев
По сути, я обнаружил, что github.com/renatosilva/easyoptions делает то же самое, но немного сложнее, чем эта функция.
Алексей Чекулаев,
10

EasyOptions не требует разборов:

## Options:
##   --verbose, -v  Verbose mode
##   --output=FILE  Output filename

source easyoptions || exit

if test -n "${verbose}"; then
    echo "output file is ${output}"
    echo "${arguments[@]}"
fi
Ренато Сильва
источник
Мне потребовалась минута, чтобы понять, что комментарии в верхней части вашего примера сценария анализируются, чтобы предоставить строку справки по использованию по умолчанию, а также спецификации аргументов. Это блестящее решение, и мне жаль, что за 2 года оно набрало всего 6 голосов. Возможно, этот вопрос слишком затуманен, чтобы люди могли его заметить.
Метаморфизм
В каком-то смысле ваше решение на сегодняшний день самое лучшее (кроме @ OleksiiChekulaiev, которое не поддерживает синтаксис «стандартного» параметра). Это потому, что ваше решение требует, чтобы автор сценария указывал имя каждой опции только один раз . Тот факт, что другие решения требуют, чтобы он был указан 3 раза - при использовании, в шаблоне 'case' и при настройке переменной - меня постоянно раздражал. Даже у getopt есть эта проблема. Однако ваш код работает медленно на моей машине - 0.11 с для реализации Bash, 0.28 с для Ruby. По сравнению с 0.02s для явного анализа "while-case".
Метаморфизм
Я хочу более быструю версию, возможно написанную на C. Кроме того, версию, которая совместима с zsh. Может быть, это заслуживает отдельного вопроса («Есть ли способ синтаксического анализа аргументов командной строки в Bash-подобных оболочках, который принимает стандартный синтаксис длинных опций и не требует, чтобы имена опций вводились более одного раза?»).
Метаморфизм
10

getopts прекрасно работает, если # 1 у вас установлен и # 2 вы собираетесь запустить его на той же платформе. OSX и Linux (например) ведут себя по-разному в этом отношении.

Вот (не getopts) решение, которое поддерживает флаги equals, non-equals и boolean. Например, вы можете запустить свой скрипт следующим образом:

./script --arg1=value1 --arg2 value2 --shouldClean

# parse the arguments.
COUNTER=0
ARGS=("$@")
while [ $COUNTER -lt $# ]
do
    arg=${ARGS[$COUNTER]}
    let COUNTER=COUNTER+1
    nextArg=${ARGS[$COUNTER]}

    if [[ $skipNext -eq 1 ]]; then
        echo "Skipping"
        skipNext=0
        continue
    fi

    argKey=""
    argVal=""
    if [[ "$arg" =~ ^\- ]]; then
        # if the format is: -key=value
        if [[ "$arg" =~ \= ]]; then
            argVal=$(echo "$arg" | cut -d'=' -f2)
            argKey=$(echo "$arg" | cut -d'=' -f1)
            skipNext=0

        # if the format is: -key value
        elif [[ ! "$nextArg" =~ ^\- ]]; then
            argKey="$arg"
            argVal="$nextArg"
            skipNext=1

        # if the format is: -key (a boolean flag)
        elif [[ "$nextArg" =~ ^\- ]] || [[ -z "$nextArg" ]]; then
            argKey="$arg"
            argVal=""
            skipNext=0
        fi
    # if the format has not flag, just a value.
    else
        argKey=""
        argVal="$arg"
        skipNext=0
    fi

    case "$argKey" in 
        --source-scmurl)
            SOURCE_URL="$argVal"
        ;;
        --dest-scmurl)
            DEST_URL="$argVal"
        ;;
        --version-num)
            VERSION_NUM="$argVal"
        ;;
        -c|--clean)
            CLEAN_BEFORE_START="1"
        ;;
        -h|--help|-help|--h)
            showUsage
            exit
        ;;
    esac
done
vangorra
источник
8

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

function waitForWeb () {
   local OPTIND=1 OPTARG OPTION
   local host=localhost port=8080 proto=http
   while getopts "h:p:r:" OPTION; do
      case "$OPTION" in
      h)
         host="$OPTARG"
         ;;
      p)
         port="$OPTARG"
         ;;
      r)
         proto="$OPTARG"
         ;;
      esac
   done
...
}
akostadinov
источник
8

Расширяя ответ @ bruno-bronosky, я добавил «препроцессор» для обработки некоторых распространенных форматов:

  • Расширяется --longopt=valв--longopt val
  • Расширяется -xyzв-x -y -z
  • опоры -- указание конца флагов
  • Показывает ошибку для неожиданных опций
  • Компактный и легко читаемый переключатель параметров
#!/bin/bash

# Report usage
usage() {
  echo "Usage:"
  echo "$(basename $0) [options] [--] [file1, ...]"

  # Optionally exit with a status code
  if [ -n "$1" ]; then
    exit "$1"
  fi
}

invalid() {
  echo "ERROR: Unrecognized argument: $1" >&2
  usage 1
}

# Pre-process options to:
# - expand -xyz into -x -y -z
# - expand --longopt=arg into --longopt arg
ARGV=()
END_OF_OPT=
while [[ $# -gt 0 ]]; do
  arg="$1"; shift
  case "${END_OF_OPT}${arg}" in
    --) ARGV+=("$arg"); END_OF_OPT=1 ;;
    --*=*)ARGV+=("${arg%%=*}" "${arg#*=}") ;;
    --*) ARGV+=("$arg"); END_OF_OPT=1 ;;
    -*) for i in $(seq 2 ${#arg}); do ARGV+=("-${arg:i-1:1}"); done ;;
    *) ARGV+=("$arg") ;;
  esac
done

# Apply pre-processed options
set -- "${ARGV[@]}"

# Parse options
END_OF_OPT=
POSITIONAL=()
while [[ $# -gt 0 ]]; do
  case "${END_OF_OPT}${1}" in
    -h|--help)      usage 0 ;;
    -p|--password)  shift; PASSWORD="$1" ;;
    -u|--username)  shift; USERNAME="$1" ;;
    -n|--name)      shift; names+=("$1") ;;
    -q|--quiet)     QUIET=1 ;;
    -C|--copy)      COPY=1 ;;
    -N|--notify)    NOTIFY=1 ;;
    --stdin)        READ_STDIN=1 ;;
    --)             END_OF_OPT=1 ;;
    -*)             invalid "$1" ;;
    *)              POSITIONAL+=("$1") ;;
  esac
  shift
done

# Restore positional parameters
set -- "${POSITIONAL[@]}"
jchook
источник
6

Существует несколько способов анализа аргументов cmdline (например, GNU getopt (непереносимый) против BSD (OSX) getopt против getopts) - все это проблематично. Это решение

  • портативный!
  • имеет нулевые зависимости, зависит только от встроенных команд bash
  • допускает как короткие, так и длинные варианты
  • обрабатывает пробел между опцией и аргументом, но также может использовать =разделитель
  • поддерживает объединенный стиль коротких опций -vxf
  • обрабатывает опцию с необязательными аргументами (см. пример) и
  • не требует раздувания кода по сравнению с альтернативами для того же набора функций. Т.е. сжато, а потому проще в обслуживании

Примеры: любой из

# flag
-f
--foo

# option with required argument
-b"Hello World"
-b "Hello World"
--bar "Hello World"
--bar="Hello World"

# option with optional argument
--baz
--baz="Optional Hello"

#!/usr/bin/env bash

usage() {
  cat - >&2 <<EOF
NAME
    program-name.sh - Brief description

SYNOPSIS
    program-name.sh [-h|--help]
    program-name.sh [-f|--foo]
                    [-b|--bar <arg>]
                    [--baz[=<arg>]]
                    [--]
                    FILE ...

REQUIRED ARGUMENTS
  FILE ...
          input files

OPTIONS
  -h, --help
          Prints this and exits

  -f, --foo
          A flag option

  -b, --bar <arg>
          Option requiring an argument <arg>

  --baz[=<arg>]
          Option that has an optional argument <arg>. If <arg>
          is not specified, defaults to 'DEFAULT'
  --     
          Specify end of options; useful if the first non option
          argument starts with a hyphen

EOF
}

fatal() {
    for i; do
        echo -e "${i}" >&2
    done
    exit 1
}

# For long option processing
next_arg() {
    if [[ $OPTARG == *=* ]]; then
        # for cases like '--opt=arg'
        OPTARG="${OPTARG#*=}"
    else
        # for cases like '--opt arg'
        OPTARG="${args[$OPTIND]}"
        OPTIND=$((OPTIND + 1))
    fi
}

# ':' means preceding option character expects one argument, except
# first ':' which make getopts run in silent mode. We handle errors with
# wildcard case catch. Long options are considered as the '-' character
optspec=":hfb:-:"
args=("" "$@")  # dummy first element so $1 and $args[1] are aligned
while getopts "$optspec" optchar; do
    case "$optchar" in
        h) usage; exit 0 ;;
        f) foo=1 ;;
        b) bar="$OPTARG" ;;
        -) # long option processing
            case "$OPTARG" in
                help)
                    usage; exit 0 ;;
                foo)
                    foo=1 ;;
                bar|bar=*) next_arg
                    bar="$OPTARG" ;;
                baz)
                    baz=DEFAULT ;;
                baz=*) next_arg
                    baz="$OPTARG" ;;
                -) break ;;
                *) fatal "Unknown option '--${OPTARG}'" "see '${0} --help' for usage" ;;
            esac
            ;;
        *) fatal "Unknown option: '-${OPTARG}'" "See '${0} --help' for usage" ;;
    esac
done

shift $((OPTIND-1))

if [ "$#" -eq 0 ]; then
    fatal "Expected at least one required argument FILE" \
    "See '${0} --help' for usage"
fi

echo "foo=$foo, bar=$bar, baz=$baz, files=${@}"
tmoschou
источник
5

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

-s p1
--stage p1
-w somefolder
--workfolder somefolder
-sw p1 somefolder
-e=hello

Также позволяет это (может быть нежелательным):

-s--workfolder p1 somefolder
-se=hello p1
-swe=hello p1 somefolder

Вы должны решить перед использованием, будет ли = использоваться с опцией или нет. Это делается для того, чтобы код оставался чистым (ish).

while [[ $# > 0 ]]
do
    key="$1"
    while [[ ${key+x} ]]
    do
        case $key in
            -s*|--stage)
                STAGE="$2"
                shift # option has parameter
                ;;
            -w*|--workfolder)
                workfolder="$2"
                shift # option has parameter
                ;;
            -e=*)
                EXAMPLE="${key#*=}"
                break # option has been fully handled
                ;;
            *)
                # unknown option
                echo Unknown option: $key #1>&2
                exit 10 # either this: my preferred way to handle unknown options
                break # or this: do this to signal the option has been handled (if exit isn't used)
                ;;
        esac
        # prepare for next option in this key, if any
        [[ "$key" = -? || "$key" == --* ]] && unset key || key="${key/#-?/-}"
    done
    shift # option(s) fully processed, proceed to next input argument
done
galmok
источник
1
что означает "+ x" на $ {key + x}?
Лука Даванцо
1
Это проверка, чтобы увидеть, присутствует ли «ключ» или нет. Далее я сбрасываю ключ, и это нарушает внутренний цикл while.
Галмок
5

Решение, которое сохраняет необработанные аргументы. Демоверсии включены.

Вот мое решение. Он ОЧЕНЬ гибок и, в отличие от других, не требует внешних пакетов и аккуратно обрабатывает оставшиеся аргументы.

Использование это: ./myscript -flag flagvariable -otherflag flagvar2

Все, что вам нужно сделать, это отредактировать строку validflags. Он добавляет дефис и ищет все аргументы. Затем он определяет следующий аргумент как имя флага, например

./myscript -flag flagvariable -otherflag flagvar2
echo $flag $otherflag
flagvariable flagvar2

Основной код (короткая версия, подробный с примерами ниже, также версия с ошибками):

#!/usr/bin/env bash
#shebang.io
validflags="rate time number"
count=1
for arg in $@
do
    match=0
    argval=$1
    for flag in $validflags
    do
        sflag="-"$flag
        if [ "$argval" == "$sflag" ]
        then
            declare $flag=$2
            match=1
        fi
    done
        if [ "$match" == "1" ]
    then
        shift 2
    else
        leftovers=$(echo $leftovers $argval)
        shift
    fi
    count=$(($count+1))
done
#Cleanup then restore the leftovers
shift $#
set -- $leftovers

Подробная версия со встроенными демонстрациями эха:

#!/usr/bin/env bash
#shebang.io
rate=30
time=30
number=30
echo "all args
$@"
validflags="rate time number"
count=1
for arg in $@
do
    match=0
    argval=$1
#   argval=$(echo $@ | cut -d ' ' -f$count)
    for flag in $validflags
    do
            sflag="-"$flag
        if [ "$argval" == "$sflag" ]
        then
            declare $flag=$2
            match=1
        fi
    done
        if [ "$match" == "1" ]
    then
        shift 2
    else
        leftovers=$(echo $leftovers $argval)
        shift
    fi
    count=$(($count+1))
done

#Cleanup then restore the leftovers
echo "pre final clear args:
$@"
shift $#
echo "post final clear args:
$@"
set -- $leftovers
echo "all post set args:
$@"
echo arg1: $1 arg2: $2

echo leftovers: $leftovers
echo rate $rate time $time number $number

Последний, этот выводит ошибку, если пропущен неверный аргумент.

#!/usr/bin/env bash
#shebang.io
rate=30
time=30
number=30
validflags="rate time number"
count=1
for arg in $@
do
    argval=$1
    match=0
        if [ "${argval:0:1}" == "-" ]
    then
        for flag in $validflags
        do
                sflag="-"$flag
            if [ "$argval" == "$sflag" ]
            then
                declare $flag=$2
                match=1
            fi
        done
        if [ "$match" == "0" ]
        then
            echo "Bad argument: $argval"
            exit 1
        fi
        shift 2
    else
        leftovers=$(echo $leftovers $argval)
        shift
    fi
    count=$(($count+1))
done
#Cleanup then restore the leftovers
shift $#
set -- $leftovers
echo rate $rate time $time number $number
echo leftovers: $leftovers

Плюсы: то, что он делает, он обрабатывает очень хорошо. Он сохраняет неиспользуемые аргументы, которых нет во многих других решениях. Это также позволяет вызывать переменные, не определяясь вручную в скрипте. Это также позволяет предварительно заполнять переменные, если не указан соответствующий аргумент. (См. Подробный пример).

Минусы: Невозможно проанализировать одну сложную строку аргумента, например -xcvf будет обрабатываться как один аргумент. Вы можете легко написать дополнительный код в мой, который добавляет эту функциональность, хотя.


источник
5

Я хочу представить свой проект: https://github.com/flyingangel/argparser

source argparser.sh
parse_args "$@"

Просто как тот. Окружение будет заполнено переменными с тем же именем, что и аргументы.

Тхан Чунг
источник
3

Обратите внимание, что это getopt(1)была недолгая ошибка AT & T.

getopt был создан в 1984 году, но уже похоронен в 1986 году, потому что он не был действительно применим.

Доказательством того факта, что getoptон очень устарел, является то, что вместо этого в getopt(1)справочной странице все еще упоминается то , что было добавлено в оболочку Bourne в 1986 году вместе со встроенной оболочкой для обработки аргументов с пробелами внутри."$*""$@"getopts(1)

Кстати: если вы заинтересованы в разборе длинных опций в сценариях оболочки, может быть интересно узнать, что getopt(3)реализация из libc (Solaris) и ksh93обе добавили единую реализацию длинных опций, которая поддерживает длинные опции как псевдонимы для коротких опций. Это заставляет ksh93и Bourne Shellреализовывать единый интерфейс для длинных опций getopts.

Пример длинных опций, взятых из справочной страницы Bourne Shell:

getopts "f:(file)(input-file)o:(output-file)" OPTX "$@"

показывает, как долго псевдонимы опций могут использоваться как в Bourne Shell, так и в ksh93.

Смотрите справочную страницу недавней Bourne Shell:

http://schillix.sourceforge.net/man/man1/bosh.1.html

и справочную страницу для getopt (3) из OpenSolaris:

http://schillix.sourceforge.net/man/man3c/getopt.3c.html

и, наконец, справочная страница getopt (1) для проверки устаревшего $ *:

http://schillix.sourceforge.net/man/man1/getopt.1.html

Шили
источник
3

Я написал Bash Helper, чтобы написать хороший инструмент Bash

Домашняя страница проекта: https://gitlab.mbedsys.org/mbedsys/bashopts

пример:

#!/bin/bash -ei

# load the library
. bashopts.sh

# Enable backtrace dusplay on error
trap 'bashopts_exit_handle' ERR

# Initialize the library
bashopts_setup -n "$0" -d "This is myapp tool description displayed on help message" -s "$HOME/.config/myapprc"

# Declare the options
bashopts_declare -n first_name -l first -o f -d "First name" -t string -i -s -r
bashopts_declare -n last_name -l last -o l -d "Last name" -t string -i -s -r
bashopts_declare -n display_name -l display-name -t string -d "Display name" -e "\$first_name \$last_name"
bashopts_declare -n age -l number -d "Age" -t number
bashopts_declare -n email_list -t string -m add -l email -d "Email adress"

# Parse arguments
bashopts_parse_args "$@"

# Process argument
bashopts_process_args

окажет помощь:

NAME:
    ./example.sh - This is myapp tool description displayed on help message

USAGE:
    [options and commands] [-- [extra args]]

OPTIONS:
    -h,--help                          Display this help
    -n,--non-interactive true          Non interactive mode - [$bashopts_non_interactive] (type:boolean, default:false)
    -f,--first "John"                  First name - [$first_name] (type:string, default:"")
    -l,--last "Smith"                  Last name - [$last_name] (type:string, default:"")
    --display-name "John Smith"        Display name - [$display_name] (type:string, default:"$first_name $last_name")
    --number 0                         Age - [$age] (type:number, default:0)
    --email                            Email adress - [$email_list] (type:string, default:"")

наслаждаться :)

Эмерик Вершуур
источник
Я получаю это в Mac OS X: `` `lib / bashopts.sh: строка 138: объявить: -A: недопустимая опция объявить: использование: объявить [-afFirtx] [-p] [имя [= значение] ...] Ошибка в lib / bashopts.sh: 138. 'Declare -x -A bashopts_optprop_name' выход со статусом 2 Дерево вызовов: 1: lib / controller.sh: 4 source (...) Выход со статусом 1 `` `
Джош Вульф
Вам нужна Bash версии 4, чтобы использовать это. На Mac по умолчанию установлена ​​версия 3. Вы можете использовать home brew для установки bash 4.
Джош Вульф
3

Вот мой подход - использование регулярных выражений.

  • нет getopts
  • он обрабатывает блок коротких параметров -qwerty
  • он обрабатывает короткие параметры -q -w -e
  • он обрабатывает длинные варианты --qwerty
  • Вы можете передать атрибут короткой или длинной опции (если вы используете блок коротких опций, атрибут присоединяется к последней опции)
  • Вы можете использовать пробелы или =предоставлять атрибуты, но атрибуты совпадают, пока не встретите дефис + пробел "разделитель", поэтому в--q=qwe ty qwe ty одном атрибуте
  • он обрабатывает смесь всего вышеперечисленного, поэтому -o a -op attr ibute --option=att ribu te --op-tion attribute --option att-ributeдействительно

сценарий:

#!/usr/bin/env sh

help_menu() {
  echo "Usage:

  ${0##*/} [-h][-l FILENAME][-d]

Options:

  -h, --help
    display this help and exit

  -l, --logfile=FILENAME
    filename

  -d, --debug
    enable debug
  "
}

parse_options() {
  case $opt in
    h|help)
      help_menu
      exit
     ;;
    l|logfile)
      logfile=${attr}
      ;;
    d|debug)
      debug=true
      ;;
    *)
      echo "Unknown option: ${opt}\nRun ${0##*/} -h for help.">&2
      exit 1
  esac
}
options=$@

until [ "$options" = "" ]; do
  if [[ $options =~ (^ *(--([a-zA-Z0-9-]+)|-([a-zA-Z0-9-]+))(( |=)(([\_\.\?\/\\a-zA-Z0-9]?[ -]?[\_\.\?a-zA-Z0-9]+)+))?(.*)|(.+)) ]]; then
    if [[ ${BASH_REMATCH[3]} ]]; then # for --option[=][attribute] or --option[=][attribute]
      opt=${BASH_REMATCH[3]}
      attr=${BASH_REMATCH[7]}
      options=${BASH_REMATCH[9]}
    elif [[ ${BASH_REMATCH[4]} ]]; then # for block options -qwert[=][attribute] or single short option -a[=][attribute]
      pile=${BASH_REMATCH[4]}
      while (( ${#pile} > 1 )); do
        opt=${pile:0:1}
        attr=""
        pile=${pile/${pile:0:1}/}
        parse_options
      done
      opt=$pile
      attr=${BASH_REMATCH[7]}
      options=${BASH_REMATCH[9]}
    else # leftovers that don't match
      opt=${BASH_REMATCH[10]}
      options=""
    fi
    parse_options
  fi
done
a_z
источник
Как этот. Может быть, просто добавьте параметр -e в echo с новой строкой.
mauron85
3

Предположим , мы создаем скрипт с именем test_args.shследующим

#!/bin/sh
until [ $# -eq 0 ]
do
  name=${1:1}; shift;
  if [[ -z "$1" || $1 == -* ]] ; then eval "export $name=true"; else eval "export $name=$1"; shift; fi  
done
echo "year=$year month=$month day=$day flag=$flag"

После того, как мы запустим следующую команду:

sh test_args.sh  -year 2017 -flag  -month 12 -day 22 

Выход будет:

year=2017 month=12 day=22 flag=true
Джон
источник
5
Это принимает тот же подход, что и ответ Ноя , но имеет меньше проверок безопасности. Это позволяет нам записывать произвольные аргументы в среду скрипта, и я уверен, что использование здесь eval может позволить внедрение команд.
Уилл Барнвелл
2

Используйте модуль «Аргументы» из bash-модулей.

Пример:

#!/bin/bash
. import.sh log arguments

NAME="world"

parse_arguments "-n|--name)NAME;S" -- "$@" || {
  error "Cannot parse command line."
  exit 1
}

info "Hello, $NAME!"
Владимир Михайлович Лисовка
источник
2

Смешивание позиционных и флаговых аргументов

--param = arg (равно с разделителями)

Свободно смешивая флаги между позиционными аргументами:

./script.sh dumbo 127.0.0.1 --environment=production -q -d
./script.sh dumbo --environment=production 127.0.0.1 --quiet -d

может быть достигнуто с довольно кратким подходом:

# process flags
pointer=1
while [[ $pointer -le $# ]]; do
   param=${!pointer}
   if [[ $param != "-"* ]]; then ((pointer++)) # not a parameter flag so advance pointer
   else
      case $param in
         # paramter-flags with arguments
         -e=*|--environment=*) environment="${param#*=}";;
                  --another=*) another="${param#*=}";;

         # binary flags
         -q|--quiet) quiet=true;;
                 -d) debug=true;;
      esac

      # splice out pointer frame from positional list
      [[ $pointer -gt 1 ]] \
         && set -- ${@:1:((pointer - 1))} ${@:((pointer + 1)):$#} \
         || set -- ${@:((pointer + 1)):$#};
   fi
done

# positional remain
node_name=$1
ip_address=$2

--param arg (разделенный пробелом)

Это понятнее обычно работают на жидком не смешивать --flag=valueи --flag valueстили.

./script.sh dumbo 127.0.0.1 --environment production -q -d

Это немного рискованно читать, но все еще действует

./script.sh dumbo --environment production 127.0.0.1 --quiet -d

Источник

# process flags
pointer=1
while [[ $pointer -le $# ]]; do
   if [[ ${!pointer} != "-"* ]]; then ((pointer++)) # not a parameter flag so advance pointer
   else
      param=${!pointer}
      ((pointer_plus = pointer + 1))
      slice_len=1

      case $param in
         # paramter-flags with arguments
         -e|--environment) environment=${!pointer_plus}; ((slice_len++));;
                --another) another=${!pointer_plus}; ((slice_len++));;

         # binary flags
         -q|--quiet) quiet=true;;
                 -d) debug=true;;
      esac

      # splice out pointer frame from positional list
      [[ $pointer -gt 1 ]] \
         && set -- ${@:1:((pointer - 1))} ${@:((pointer + $slice_len)):$#} \
         || set -- ${@:((pointer + $slice_len)):$#};
   fi
done

# positional remain
node_name=$1
ip_address=$2
Марк Фокс
источник
2

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

В принципе eval "local key='val'"

function myrsync() {

        local backup=("${@}") args=(); while [[ $# -gt 0 ]]; do k="$1";
                case "$k" in
                    ---sourceuser|---sourceurl|---targetuser|---targeturl|---file|---exclude|---include)
                        eval "local ${k:3}='${2}'"; shift; shift    # Past two arguments
                    ;;
                    *)  # Unknown option  
                        args+=("$1"); shift;                        # Past argument only
                    ;;                                              
                esac                                                
        done; set -- "${backup[@]}"                                 # Restore $@


        echo "${sourceurl}"
}

Объявляет переменные как локальные, а не глобальные, как большинство ответов здесь.

Называется как:

myrsync ---sourceurl http://abc.def.g ---sourceuser myuser ... 

$ {K: 3} в основном является подстрокой для удаления первого ---из ключа.

ммм
источник
1

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

myscript.sh -f ./serverlist.txt или просто ./myscript.sh (и он принимает значения по умолчанию)

    #!/bin/bash
    # --- set the value, if there is inputs, override the defaults.

    HOME_FOLDER="${HOME}/owned_id_checker"
    SERVER_FILE_LIST="${HOME_FOLDER}/server_list.txt"

    while [[ $# > 1 ]]
    do
    key="$1"
    shift

    case $key in
        -i|--inputlist)
        SERVER_FILE_LIST="$1"
        shift
        ;;
    esac
    done


    echo "SERVER LIST   = ${SERVER_FILE_LIST}"
Майк К
источник
1

Другое решение без getopt [s], POSIX, старого стиля Unix

Аналогично решению, опубликованному Бруно Броноским, это здесь без использования getopt(s).

Главная особенность моего решения заключается в том, что оно позволяет объединять параметры, как и tar -xzf foo.tar.gzв случае с ними tar -x -z -f foo.tar.gz. И так же, как в tar,ps т. Д., Ведущий дефис необязателен для блока коротких опций (но это можно легко изменить). Также поддерживаются длинные опции (но когда блок начинается с одного, тогда требуются два ведущих дефиса).

Код с примерами вариантов

#!/bin/sh

echo
echo "POSIX-compliant getopt(s)-free old-style-supporting option parser from phk@[se.unix]"
echo

print_usage() {
  echo "Usage:

  $0 {a|b|c} [ARG...]

Options:

  --aaa-0-args
  -a
    Option without arguments.

  --bbb-1-args ARG
  -b ARG
    Option with one argument.

  --ccc-2-args ARG1 ARG2
  -c ARG1 ARG2
    Option with two arguments.

" >&2
}

if [ $# -le 0 ]; then
  print_usage
  exit 1
fi

opt=
while :; do

  if [ $# -le 0 ]; then

    # no parameters remaining -> end option parsing
    break

  elif [ ! "$opt" ]; then

    # we are at the beginning of a fresh block
    # remove optional leading hyphen and strip trailing whitespaces
    opt=$(echo "$1" | sed 's/^-\?\([a-zA-Z0-9\?-]*\)/\1/')

  fi

  # get the first character -> check whether long option
  first_chr=$(echo "$opt" | awk '{print substr($1, 1, 1)}')
  [ "$first_chr" = - ] && long_option=T || long_option=F

  # note to write the options here with a leading hyphen less
  # also do not forget to end short options with a star
  case $opt in

    -)

      # end of options
      shift
      break
      ;;

    a*|-aaa-0-args)

      echo "Option AAA activated!"
      ;;

    b*|-bbb-1-args)

      if [ "$2" ]; then
        echo "Option BBB with argument '$2' activated!"
        shift
      else
        echo "BBB parameters incomplete!" >&2
        print_usage
        exit 1
      fi
      ;;

    c*|-ccc-2-args)

      if [ "$2" ] && [ "$3" ]; then
        echo "Option CCC with arguments '$2' and '$3' activated!"
        shift 2
      else
        echo "CCC parameters incomplete!" >&2
        print_usage
        exit 1
      fi
      ;;

    h*|\?*|-help)

      print_usage
      exit 0
      ;;

    *)

      if [ "$long_option" = T ]; then
        opt=$(echo "$opt" | awk '{print substr($1, 2)}')
      else
        opt=$first_chr
      fi
      printf 'Error: Unknown option: "%s"\n' "$opt" >&2
      print_usage
      exit 1
      ;;

  esac

  if [ "$long_option" = T ]; then

    # if we had a long option then we are going to get a new block next
    shift
    opt=

  else

    # if we had a short option then just move to the next character
    opt=$(echo "$opt" | awk '{print substr($1, 2)}')

    # if block is now empty then shift to the next one
    [ "$opt" ] || shift

  fi

done

echo "Doing something..."

exit 0

Для примера использования смотрите примеры ниже.

Положение вариантов с аргументами

Для чего бы то ни было, опции с аргументами не являются последними (нужны только длинные опции). Поэтому, например, в tar(по крайней мере, в некоторых реализациях) fпараметры должны быть последними, потому что имя файла следует ( tar xzf bar.tar.gzработает, но tar xfz bar.tar.gzне работает), но здесь это не так (см. Более поздние примеры).

Несколько вариантов с аргументами

В качестве еще одного бонуса параметры опций потребляются в порядке опций параметрами с необходимыми опциями. Просто посмотрите на вывод моего скрипта с помощью командной строки abc X Y Z(или -abc X Y Z):

Option AAA activated!
Option BBB with argument 'X' activated!
Option CCC with arguments 'Y' and 'Z' activated!

Длинные варианты также объединены

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

  • -cba Z Y X
  • cba Z Y X
  • -cb-aaa-0-args Z Y X
  • -c-bbb-1-args Z Y X -a
  • --ccc-2-args Z Y -ba X
  • c Z Y b X a
  • -c Z Y -b X -a
  • --ccc-2-args Z Y --bbb-1-args X --aaa-0-args

Все это приводит к:

Option CCC with arguments 'Z' and 'Y' activated!
Option BBB with argument 'X' activated!
Option AAA activated!
Doing something...

Не в этом решении

Необязательные аргументы

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

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

Я лично предпочитаю дополнительные опции вместо необязательных аргументов.

Опция аргументов представлена ​​знаком равенства

Как и с необязательными аргументами, я не фанат этого (кстати, есть ли тема для обсуждения плюсов и минусов различных стилей параметров?), Но если вы хотите это, вы, вероятно, можете реализовать это сами, как это было сделано на http: // mywiki.wooledge.org/BashFAQ/035#Manual_loop с оператором --long-with-arg=?*case и последующим удалением знака равенства (кстати, это сайт, который говорит, что создание объединения параметров возможно с некоторыми усилиями, но «оставил [это] как упражнение для читателя»). «что заставило меня поверить им на слово, но я начал с нуля).

Другие заметки

POSIX-совместимый, работает даже на старых установках Busybox, с которыми мне приходилось иметь дело (например cut, headс getoptsотсутствующим).

PHK
источник