Published: 16. 3. 2014   Category: GNU/Linux

Zpracování parametrů příkazové řádky v shell-skriptech

Základním způsobem ovládání programů příkazové řádky je pomocí parametrů napsaných v promtu za názvem příkazu oddělených bílými znaky (mezera, tabelátor). Mohou to být:

Doporučované konvence

POSIX.1-2008 doporučuje v dokumentaci stručně uvést volby, obvykle následovně:


utility_name [-abcDxyz][-p arg] operand

utility_name [options] [operands…]

Volby jsou uvedené v hranatých závorkách jsou volitelné, zde je skupina -a, -b, -c, -D, -x, -y, -z bez parametrů, u hodně komplexních utilit bývá skrytá za „[options]“ a volby jsou podrobně rozepsané v manuálové stránce a helpu. Volba -p pak vyžaduje nějaký argument, operand není v hranatých závorkách, je tedy povinný.

Např. příkaz pro smazání adresáře provedeme pomocí: rm -r -f adresář, volby lze zkrácené zapsat i jako rm -rf adresář. Volby -r a -f jsou nepovinné, ovšem protože si chceme vynutit smazání adresáře -f, rekurzivně -r, musíme tyto volby uvést. V prostředí GNU/Linuxu se také používají dlouhé volby, takže zápis by pak vypadal: rm --recursive --force adresář, to se s oblibou používá při psaní skriptů, kdy je pak skript lépe čitelný a není potřeba si znovu připomínat volby v manuálové stránce.

Přestože jsou volby u POSIXových utilit standardizovány, takže rm -rf nebo ls -la bude fungovat téměř všude, dlouhé volby už nemusí fungovat všude a komerční unixy je obvykle nemají, takže pokud preferujeme přenositelnost budeme je muset oželet.

Parametr „--“ a „-“

Další z běžných konvencí je použití dvojité pomlčky „--“ jako ukončení sledu pozičních parametrů. Hodí se v situacích kdy parametrem programu je jiný příkaz s vlastními parametry a ty chceme předat tomu volanému programu, nebo pokud je parametrem řetězec uvozený pomlčkou. Např. grep -- -v soubor.txt, bere -v jako regulární výraz, nebo příkaz rm -- -rf vymaže soubor se zlotřilým názvem -rf.

U některých příkazů se také setkáte s použitím jedné pomlčky, většinou jako parametru u volby, která nastavuje název souboru. Tím se programu sdělíme, aby místo souboru použil std. vstup nebo výstup:

Zpracování parametrů

Uvnitř skriptu přistupujeme k těmto parametrům podle jejich číselné pozice: $1 – první parametr, $2 – druhý parametr, atd. až $9. Pokud potřebujeme parametr $10, shell rozvine pouze $1 a přidá znak 0, v tomto případě je nutné číslo pozice zapisovat ve složených závorkách, tedy ${10}, zde můžeme přistupovat k parametrům ${1}${255}. Ve specifikaci IEEE Std 1003.1TM je také definováno, že čísla parametrů uvozené nulou se vyhodnocují dekadicky, tj. ${010} je stále ${10} (a nikoliv 8, jak bývá obvyklé v zápise oktalových čísel).

Hodnoty $1, $2,… existují po celou dobu vykonávání skriptu, ale při vstupu do funkce a po dobu jejího vykonávání jsou pak nahrazeny funkčními parametry, se kterými se pak v těle funkce pracuje stejně jako v těle skriptu.

Další proměnné a funkce, které souvisejí s načítáním parametrů:

Bash má ještě dvě globální proměnné, pomocí kterých k nim lze přistupovat:

Díky tomu je možné přistupovat k parametrům řádky i uvnitř funkcí, ale nevýhodou je neobvyklé uložení BASH_ARGV a to, že se připravíme se ovšem o kompatibilitu s jinými POSIX kompatibilními shelly.

Osobně preferuji způsob, kdy na začátku skriptu jsou nadefinovány globální proměnné odpovídající nastavením přepínačů a hodnot voleb. Pomocí nich pak přistupuji k těmto hodnotám i uvnitř funkcí.

Ukončit skript volaný bez parametrů

Pokud skript vyžaduje parametry, ale uživatel žádný nezadá, měl by se korektně ukončit. Možnosti kontroly počtu parametrů:



