Lidando com opções longas com getopts

Lidando com opções longas com getopts

Estou analisando opções, getoptsmas também gostaria de lidar com opções longas.

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

Mas me pergunto se o uso de set -- "${aggr[@]}"está correto.

Ou o seguinte (usando eval) é mais apropriado?

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

Eu realizei um teste mostrado abaixo. Com eval, a string "Gunga Din" é dividida, enquanto com set -- "${aggr[@]}", ela está sendo analisada corretamente como uma única string.

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|

Então executei outra função que usa o não-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\"" "$@"

}

Isto resultou no seguinte

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|

Conforme mostrado, o resultado de getopt é uma entrada única com argumentos posicionais reorganizados. Isso mostra a necessidade de eval set -- "$opts"dividir os argumentos posicionais na optsstring em cinco entradas para análise e processamento de opções.

Responder1

A ideia é pré-processar os argumentos e alterar cada um --contextpara -Cque getoptspossa então processar? Suponho que funcionaria, mas observe que as opções longas no estilo GNU também podem receber argumentos no format --context=foobar, e sua construção aqui não suporta isso. O usuário precisaria saber que esta ferramenta específica aqui requer --context foobardois argumentos distintos. Ou você precisaria tornar o pré-processamento mais complexo.

Você também pode querer verificar todos os argumentos que começam com --, caso contrário, por exemplo, um erro digitado --cotnextiria para getoptso estado em que se encontra e você receberia reclamações sobre opções desconhecidas. (Ou pior, opções erradas seriam habilitadas.)

Mas me pergunto se o uso de set -- "${aggr[@]}"está correto.

Ou o seguinte (usando eval) é mais apropriado?

set -- "${aggr[@]}"expande os elementos da matriz para palavras distintas e, em seguida, atribui essas palavras aos parâmetros posicionais. Cada elemento do array se tornará exatamente um parâmetro posicional, sem alterações.

eval set -- "${aggr[@]}"expandiria todos os elementos da matriz, depois os uniria com espaços, acrescentaria set --e avaliaria o resultado como um comando shell. Ou seja, se você tiver os elementos do array abc def, $(date >&2), ghi'jkl, o comando seria

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

que terminaria com abce defcomo dois parâmetros distintos e imprimiria a data em stderr, exceto que a aspa simples causaria um erro de sintaxe.

Usar evalseria apropriado se você tiver algo projetado para produzir uma saída citada para entrada do shell.


Se você estiver no Linux (e não se importa com portabilidade), você pode fazer o que roaima sugeriu nos comentários e usar a versão util-linux getopt(sem o s). Ele também suporta opções longas, há respostas mostrando como usá-lo emgetopt, getopts ou análise manual - o que usar quando desejo oferecer suporte a opções curtas e longas?e emesta resposta SOe tambémminha resposta aqui.

Aliás, com isso getopt, vocêseriause eval, já que como um comando, ele se limita a produzir apenas uma única string como saída, e não uma lista como uma matriz, por isso usa aspas do shell para solucionar o problema.

Responder2

Você pode analisar --fooopções longas no estilo - com o getoptsbuiltin adicionando -como uma opção curta, levando um argumento para a optstring e recuperando a opção longa real de $OPTARG. Exemplo simples:

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

que você pode usar como script -c fooou script --context=foo.

Se você também deseja que as opções longas sejam validadas assim como as curtas, e também aceite formas abreviadas, você precisa de algo mais complexo. Não há muita sabedoria em projetar demais um script de shell ruim como esse, mas se você quiser um exemplo, aqui está:

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

então

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

informação relacionada