bash getopts, solo opciones cortas, todos requieren valores, validación propia

8

Estoy tratando de construir un script de shell que acepte varias opciones y getoptsparezca una buena solución, ya que puede manejar el orden variable de las opciones y argumentos (¡creo!).

Solo usaré opciones cortas y cada opción corta requerirá un valor correspondiente, por ejemplo: ./command.sh -a arga -g argg -b argbpero me gustaría permitir que las opciones se ingresen en un orden no específico, como es la forma en que la mayoría de la gente está acostumbrada a trabajar con comandos de shell .

El otro punto es que me gustaría hacer mi propia verificación de los valores de argumento de opción, idealmente dentro de las casedeclaraciones. La razón de esto es que mi prueba de :)en mi casedeclaración ha arrojado resultados inconsistentes (probablemente por falta de comprensión de mi parte).
Por ejemplo:

#!/bin/bash
OPTIND=1 # Reset if getopts used previously
if (($# == 0)); then
        echo "Usage"
        exit 2
fi
while getopts ":h:u:p:d:" opt; do
        case "$opt" in

                h)
                        MYSQL_HOST=$OPTARG
                        ;;
                u)
                        MYSQL_USER=$OPTARG
                        ;;
                p)
                        MYSQL_PASS=$OPTARG
                        ;;
                d)
                        BACKUP_DIR=$OPTARG
                        ;;
                \?)
                        echo "Invalid option: -$OPTARG" >&2
                        exit 2;;
                :)
                       echo "Option -$OPTARG requires an argument" >&2
                       exit 2;;
        esac
done
shift $((OPTIND-1))
echo "MYSQL_HOST='$MYSQL_HOST'  MYSQL_USER='$MYSQL_USER'  MYSQL_PASS='$MYSQL_PASS'  BACKUP_DIR='$BACKUP_DIR' Additionals: $@"

Estaba fallando en casos como este ... ./command.sh -d -h
Cuando quiero que marque -d como que requiere un argumento, pero obtengo el valor de lo -d=-hcual no es lo que necesito.

Así que pensé que sería más fácil ejecutar mi propia validación dentro de las declaraciones de caso para garantizar que cada opción se establezca y se configure solo una vez.

Estoy tratando de hacer lo siguiente pero mis if [ ! "$MYSQL_HOST" ]; thenbloques no se activan.

OPTIND=1 # Reset if getopts used previously

if (($# == 0)); then
        echo "Usage"
        exit 2
fi

while getopts ":h:u:p:d:" opt; do
        case "$opt" in

                h)
                        MYSQL_HOST=$OPTARG
                        if [ ! "$MYSQL_HOST" ]; then
                                echo "host not set"
                                exit 2
                        fi
                        ;;
                u)
                        MYSQL_USER=$OPTARG
                        if [ ! "$MYSQL_USER" ]; then
                                echo "username not set"
                                exit 2
                        fi
                        ;;
                p)
                        MYSQL_PASS=$OPTARG
                        if [ ! "$MYSQL_PASS" ]; then
                                echo "password not set"
                                exit 2
                        fi
                        ;;
                d)
                        BACKUP_DIR=$OPTARG
                        if [ ! "$BACKUP_DIR" ]; then
                                echo "backup dir not set"
                                exit 2
                        fi
                        ;;
                \?)
                        echo "Invalid option: -$OPTARG" >&2
                        exit 2;;
                #:)
                #       echo "Option -$opt requires an argument" >&2
                #       exit 2;;
        esac
done
shift $((OPTIND-1))

echo "MYSQL_HOST='$MYSQL_HOST'  MYSQL_USER='$MYSQL_USER'  MYSQL_PASS='$MYSQL_PASS'  BACKUP_DIR='$BACKUP_DIR' Additionals: $@"

¿Hay alguna razón por la que no puedo verificar si una OPTARGtiene longitud cero desde dentro getopts ... while ... case?

¿Cuál es la mejor manera de ejecutar mi propia validación de argumentos getoptsen un caso en el que no quiero confiar en el :). Realizar la validación de mi argumento fuera de while ... case ... esac?
Entonces podría terminar con valores de argumento de -detc. y no detectar una opción faltante.

Batfastad
fuente

Respuestas:

4

Cuando llamas a tu segundo script (lo guardé como getoptit) con:

getoptit -d -h

Esto imprimirá:

MYSQL_HOST=''  MYSQL_USER=''  MYSQL_PASS=''  BACKUP_DIR='-h' Additionals: 

Entonces BACKUP_DIR está configurado, y está probando if [ ! "$BACKUP_DIR" ]; thensi no está configurado, por lo que es normal que el código dentro de él no se active.

