Manejo de opciones largas con getopts

Manejo de opciones largas con getopts

Estoy analizando opciones getoptspero también me gustaría manejar opciones largas.

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

Pero me pregunto si el uso de set -- "${aggr[@]}"es correcto.

¿O es más apropiado lo siguiente (usar eval)?

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

He realizado una prueba que se muestra a continuación. Con eval, la cadena "Gunga Din" se divide, mientras que con set -- "${aggr[@]}", se analiza correctamente como una sola cadena.

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|

Luego ejecuté otra función que usa no 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\"" "$@"

}

Esto resultó en lo siguiente

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|

Como se muestra, el resultado de getopt es una entrada única con argumentos posicionales reorganizados. Esto muestra la necesidad de utilizar eval set -- "$opts"para dividir los argumentos posicionales de la optscadena en cinco entradas para el análisis y procesamiento de opciones.

Respuesta1

¿La idea es preprocesar los argumentos y cambiar cada uno --contextde -Clos cuales getoptsluego se pueden procesar? Supongo que eso funcionaría, pero tenga en cuenta que las opciones largas de estilo GNU también pueden tomar argumentos en el formato --context=foobary su construcción aquí no lo admite. El usuario necesitaría saber que esta herramienta en particular requiere --context foobardos argumentos distintos. O necesitarías hacer el preprocesamiento más complejo.

También es posible que desee verificar todos los argumentos que comienzan con --, ya que de lo contrario, por ejemplo, un error escrito --cotnextquedaría getoptstal cual y recibiría quejas sobre opciones desconocidas. (O peor aún, se habilitarían opciones incorrectas).

Pero me pregunto si el uso de set -- "${aggr[@]}"es correcto.

¿O es más apropiado lo siguiente (usando eval)?

set -- "${aggr[@]}"expande los elementos de la matriz a palabras distintas y luego asigna esas palabras a los parámetros posicionales. Cada elemento de la matriz se convertirá exactamente en un parámetro posicional, sin cambios.

eval set -- "${aggr[@]}"expandiría todos los elementos de la matriz, luego los uniría con espacios, antepondría set --y evaluaría el resultado como un comando de shell. Es decir, si tiene los elementos de la matriz ,,, abc defel comando sería$(date >&2)ghi'jkl

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

lo que terminaría con abcy defcomo dos parámetros distintos, e imprimiría la fecha en stderr, excepto que la única comilla simple provocará un error de sintaxis.

Usar evalsería apropiado si tiene algo diseñado para producir una salida citada para la entrada del shell.


Si está en Linux (y no le importa la portabilidad), puede hacer lo que roaima sugirió en los comentarios y usar la versión util-linux de getopt(sin s). También admite opciones largas, hay respuestas que muestran cómo usarlas engetopt, getopts o análisis manual: ¿qué usar cuando quiero admitir opciones cortas y largas?y enesta respuesta SOy tambiénmi respuesta aquí.

Por cierto, con eso getopt, túharíause eval, ya que como comando, se limita a producir una sola cadena como salida, no una lista como una matriz, por lo que utiliza comillas de shell para solucionar el problema.

Respuesta2

Puede analizar --fooopciones largas de estilo con la getoptsfunción incorporada agregando -como opción corta, tomando un argumento a la cadena de opciones y recuperando la opción larga real de $OPTARG. Ejemplo sencillo:

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 luego puedes usar como script -c fooo script --context=foo.

Si además quieres tener validadas las opciones largas al igual que las cortas, y además aceptar formularios abreviados, necesitas algo más complejo. No hay mucha sabiduría en sobre-diseñar un script de shell pobre como ese, pero si quieres un ejemplo, aquí lo tienes:

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

entonces

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

información relacionada