我希望使用shell脚本调用长形式和短形式的命令行选项。

我知道可以使用getopts,但是像在Perl中一样,我不能用shell做同样的事情。

关于如何做到这一点的任何想法,这样我就可以使用如下选项:

./shell.sh --copyfile abc.pl /tmp/
./shell.sh -c abc.pl /tmp/

在上面,这两个命令对我的shell意味着同样的事情,但使用getopts,我还不能实现这些?


当前回答

为了保持跨平台兼容性,避免依赖外部可执行文件,我从另一种语言移植了一些代码。

我发现它很容易使用,这里有一个例子:

ArgParser::addArg "[h]elp"    false    "This list"
ArgParser::addArg "[q]uiet"   false    "Supress output"
ArgParser::addArg "[s]leep"   1        "Seconds to sleep"
ArgParser::addArg "v"         1        "Verbose mode"

ArgParser::parse "$@"

ArgParser::isset help && ArgParser::showArgs

ArgParser::isset "quiet" \
   && echo "Quiet!" \
   || echo "Noisy!"

local __sleep
ArgParser::tryAndGetArg sleep into __sleep \
   && echo "Sleep for $__sleep seconds" \
   || echo "No value passed for sleep"

# This way is often more convienient, but is a little slower
echo "Sleep set to: $( ArgParser::getArg sleep )"

所需的BASH有点长,但我希望避免依赖BASH 4的关联数组。你也可以直接从http://nt4.com/bash/argparser.inc.sh下载

#!/usr/bin/env bash

# Updates to this script may be found at
# http://nt4.com/bash/argparser.inc.sh

# Example of runtime usage:
# mnc.sh --nc -q Caprica.S0*mkv *.avi *.mp3 --more-options here --host centos8.host.com

# Example of use in script (see bottom)
# Just include this file in yours, or use
# source argparser.inc.sh