[ $# -lt 1 ] && exit 1

Pokud je počet parametrů menší než 1, ukonči skript s návratovou hodnotou 1. Tenhle one-linerem je nejjednodušší způsob jak to provést a navíc nenulová návratová hodnota sdělí, že něco není v pořádku, což je důležité, když se pak skript začlení do větších celků a návratová hodnota ($?) se zde testuje.


if [ $# -eq 0 ]
then
	echo "Očekávám parametr!" 1>&2
	exit 1
fi

Tento způsob dělá přesně to samé, ale uvnitř IF-bloku ještě vypíše chybovou hlášku. Všimněte si, že na konci echa je 1>&2, tím se standardní výstup (deskriptor 1, stdout), přesměruje do standardního chybového výstupu (deskriptor 2, stderr). Pokud uživatel přesměruj std. výstup do souboru, pak by se chybová hláška uložila do něj, takhle se zobrazí na terminálu k uživatelově potěšení :)

Uživatelský help

Následující způsob je výhodný pro spojení dokumentace skriptu a uživatelského helpu dohromady, následuje mustr jak takový skript obvykle vytvářím já:


#!/bin/bash
#%A
#Návod k použití
#===============
#
#example.sh [*volby*]
#
#Skript example.sh je příklad způsobů práce s parametry příkazové
#řádky.
#
#Volby
#-----
#
#  * `-h`, `--help` -- tento help
#  * `-x` soubor    -- soubor určený k vypsání pozpátku
#  * `-v`           -- verze skriptu
#
#%B

function print_help() {
	sed -ne '/#%A/,/#%B/s/\(^#\|^#%[AB]\)//p' $0
}

if [ $# -eq 0 ] ; then print_help 1>&2; exit 1; fi

if [[ "$*" =~ -h ]] ; then print_help; exit 0; fi 

# … tady následuje zbytek skriptu

Takto vytvořeným mustrem je zabito několik much dohromady, za prvé na začátku skriptu je jeho dokumentace, to je pro ty uživatele, kteří se budou ještě před prvním spuštěním zajímat o jeho funkci, všimněte si značek #%A a #%B, těmi jsem si označil „dokumentační blok“, tento blok se také vypíše pokud je volána funkce print_help, navíc tento hezoučký ascii formát s podtrženým nadpisem, podnadpisem a odrážkovaným seznamem je formát pandoc. Pandoc tento text umí zkonvertovat do jiného značkovacího jazyka, tj. HTML, u, groffu a dalších, vyzkoušejte si to online!.

Tím máme dokumentaci i help vyřešený jednou ranou. IF-blok s podmínkou „[[ "$*" =~ -h ]]“, je testem na regexp, zda se na příkazové řádce se nachází „-h“, tj. uživatel se dožaduje nápovědy, ať už pomocí „-h“ a nebo i „--help“, protože je to chtěné chování, návratový kód je pro změnu 0.

Zpracování krátkých přepínačů pomocí getopts

Ulehčit čtení voleb a jejich parametrů z CLI umožňuje vestavěný shellový příkaz getopts. Použití je celkem snadné:


while getopts "a:b" opt
do
     if [[ $OPTARG =~ ^-.* ]] ; then
         echo "Option '$opt' is missing argument!" 1>&2
         exit 1
     fi

	 case $opt in
	 	a) echo a = $OPTARG ;;
	 	b) echo b ;;
	 	\?) echo "Unknown parameter!" 1>&2; exit 1 ;;
	esac;
done

shift $(($OPTIND - 1)); echo "Other: $*"

Příkaz getopts má dva parametry, prvním je seznam voleb a druhým je proměnná do které je bude ukládat. Zde seznam voleb udává -a a -b, dvojtečka značí, že volba -a má povinný parametr.

Celé zpracování je pak provedeno v jednom while cyklu, kdy v $opt jsou uloženy načtené proměnné, pokud volba má parametr, pak ten se uloží do globální proměnné $OPTARG. Při načtení neplatného parametru se do $opt uloží otazník a je na vás, jak se má skript zachovat.

Protože vstupem skriptu nemusí být jen přepínače, ale po nich následuje třeba seznam souborů, pro zpracování getopts ještě zavoláme: shift $(($OPTIND - 1)) (proměnná $OPTIND obsahuje číslo pozice následujícího argumentu), a tím zbylé hodnoty posuneme na místa $1, $2,…

Nepříjemnou vlastností je to, že pokud -a nemá parametr, uloží se do $OPTARG to co následuje, testováním [[ $OPTARG =~ ^-.* ]], zkontrolujeme zda tam není uložená jiná volba, string začínající na „-“. Pokud je volba s chybějícím parametrem jako poslední, getopts zahlásí chybu (option requires an argument).

Pokud zadáme volby s parametrem bez mezery např. -aaa, pak getopts do $OPTARG uloží správně „aa“.

Dlouhé přepínače pomocí getopt

Součástí balíčku util-linux (ftp://ftp.kernel.org/pub/linux/utils/util-linux/), je program getopt (bez „s“ na konci :), který dokáže dokáže zastoupit shellový příkaz getopts a navíc ho vylepšuje o podporu dlouhých voleb. V prostředí GNU/Linuxu bývá většinou už v základní instalaci.

Použití si ukážeme na příkladu, náš skript bude mít následující volby a použijeme společně krátké a dlouhé:


opt_short="hvf:"
opt_long="help,verbose,file:"

OPTS=$(getopt -o "$opt_short" -l "$opt_long" -- "$@")

if [ $? -ne 0 ] ; then
        echo "Wrong input parameter!"; 1>&2
        exit 1;
fi

eval set -- "$OPTS"

while true
do
    case "$1" in
        -h|--help)    echo "User help"; exit 0;;
        -v|--verbose) echo VERBOSE=1; shift;;
        -f|--file)
            [[ ! "$2" =~ ^- ]] && FILE=$2
            shift 2 ;;
        --) # End of input reading
            shift; break ;;
    esac
done

if [ -f "$FILE" ] ; then
    echo "File '$FILE' doesn't exist!" 1>&2
    exit 1
fi

echo "Remaining options: $*"

Na začátku je seznam voleb $opt_short, $opt_long ve stejném pořadí, dvojtečka opět značí parametry. Tyto volby jsou pak argumentem pro -o krátké volby resp. -l dlouhé volby. Výstup getopt se uloží do $OPTS, navíc se kontroluje jeho návratová hodnota při chybě. Protože getopt není vestavěný příkaz shellu, manipulaci se proměnnými $1, $2, atd. zařídí až příkaz eval set -- "$OPTS". Proměnné se pak čtou v nekonečném cyklu a vyhodnocují v CASE-bloku, po každém načtení proměnné se musí použít příkaz shift, pokud má volba argument, ten se pak nachází v $2 a musí se provést posun o dva, tedy shift 2.

Smyčka se ukončí až narazí na „--“. Kvůli kontrole chybějícího parametru se hodnota do FILE uloží až po zjištění, že nezačíná pomlčkou ([[ ! "$2" =~ ^- ]]). Zda $FILE obsahuje co má, se zjistí až po zpracování příkazové řádky.

Nezpracované argumenty se pak nachází v $1, $2, …