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

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

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

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

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


当前回答

内置的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优越的原因,尽管它只能处理单字母选项。

其他回答

我只是偶尔写一些shell脚本,但没有实践经验,所以任何反馈都很感激。

使用@Arvid Requate提出的策略,我们注意到一些用户错误。忘记包含值的用户会意外地将下一个选项的名称视为值:

./getopts_test.sh --loglevel= --toc=TRUE

将导致"loglevel"的值被视为"——toc=TRUE"。这可以 被避免的。

我从http://mwiki.wooledge.org/BashFAQ/035关于手动解析的讨论中改编了一些关于检查CLI用户错误的想法。我在处理"-"和"——"参数时插入了错误检查。

然后我开始摆弄语法,所以这里的任何错误都是我的错,而不是原始作者的错。

我的方法可以帮助那些喜欢输入带等号或不带等号的长字符的用户。也就是说,它对“——loglevel 9”的响应应该与“——loglevel=9”的响应相同。在——/space方法中,不可能确定用户是否忘记了一个参数,因此需要进行一些猜测。

如果用户使用长/等号格式(——opt=),则=后的空格会触发错误,因为没有提供参数。 如果user有长/空格参数(——opt),如果后面没有参数(命令结束)或参数以破折号开头,该脚本将导致失败。

In case you are starting out on this, there is an interesting difference between "--opt=value" and "--opt value" formats. With the equal sign, the command line argument is seen as "opt=value" and the work to handle that is string parsing, to separate at the "=". In contrast, with "--opt value", the name of the argument is "opt" and we have the challenge of getting the next value supplied in the command line. That's where @Arvid Requate used ${!OPTIND}, the indirect reference. I still don't understand that, well, at all, and comments in BashFAQ seem to warn against that style (http://mywiki.wooledge.org/BashFAQ/006). BTW, I don't think previous poster's comments about importance of OPTIND=$(( $OPTIND + 1 )) are correct. I mean to say, I see no harm from omitting it.

在该脚本的最新版本中,flag -v表示详细打印输出。

将其保存在一个名为“cli-5.sh”的文件中,使其可执行,其中任何一个都将以预期的方式工作或失败

./cli-5.sh  -v --loglevel=44 --toc  TRUE
./cli-5.sh  -v --loglevel=44 --toc=TRUE
./cli-5.sh --loglevel 7
./cli-5.sh --loglevel=8
./cli-5.sh -l9

./cli-5.sh  --toc FALSE --loglevel=77
./cli-5.sh  --toc=FALSE --loglevel=77

./cli-5.sh   -l99 -t yyy
./cli-5.sh   -l 99 -t yyy

下面是对用户intpu进行错误检查的示例输出

$ ./cli-5.sh  --toc --loglevel=77
ERROR: toc value must not have dash at beginning
$ ./cli-5.sh  --toc= --loglevel=77
ERROR: value for toc undefined

您应该考虑打开-v,因为它会打印OPTIND和OPTARG的内部内容

#/usr/bin/env bash

## Paul Johnson
## 20171016
##

## Combines ideas from
## https://stackoverflow.com/questions/402377/using-getopts-in-bash-shell-script-to-get-long-and-short-command-line-options
## by @Arvid Requate, and http://mwiki.wooledge.org/BashFAQ/035

# What I don't understand yet: 
# In @Arvid REquate's answer, we have 
# val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
# this works, but I don't understand it!


die() {
    printf '%s\n' "$1" >&2
    exit 1
}

printparse(){
    if [ ${VERBOSE} -gt 0 ]; then
        printf 'Parse: %s%s%s\n' "$1" "$2" "$3" >&2;
    fi
}

showme(){
    if [ ${VERBOSE} -gt 0 ]; then
        printf 'VERBOSE: %s\n' "$1" >&2;
    fi
}


VERBOSE=0
loglevel=0
toc="TRUE"

optspec=":vhl:t:-:"
while getopts "$optspec" OPTCHAR; do

    showme "OPTARG:  ${OPTARG[*]}"
    showme "OPTIND:  ${OPTIND[*]}"
    case "${OPTCHAR}" in
        -)
            case "${OPTARG}" in
                loglevel) #argument has no equal sign
                    opt=${OPTARG}
                    val="${!OPTIND}"
                    ## check value. If negative, assume user forgot value
                    showme "OPTIND is {$OPTIND} {!OPTIND} has value \"${!OPTIND}\""
                    if [[ "$val" == -* ]]; then
                        die "ERROR: $opt value must not have dash at beginning"
                    fi
                    ## OPTIND=$(( $OPTIND + 1 )) # CAUTION! no effect?
                    printparse "--${OPTARG}" "  " "${val}"
                    loglevel="${val}"
                    shift
                    ;;
                loglevel=*) #argument has equal sign
                    opt=${OPTARG%=*}
                    val=${OPTARG#*=}
                    if [ "${OPTARG#*=}" ]; then
                        printparse "--${opt}" "=" "${val}"
                        loglevel="${val}"
                        ## shift CAUTION don't shift this, fails othewise
                    else
                        die "ERROR: $opt value must be supplied"
                    fi
                    ;;
                toc) #argument has no equal sign
                    opt=${OPTARG}
                    val="${!OPTIND}"
                    ## check value. If negative, assume user forgot value
                    showme "OPTIND is {$OPTIND} {!OPTIND} has value \"${!OPTIND}\""
                    if [[ "$val" == -* ]]; then
                        die "ERROR: $opt value must not have dash at beginning"
                    fi
                    ## OPTIND=$(( $OPTIND + 1 )) #??
                    printparse "--${opt}" " " "${val}"
                    toc="${val}"
                    shift
                    ;;
                toc=*) #argument has equal sign
                    opt=${OPTARG%=*}
                    val=${OPTARG#*=}
                    if [ "${OPTARG#*=}" ]; then
                        toc=${val}
                        printparse "--$opt" " -> " "$toc"
                        ##shift ## NO! dont shift this
                    else
                        die "ERROR: value for $opt undefined"
                    fi
                    ;;

                help)
                    echo "usage: $0 [-v] [--loglevel[=]<value>] [--toc[=]<TRUE,FALSE>]" >&2
                    exit 2
                    ;;
                *)
                    if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then
                        echo "Unknown option --${OPTARG}" >&2
                    fi
                    ;;
            esac;;
        h|-\?|--help)
            ## must rewrite this for all of the arguments
            echo "usage: $0 [-v] [--loglevel[=]<value>] [--toc[=]<TRUE,FALSE>]" >&2
            exit 2
            ;;
        l)
            loglevel=${OPTARG}
            printparse "-l" " "  "${loglevel}"
            ;;
        t)
            toc=${OPTARG}
            ;;
        v)
            VERBOSE=1
            ;;

        *)
            if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then
                echo "Non-option argument: '-${OPTARG}'" >&2
            fi
            ;;
    esac
