Publi

Procesar argumentos de entrada en nuestros shell scripts con getopt

mixer_4

Hace unos días analizamos cómo tratar los argumentos de entrada desde un shell script en Bash de una manera sencilla. Aunque, cuando la cosa se complica, debemos utilizar herramientas algo más avanzadas. Tal y como hicimos con getopt para C [parte 1, parte 2], vamos a hacer lo mismo en un shell script.

Aunque aquí tenemos dos posibilidades, que hacen prácticamente lo mismo getopt y getopts. Vamos a verlas detenidamente.

El programa de ejemplo

En este ejemplo, vamos a imaginar que el script hace una copia de seguridad. Dicha copia de seguridad, tendrá varios argumentos de entrada:

  • -v : Escribe en pantalla todo lo que está haciendo en cada momento
  • -l [archivo] : Escribe un log en el archivo
  • -z : Comprime la copia de seguridad
  • -c : Copia remotamente el backup a un servidor
  • -h : Servidor donde vamos a copiar el backup

Además, debemos decirle los directorios que vamos a copiar. Al menos uno será obligatorio. Además, si se especifica -c, será también obligatorio especificar -h.

getopts, terminado en s

Es el más sencillo, básicamente el uso de getopts lo haremos para no complicarnos la vida cuando un argumento tiene parámetro o no, ya que no tenemos que hacer shift como pasaba en la parte 1 de este tutorial.
Veamos un fragmento de código:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/bin/bash

VERBOSE=0
COPIAREMOTA=0

while getopts vzcl:h: option; do
  case $option in
    v)
      VERBOSE=1
      ;;
    z)
      COMPRIMIDO=1
      ;;
    c)
      COPIAREMOTA=1
      ;;
    l)
      LOGFILE=$OPTARG
      ;;
    h)
      HOSTCOPY=$OPTARG
      ;;
  esac
done

if [ $COPIAREMOTA -eq 1 ] && [ -z "$HOSTCOPY" ]
then
    echo "Si especifica copia remota -c DEBE especificar también host (-h)"
    exit 1
fi

shift $(( OPTIND - 1 ));
if [ $# -le 0 ]
then
    echo "Por favor, especifique los directorios a copiar"
    exit 1
fi

echo "VERBOSE: $VERBOSE"
echo "COMPRIMIDO: $COMPRIMIDO"
echo "LOGFILE: $LOGFILE"
echo "COPIA REMOTA: $COPIAREMOTA"
echo "HOSTCOPY: $HOSTCOPY"
echo "Directorios a copiar: $@"

El formato de utilización de getopts es:

getopts FORMATO VARIABLE [ARGS]

donde,

  • FORMATO será una cadena donde especificaremos los argumentos que serán flags, es decir, los que no necesitan nada más, y los que necesitan un segundo argumento para definir su funcionalidad. Estos últimos, vendrán acompañados de dos puntos (:).
  • VARIABLE será la variable de nuestro shell script donde almacenaremos el argumento que está analizándose ahora mismo
  • ARGS, si se especifica, indica de dónde se van a sacar los argumentos. Por defecto son los parámetros posicionales $1, $2, $3…

Lo bueno de utilizar getopts y no utilizar los métodos de la primera parte del tutorial es, además, que podemos combinar varios argumentos de entrada en un mismo argumento físico. Dicho de otra forma, no tenemos por qué llamar al programa así:

./test -v -z -c -h SERVIDOR directorio1, directorio2, …, directorioN

podemos hacerlo así:

./test vzch SERVIDOR directorio1, directorio2, …, directorioN

y esto lo hace mucho más amigable al usuario.

El gran problema de getopts, es que si los directorios a copiar los ponemos al principio, o incluso en medio, esos argumentos no se parsearán, y tal vez el comportamiento del programa no sea el deseado.

getopt, sin s, el más flexible

Pero claro, si queremos que nuestro programa acepte argumentos largos (–verbose para -v ; –zip para -z ; –copy para -c ; –log para -l ; –host para -h ) y, además, no queremos tener el problema de que el resto de argumentos se especifiquen en cualquier lugar (al principio, en medio o al final), la solución es getopt, sin s. Con esta utilidad, lo malo es que tenemos que andarnos con los shift de nuevo, pero yo creo que la flexibilidad que nos brinda compensa ese pequeño dolor.

Veamos un ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#!/bin/bash

ARGS=$(getopt -q -o "vzcl:h:" -l "verbose,zip,copy,log:,host:" -n "argumentos" -- "$@");

if [ $? -ne 0 ];
then
    echo "Ha habido un error al parsear los argumentos"
    exit 1
fi

eval set -- "$ARGS";

VERBOSE=0
COPIAREMOTA=0

while [ $# -gt 0 ]; do
  case "$1" in
    -v|--verbose)
      VERBOSE=1
      ;;
    -z|--zip)
      COMPRIMIDO=1
      ;;
    -c|--copy)
      COPIAREMOTA=1
      ;;
    -l|--log)
      LOGFILE="$2"
      shift;
      ;;
    -h|--host)
      HOSTCOPY="$2"
      shift;
      ;;
    --)
      shift;
      break;
      ;;
  esac
  shift
