Упороборос или самоперезаписываемые значения в shell скриптах
Периодически, когда мне нужно написать какой-то скрипт на шелле, я сталкиваюсь с типовыми задачами, которые можно решить быстро, просто и в лоб, а можно написать некоторое количество кода, что займёт больше времени, но при этом позволит использовать эту наработку в будущем. Одним словом, на шелле с этим всё обстоит точно так же, как и в других языках. Хотя наверное сочетание слов "библиотека на шелле" звучит достаточно дико и непривычно. Когда -то я уже писал о библиотеке логирования на шелле, которую я сделал много лет назад и достаточно часто ей пользуюсь, с тех пор не было нужды что-то в ней менять.
Вчера я столкнулся с задачей, для которой целая библиотека на гитхабе - это явный перебор, но метод, который можно включать в различные скрипты мне бы совсем не помешал. И вот чем прекрасен собственный блог - это самое подходящее место для того, чтобы оставить заметочку с небольшим куском кода, который потом при необходимости оттуда можно быстро скопировать. Как это нередко бывает при попытке написать что-то на шелле, сам код оказался куда проще, компактнее и универсальнее, чем изначально казалось, должен был быть.
Общий смысл решаемой задачи следующий: для работы скрипта необходимо задание ряда переменных, в случае, если они не заданы, хотелось бы опросить пользователя в интерактивном режиме, заодно проверить соответствие этих переменных неким шаблонам с предложением повторить ввод в случае несоответствия, и, что самое главное, сохранить знaчения этих переменных внутри самого запущенного скрипта. Последнее будет полезным в случае, если этот скрипт потом потребует переноса на другой инстанс. Помимо сохранения значения переменных, так же происходит и их экспорт, что позволяет сразу же после ввода использовать их так, как если бы они уже были прописаны в скрипте изначально. Приведу пример, для чего мне это понадобилось. Сейчас я пишу скрипт для обновления dns записей в CloudFlare, для чего нужно ввести необходимые для авторизации данные. При первом запуске данные будут запрошены у пользователя и сохранены в скрипте. При повторном никаких вопросов уже не будет, а заодно можно будет пропустить лишний вызов апи для получения идентификатора зоны по её имени - мы просто сохраним полученный идентификатор так же внутри скрипта. Ниже приведу код метода:
# Updating variables
# 1 - variable name
# 2 - optional check mask
# 3 - optional comment (will taken from variable comment if not exists)
# 4 - optional value for non-interactive update
selfUpdate() {
[ -z "$1" ] && l fe "Variable name not exists"
l d "Updating variable $1"
var=`cat -n "$0" | sed '1,/###VARSBEGIN###/d;/###VARSEND###/,$d;s/^[^0-9]\+//;/\t'"$1"'=/!d'`
[ -n "$3" ] && comm="$3" || comm=`echo "$var" | sed '/#/!d;s/^.*#[ \t]*//'`
[ -z "$comm" ] && comm="$1"
val=`echo "$var" | sed 's/[ \t]*#.*$//;s/^[0-9]\+[ \t]*'"$1"'=//;s/^["'"'"']//;s/["'"'"']$//'`
nline=`echo "$var" | awk '{print $1}'`
if [ -z "$4" ]; then
res=''
while [ -z "$res" ]; do
[ -z "$val" ] && echo -n "Input $comm: " || echo -n "Input $comm (current is $val): "
read res
if [ -n "$2" ]; then
[ -z "$(echo "$res" | grep -E "$2")" ] && l w "Variable $1 check is failed, try again" && res=''
else
break
fi
done
else
res="$4"
fi
if [ -n "$nline" ]; then
sed -i "$nline"'s/^.*$/'"$1"'='"'""$res""'"" # $comm"'/' "$0"
else
nline=`cat -n "$0" | awk '$2~/###VARSEND###/ {print $1}'`
sed -i "$nline"'i'"$1=\'$res\' # $comm" "$0"
fi
l d "Variable $1 is setted with value $res"
export "$1"="$res"
}
Метод использует упомянутую выше библиотеку логирования, но при желании можно легко замениить вызовы "l d" и "l fe" на простые echo и exit (в случае с fe).
В начале скрипта, где мы будем использовать этот метод, нужно с помощью двух меток определить место для инициализации переменных, все действия метода будут производиться с переменными, расположенными между этих меток, строки с метками не должны содержать никаких дополнительных символов ни до ни после меток. Так же есть ограничение - значения переменных не должны содержать одиночных кавычек. Пример первоначального задания переменных:
#!/bin/sh
cd `dirname "$0"` # переходим в текущую директорию
. ./eslogger # импортируем библиотеку логирования
LOGLEVEL='debug'
###VARSBEGIN###
domain='' # domain name (for example: test.yourdomain.xx)
authemail='' # email, used for authentication
authkey='' # api key
ttl='1440' # default ttl for new records
zid='' # zone id
###VARSEND###
Передаваемых методу параметров - четыре. Первый - это имя переменной. Второй - это regexp для проверки на соответствие значению. Если параметр не задан, то по умолчанию будет проверка на отличие от пустой строки. Третий параметр - это комментарий. При задании переменной будет выведено приглашение ко вводу вида: "Input comment: ", где comment - это тот самый комментарий, указанный третьим параметром. В случае, если комментарий не передан методу, он будет взят из строки с определением переменной. То есть, в нашем случае для переменной authkey без передачи третьего параметра мы получим приглашение ко вводу вида "Input api key: ". Если же комментарий не задан и в строке инициализации переменной, то в качестве комментария будет взято имя самой переменной.
Если мы вызовем данный метод с именем неописанной между метками переменной, то она будет добавлена в конец списка перед меткой ###VARSEND### с использованием указанного комментария или имени переменной в качестве комментария в соответствии с описанной выше логикой. Вот пример вызова метода для описанных переменных (кроме переменной zid, которя будет обновлена после вызова метода api).
[ -z "$domain" ] && selfUpdate domain '^([a-zA-Z0-9_-]+\.)*[a-zA-Z0-9][a-zA-Z0-9_-]+\.[a-zA-Z][a-zA-Z]{1,11}$'
[ -z "$authemail" ] && selfUpdate authemail '^([a-zA-Z0-9_-]+\.?)*@([a-zA-Z0-9_-]+\.)[a-zA-Z][a-zA-Z]{1,11}$'
[ -z "$authkey" ] && selfUpdate authkey '^[a-z0-9]{38}$'
[ -z "$ttl" ] && selfUpdate ttl '^[0-9]{2,5}$'
Да, я знаю, что мои regexp не соответствуют RFC, но как минимальная защита от неверного ввода они меня устраивают. Теперь в качестве примера я попробую вызвать скрипт и наделать кучу ошибок при вводе.
$ ./test
2019.02.02-20:42:16 DEBUG: Updating variable domain
Input domain name (for example: test.yourdomain.xx):
2019.02.02-20:42:18 WARNING: Variable domain check is failed, try again
Input domain name (for example: test.yourdomain.xx): 123
2019.02.02-20:42:20 WARNING: Variable domain check is failed, try again
Input domain name (for example: test.yourdomain.xx): aaa
2019.02.02-20:42:22 WARNING: Variable domain check is failed, try again
Input domain name (for example: test.yourdomain.xx): test.
2019.02.02-20:42:26 WARNING: Variable domain check is failed, try again
Input domain name (for example: test.yourdomain.xx): !a@b#c
2019.02.02-20:42:38 WARNING: Variable domain check is failed, try again
Input domain name (for example: test.yourdomain.xx): test1.test2.com
2019.02.02-20:43:47 DEBUG: Variable domain is setted with value test1.test2.com
2019.02.02-20:43:47 DEBUG: Updating variable authemail
Input email, used for authentication: 123
2019.02.02-20:43:55 WARNING: Variable authemail check is failed, try again
Input email, used for authentication:
2019.02.02-20:43:58 WARNING: Variable authemail check is failed, try again
Input email, used for authentication: aaa
2019.02.02-20:43:59 WARNING: Variable authemail check is failed, try again
Input email, used for authentication: evgeniy.shumilov@gmail.com
2019.02.02-20:44:09 DEBUG: Variable authemail is setted with value evgeniy.shumilov@gmail.com
2019.02.02-20:44:09 DEBUG: Updating variable authkey
Input api key: 123
2019.02.02-20:44:17 WARNING: Variable authkey check is failed, try again
Input api key: aaa
2019.02.02-20:44:18 WARNING: Variable authkey check is failed, try again
Input api key: 012345678901234567890123456789abdcefg
2019.02.02-20:56:11 DEBUG: Variable authkey is setted with value 012345678901234567890123456789abdcefg
$
Скрипт не стал запрашивать ввода значения для переменной tty, так как она уже была задана. Попробуем запустить скрипт ещё раз.
$ ./test
$
Как видим, скрипт больше не требует от нас никаких действий. Посмотрим, как изменилась наша секция с переменными:
###VARSBEGIN###
domain='test1.test2.com' # domain name (for example: test.yourdomain.xx)
authemail='evgeniy.shumilov@gmail.com' # email, used for authentication
authkey='012345678901234567890123456789abdcefg' # api key
ttl='1440' # default ttl for new records
zid='' # zone id
###VARSEND###
Теперь попробуем отключить проверку и задать переменную принудительно, в этом случае при вводе нового значения мы получим в приглашении ко вводу помимо комментария ещё и текущее значение переменной. Вызов будет следующим:
selfUpdate ttl '^[0-9]{2,5}$'
$ ./test
2019.02.02-21:11:27 DEBUG: Updating variable ttl
Input default ttl for new records (current is 1440): 120
2019.02.02-21:11:35 DEBUG: Variable ttl is setted with value 120
$
А теперь попробуем задать значение для ttl в неинтерактивном режиме (не забываем, что значение у нас является не третьим, а четвёртым параметром):
selfUpdate ttl '^[0-9]{2,5}$' '' 600
$ ./test
2019.02.02-21:16:54 DEBUG: Updating variable ttl
2019.02.02-21:16:54 DEBUG: Variable ttl is setted with value 600
$
На этом на сегодня всё.
Теги: shell