unset EXPLODED
declare -a EXPLODED
function explode 
{
    local c=$# 
    (( c < 2 )) && 
    {
        echo function "$0" is missing parameters 
        return 1
    }

    local delimiter="$1"
    local string="$2"
    local limit=${3-99}

    local tmp_delim=$'\x07'
    local delin=${string//$delimiter/$tmp_delim}
    local oldifs="$IFS"

    IFS="$tmp_delim"
    EXPLODED=($delin)
    IFS="$oldifs"
}


# See: http://fvue.nl/wiki/Bash:_Passing_variables_by_reference
# Usage: local "$1" && upvar $1 "value(s)"
upvar() {
    if unset -v "$1"; then           # Unset & validate varname
        if (( $# == 2 )); then
            eval $1=\"\$2\"          # Return single value
        else
            eval $1=\(\"\${@:2}\"\)  # Return array
        fi
    fi
}

function decho
{
    :
}

function ArgParser::check
{
    __args=${#__argparser__arglist[@]}
    for (( i=0; i<__args; i++ ))
    do
        matched=0
        explode "|" "${__argparser__arglist[$i]}"
        if [ "${#1}" -eq 1 ]
        then
            if [ "${1}" == "${EXPLODED[0]}" ]
            then
                decho "Matched $1 with ${EXPLODED[0]}"
                matched=1

                break
            fi
        else
            if [ "${1}" == "${EXPLODED[1]}" ]
            then
                decho "Matched $1 with ${EXPLODED[1]}"
                matched=1

                break
            fi
        fi
    done
    (( matched == 0 )) && return 2
    # decho "Key $key has default argument of ${EXPLODED[3]}"
    if [ "${EXPLODED[3]}" == "false" ]
    then
        return 0
    else
        return 1
    fi
}

function ArgParser::set
{
    key=$3
    value="${1:-true}"
    declare -g __argpassed__$key="$value"
}

function ArgParser::parse
{

    unset __argparser__argv
    __argparser__argv=()
    # echo parsing: "$@"

    while [ -n "$1" ]
    do
        # echo "Processing $1"
        if [ "${1:0:2}" == '--' ]
        then
            key=${1:2}
            value=$2
        elif [ "${1:0:1}" == '-' ]
        then
            key=${1:1}               # Strip off leading -
            value=$2
        else
            decho "Not argument or option: '$1'" >& 2
            __argparser__argv+=( "$1" )
            shift
            continue
        fi
        # parameter=${tmp%%=*}     # Extract name.
        # value=${tmp##*=}         # Extract value.
        decho "Key: '$key', value: '$value'"
        # eval $parameter=$value
        ArgParser::check $key
        el=$?
        # echo "Check returned $el for $key"
        [ $el -eq  2 ] && decho "No match for option '$1'" >&2 # && __argparser__argv+=( "$1" )
        [ $el -eq  0 ] && decho "Matched option '${EXPLODED[2]}' with no arguments"        >&2 && ArgParser::set true "${EXPLODED[@]}"
        [ $el -eq  1 ] && decho "Matched option '${EXPLODED[2]}' with an argument of '$2'"   >&2 && ArgParser::set "$2" "${EXPLODED[@]}" && shift
        shift
    done
}

function ArgParser::isset
{
    declare -p "__argpassed__$1" > /dev/null 2>&1 && return 0
    return 1
}

function ArgParser::getArg
{
    # This one would be a bit silly, since we can only return non-integer arguments ineffeciently
    varname="__argpassed__$1"
    echo "${!varname}"
}

##
# usage: tryAndGetArg <argname> into <varname>
# returns: 0 on success, 1 on failure
function ArgParser::tryAndGetArg
{
    local __varname="__argpassed__$1"
    local __value="${!__varname}"
    test -z "$__value" && return 1
    local "$3" && upvar $3 "$__value"
    return 0
}

function ArgParser::__construct
{
    unset __argparser__arglist
    # declare -a __argparser__arglist
}

##
# @brief add command line argument
# @param 1 short and/or long, eg: [s]hort
# @param 2 default value
# @param 3 description
##
function ArgParser::addArg
{
    # check for short arg within long arg
    if [[ "$1" =~ \[(.)\] ]]
    then
        short=${BASH_REMATCH[1]}
        long=${1/\[$short\]/$short}
    else
        long=$1
    fi
    if [ "${#long}" -eq 1 ]
    then
        short=$long
        long=''
    fi
    decho short: "$short"
    decho long: "$long"
    __argparser__arglist+=("$short|$long|$1|$2|$3")
}

## 
# @brief show available command line arguments
##
function ArgParser::showArgs
{
    # declare -p | grep argparser
    printf "Usage: %s [OPTION...]\n\n" "$( basename "${BASH_SOURCE[0]}" )"
    printf "Defaults for the options are specified in brackets.\n\n";

    __args=${#__argparser__arglist[@]}
    for (( i=0; i<__args; i++ ))
    do
        local shortname=
        local fullname=
        local default=
        local description=
        local comma=

        explode "|" "${__argparser__arglist[$i]}"

        shortname="${EXPLODED[0]:+-${EXPLODED[0]}}" # String Substitution Guide: 
        fullname="${EXPLODED[1]:+--${EXPLODED[1]}}" # http://tldp.org/LDP/abs/html/parameter-substitution.html
        test -n "$shortname" \
            && test -n "$fullname" \
            && comma=","

        default="${EXPLODED[3]}"
        case $default in
            false )
                default=
                ;;
            "" )
                default=
                ;;
            * )
                default="[$default]"
        esac

        description="${EXPLODED[4]}"

        printf "  %2s%1s %-19s %s %s\n" "$shortname" "$comma" "$fullname" "$description" "$default"
    done
}

function ArgParser::test
{
    # Arguments with a default of 'false' do not take paramaters (note: default
    # values are not applied in this release)

    ArgParser::addArg "[h]elp"      false       "This list"
    ArgParser::addArg "[q]uiet" false       "Supress output"
    ArgParser::addArg "[s]leep" 1           "Seconds to sleep"
    ArgParser::addArg "v"           1           "Verbose mode"

    ArgParser::parse "$@"

    ArgParser::isset help && ArgParser::showArgs

    ArgParser::isset "quiet" \
        && echo "Quiet!" \
        || echo "Noisy!"

    local __sleep
    ArgParser::tryAndGetArg sleep into __sleep \
        && echo "Sleep for $__sleep seconds" \
        || echo "No value passed for sleep"

    # This way is often more convienient, but is a little slower
    echo "Sleep set to: $( ArgParser::getArg sleep )"

    echo "Remaining command line: ${__argparser__argv[@]}"

}

if [ "$( basename "$0" )" == "argparser.inc.sh" ]
then
    ArgParser::test "$@"
fi

其他回答

下面你可以找到bash中复杂选项解析的几种不同方法: http://mywiki.wooledge.org/ComplexOptionParsing

我确实创建了下面的一个,而且我认为它很好,因为它的代码最少 多头和空头选择都有效。使用这种方法,长选项也可以有多个参数。

#!/bin/bash
# Uses bash extensions.  Not portable as written.

declare -A longoptspec
longoptspec=( [loglevel]=1 ) #use associative array to declare how many arguments a long option expects, in this case we declare that loglevel expects/has one argument, long options that aren't listed i n this way will have zero arguments by default
optspec=":h-:"
while getopts "$optspec" opt; do
while true; do
    case "${opt}" in
        -) #OPTARG is name-of-long-option or name-of-long-option=value
            if [[ "${OPTARG}" =~ .*=.* ]] #with this --key=value format only one argument is possible
            then
                opt=${OPTARG/=*/}
                OPTARG=${OPTARG#*=}
                ((OPTIND--))    
            else #with this --key value1 value2 format multiple arguments are possible
                opt="$OPTARG"
                OPTARG=(${@:OPTIND:$((longoptspec[$opt]))})
            fi
            ((OPTIND+=longoptspec[$opt]))
            continue #now that opt/OPTARG are set we can process them as if getopts would've given us long options
            ;;
        loglevel)
          loglevel=$OPTARG
            ;;
        h|help)
            echo "usage: $0 [--loglevel[=]<value>]" >&2
            exit 2
            ;;
    esac
break; done
done

# End of file

下面是一个实际使用长选项的getopt的示例:

aflag=no
bflag=no
cargument=none

# options may be followed by one colon to indicate they have a required argument
if ! options=$(getopt -o abc: -l along,blong,clong: -- "$@")
then
    # something went wrong, getopt will put out an error message for us
    exit 1
fi

set -- $options

while [ $# -gt 0 ]
do
    case $1 in
    -a|--along) aflag="yes" ;;
    -b|--blong) bflag="yes" ;;
    # for options with required arguments, an additional shift is required
    -c|--clong) cargument="$2" ; shift;;
    (--) shift; break;;
    (-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;;
    (*) break;;
    esac
    shift
done

长选项可以被内置的标准getopts作为- " option "的"参数"解析。

这是可移植的本地POSIX shell -不需要外部程序或bashisms。

本指南将长选项作为-选项的参数实现,因此——alpha被getopts视为参数alpha为-,而——bravo=foo被参数bravo=foo视为-。true实参通过shell参数展开获取,更新$OPT和$OPTARG。

在本例中,-b和-c(以及它们的长形式——bravo和——charlie)具有强制实参。长选项的参数出现在等号之后,例如——bravo=foo(长选项的空格分隔符很难实现,参见下文)。

因为它使用了内置的getopts,所以这个解决方案支持像cmd——bravo=foo -ac FILE这样的用法(它组合了选项-a和-c,并将长选项与标准选项交织在一起),而这里的大多数其他答案要么很难做到,要么无法做到这一点。

die() { echo "$*" >&2; exit 2; }  # complain to STDERR and exit with error
needs_arg() { if [ -z "$OPTARG" ]; then die "No arg for --$OPT option"; fi; }

while getopts ab:c:-: OPT; do
  # support long options: https://stackoverflow.com/a/28466267/519360
  if [ "$OPT" = "-" ]; then   # long option: reformulate OPT and OPTARG
    OPT="${OPTARG%%=*}"       # extract long option name
    OPTARG="${OPTARG#$OPT}"   # extract long option argument (may be empty)
    OPTARG="${OPTARG#=}"      # if long option argument, remove assigning `=`
  fi
  case "$OPT" in
    a | alpha )    alpha=true ;;
    b | bravo )    needs_arg; bravo="$OPTARG" ;;
    c | charlie )  needs_arg; charlie="$OPTARG" ;;
    ??* )          die "Illegal option --$OPT" ;;  # bad long option
    ? )            exit 2 ;;  # bad short option (error reported via getopts)
  esac
