Аргументы в bash

Если скрипт имеет более одного параметра - придется проанализировать аргументы ради гарантии правильной обработки всех возможных способов передачи аргументов юзером. Да даже скрипту с одним параметром не помешает обработка параметров -h/--help.

Не стоит использовать позиционные параметры $1, $2, …) как универсальное имя, ибо не понятно, что делает этот параметр и зачем. Лучше в начале скрипта присвоить их значения переменным с “говорящими” именами.

Простая проверка кол-ва аргументов $# - кол-во аргументов):

if (($# != 2)); then
	echo "usage: $0 <input_file> <output_file>"
	exit 1
fi

Чтобы сослаться на все аргументы скрипта и заключить каждый из них в кавычки, можно использовать $@ (строка) или ${@} (список). $* вернет одну большую строку в кавычках со всеми аргументами. Например, при таком вызове скрипта:

some_script file1.dat "alt data" "local data" summary.txt

$* вернет одну строку "file1.dat alt data local data summary.txt", тогда как $@ - четыре отдельных слова "file1.dat" "alt data" "local data" "summary.txt".

ARG

Для перебора всех параметров можно просто в скрипте написать такую конструкцию (и если ему ничего не будет будет - будет воркать):

for ARG; do
	echo "here is an argument: $ARG"
done
 
$ ./args.sh bash is tuff
 
here is an argument: bash
here is an argument: is
here is an argument: tuff

Ключи

Ключи (options) дают возможность изменить выполнение команды. Классический идиоматический способ представления ключей в Unix/Linux - дефис/минус/тире и одна буква.

Анализ ключей

Для анализа ключей юзают встроенную команду getopts.

Используется, обычно, связка while+getopts+case, где while юзает многократно getopts (ибо при каждом вызове getopts будет находить только один ключ), пока не будут получены все ключи, a case уже будет работать с переменной, которой getopts при каждом вызове присваивает новый найденный ключ. getopts распознает и ключи, указанные по отдельности -a -v), и сгруппированные -av). Можно указать, что ключ должен идти вместе с доп. аргументом -o output или -ooutput), указав после символа ключа двоеточие :).

Предполагается, что ключи идут до остальных аргументов.
getopts-у нужно передать два слова: список параметров и имя переменной, которой он присвоит очередной обнаруженный ключ.
getopts возвращает true, когда обнаруживает ключ (дефис, за которым следует любая буква, допустимая или нет), и false, когда дойдет до конца списка параметров; так он и работает с while.

getopts usage:

while getopts ':ao:v' VAL ; do :; done

Строкой ':ao:v' переданы поддерживаемые ключи. Двоеточие :) в начале указывает команде не сообщать об ошибке при обнаружении неподдерживаемого параметра, ибо обработка таких ошибок будет реализована в case. Двоеточие :) после o указывает, что ключ o должен сопровождаться доп. аргументом. А VAL - имя переменной, куда и запишется очередной найденный ключ.

Не обнаружив ключ, getopts присвоит переменной VAL двоеточие :), а переменной $OPTARG символ ключа, для которого не был указан доп. аргумент (то есть, o).

Обнаружив неподдерживаемый параметр, getopts присвоит переменной VAL знак вопроса ?, а $OPTARG - символ нераспознанного ключа.

Именно с помощью полученных в переменной VAL двоеточия :) или знака вопроса ?) можно будет в case обработать эти ошибки:

case "$VAL" in
...
	: ) echo "Error: No arg supplied to [$OPTARG] option" ;;
	* )
		echo "Error: unknown option [$OPTARG]"
		echo "Valid options are: aov"
	;;
...
esac

Полный код для анализа коротких ключей:

while getopts ':ao:v' VAL ; do
	case "$VAL" in
		a ) AMODE=1 ;;
		o ) OFILE="$OPTARG" ;;
		v ) VERBOSE=1 ;;
		: ) echo "Error: No arg supplied to [$OPTARG] option" ;;
		* )
			echo "Error: unknown option [$OPTARG]"
			echo "Valid options are: aov"
		;;
	esac
done
shift $((OPTIND - 1))

shift использует арифметическую операцию $((OPTIND - 1)) ($OPTIND хранит индекс следующего рассматриваемого параметра) для определения кол-ва позиций, на которые нужно сдвинуть позиционные параметры. После обработки ключей, то есть, после завершения цикла while, shift сдвигает параметры; это означает, что обработанные параметры будут удалены и следующий параметр станет первым позиционным параметром. Это нужно для исключения из дальнейшего рассмотрения всех аргументов, связанных с ключами. Неважно, каким образом будет вызван скрипт:

script -a -o out.txt -v file1
# Аргументы в bash
script file1

после выполнения shift $((OPTIND - 1)) $1 будет хранить значение file1.

Длинные ключи

Длинные ключи начинаются с двух дефисов --last), иначе длинный ключ -last был бы неотличим от сгруппированных -l -a -s -t .

getopts поддерживает и длинные ключи, правда, с помощью еще одного вложенного case.

Чтобы getopts мог анализировать длинные ключи, нужно в список параметров передать ’-:’ (знак минуса и двоеточие) и еще один вложенный case для их обработки. Двоеточие нужно передать в список параметров, даже если длинный ключ не принимает аргументов.

Длинный ключ и его аргумент можно передать в скрипт двумя способами:

--outfile=out.txt
--outfile out.txt

Анализ коротких и длинных ключей:

#!/usr/bin/env bash
 
VERBOSE=":"  # По дефолту включен
 
while getopts ':-:ao:v' VAL ; do  # (1)
	case
		a ) AMODE=1 ;;
		o ) OFILE="$OPTARG" ;;
		v ) VERBOSE=1 ;;
