Shells
12 March 2013

Проверка входных параметров или косвенные ссылки на BASH

From Sandbox
Проблема

По долгу службы приходится активно использовать Shell скрипты на ОС Linux.
Притом что все скрипты фактически одинаковые по своей сути – генерация данных. Немалое количество времени уходит на написание и отладку правильности проверки входной информации от заказчика. И соответственно определение параметров генерации данных на основании этих входных параметров.

В основе проверок лежат статические определенные спецификациями данные, зачастую таблицы, в результате сверок формируются новые необходимые параметры для дальнейшей генерации. При этом работа по настройке проверочной информации требует аккуратности и внимательности, поскольку возможные ошибки могут стоить дорого.
Стадия проверки в основном состоит из нагромождения вложенных конструкций case и if.Была мысль делать разбор табличек через систему cut ( таблица )> read var1 var2 var3 потом if-ы, а потом куда-то формировать результат — но все это не очень удобно и не красиво, хотелось минимального синтаксиса.

И давно уже «…залегла забота в сердце мглистом…» (Есенин С.А.), а решения оптимизации все не было.

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

Косвенные ссылки на переменные
#!/bin/bash
# Косвенные ссылки на переменные.
Var=var_value
name_of_var=Var

# Прямое обращение к переменной.
echo "name_of_var = $name_of_var"

# Косвенное обращение к переменной , через eval.
eval val=\$${name_of_var}
echo "Now val = $val"

#значительно нагляднее и удобнее:
# Косвенное обращение к переменной bash, начиная с версии 2 
#к сожалению, в ksh который у нас принято использовать такой синтаксис не поддерживает
val=${!name_of_var}
echo "by new version now  val= $val"

Результат:
name_of_var = Var
Now val = var_value
by new version now val= var_value


И тут понесло

Было решено сделать красивый синтаксис описания задачи, и написать хороший его разбор.
Большинство условий легко укладывались в таблицу входных значений, и выходная зависимость также замечательно складывалась в таблицу. Рассуждая далее. Входная информация и соответствующая ей выходная в одной строке — отлично. Нужен красивый разделитель. Разделителем сделал “=>” символ следования в математике, и тут вроде как из одного набора входных параметров следуют соответствующие выходные параметры.

Синтаксис:

Имена входных переменных через табуляцию => имена выходных переменных через табуляцию
Значения входных параметров через табуляцию => табуляция соответствующие выходные значения
Возможно вычисляемое выражение в обратных кавычках `[чего-то вычислить]`.
Пустые строки и строки, начинающиеся с символа “#” не обрабатываются.
Разделитель полей табуляция.
#коментарий
varName1	varName2	… varNameN	=>	varRes1		varRes2		…	varResN
varValIn11	varValIn21	… varValInN1	=>	varVRes11	varRes21	…	varResN1
varValIn12	varValIn22	… varValInN2	=>	varVRes12	varRes22	…	varResN2
…
varValIn1n	varValIn2n	… varValInNn	=>	varVRes1n	varRes2n	…	varResNn
<ERROR>echo “error message”;exit;	=>	varRes1	Err	varRes2	Err	…	varResNErr

Блок условия заканчивается специальным выражением
 после выражения идет исполняемая команда выполняемая в случае ошибки, затем знак => и значения выходных переменных в случае ошибки.
Далее могут идти следующие блоки условий.
Введены дополнительные шаблоны :
  • <?> - любое значение;
  • <_>- пустое значение;
  • <.>- не пустое значение.

Шаблоны удобно использовать, если точное значение входного параметра для данного выражения не важно, или важно что значение не пустое, или наоборот пустое.

Оказалось, что bash при обработке, сам множественную табуляцию превращает в одинарную. При этом получается красивое и наглядное оформление.
Если параметр состоит из нескольких слов через пробел, то не забываем правильно использовать кавычки при присвоении переменным значений.
Возможно, вам нужно использовать табуляцию в параметрах, к сожалению, без переделки этот функционал не работает.
Результат предыдущей серии сравнений влияет на входные параметры последующих условий.

Пример

Несколько надуманно, но для примера. Действие водителя в зависимости от дорожного знака (светофора) и текущей скорости, получение нового значение скорости.
Начальные параметры, по идеи, берутся откуда-то извне и (или) дополнительных обработок.
В результате:
Speed=быстро Sign=светофор State=желтый


Далее осуществляется проверка правильности значений параметров.
Обычно используется конструкция if else , а можно и case:
if [[ "$Speed" == "стоим" ]] || [[ "$Speed" == "быстро" ]] || [[ "$Speed" == "медленно" ]]
then
	echo "Speed ok"