done
shift $((OPTIND-1)) # remove parsed options and args from $@ list

When the option is a dash (-), it is a long option. getopts will have parsed the actual long option into $OPTARG, e.g. --bravo=foo originally sets OPT='-' and OPTARG='bravo=foo'. The if stanza sets $OPT to the contents of $OPTARG before the first equals sign (bravo in our example) and then removes that from the beginning of $OPTARG (yielding =foo in this step, or an empty string if there is no =). Finally, we strip the argument's leading =. At this point, $OPT is either a short option (one character) or a long option (2+ characters).

The case then matches either short or long options (the pipe, |, indicates that "or" operation. A long-only option like delta ) delta=true ;; doesn't need a pipe). For short options, getopts automatically complains about options and missing arguments, so we have to replicate those manually using the needs_arg function, which fatally exits when $OPTARG is empty. The ??* condition will match any remaining long option (? matches a single character and * matches zero or more, so ??* matches 2+ characters), allowing us to issue the "Illegal option" error before exiting.

与正常的gnu风格的长选项一样,提供——将停止解析,因此- -- --bravo=4将把$alpha设置为true,但$bravo将保持不变,$1将-bravo=4。我不能说我建议用前导破折号来命名文件,但这是表示它们不是选项的方法。


小错误:如果有人给出了一个无效的单字符长选项(它也不是一个短选项),这将退出一个错误,但没有消息(这个实现假设它是一个短选项)。您可以在case之前的条件中使用一个额外的变量来跟踪它,然后在最后的case条件中测试它,但我认为这是一个太麻烦的角落情况。

