使用 getopts 处理长选项

使用 getopts 处理长选项

我正在解析选项,getopts但也想处理长选项。

print-args ()
{
 title="$1" ; shift
 printf "\n%s\n" "${title}: \$@:"
 for arg in "$@"; do
   (( i = i + 1 ))
   printf "%s |%s|\n" "${i}." "$arg"
 done
}

getopts_test ()
{
 aggr=()
 for arg in "$@"; do
   case $arg in
    ("--colour"|"--color")     aggr+=( "-c" ) ;;
    ("--colour="*|"--color="*) aggr+=( "-c" "${arg#*=}" ) ;;
    (*)  aggr+=( "$arg" ) ;;
   esac
 done

 print-args "print" "$@"

 eval set -- "${aggr[@]}"
 print-args "eval" "$@"

 set -- "${aggr[@]}"
 print-args "set" "$@"

 local OPTIND OPTARG
 local shortopts="C:"
 while getopts "$shortopts" arg; do
   case $arg in
    ("c") context="$OPTARG" ;;
    (*) break ;;
   esac
 done
 shift $(( OPTIND - 1 ))
}

但我想知道使用是否set -- "${aggr[@]}"正确。

或者以下(使用eval)更合适?

eval set -- "${aggr[@]}"

我进行了如下所示的测试。使用 eval 时,字符串“Gunga Din”被拆分,而使用 时set -- "${aggr[@]}",它被正确解析为单个字符串。

getopts_test -f -g 130 --colour="170 20" "Gunga Din"

print: $@:
1. |-f|
2. |-g|
3. |130|
4. |--colour=170 20|
5. |Gunga Din|

eval: $@:
1. |-f|
2. |-g|
3. |130|
4. |-c|
5. |170|
6. |20|
7. |Gunga|
8. |Din|

set: $@:
1. |-f|
2. |-g|
3. |130|
4. |-c|
5. |170 20|
6. |Gunga Din|

然后我运行了另一个使用非 GNU 的函数getopt

getopt_test ()
{
 shortopts="Vuhv::H::w::e::n::l::C:"
 shortopts="${shortopts}bgcrmo"
 longopts="version,usage,help,verbosity::"
 longopts="${longopts},heading::,warning::,error::"
 longopts="${longopts},blu,grn,cyn,red,mgn,org"
 
 opts=$( getopt -o "$shortopts" -l "$longopts" -n "${0##*/}" -- "$@" )

 print-args "\$@:" "$@"
 print-args "opts:" "$opts"

 set -- "$opts"
 print-args "set -- \"$opts\"" "$@"

 eval set -- "$opts"
 print-args "eval set -- \"$opts\"" "$@"

}

这导致了以下结果

getopt_test --warning=3 "foo'bar" "Gunga Din"

$@:
1. |--warning=3|
2. |foo'bar|
3. |Gunga Din|

opts:
1. | --warning '3' -- 'foo'\''bar' 'Gunga Din'|

set -- "$opts"
1. | --warning '3' -- 'foo'\''bar' 'Gunga Din'|

eval set -- "$opts"
1. |--warning|
2. |3|
3. |--|
4. |foo'bar|
5. |Gunga Din|

如图所示,getopt 的结果是一个带有重新排列的位置参数的条目。这表明需要将字符串eval set -- "$opts"中的位置参数拆分opts为五个条目以进行选项解析和处理。

答案1

其想法是预处理参数并将每个参数更改--context为可以处理的参数-Cgetopts我想这会起作用,但请注意,GNU 风格的长选项也可以采用 format 的参数--context=foobar,而您的构造在这里不支持这一点。用户需要知道这里的这个特定工具需要--context foobar两个不同的参数。或者您需要使预处理更加复杂。

您可能还想检查以 开头的所有参数--,否则,例如,输入错误将按原样进行,并且您会收到有关未知选项的投诉--cotnextgetopts(或更糟糕的是,将启用错误的选项。)

但我想知道使用是否set -- "${aggr[@]}"正确。