else
	echo "Wrong Speed:${Speed}.";exit 0;
fi


Но мне понравилось по новому, правда надо соблюдать синтаксис и ввести вспомогательную переменную Result.
Далее условие состоящее из двух: проверка правильности параметра Speed
и собственно основного условия нахождения необходимого действия:
Conditions='
#проверка правильности значения скорости 
Speed		=>	Result
стоим		=>	OK
быстро		=>	OK
медленно	=>	OK
<ERROR>	echo "Wrong Speed:${Speed}.";exit 0;	=>	Error

#что делать на светофоре
Sign		State	Speed		=>	ToDo			Speed
светофор	красный	стоим		=>	ничего			стоим
светофор	красный	<?>		=>	тормозим		стоим
светофор	желтый	стоим		=>	ничего			стоим
светофор	желтый	медленно	=>	тормозим		стоим
светофор	желтый	быстро		=>	тормозим		медленно
светофор	зеленый	стоим		=>	начать движение		медленно
светофор	зеленый	<?>		=>	ничего			$Speed
<ERROR>	echo "Wrong traffic lights";	=>	$ToDo			$Speed
 ‘

Условие сформировано и теперь запускаем обработчик CheckTabel.sh и выводим результат :
source $HOME/bin/CheckTabel.sh # execute check and set values for vars
echo "Todo=$ToDo Speed=$Speed "

Результат:
Todo= тормозим Speed= медленно

Полный текст примера Test.sh
#!/bin/ksh
#!/bin/ksh
#/bin/bash
# 11.03.2013	aap	script "Test.sh"
#
#Speed=стоим
Speed=быстро
Sign=светофор
#Sign="лежачий полицейский"
#Sign="уступи дорогу"
#Sign="нет такого"
State=желтый

#для примера
#как проверить допустимость значений параметра Speed
#
if [[ "$Speed" == "стоим" ]] || [[ "$Speed" == "быстро" ]] || [[ "$Speed" == "медленно" ]]
then
	echo "Speed ok"
else
	echo "Wrong Speed:${Speed}.";exit 0;
fi

Conditions='
Sign				=>	Result
светофор			=>	OK
уступи дорогу		=>	OK
лежачий полицейский	=>	OK
<ERROR>	echo "Wrong Sign:${Sign}.";exit 0;	=>	Error

Speed		=>	Result
стоим		=>	OK
быстро		=>	OK
медленно	=>	OK
<ERROR>	echo "Wrong Speed:${Speed}.";exit 0;	=>	Error

Sign		State	Speed		=>	ToDo			Speed
светофор	красный	стоим		=>	ничего			стоим
светофор	красный	<?>			=>	тормозим		стоим
светофор	желтый	стоим		=>	ничего			стоим
светофор	желтый	медленно	=>	тормозим		стоим
светофор	желтый	быстро		=>	тормозим		медленно
светофор	зеленый	стоим		=>	начать движение	медленно
светофор	зеленый	<?>			=>	ничего			$Speed
<ERROR>	echo "Wrong traffic lights";	=>	$ToDo	$Speed

Sign				Speed	=>	ToDo		Speed
лежачий полицейский	быстро	=>	тормозим	медленно
лежачий полицейский	<?>		=>	ничего		медленно
<ERROR>	echo "Wrong ramp police";	=>	$ToDo	$Speed

Sign			Speed	=>	ToDo		Speed
уступи дорогу	быстро	=>	тормозим	медленно
уступи дорогу	<?>		=>	ничего		медленно
<ERROR>	echo "Wrong let have road";	=>	$ToDo	$Speed

# можно предыдущие три условия записать одним , заменив вставив колонку State с параметром <?> - любое значение
# ВНИМАНИЕ при экспериментах . Результаты изменений параметра Speed предыдущими проверками
# могут повлиять на входной параметр Speed этого сравнения. В итоге результаты могут не совпадать !

Sign				State	Speed		=>	ToDoAlter		SpeedAlter
светофор			красный	стоим		=>	ничего			стоим
светофор			красный	<?>			=>	тормозим		стоим
светофор			желтый	стоим		=>	ничего			стоим
светофор			желтый	медленно	=>	тормозим		стоим
светофор			желтый	быстро		=>	тормозим		медленно
светофор			зеленый	стоим		=>	начать движение	медленно
светофор			зеленый	<?>			=>	ничего			$Speed
лежачий полицейский	<?>		быстро		=>	тормозим		медленно
лежачий полицейский	<?>		<?>			=>	ничего			медленно
уступи дорогу		<?>		быстро		=>	тормозим		медленно
уступи дорогу		<?>		<?>			=>	ничего			медленно
<ERROR>	echo "Wrong ";					=>	тормозим		${Speed}
'
source $HOME/bin/CheckTabel.sh # execute check and set values for vars

