Umgang mit langen Optionen mit getopts

Umgang mit langen Optionen mit getopts

Ich analysiere Optionen mit, getoptsmöchte aber auch lange Optionen verarbeiten.

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 ))
}

Ich frage mich jedoch, ob die Verwendung von set -- "${aggr[@]}"korrekt ist.

Oder ist Folgendes (mit eval) angemessener?

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

Ich habe einen unten gezeigten Test durchgeführt. Mit eval wird die Zeichenfolge „Gunga Din“ aufgeteilt, während set -- "${aggr[@]}"sie mit korrekt als einzelne Zeichenfolge analysiert wird.

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|

Dann habe ich eine andere Funktion ausgeführt, die das Nicht-GNU verwendet 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\"" "$@"

}

Daraus ergab sich folgendes

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|

Wie gezeigt ist das Ergebnis von getopt ein einzelner Eintrag mit neu angeordneten Positionsargumenten. Dies zeigt, dass eval set -- "$opts"die Positionsargumente in der optsZeichenfolge für die Optionenanalyse und -verarbeitung in fünf Einträge aufgeteilt werden müssen.

Antwort1

Die Idee besteht darin, die Argumente vorzuverarbeiten und jedes --contextin zu ändern -C, was getoptsdann verarbeitet werden kann? Ich nehme an, das würde funktionieren, aber beachten Sie, dass lange Optionen im GNU-Stil auch Argumente im Format annehmen können --context=foobar, und Ihre Konstruktion hier unterstützt das nicht. Der Benutzer müsste wissen, dass dieses spezielle Tool hier --context foobarzwei verschiedene Argumente erfordert. Oder Sie müssten die Vorverarbeitung komplexer gestalten.

Möglicherweise möchten Sie auch alle Argumente überprüfen, die mit beginnen --, da sonst beispielsweise ein Tippfehler --cotnextunverändert bleibt getoptsund Sie Beschwerden über unbekannte Optionen erhalten. (Oder schlimmer noch, es würden falsche Optionen aktiviert.)

Ich frage mich jedoch, ob die Verwendung von set -- "${aggr[@]}"korrekt ist.

Oder ist Folgendes (mit eval) angemessener?

set -- "${aggr[@]}"erweitert die Elemente des Arrays in einzelne Wörter und weist diese Wörter dann den Positionsparametern zu. Jedes Array-Element wird ohne Änderungen zu genau einem Positionsparameter.

eval set -- "${aggr[@]}"würde alle Elemente des Arrays erweitern, sie dann mit Leerzeichen zusammenfügen, voranstellen set --und das Ergebnis als Shell-Befehl auswerten. Das heißt, wenn Sie die Array-Elemente abc def, $(date >&2), haben ghi'jkl, wäre der Befehl

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

abcDies würde mit und als zwei unterschiedlichen Parametern enden defund das Datum würde auf stderr gedruckt, außer dass das einzelne einfache Anführungszeichen einen Syntaxfehler verursachen würde.

Die Verwendung evalist angemessen, wenn Sie über etwas verfügen, das für die Erzeugung einer Ausgabe in Anführungszeichen für die Shell-Eingabe konzipiert ist.


Wenn Sie Linux verwenden (und Portabilität nicht wichtig ist), können Sie das tun, was roaima in den Kommentaren vorgeschlagen hat, und die util-linux-Version von getopt(ohne s) verwenden. Es unterstützt auch lange Optionen, es gibt Antworten, die zeigen, wie man es ingetopt, getopts oder manuelles Parsen – was verwende ich, wenn ich sowohl kurze als auch lange Optionen unterstützen möchte?und indiese SO-Antwortund auchmeine Antwort hier.

Übrigens, getoptdamitwürdeverwenden eval, da es als Befehl darauf beschränkt ist, als Ausgabe nur einen einzelnen String zu produzieren und keine Liste wie ein Array. Daher werden Shell-Anführungszeichen verwendet, um das Problem zu umgehen.

Antwort2

Sie können --foolange Optionen im -Stil mit dem getoptsintegrierten Parser analysieren, indem Sie -als kurze Option ein Argument für die Optionszeichenfolge hinzufügen und die eigentliche lange Option von abrufen $OPTARG. Einfaches Beispiel:

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 $*"

die Sie dann entweder als script -c foooder verwenden können script --context=foo.

Wenn Sie möchten, dass die langen Optionen genauso wie die kurzen validiert werden und auch abgekürzte Formen akzeptiert werden, benötigen Sie etwas Komplexeres. Es ist nicht sehr sinnvoll, ein so schlechtes Shell-Skript zu überentwickeln, aber wenn Sie ein Beispiel möchten, hier ist es:

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 $*"

Dann

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

verwandte Informationen