done

if [ $COPIAREMOTA -eq 1 ] && [ -z "$HOSTCOPY" ]
then
    echo "Si especifica copia remota -c DEBE especificar también host (-h)"
    exit 1
fi

if [ $# -le 0 ]
then
    echo "Por favor, especifique los directorios a copiar"
    exit 1
fi

echo "VERBOSE: $VERBOSE"
echo "COMPRIMIDO: $COMPRIMIDO"
echo "LOGFILE: $LOGFILE"
echo "COPIA REMOTA: $COPIAREMOTA"
echo "HOSTCOPY: $HOSTCOPY"
echo "Directorios a copiar: $@"

Viendo esto, getopt, en realidad lo que hace es reordenar los argumentos, es decir, los argumentos que estén juntos (por ejemplo -cvzl) los separa (quedando -c -v -z -l), para que los podamos analizar mejor. Además, los argumentos que no tengan ninguna clave primero, vamos los que quedan sueltos, los pone al final, después de un último argumento “–” (dos guiones, WordPress me pone un guión sólo y me da cosa quitarlo)

Entonces, podemos analizar los argumentos desde la salida de getopt, no sin antes pasar esa variable a los argumentos posicionales (con eval set — “$ARGS”). Cuando estemos analizando, es importante marcar un fin al while, como en el ejemplo, no encontrar más argumentos de entrada, aunque si encontramos “–” hacemos un break y salimos, pero nunca se sabe lo que puede pasar en las salidas, o si un IFS se nos va de madre…

O si preferimos, podemos hacerlo con un for sobre los mismos argumentos, y olvidarnos del case, aunque también tiene sus contras, si queremos extraer los archivos sueltos, debemos hacerlo de otra forma también:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#!/bin/bash

ARGX=($(getopt -q -o "vzcl:h:" -l "verbose,zip,copy,log:,host:" -n "argumentos" -- "$@"));

if [ $? -ne 0 ];
then
    echo "Ha habido un error al parsear los argumentos"
    exit 1
fi

VERBOSE=0
COPIAREMOTA=0

for (( arg=0; $arg<$# ; arg++ ))
do
  case "${ARGX[$arg]}" in
    -v|--verbose)
      VERBOSE=1
      ;;
    -z|--zip)
      COMPRIMIDO=1
      ;;
    -c|--copy)
      COPIAREMOTA=1
      ;;
    -l|--log)
      ((arg++))
      LOGFILE="${ARGX[$arg]}"
      ;;
    -h|--host)
      ((arg++))
      HOSTCOPY="${ARGX[$arg]}"
      ;;
    --)
      ULTIMO=$arg+1;
      break;
      ;;
  esac
done

if [ $COPIAREMOTA -eq 1 ] && [ -z "$HOSTCOPY" ]
then
    echo "Si especifica copia remota -c DEBE especificar también host (-h)"
    exit 1
fi

if [ $# -le 0 ]
then
    echo "Por favor, especifique los directorios a copiar"
    exit 1
fi

echo "VERBOSE: $VERBOSE"
echo "COMPRIMIDO: $COMPRIMIDO"
echo "LOGFILE: $LOGFILE"
echo "COPIA REMOTA: $COPIAREMOTA"
echo "HOSTCOPY: $HOSTCOPY"
echo "Directorios a copiar: "
for ((dir=$ULTIMO; $dir<$#; dir++))
do
    echo ${ARGX[dir]}
done

Extra, getopts en KornShell


En Twitter, Ingenieria.inversa() nos envía su versión para KornShell utilizando getopts. Aquí tenéis el código:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/bin/ksh
if [ $# == 9 ]
then
    exit 1
else
    for op in `echo "$@"`
    do
        if [[ $op == -* ]] && [[ $op != -[hmps] ]]
        then
            OPS="$OPS $op"
        fi
        export OPS
    done
    if [ -n "$OPS" ]
    then
        if [ ${#OPS} -gt 3 ]
        then
            printf "\n ERROR:\tLos paranetros $OPS no son validos.\n"
        else
            printf "\n ERROR:\tE1 paranetro $OPS no es valido.\n"
        fi
        uso
        exit 1
    else
        while getopts hm:p:s: flag
        do
            case $flag in
                h) echo "BlablabIa..." ;;
                m) VAR1="$2" ;;
                p)
                    case $4 in
                        "uno") VAR2="$4" ;;
                        "dos") VAR2="$4" ;;
                        "tres") VAR2="$4" ;;
                        *) exit 1 ;;
                    esac
                    ;;
                s) VAR3="$6" ;;
                *) exit 2 ;;
            esac
        done
        shift $(($OPTIND -1))
    fi
fi

Para los curiosos

Algo que podemos probar es que, estas herramientas no están limitadas a los argumentos de entrada al script. Pueden ser argumentos de entrada de una función de Bash, o incluso podremos especificar en una cadena de caracteres lo que queremos que getopt o getopts parsee.

Más info

Getopts tutorial
Command line options
Foto principal: freestocks.org

También podría interesarte....

Leave a Reply