#echo "$Conditions"
echo "Todo=$ToDo Speed=$Speed "
echo "alternative:"
echo "Todo=$ToDoAlter Speed=$SpeedAlter "

exit 0

Результат:
Todo= тормозим Speed= медленно
alternative:
Todo= тормозим Speed= стоим

Тут видим разный результат, поскольку значение Speed после первого условия изменилось.


Как это работает

Теперь вкратце, что делает обработка CheckTabel.sh, полный текст в конце статьи.
В начале был сохранен старый и установлен новый разделитель табуляция.
#echo ifs=$IFS
OLD_IFS="$IFS"
IFS=$'	'# 

Далее читаем из $Conditions построчно и строки целиком сохраняем в Line.
echo  "${Conditions}" |
while read Line
do
…тут много чего
done #while read Line

Я не знал, что значения переменных можно вот так легко получать во вложенном скрипте из вызываемого, и это пригодится дальше.

Внутри опишу тонкие, на мой взгляд, моменты.

Пропускаем пустые строки, строки с комментариями и прочими не нужностями.

Разбиваем каждую строку на параметры разделенные табуляцией и загоняем параметры в массив. Из нужных строк извлекаем имена переменных. Делается это у меня так:
eVarLine=(`echo "$Line"`)


Далее интересное, в цикле используя косвенные ссылки на переменные , формируем массив значений входных параметров eVarDatIN.
	tmp=${eVarLine[$i]}	# get name of input var
	eval tmp=\$$tmp	# gat var value by var name
	eVarDatIN[${i}]=${tmp}

Имена переменных после символа => являются выходными , их сохраняем в массиве eVarOUT.

Из строк со значениями в цикле, извлекаем значения переменных, и пишем в массив eDatLine.
eDatLine=( `echo "$Line"` )

Эти значения сравниваем со значениями входных параметров сохраненных в eVarDatIN, если значения совпали учитывая шаблоны (<?>,<.>,<_>), набиваем значениями массив DataOUT .Если все совпало - вываливаемся , если нет дойдем до строки
.