大写变量名:一般情况下,建议保留全大写变量供系统使用。我将$OPT保留为全大写,以与$OPTARG保持一致,但这确实打破了这种约定。我认为这很合适,因为这是系统应该做的事情,它应该是安全的;我还没有听说过任何使用这个变量名的标准。

要抱怨长选项的非预期实参:用翻转测试模拟needs_argg来抱怨一个非预期实参:

no_arg() { if [ -n "$OPTARG" ]; then die "No arg allowed for --$OPT option"; fi; }

要接受带空格分隔参数的长选项:您可以用eval "ARG_B=\"\$$OPTIND\""(或使用bash的间接展开,ARG_B="${!OPTIND}")拉入下一个参数,然后增加$OPTIND,正如这个答案的旧版本所指出的那样,但它不可靠;getopts可以过早地终止,假设参数超出了它的作用域,并且一些实现不太适合手动操作$OPTIND。

内置的getopts命令仍然仅限于单字符选项。

现在有(或者曾经有)一个外部程序getopt,它可以重新组织一组选项,使其更容易解析。你也可以调整这种设计来处理长选项。使用示例:

aflag=no
bflag=no
flist=""
set -- $(getopt abf: "$@")
while [ $# -gt 0 ]
do
    case "$1" in
    (-a) aflag=yes;;
    (-b) bflag=yes;;
    (-f) flist="$flist $2"; shift;;
    (--) shift; break;;
    (-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;;
    (*)  break;;
    esac
    shift
done

# Process remaining non-option arguments
...

您可以对getoptlong命令使用类似的方案。

请注意,外部getopt程序的基本弱点是难以处理其中包含空格的参数,以及难以准确地保留这些空格。这就是内置getopts优越的原因,尽管它只能处理单字母选项。

Getopts“可以用来”解析长选项,只要你不希望它们有参数……

以下是如何:

$ cat > longopt
while getopts 'e:-:' OPT; do
  case $OPT in
    e) echo echo: $OPTARG;;
    -) #long option
       case $OPTARG in
         long-option) echo long option;;
         *) echo long option: $OPTARG;;
       esac;;
  esac
done

$ bash longopt -e asd --long-option --long1 --long2 -e test
echo: asd
long option
long option: long1
long option: long2
echo: test

如果尝试使用OPTIND获取长选项的参数,getopts将把它作为第一个不可选的位置参数,并停止解析任何其他参数。 在这种情况下,您最好使用简单的case语句手动处理它。

这将“总是”工作:

$ cat >longopt2
while (($#)); do
    OPT=$1
    shift
    case $OPT in
        --*) case ${OPT:2} in
            long1) echo long1 option;;
            complex) echo comples with argument $1; shift;;
        esac;;

        -*) case ${OPT:1} in
            a) echo short option a;;
            b) echo short option b with parameter $1; shift;;
        esac;;
    esac
done


$ bash longopt2 --complex abc -a --long -b test
comples with argument abc
short option a
short option b with parameter test

尽管它不像getopts那么灵活,而且你必须在用例实例中自己做很多错误检查代码……

但这是一种选择。