¿Cómo analizo argumentos opcionales en un script bash si no se da ninguna orden?

13

Estoy confundido sobre cómo incluir argumentos / indicadores opcionales al escribir un script bash para el siguiente programa:

El programa requiere dos argumentos:

run_program --flag1 <value> --flag2 <value>

Sin embargo, hay varias banderas opcionales:

run_program --flag1 <value> --flag2 <value> --optflag1 <value> --optflag2 <value> --optflag3 <value> --optflag4 <value> --optflag5 <value> 

Me gustaría ejecutar el script bash de modo que tome argumentos del usuario. Si los usuarios solo ingresan dos argumentos en orden, entonces sería:

#!/bin/sh

run_program --flag1 $1 --flag2 $2

Pero, ¿qué pasa si se incluye alguno de los argumentos opcionales? Yo pensaría que sería

if [ --optflag1 "$3" ]; then
    run_program --flag1 $1 --flag2 $2 --optflag1 $3
fi

Pero, ¿qué pasa si se dan $ 4 pero no $ 3?

ShanZhengYang
fuente
getoptses lo que quieres Sin eso, podría usar un bucle con una instrucción switch para detectar cada bandera, opcional o no.
orion
@orion Con getopts, ¿necesitaría especificar cada combinación de argumentos? 3 y 4, 3 y 5, 3 y 4 y 5, etc.
ShanZhengYang
No, solo los configura si los obtiene, de lo contrario informa que no se encontró, por lo que básicamente "obtiene" cada opción, en cualquier orden, y las especifica en cualquier orden, si es que lo hace. Pero solo lea la página de manual de bash, está todo ahí.
Orion
@orion Lo siento, pero aún no lo entiendo getopts. Digamos que fuerzo a los usuarios a ejecutar el script con todos los argumentos: run_program.sh VAL VAL FALSE FALSE FALSE FALSE FALSEque ejecuta el programa como program --flag1 VAL --flag2 VAL. Si ejecutó run_program.sh VAL VAL FALSE 10 FALSE FALSE FALSE, el programa se ejecutará como program --flag1 VAL --flag2 VAL --optflag2 10. ¿Cómo puedes conseguir ese comportamiento getopts?
ShanZhengYang

Respuestas:

25

Este artículo muestra dos formas diferentes shifty getopts(y analiza las ventajas y desventajas de los dos enfoques).

Con shiftsu secuencia de comandos observa $1, decide qué acción tomar y luego ejecuta shift, se mueve $2a $1, $3a $2, etc.

Por ejemplo:

while :; do
    case $1 in
        -a|--flag1) flag1="SET"            
        ;;
        -b|--flag2) flag2="SET"            
        ;;
        -c|--optflag1) optflag1="SET"            
        ;;
        -d|--optflag2) optflag2="SET"            
        ;;
        -e|--optflag3) optflag3="SET"            
        ;;
        *) break
    esac
    shift
done

Con getoptsusted define las opciones (cortas) en la whileexpresión:

while getopts abcde opt; do
    case $opt in
        a) flag1="SET"
        ;;
        b) flag2="SET"
        ;;
        c) optflag1="SET"
        ;;
        d) optflag2="SET"
        ;;
        e) optflag3="SET"
        ;;
    esac
done

Obviamente, estos son solo fragmentos de código, y he omitido la validación, verificando que los argumentos obligatorios flag1 y flag2 estén configurados, etc.

El enfoque que utiliza es, en cierta medida, una cuestión de gustos: qué tan portátil desea que sea su script, si puede vivir solo con opciones cortas (POSIX) o si desea opciones largas (GNU), etc.

