Как мне разобрать аргументы командной строки в Bash? - PullRequest
1611 голосов
/ 10 октября 2008

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

./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?

Ответы [ 31 ]

2270 голосов
/ 08 января 2013

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

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

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

Использование ./myscript.sh -e conf -s /etc -l /usr/lib /etc/hosts

#!/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

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

Использование ./myscript.sh -e=conf -s=/etc -l=/usr/lib /etc/hosts

#!/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 "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

Чтобы лучше понять ${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:

#!/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: $@"

# End of file

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

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

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

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

452 голосов
/ 20 апреля 2015

Нет ответа упоминает расширенный 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 не делает этого)
    • допускает опции = в стиле long: 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 установить gnu-getopt или sudo port install getopt
2 Соглашения POSIX exec() не имеют надежного способа передачи двоичного NULL в аргументах командной строки; эти байты преждевременно заканчивают аргумент
3 первая версия, выпущенная в 1997 году или ранее (я отслеживал ее только в 1997 году)

117 голосов
/ 13 ноября 2012

от: 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/[^=]*=//'`, который вызывает два ненужных подпроцесса.

103 голосов
/ 10 октября 2008

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

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

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

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

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

91 голосов
/ 20 ноября 2015

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

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
90 голосов
/ 16 июля 2015

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

  • ручки -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
39 голосов
/ 01 июля 2014

Я на 4 года опоздал на этот вопрос, но хочу вернуть. Я использовал предыдущие ответы в качестве отправной точки, чтобы привести в порядок мой старый синтаксический анализ adhoc. Затем я переработал следующий код шаблона. Он обрабатывает как длинные, так и короткие параметры, используя = или разделенные пробелом аргументы, а также несколько коротких параметров, сгруппированных вместе. Наконец, он повторно вставляет все непарамальные аргументы обратно в переменные $ 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
26 голосов
/ 11 июля 2016

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

https://argbash.io

26 голосов
/ 08 сентября 2016

Мой ответ в значительной степени основан на ответе Бруно Броноски , но я как бы скомбинировал его две чистые реализации 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 value

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

    • Значение -a или -arg или --arg или -a-r-g или что-то еще
  • Чистый удар. Нет необходимости изучать / использовать getopt или getopts

МИНУСЫ:

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

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

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

13 голосов
/ 01 марта 2012

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

#!/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
...