Проблема
По долгу службы приходится активно использовать Shell скрипты на ОС Linux.
Притом что все скрипты фактически одинаковые по своей сути – генерация данных. Немалое количество времени уходит на написание и отладку правильности проверки входной информации от заказчика. И соответственно определение параметров генерации данных на основании этих входных параметров.
В основе проверок лежат статические определенные спецификациями данные, зачастую таблицы, в результате сверок формируются новые необходимые параметры для дальнейшей генерации. При этом работа по настройке проверочной информации требует аккуратности и внимательности, поскольку возможные ошибки могут стоить дорого.
Стадия проверки в основном состоит из нагромождения вложенных конструкций case и if.Была мысль делать разбор табличек через систему cut ( таблица )> read var1 var2 var3 потом if-ы, а потом куда-то формировать результат — но все это не очень удобно и не красиво, хотелось минимального синтаксиса.
И давно уже «…залегла забота в сердце мглистом…» (Есенин С.А.), а решения оптимизации все не было.
Решение как всегда пришло внезапно. Копая в очередной раз доки по shell и прокручивая идею об обращении к переменной по ее имени находящемся в другой переменной – нашел тему «косвенные ссылки»! Возможно, уважаемые форумчане знакомы с такой возможностью shell, ну а для тех, кто не в курсе, небольшой поясняющий примерчик:
Косвенные ссылки на переменные
Результат:
name_of_var = Var
Now val = var_value
by new version now val= var_value
#!/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