Si desea probar si cada opción está configurada una vez, debe hacerlo antes de realizar la asignación del valor $ OPTARG. Y probablemente también debería verificar si $ OPTARG comienza con un '-'(para el -d -herror) antes de asignar:

...
            d)
                    if [ ! -z "$BACKUP_DIR" ]; then
                            echo "backup dir already set"
                            exit 2
                    fi
                    if [ z"${OPTARG:0:1}" == "z-" ]; then
                            echo "backup dir starts with option string"
                            exit 2
                    fi
                    BACKUP_DIR=$OPTARG
                    ;;
...
Anthon
fuente
Excelente explicación Tiene mucho sentido una vez que pensé en el hecho de que en realidad hay un whilebucle sobre las opciones. Este es el método que utilicé, ya que aunque es detallado en comparación con otros, está muy claro lo que está sucediendo en el script. Y la mantenibilidad por otros es importante en este caso.
Batfastad
3

Dado que las otras respuestas en realidad no responden tanto a su pregunta: este es el comportamiento esperado y se basa en cómo se interpreta POSIX :

Cuando la opción requiere un argumento de opción, la utilidad getopts la colocará en la variable de shell OPTARG. Si no se encontró ninguna opción, o si la opción que se encontró no tiene un argumento de opción, OPTARG no se establecerá).

Esto es todo lo que se está diciendo, y para resumir (no tanto) la historia: getoptsno sabe mágicamente que el argumento perfectamente válido que ve para la opción que necesita para tener un argumento no es en realidad un argumento de opción, sino otra opción . Nada le impide tener -hun argumento válido para la -dopción, lo que en la práctica significa que getoptssolo arrojará un error si su opción que requiere un argumento ocupa el último lugar en la línea de comando, por ejemplo:

test.sh:

#!/bin/sh

while getopts ":xy:" o; do
    case "${o}" in
        :) echo "${OPTARG} requires an argument"; exit 1;
    esac
done

Ejemplo:

$ ./test.sh -y -x
$

$ ./test.sh -x -y
y requires an argument

La razón por la cual su segundo enfoque no funciona es porque el análisis que getoptssigue siendo el mismo, por lo que una vez que está dentro del bucle, el "daño" ya está hecho.

Si absolutamente quiere prohibir esto, entonces, como señaló Benubird, tendrá que verificar sus argumentos de opción usted mismo y lanzar un error si equivalen a una opción válida.

Adrian Frühwirth
fuente
2

Continuaría usando, getoptspero haría algunas comprobaciones adicionales después de: (no probado)

errs=0
declare -A option=(
    [MYSQL_HOST]="-h"
    [MYSQL_USER]="-u"
    [MYSQL_PASS]="-p"
    [BACKUP_DIR]="-d" 
)
for var in "${!option[@]}"; do
    if [[ -z "${!var}" ]]; then
        echo "error: specify a value for $var with ${option[var]}"
        ((errs++))
    fi
done
((errs > 0)) && exit 1

requiere bash versión 4

Glenn Jackman
fuente
1

El getopt incorporado no shell de Gnu funciona menos para usted, pero permite una mayor flexibilidad a medida que determina qué sucede después de que se encuentra una bandera, incluido el desplazamiento de los argumentos usted mismo.

Encontré un artículo que compara getopts y getopt que probablemente será útil, ya que es difícil entender el manual (al menos antes del café).

msw
fuente
1

Primero, deberías estar usando if [ -z "$MYSQL_USER" ].

En segundo lugar, no hay necesidad de la asignación if [ -z "$OPTARG" ], funcionará bien.

En tercer lugar, sospecho que lo que realmente quieres es if [ ${#OPTARG} = 0 ]. $ {# x} es una cosa bash que devuelve la longitud de la cadena $ x (vea más aquí ).

En cuarto lugar, si está haciendo su propia validación, recomendaría getopt en lugar de getopts, ya que proporciona mucha más flexibilidad.

Finalmente, para responder a su primera pregunta, puede detectar cuándo se pasa una bandera como argumento colocando una lista de banderas en la parte superior, como esta:

args=( -h -u -p -d )

Y luego verificando la opción en la que desea verificar si el argumento proporcionado es una opción, algo como esto:

d)
    BACKUP_DIR=$OPTARG
    if [ "$(echo ${args[@]/$OPTARG/})" = "$(echo ${args[@]})" ]
    then
        echo "Argument is an option! Error!"
    fi

No es una respuesta perfecta, ¡pero funciona! Su problema es que "-h" es un argumento perfectamente válido, y no le da forma al shell de saber que solo está buscando parámetros que no sean también indicadores válidos.

Benubird
fuente