Shell script argument parsing

Ha shell scriptet írunk, gyakran előfordulhat, hogy szeretnénk azt paraméterezhetővé tenni.

Az alapvető módszerem, hogy vannak bizonyos beállítások definiálva a fájl elején egy alapértelmezett értékkel, majd a paraméterektől függően változtatom ezek értékét:

#!/bin/sh

IS_FLAG_A_SET=0

for i in $@; do
    case $i in
        '-a')
            IS_FLAG_A_SET=1
            ;;
        *)
            echo "Usage $0 -a"
            exit 1
    esac
done

if [ $IS_FLAG_A_SET -eq 1 ]; then
    echo "FLAG A"
fi

Lekezeltük még azt is, hogy össze-vissza paramétereket ne engedjünk csak úgy bepasszintani, ekkor ugynis kiírjuk a használati utasítást és hibakóddal kilépünk.

Izgalmasabb a helyzet, ha nem csak flagekkel dolgozunk, hanem szeretnénk valamilyen értéket átadni egy opcióval, mondjuk egy fájlnevet. Ha tudjuk, hogy csak egy ilyen lehet az adott programban, akkor megtehetjük, hogy azt mondjuk, hogy legyen az utolsó paraméter mindig a fájlnév.

FILE_NAME=${@:-1}
echo $FILE_NAME

De ez egyáltalán nem dinamikus, hiszen bármikor lehet olyan igény, hogy több értéket szeretnénk átadni. Ekkor például elkezdhetünk úgy dolgozni a paramétereken, hogy ha például -f kapcsolót kaptuk, akkor a következő paraméter a fájlnévnek vesszük:

IS_FLAG_A_SET=0
IS_FLAG_B_SET=0

FILE_NAME=""
USER_NAME=""

while [ $# -gt 0 ]; do
    case $1 in
        '-a')
            IS_FLAG_A_SET=1
            ;;
        '-b')
            IS_FLAG_B_SET=1
            ;;
        '-f')
            shift
            FILE_NAME=$1
            ;;
        '-u')
            shift
            USER_NAME=$1
            ;;
        *)
            echo "Usage $0 -a [-b] [-f filename] [-u username]"
            exit 1
    esac

    shift
done

if [ $IS_FLAG_A_SET -eq 1 ]; then
    echo "FLAG A"
else
    echo "FLAG A is mandantory"
    exit 1
fi

if [ $IS_FLAG_B_SET -eq 1 ]; then
    echo "FLAG B"
fi

echo "FILE_NAME: $FILE_NAME"
echo "USER_NAME: $USER_NAME"

Így tetszőleges sorrendben adhatunk meg tetszőleges mennyiségű paramétert:

$ ./script.sh -f file.txt -a -u user
FLAG A
FILE_NAME: file.txt
USER_NAME: user

Működik meg minden, de biztos, hogy lehetne másképpen is, tehát ideje elrontani azt, ami működik.

Getopts

Habár egész sok esetet le tudunk kezelni a fenti (és ezernyi más) kóddal is, de azért mégsem teljesen kényelmes. Léteznek megoldások, amik megszabadítanak minket a shiftelésektől, amit elég könnyű elfelejteni. Az egyik ilyen ránk váró megoldás, amit én kedvelek a getopts.

Automatikusan kezeli a fenti problémát és még ennél egy kicsit többet is. A módosított változat egészen hasonlít a korábbi verzióhoz:

#!/bin/sh

IS_FLAG_A_SET=0
IS_FLAG_B_SET=0
FILE_NAME=""
USER_NAME=""

while getopts "f:u:ba" opt; do
    case $opt in
        'a')
            IS_FLAG_A_SET=1
            ;;
        'b')
            IS_FLAG_B_SET=1
            ;;
        'f')
            FILE_NAME=$OPTARG
            ;;
        'u')
            USER_NAME=$OPTARG
            ;;
        [?])
            echo "Usage $0 -a [-b] [-f filename] [-u username]"
            exit 1
    esac
done

if [ $IS_FLAG_A_SET -eq 1 ]; then
    echo "FLAG A"
else
    echo "FLAG A is mandantory"
    exit 1
fi

if [ $IS_FLAG_B_SET -eq 1 ]; then
    echo "FLAG B"
fi

echo "FILE_NAME: $FILE_NAME"
echo "USER_NAME: $USER_NAME"

A legfontosabb változás a while ciklusnál látható. A getopts első paramétere a paramétereket definiálja. Ha egy paraméter nevét : (kettőspont) követi, akkor hasonlóan a fenti scripthez, a paraméter utáni rész az adott paraméter értékeként lesz értelmezve.
A getopts második paramétere a változó nevét definiálja, amibe az éppen aktuális paraméter neve lesz eltárolva (így f, ha épen a -f-et értelmezi, a, ha a -a-t, és így tovább). Ezen kívül bevezeti még az _OPTARG_ és _OPTIND_ változókat. Az _OPTARG_-ban lesz eltárolva a `:`-tal jelzett paraméterekhez tartozó érték, így a beadott fájlnév és felhasználó név. Az _OPTIND_-ben az utoljára "elfogyasztott" paraméter indexét tárolja, ez a miénknél bonyolultabb paraméter kezelésnél jön jól.

Állítsuk a feje tetejére mindezt

A legextrémebb igényem eddig az volt, amikor szerettem volna minimalizálni a paraméterek használatát. A beállítások alapértelmezett értéke jó volt, ezért általában csak egy paraméterre volt szükségem, mégpedig a fájlnévre. ./script.sh -f file.txt. Ekkor már a -f is borzasztó feleslegesnek tűnik, szeretném, ha csak a fájlnevet kéne megadnom: ./script.sh file.txt.

Itt jön jól az OPTIND változó, amit a getopts állt be.

A while ciklus után írjuk a következőt:

if [ -z "$FILE_NAME" ]; then
    shift $((OPTIND - 1))
    FILE_NAME=${@:-1}
fi

Ha a FILE_NAME üres, akkor a shift-tel kivesszük az utolsó paramétert a paraméter listából, és fogjuk a maradék legutolsó darabját és arra állítjuk be a FILE_NAME értékét. Ezzel vissza is kanyarodtunk az első próbálkozásunkhoz, ahol pontosan ugyanezt csináltuk, csak a shift nélkül. Lám, az sem volt teljesen haszontalan.

A script:

#!/bin/sh

IS_FLAG_A_SET=0
IS_FLAG_B_SET=0
FILE_NAME=""
USER_NAME=""

while getopts "f:u:ba" opt; do
    case $opt in
        'a')
            IS_FLAG_A_SET=1
            ;;
        'b')
            IS_FLAG_B_SET=1
            ;;
        'f')
            FILE_NAME=$OPTARG
            ;;
        'u')
            USER_NAME=$OPTARG
            ;;
        [?])
            echo "Usage $0 -a [-b] [-f filename] [-u username]"
            exit 1
    esac
done

if [ -z "$FILE_NAME" ]; then
    shift $((OPTIND - 1))
    FILE_NAME=${@:-1}
fi

if [ $IS_FLAG_A_SET -eq 1 ]; then
    echo "FLAG A"
else
    echo "FLAG A is mandantory"
    exit 1
fi

if [ $IS_FLAG_B_SET -eq 1 ]; then
    echo "FLAG B"
fi

echo "FILE_NAME: $FILE_NAME"
echo "USER_NAME: $USER_NAME"

Hozzászóláshoz a Disqus szolgáltatását használom, korábbi vélemények elovlasásához és új hozzászólás írásához engedélyezd a Disqus-tól származó JavaScripteteket.