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 shift
elé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.