Тут очередной интересный момент.
Как получить значение переменной используя косвенные ссылки понятно , но вот как теперь записать в переменную зная ее имя – вопрос ? Но я то теперь знал, что значения переменных можно получать во вложенном скрипте из вызываемого и на оборот, это теперь пригодилось.
Все просто, просто формируем временный скриптовый файл,в котором задаются значения переменных.
( for (( i = 0 ; i < ${#eVarOUT[@]} ; i++ )) do echo "${eVarOUT[$i]}=\"${eDataOUT[$i]}\"" done echo "${LineErr}" # error command ) >> $eTmpFile # create temporary file for setup vars and show error message

Теперь запустим его. А затем очистим, чтобы правильно значения переменных присваивались.
				# ---------- execute --------------
				eISVAR=1		#	its mean , that next line will with varnames
				source $eTmpFile #. data-file    	# execute file and set values for vars
				> $eTmpFile  				# erase temporary file for setup vars


Временный файл tmpSetVar.sh ,если убрать зачистку
Result="OK"
Result="OK"
ToDo="тормозим"
Speed="медленно"
ToDo="$ToDo"
Speed="$Speed"
echo "Wrong ramp police";
ToDo="$ToDo"
Speed="$Speed"
echo "Wrong let have road";
ToDoAlter="тормозим"
SpeedAlter="стоим"


Я написал этот скрипт и начал использовать. Так вот что характерно – понравилось.
Начали мучить мысли, почему в языках программирования нет столь удобного оператора табличного поиска значения.
Немного копнув Perl обнаружил намек на что-то подобное, но так и не нашел конкретной информации.
Надеюсь опытные скриптописатели меня поправят.



Исходник CheckTabel.sh
#/bin/ksh
#/bin/bash
#
# script CheckTabel.sh - check tabel data
#
# See on : http://www.linuxcookbook.ru/books/absguide/ch09s05.html
#
#12.02.2013	_aap_ 	created by Patratskiy Aleksey
#26.02.2013	_aap_ 	RELAEASE work with errors, get first right parametrs
#28.02.2013	_aap_	fix same problem var counts
#05.03.2013 _aap_ 	can use without <END>
#

#Limits :
#Comment '#' work only for first position in string
#Devior TAB  If parameters use TAB it can not be used
#input data: Conditions
#
# after you can check variable "CheckTabelErrors" for errors count


#exaple of var :Conditions
#Conditions='
##check stuff of vars and data
#Check var parametrs
#varIn1	varIn2	varIn3	varIn4	=>	varOut1	varOut2
#varIn1varIn1	varIn2varIn2	varIn3varIn3	varIn4varIn4	=>	AAPvarOut1varOut1	AAPvarOut2varOut2
#<.> <_> <> dsf => sdf sdf
#<ERROR> Error `exit 1`	=>	<>	<>
#<END>
#'



#internals Vars:
eTmpFile=$HOME/tmp/tmpSetVar.sh
eDIVISOR='	'
eInOutDevisor='=>'
eEND='<END>'
eERROR='<ERROR>'
eANY='<?>'
eEMPTY='<_>'
eNOTEMPTY='<.>'
eISVAR=1
eDeb=""
eIsFind=0
((CheckTabelErrors=0))
((eCurLine=0))
#for ksh
typeset -A eDataIN
typeset -A eDataOUT
typeset -A eVarIN
typeset -A eVarDatIN
typeset -A eVarOUT
typeset -A eVarLine
typeset -A eDatLine


#for bash
#typeset -a eDataIN
#typeset -a eDataOUT
#typeset -a eVarIN
#typeset -a eVarDatIN
#typeset -a eVarOUT
#typeset -a eVarLine
#typeset -a eDatLine



#source $eTmpFile  # erase temporary file for setap vars
> $eTmpFile  # erase temporary file for setap vars

#echo ifs=$IFS
OLD_IFS="$IFS"
IFS=$'	'#


echo  "${Conditions}" |
while read Line
do

	((eCurLine++))

#	echo "LineErr: ${eCurLine}" ;echo "Line: ${Line}"

	if [[ "${Line:0:1}" != "#"  && "$Line" != "" && "${Line:0:${#eEND}}" != "${eEND}" ]] #skeep empty and commets lines AND with <END>
	then
		if [[ "eISVAR" -eq "1" ]]   # check for var names line
		then
			eISVAR=0
			eVarLine=(`echo "$Line"`)
			cntIn=${#eVarLine[@]}

#		echo ${#eVarLine[@]} ${eVarLine[@]}


			for (( i = 0 ; i < cntIn ; i++ ))	do	#get names of input vars
				if [ "${eVarLine[${i}]}" == "${eInOutDevisor}" ]
				then
					((outBgn=i + 1))
					break

				fi
				eVarIN[${i}]=${eVarLine[$i]}
				tmp=${eVarLine[$i]}	# get name of input var
				eval tmp=\$$tmp		# gat var value by var name
				eVarDatIN[${i}]=${tmp}
#	   	echo DEBUG Element [$i]: ${eVarLine[$i]} val=${eVarIN[${i}]} eVarDatIN=${eVarDatIN[${i}]}
			done
#		echo DEBUG outBgn=$outBgn cntIn=$cntIn

			if (( cntIn == 0 || outBgn == 0 ))
			then
				echo !!! Error CheckTabel.sh  formating outBgn=$outBgn cntIn=$cntIn wrong  eDIVISOR TAB
				continue

			fi

			(( j = 0 ))
			for (( i = $outBgn ; i < cntIn ; i++ ))	do # get name of output vars
#	   	echo "${eVarLine[$i]}=Set${i}"
					eVarOUT[$j]=${eVarLine[$i]}
					((j++))
			done

			eIsFind=0 # not finde yet

#		echo eVarIN  ${#eVarIN[@]}: ${eVarIN[@]} ;	echo eVarOUT  ${#eVarOUT[@]}: ${eVarOUT[@]}


###################################################################################
		else

			if [[ "${Line:0:${#eEND}}" == "${eEND}" ]] # check for end of sentence
			then
#				echo "END: $Line" #echo "Line: ${Line:0:${#eEND}} == "
#				eISVAR=1
#				source $eTmpFile #. data-file    	# execute file and set values for vars
#				> $eTmpFile  						# erase temporary file for setap vars
				continue  # while read Line

			fi
#
#Check for error message and error vars set
#
			if [[ "${Line:0:${#eERROR}}" == "${eERROR}" ]] # check for ERROR of sentence
			then
#		echo "<ERROR>:${eIsFind} $Line"  #echo "Line: ${Line:0:${#eEND}} == "
				LineErr=""
				if [[ "eIsFind" -eq "0" ]]
				then
				    ((CheckTabelErrors++))
					LineErr="${Line#${eERROR}}"  #remove <Error>

					echo "!!! ERROR CheckTabel.sh LineErr: ${eCurLine}" #;echo "Line: ${Line}"

					ErrVarOut="${LineErr#*${eInOutDevisor}}" #+ ${#eInOutDevisor}+
					LineErr="${LineErr%${eInOutDevisor}*}"
					eDataOUT=( `echo "$ErrVarOut"` )

					(
						for (( i = 0 ; i < ${#eVarOUT[@]} ; i++ ))	do
					    	echo "${eVarOUT[$i]}=\"${eDataOUT[$i]}\""
						done
						echo "${LineErr}"	# error command
					) >> $eTmpFile # create temporary file for setup vars and show error message
				fi

				# ---------- execute --------------
				eISVAR=1		#	its mean , that next line will with varnames
				source $eTmpFile #. data-file    	# execute file and set values for vars
				> $eTmpFile  						# erase temporary file for setap vars

				continue  # while read Line
			fi


			#for skeep all next currient tabel parametrs
			if [[ "eIsFind" -eq "1" ]]
			then
				continue  # while read Line
			fi

			eDatLine=( `echo "$Line"` )

#	echo OUT ${#eDatLine[@]} ${eDatLine[@]}
   			cntOut=${#eDatLine[@]}


			if (( $cntIn !=  $cntOut ))
			then
				echo !!! Error CheckTabel.sh in Conditions line=${eCurLine} formating Vars $cntIn but Data $cntOut, count of parametrs not equel ! ;echo !!! ${eVarIN[@]} === ${eDatLine[@]}
				continue # while read Line

			fi

			#
			# main check for compare tabale value and var value in currient string
			#
			((cmpCnt=0))
			for (( i = 0 ; i < cntOut ; i++ ))	do	#get input data
				if [ "${eDatLine[${i}]}" == "${eInOutDevisor}" ] ; then
					((outBgn=i + 1))
					break
				fi
#    	echo DEBUG Element [$i]: ${eDatLine[$i]} val=${!eDatLine[$i]}  eDatLine=${eDatLine[${i}]} == eVarDatIN="${eVarDatIN[$i]}"
				if [ "${eDatLine[${i}]}" == "${eVarDatIN[$i]}" ]  ; then
#		echo ${eDatLine[${i}]} == ${eVarDatIN[$i]}
					((cmpCnt++))
					eDataIN[$i]=${eDatLine[${i}]}

				fi
				if [[ "${eDatLine[${i}]}" ==  "${eANY}"  ]]  ; then
#					echo ${eDatLine[${i}]} == ${eVarDatIN[$i]}
					((cmpCnt++))
					eDataIN[$i]=${eDatLine[${i}]}
				fi
				if [[ "${eDatLine[${i}]}" ==  "${eNOTEMPTY}"  &&  "${eVarDatIN[$i]}" != "" ]]  ; then
#					echo ${eDatLine[${i}]} == ${eVarDatIN[$i]}
					((cmpCnt++))
					eDataIN[$i]=${eDatLine[${i}]}
				fi
				if [[ "${eDatLine[${i}]}" ==  "${eEMPTY}"  &&  "${eVarDatIN[$i]}" == "" ]]  ; then
#					echo ${eDatLine[${i}]} == ${eVarDatIN[$i]}
					((cmpCnt++))
					eDataIN[$i]=${eDatLine[${i}]}
				fi

			done

#		echo DEBUG "$cmpCnt" == "(($outBgn - 1 ))"     outBgn=$outBgn cntIn=$cntIn


			if (( "$cmpCnt" == "(($outBgn - 1 ))" )) # there are data in this string  #${#eVarIN[@]}
			then

				(( j = 0 ))
				for (( i = $outBgn ; i < cntOut ; i++ ))	do # get value for  output vars
						eDataOUT[$j]=${eDatLine[$i]}
#				    	echo "DEBUG ${eVarOUT[$j]}=${eDataOUT[$j]}"
						((j++))
				done

				(
					for (( i = 0 ; i < ${#eVarOUT[@]} ; i++ ))	do
				    	echo "${eVarOUT[$i]}=\"${eDataOUT[$i]}\""
					done
				) >> $eTmpFile # create temporary file for setap vars
                eIsFind=1
#				echo OK:  ${eVarIN[@]} == ${eDataIN[@]} '=>' ${eVarOUT[@]} == ${eDataOUT[@]}
			fi

		fi


	else #skeep empty and commets lines
#			echo Comment: $Line
		eDeb=""	# for skeep warning
	fi

done #while read Line
######################################################################3



IFS=$OLD_IFS

#rm $eTmpFile



+25
6.1k 99
Comments 3