John N
fuente
Por lo general, lo hago en while (( $# ))lugar de hacerlo while :;y, a menudo, abandono con un error en el *caso
Jasen
Esto responde al título pero no a la pregunta.
Jasen
@Jasen Miré esto anoche y no pude ver por qué. Esta mañana es mucho más claro (y ahora también he visto la respuesta de Orion). Eliminaré esta respuesta más tarde hoy (quería reconocer su comentario primero, y esta parecía la forma más fácil de hacerlo).
John N
no hay problema, sigue siendo una buena respuesta, solo la pregunta lo esquivó.
Jasen
1
Nota: en el caso de set -o nounsetla primera solución, se producirá un error si no se proporciona ningún parámetro. Arreglo:case ${1:-} in
Raphael
3

usa una matriz.

#!/bin/bash

args=( --flag1 "$1" --flag2 "$2" )
[  "x$3" = xFALSE ] ||  args+=( --optflag1 "$3" )
[  "x$4" = xFALSE ] ||  args+=( --optflag2 "$4" )
[  "x$5" = xFALSE ] ||  args+=( --optflag3 "$5" )
[  "x$6" = xFALSE ] ||  args+=( --optflag4 "$6" )
[  "x$7" = xFALSE ] ||  args+=( --optflag5 "$7" )

program_name "${args[@]}"

esto manejará argumentos con espacios en ellos correctamente.

[editar] Estaba usando la sintaxis más o menos equivalente, args=( "${args[@]}" --optflag1 "$3" )pero G-Man sugirió una mejor manera.

Jasen
fuente
1
Puedes simplificar esto un poco diciendo args+=( --optflag1 "$3" ). Es posible que desee ver mi respuesta a nuestro trabajo de referencia, Implicaciones de seguridad de no citar una variable en shells bash / POSIX , donde analizo esta técnica (de construir una línea de comando en una matriz agregando condicionalmente argumentos opcionales).
G-Man dice 'Reincorporar a Monica'
@ G-Man Gracias, no había visto eso en la documentación, entendiendo [@]que tenía una herramienta suficiente para resolver mi problema.
Jasen
1

En un script de shell, los argumentos son "$ 1", "$ 2", "$ 3", etc. El número de argumentos es $ #.
Si su script no reconoce las opciones, puede omitir la detección de opciones y tratar todos los argumentos como operandos.
Para reconocer las opciones, use los getopts incorporados

Michael Durrant
fuente
0

Si sus opciones de entrada son posicionales (sabe en qué lugares se encuentran) y no se especifican con banderas, entonces lo que desea es construir la línea de comando. Simplemente prepare los argumentos de comando para todos ellos:

FLAG1="--flag1 $1"
FLAG2="--flag2 $2"
OPTFLAG1=""
OPTFLAG2=""
OPTFLAG3=""
if [ xFALSE != x"$3" ]; then
   OPTFLAG1="--optflag1 $3"
fi
if [ xFALSE != x"$4" ]; then
   OPTFLAG2="--optflag2 $4"
fi
#the same for other flags

#now just run the program
runprogram $FLAG1 $FLAG2 $OPTFLAG1 $OPTFLAG2 $OPTFLAG3

Si no se especifican los parámetros, las cadenas correspondientes están vacías y se expanden en nada. Tenga en cuenta que en la última línea, no hay comillas. Esto se debe a que desea que el shell divida los parámetros en palabras (para dar --flag1y $1como argumentos separados a su programa). Por supuesto, esto saldrá mal si sus parámetros originales contienen espacios. Si usted es el que ejecuta esto, puede dejarlo, pero si se trata de un script general, puede tener un comportamiento inesperado si el usuario ingresa algo con espacios. Para manejar eso, deberás hacer que el código sea un poco más feo.

El xprefijo en el []ensayo es allí en caso de $3o $4está vacía. En ese caso, bash se expandiría [ FALSE != $3 ]en [ FALSE != ]un error de sintaxis, por lo que hay otro personaje arbitrario para evitar esto. Esta es una forma muy común, lo verá en muchos scripts.

Lo configuré OPTFLAG1y el resto de ellos al ""principio solo para estar seguro (en caso de que se establecieran algo antes), pero si en realidad no se declararon en el entorno, entonces no es necesario que lo haga estrictamente.

Un par de comentarios adicionales:

  • En realidad, podría recibir los parámetros de la misma manera que lo runprogramhace: con banderas. De eso es de lo John Nque estamos hablando. Ahí es donde se getoptsvuelve útil.
  • Usar FALSE para ese propósito es un poco poco estándar y bastante largo. Por lo general, uno usaría un solo carácter (posiblemente -) para señalar un valor vacío, o simplemente pasar una cadena vacía, como "", si ese no es un valor válido del parámetro. La cadena vacía también hace que la prueba sea más corta, solo utilícela if [ -n "$3" ].
  • Si solo hay un indicador opcional, o si son dependientes, por lo que nunca puede tener OPTFLAG2, pero no OPTFLAG1, simplemente puede omitir los que no desea configurar. Si usa ""parámetros vacíos, como se sugirió anteriormente, puede omitir todos los vacíos finales de todos modos.

Este método es más o menos la forma en que las opciones se pasan a los compiladores en Makefiles.

Y de nuevo, si las entradas pueden contener espacios, se pone feo.

Orión
fuente
No, no desea que el shell divida los parámetros en palabras. Siempre debe citar todas las referencias a las variables de shell a menos que tenga una buena razón para no hacerlo y esté seguro de saber lo que está haciendo. Vea las implicaciones de seguridad de no citar una variable en los shells bash / POSIX , en caso de que no esté familiarizado con ella, y desplácese hacia abajo hasta mi respuesta , donde abordo precisamente este problema (con la misma técnica que usa Jasen ).
G-Man dice 'Reincorporar a Monica'