或者以下(使用 eval)更合适?

set -- "${aggr[@]}"将数组的元素扩展为不同的单词,然后将这些单词分配给位置参数。每个数组元素将恰好成为一个位置参数,无需更改。

eval set -- "${aggr[@]}"将展开数组的所有元素,然后用空格将它们连接在一起,在前面加上set --并将结果作为 shell 命令进行计算。也就是说,如果您有数组元素abc def, $(date >&2), ghi'jkl,则命令将是

set -- abc def $(date >&2) ghi'jkl 

abc这将以和作为两个不同的参数结束def,并且它将把日期打印到 stderr,除了单独的单引号会导致语法错误。

eval如果您有一些旨在生成为 shell 输入引用的输出的东西,则使用它是合适的。


如果您使用的是 Linux(并且不关心可移植性),您可以执行 roaima 在评论中建议的操作,并使用 util-linux 版本getopt(不带s)。它也支持长选项,有答案展示了如何使用它getopt、getopts 或手动解析 - 当我想同时支持短选项和长选项时使用什么?并在这个答案并且我的回答在这里

顺便说一句,这样getopt,你use eval,因为作为命令,它仅限于生成单个字符串作为输出,而不是像数组那样的列表,因此它使用 shell 引用来解决该问题。

答案2

您可以--foo使用内置函数解析 -style 长选项,getopts方法是将参数添加-为短选项,并将参数传递给 optstring,然后从 中检索实际的长选项$OPTARG。简单的例子:

while getopts :sc:-: o; do
    case $o in
    :) echo >&2 "option -$OPTARG needs an argument"; continue;;
    '?') echo >&2 "unknown option -$OPTARG"; continue;;
    -) o=${OPTARG%%=*}; OPTARG=${OPTARG#"$o"}; OPTARG=${OPTARG#=};;
    esac
    echo "OPT $o=$OPTARG"
done
shift "$((OPTIND - 1))"
echo "ARGS $*"

然后您可以将其用作script -c fooscript --context=foo

如果您还希望像短选项一样验证长选项,并且还接受缩写形式,那么您需要更复杂的东西。过度设计这样一个糟糕的 shell 脚本并不明智,但如果你想要一个例子,这里是:

short_opts=sc:
long_opts=silent/ch/context:/check/co   # those who take an arg END with :

# override via command line for testing purposes
# if [ "$#" -ge 2 ]; then
#   short_opts=$1; long_opts=$2; shift 2
# fi

while getopts ":$short_opts-:" o; do
    case $o in
    :) echo >&2 "option -$OPTARG needs an argument" ;continue;;
    '?') echo >&2 "bad option -$OPTARG" ;continue;;
    -)  o=${OPTARG%%=*}; OPTARG=${OPTARG#"$o"}; lo=/$long_opts/
        case $lo in
        *"/$o"[!/:]*"/$o"[!/:]*) echo >&2 "ambiguous option --$o"; continue;;
        *"/$o"[:/]*) ;;
        *) o=$o${lo#*"/$o"}; o=${o%%[/:]*} ;;
        esac
        case $lo in
        *"/$o/"*) OPTARG= ;;
        *"/$o:/"*)
            case $OPTARG in
            '='*)   OPTARG=${OPTARG#=};;
            *)  eval "OPTARG=\$$OPTIND"
                if [ "$OPTIND" -le "$#" ] && [ "$OPTARG" != -- ]; then
                    OPTIND=$((OPTIND + 1))
                else
                    echo >&2 "option --$o needs an argument"; continue
                fi;;
            esac;;
        *) echo >&2 "unknown option --$o"; continue;;
        esac
    esac
    echo "OPT $o=$OPTARG"
done
shift "$((OPTIND - 1))"
echo "ARGS $*"

然后

$ ./script --context=33
OPT context=33
$ ./script --con=33
OPT context=33
$ ./script --co
OPT co=
$ ./script --context
option --context needs an argument

相关内容