done



echo "
After Parsing values
"
echo "loglevel  $loglevel" 
echo "toc  $toc"

如果这就是您想要调用脚本的方式

myscript.sh --input1 "ABC" --input2 "PQR" --input2 "XYZ"

然后,您可以使用getopt和——longoptions来实现这个最简单的方法

试试这个,希望对你有用

# Read command line options
ARGUMENT_LIST=(
    "input1"
    "input2"
    "input3"
)



# read arguments
opts=$(getopt \
    --longoptions "$(printf "%s:," "${ARGUMENT_LIST[@]}")" \
    --name "$(basename "$0")" \
    --options "" \
    -- "$@"
)


echo $opts

eval set --$opts

while true; do
    case "$1" in
    --input1)  
        shift
        empId=$1
        ;;
    --input2)  
        shift
        fromDate=$1
        ;;
    --input3)  
        shift
        toDate=$1
        ;;
      --)
        shift
        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。

可以考虑以下三种实现方式:

Bash内置的getopts。这不支持带有双破折号前缀的长选项名。它只支持单字符选项。 BSD UNIX实现的独立getopt命令(这是MacOS使用的)。这也不支持长选项。 独立getopt的GNU实现。GNU getopt(3)(由Linux上的命令行getopt(1)使用)支持解析长选项。


其他一些答案给出了使用bash内置getopts模拟长选项的解决方案。该解决方案实际上生成了一个字符为“-”的短选项。所以你得到“——”作为标志。然后,后面的所有内容都变成OPTARG,并使用嵌套的case测试OPTARG。

这很聪明,但也需要注意:

getopts不能强制执行opt规范。如果用户提供了无效的选项,它不能返回错误。在解析OPTARG时,您必须自己进行错误检查。 OPTARG用于长选项名称,当长选项本身有参数时,这会使使用复杂化。你最终不得不自己编写代码作为一个额外的案例。

因此,虽然可以编写更多的代码来解决长选项支持不足的问题,但工作量要大得多,并且在一定程度上违背了使用getopt解析器来简化代码的目的。

公认的答案很好地指出了bash内置getopts的所有缺点。答案以:

因此,虽然可以编写更多的代码来解决长选项支持不足的问题,但工作量要大得多,并且在一定程度上违背了使用getopt解析器来简化代码的目的。

尽管我原则上同意这种说法,但我觉得我们在各种脚本中实现这个特性的次数证明,我们应该花一些精力来创建一个“标准化”的、经过良好测试的解决方案。

因此,我通过在纯bash中实现getopts_long“升级”了内置在getopts中的bash,没有外部依赖。该函数的使用与内置的getopts 100%兼容。

通过在脚本中包含getopts_long(在GitHub上托管),可以简单地实现原始问题的答案:

source "${PATH_TO}/getopts_long.bash"

while getopts_long ':c: copyfile:' OPTKEY; do
    case ${OPTKEY} in
        'c'|'copyfile')
            echo 'file supplied -- ${OPTARG}'
            ;;
        '?')
            echo "INVALID OPTION -- ${OPTARG}" >&2
            exit 1
            ;;
        ':')
            echo "MISSING ARGUMENT for option -- ${OPTARG}" >&2
            exit 1
            ;;
        *)
            echo "Misconfigured OPTSPEC or uncaught option -- ${OPTKEY}" >&2
            exit 1
            ;;
    esac
done

shift $(( OPTIND - 1 ))
[[ "${1}" == "--" ]] && shift