#-------------------------------------------------------------------
		- )  # Этот раздел добавлен для поддержки длинных ключей (2)
			case "$OPTARG" in
				amode     ) AMODE=1 ;;
				outfile=* ) OFILE="${OPTARG#*=}" ;;  # (3)
				outfile   ) { OFILE="${!OPTIND}"; ((OPTIND++)); }  ;;  # (4)
				verbose   ) VERBOSE="echo" ;;
				* )
					echo "unknown long argument: [$OPTARG]"
					exit
				;;
			esac
		;;
#--------------------------------------------------------------------
		: ) echo "Error: No arg supplied to [$OPTARG] option" ;;
		* )
			echo "Error: unknown option [$OPTARG]"
			echo "Valid options are: aov"
		;;
	esac
done
shift $((OPTIND - 1))
 
# VERBOSE="echo" нужен для такого простого вывода VERBOSE-инфы:
"$VERBOSE" 'Example verbose message...'
  1. getopts разрабатывалась для поддержки коротких ключей; знак минус в списке ее параметров ':-:ao:v') позволяет двум дефисам --) распознаваться как допустимый ключ.
  2. Любые символы после двух дефисов --) будут считаться аргументом ключа и присваиваться $OPTARG. Для сопоставления значения $OPTARG с длинными именами ключей и нужен вложенный case.
  3. Если аргумент длинного ключа передан со знаком равенства --outfile=out.txt), то getopts передаст всю строку после переменной $OPTARG, а уже достать аргумент из этой строки можно с помощью ${OPTARG#*=} или ${OPTARG#outfile=}.
  4. Доп. аргумент извлекается косвенно с помощью ${!OPTIND}. Восклицательный знак !) сообщает о косвенной ссылке, когда значение $OPTIND используется как имя извлекаемой переменной. Например, если --output был 3-м аргументом, в этот момент $OPTIND будет хранить 4, и ${!OPTIND} вернет ${4}, то есть следующий аргумент с именем файла.

Справка -h / --help)

Для простых скриптов подойдет такое решение:

function Show_Usage {
	local script_name
	script_name="$(basename "$0")"
 
	cat << EOF
Usage:
  $script_name <files> -o <output file>
  $script_name -o <output file> <files>
EOF
	exit 1
}
 
[[ $#--lt-1-|| "$1" == "-h" || "$1" == "--help" ]] && Show_Usage
  1. В качестве метки EoH (End-of-Help), можно использовать произвольную последовательность символов. Дефис -) нужен для автоматического удаления из строк ниже начальных табуляций (но не пробелов). Это позволяет юзать в коде отступы, которые в выводе будут проигнорированы.
  2. Здесь используется exit 1, ибо скрипт не сделал ничего полезного - просто вывел справку. Можно спорить долго, но и exit 0, и exit 1 подходят сюда (это как посмотреть).

Вообще, стоит использовать для вывода справки отдельную функцию, чтобы потом использовать его в обработчике ключей:

#!/usr/bin/env bash
 
VERBOSE=":"  # По дефолту выключен
 
while getopts ':-:ao:v' VAL ; do  # (1)
	case
		a ) AMODE=1 ;;
		o ) OFILE="$OPTARG" ;;
		h ) { Display_help ; exit 1; } ;;
		v ) VERBOSE=1 ;;
#-------------------------------------------------------------------
		- )  # Этот раздел добавлен для поддержки длинных ключей (2)
			case "$OPTARG" in
				amode     ) AMODE=1 ;;
				outfile=* ) OFILE="${OPTARG#*=}" ;;  # (3)
				outfile   ) { OFILE="${!OPTIND}"; ((OPTIND++)); }  ;;  # (4)
				verbose   ) VERBOSE="echo" ;;
				help      ) { Display_help ; exit 1; } ;;
				* )
					echo "unknown long argument: [$OPTARG]"
					exit
				;;
			esac
		;;
#--------------------------------------------------------------------
		: ) echo "Error: No arg supplied to [$OPTARG] option" ;;
		* )
			echo "Error: unknown option [$OPTARG]"
			echo "Valid options are: aov"
		;;
	esac
done
shift $((OPTIND - 1))

debug/verbose режимы вывода

По умолчанию, если юзер не запросил verbose вывод, $VERBOSE содержит : , то есть, команду no-op (без операции) или null, которая ничего не делает, игнорирует свои аргументы и всегда возвращает true:

VERBOSE=":"  # По дефолту выключен

А если присваивается 'echo':

...
verbose ) VERBOSE="echo" ;;
...

строка "$VERBOSE" 'Example verbose message...' превратится в:

echo 'Example verbose message...'

и выведет сообщение. Это простая идиома вывода сообщений по условию.


Вывод VERSION скрипта

Для больших или public скриптов возможность вывода номера версии может быть актуальной. Форматы значений $VERSION:

VERSION="v1.2.3"
VERSION="$PROGRAM v1.2.3"
VERSION="12."
VERSION=$ID$  # для CVS или SVN

Функция-обработчик аргументов

В скриптах с большим набором аргументов нужно разделять код, обрабатывающий аргументы, и основные функции. Код для анализа аргументов лучше запихнуть в функцию:

function parseargs {
	...
}

потом вызвать ее как parseargs "${@}" - остальной код сможет теперь юзать установленные ею флаги (в этом случае имеются в виду переменные-флаги VERSION, DEBUG и т.д.). Это позволит отказаться от усложняющих логику кода условных конструкций для выбора между нормальным и debug/verbose режимами вывода.


Соусы:
Книга “Идиомы Bash Глава 8. “Аргументы
Книга “Bash и кибербезопасность Глава 2. “Основы работы с bash

bash