使用 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 

它最終會以abcdef作為兩個不同的參數,並且會將日期列印到 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

相關內容