Как мне разобрать аргументы командной строки в 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 ]

13 голосов
/ 09 июня 2014

Расширяя превосходный ответ @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
8 голосов
/ 01 июля 2016

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

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

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

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

  1. Не поддерживает разделенные пробелом параметры (-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
8 голосов
/ 13 февраля 2015

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
7 голосов
/ 04 июля 2016

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
7 голосов
/ 19 июля 2013

Вот как я это делаю в функции, чтобы избежать прерывания одновременного запуска 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
...
}
4 голосов
/ 19 октября 2015

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

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

Доказательством того, что getopt очень устарел, является то, что на справочной странице getopt(1) все еще упоминается "$*" вместо "$@", который был добавлен в Bourne Shell в 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

4 голосов
/ 24 июня 2015

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

-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
3 голосов
/ 16 сентября 2018

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

source argparser.sh
parse_args "$@"

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

3 голосов
/ 11 октября 2017

Предположим, мы создаем сценарий оболочки с именем 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
2 голосов
/ 21 февраля 2017

Я написал помощника bash, чтобы написать хороший инструмент для 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:"")

наслаждайся:)

...