Приемы написания скриптов на Bash. #2

от автора

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

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

  • Не копируются файлы с одинаковыми датой/временем, которые уже существуют (этого можно достигнуть и командой cp с ключом -u)
  • В директории-приемнике уничтожаются все файлы и директории, которых нет в источнике
  • Скрипт может синхронизировать данные не только локально, но и на удаленный компьютер

Иными словами в приемнике мы получаем точно то, что в источнике, причем происходит это самым оптимальным образом. Это крайне полезный подход, например, при периодическом сохранении большого объема данных на внешний диск. Копируется только новое, и то, что изменилось, в то время как удаленные файлы в источнике удаляются и в приемнике. Кроме того, копируются также права доступа, атрибуты Selinux и расширенные атрибуты файлов.

Собственно, как наверное уже многие догадались, мы не будем изобретать велосипед, а воспользуемся программой rsync, которая для этого и предназначена. Здесь задача — обернуть rsync нашим скриптом, чтобы им было удобно пользоваться. Ну кому охота писать нечто подобное?:

 rsync -rlptgoDvEAH --delete --delete-excluded --super --force --progress --log-file=/var/log/rs-total.txt --log-file-format=%o %i %f %b /data/src/proj/perl/my/web/company/roga-i-kopyta/ /data/save/proj/perl/my/web/company/roga-i-kopyta/ 

Очевидно, что у нашего скрипта минимум 2 параметра — директории источник и приемник. В прошлой статье эта ситуация рассмотрена не была, а именно — как наряду с ключами обрабатывать еще и параметры фиксированного положения. Причем очень желательно, чтобы ключи можно было бы вставлять куда угодно, обтекая ими фиксированные параметры. Например:

dir-sync -key1 src-sri -key2 dest-dir key3 

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

# Добавим объявления необходимых переменных fixPrmCnt=0 pSrcDir= # Директория-источник pDstDir= # Директория-приемник  ...  while [ 1 ] ; do     if [ "$1" = "--yes" ] ; then    pYes=1     ...    else    # Сюда вставляем обработку фиксированных параметров.   # Об этих параметрах (их форме) скрипт ничего не знает.   # Следовательно, как только появился «неизвестный ключ» -   # это и есть наш фиксированный параметр    (( fixPrmCnt++ )) # Номер входного параметра по порядку    # Цифры (номер параметра) впереди — для наглядности и чтобы    # легче было править   if   [ 1 -eq $fixPrmCnt ] ; then      pSrcDir="$1"   elif [ 2 -eq $fixPrmCnt ] ; then      pDstDir="$1"    # Мы ожидаем только параметры, описанные выше   # Все остальное - ошибка   else      errMess "Ошибка: неизвестный ключ"      exit 1    fi    fi     shift  done  

Как видно из примера, обработка фиксированных параметров прекрасно укладывается в предложенную ранее схему. Кстати, я дополнил библиотеку функций несколькими функциями, не нуждающимися в рассмотрении, errMess — одна из них. В этой статье я не заостряю внимание на реализации библиотечных функций, поскольку они пока еще очень просты и очевидны. С ними Вы сможете ознакомиться в файле библиотеки (в конце статьи). Для меня главное — показать, как простые функции могут в разы повысить ясность, читабельность и простоту кода скриптов.

Теперь определим функционал нашего скрипта клонирования. Он должен:

  • При запуске без параметров выводить краткую справку.
  • Требовать подтверждения в случае, если кроме двух заданных параметров не указано ничего. Да! Скрипт супер-десктруктивен, и эта вещь никак не окажется лишней.
  • Как было описано ранее, для подавления подтверждения используется ключ —yes. Это позволит использовать скрипт в других скриптах.

И вот здесь еще одна изюминка:

  • При указании ключа -i скрипт должен стать интерактивным.

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

Ну и собственно функционал:

  • Выбор режима: точная копия/обновление
  • Включение фонового режима (через nohup)
  • Задание имени лог-файла
  • Возможность посмотреть на получившуюся команду rsync (тоже в качестве примера, не более)

На первый раз функционала достаточно. Если потребуется, разовьем его в будущем. Описывать rsync и nohup я не буду — достаточно просто прочитать их man.

Опишем все ключи:

  • —yes: Подавить запрос подтверждения
  • -i | —interactive: включить интерактивный режим
  • -lf | —log-file=: задать имя лог-файла
  • -u | —update: режим обновления (по умолчанию — точная копия)
  • -sc | —show-command: показать конечную команду rsync
  • -n | —dry-run: «холостой режим» — rsync запускается и информирует о действиях, но ничего на самом деле не делает
  • -bg | —background: выполнить в фоне

Эти ключи требуют наличия соответствующих переменных. Так что заголовочная часть скрипта будет примерно такой:

# Объявление переменных  fixPrmCnt=0   # Счетчик фиксированных параметров  pInter=       # Интерактивный режим  pLogFile=     # Имя лог-файла  pUpdate=      # Режим обновления  pShowCmd=     # Показать команду rsync  pDryRun=      # Холостой режим  pBackgr=      # Выполнять в фоне  pSrcDir=      # Директория источник  pDstDir=      # Директория приемник  RSCmd=        # Команда rsync  RSPrm=        # Дополнительные параметры rsync  

Мы не объявляем параметр pYes, так как он находится в нашей библиотеке. А теперь рассмотрим основные блоки программы.

Вот как выглядит обработка параметров:

if [ -z "$1" ] ; then     usage     exit  fi   while [ 1 ] ; do  	 if [ "$1" = "--yes" ] ; then  			pYes=1  	 elif [ "$1" = "-i" ] ; then  			pInter=1  	 elif [ "$1" = "--interactive" ] ; then  			pInter=1  	 elif procParmS "-lf" "$1" "$2" ; then  			pLogFile="$cRes" ; shift  	 elif procParmL "--log-file" "$1" ; then  			pLogFile="$cRes"  	 elif [ "$1" = "-u" ] ; then  			pUpdate=1  	 elif [ "$1" = "--update" ] ; then  			pUpdate=1  	 elif [ "$1" = "-sc" ] ; then  			pShowCmd=1  	 elif [ "$1" = "--show-command" ] ; then  			pShowCmd=1  	 elif [ "$1" = "-n" ] ; then  			pDryRun=1  	 elif [ "$1" = "--dry-run" ] ; then  			pDryRun=1  	 elif [ "$1" = "-bg" ] ; then  			pBackgr=1  	 elif [ "$1" = "--background" ] ; then  			pBackgr=1  	 elif [ -z "$1" ] ; then  			break # Ключи кончились  	 else  			(( fixPrmCnt++ ))  			if   [ 1 -eq $fixPrmCnt ] ; then  				 pSrcDir="$1"  			elif [ 2 -eq $fixPrmCnt ] ; then  				 pDstDir="$1"  			else  				 errMess "Ошибка: неизвестный ключ"  				 exit 1  			fi  	 fi     shift  done  

При отсутствии параметров выводится краткая справка. Далее обработка параметров, а за ней следует их проверка и если надо — изменение (откусывание конечного слэша).

checkParm "$pSrcDir" "Не задана директория-источник"  checkParm "$pDstDir" "Не задана директория-приемник"   if [ "$pInter" = "1" ] && [ "$pYes" = "1" ] ; then     errMess "Несовместимые параметры: --yes и -i"     exit 1  fi   # Откусывыаем конечную слэш, если она задана  pSrcDir="${pSrcDir%/}"  pDstDir="${pDstDir%/}"   checkDir "$pSrcDir"  checkDir "$pDstDir"  

Неинтерактивная часть скрипта выглядит очень простой:

# Если неинтерактивный запуск  if [ "$pInter" != "1" ] ; then   	 # Запрос подтверждения  	 if [ "$pYes" != "1" ] ; then  			echo "Скрипт ${curScript##*/} приветствует Вас!"  			showInfo  			myAskYesNo "Это может повлечь необратимые последствия! Вы уверены?" || exit  	 fi   	 createCmd  

Функции showInfo и createCmd мы еще рассмотрим — это, собственно, отображение информации о параметрах и генерация команды rsync.

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

   cat <<EOF  Скрипт ${curScript##*/} приветствует Вас!  Точкой на любой вопрос Вы сможете прервать выполнение.  Выберите желаемый режим:  ------------------------     c) clone   (полное клонирование)     u) update  (только обновление)     .) Выход  EOF     input1 "Твой выбор: " "cu."     [ "$cRes" = "." ] && exit      pBackgr= # Чтобы не наложился параметр, заданный с командной строки     input1 "Хотите ли Вы выполнить операцию копирования в фоне? (y/n): " "yn."     [ "$cRes" = "." ] && exit     [ "$cRes" = "y" ] && pBackgr=1      # А здесь может быть по умолчанию то, что пришло с командной строки     read -p "Введите имя лог-файла (по умолчанию: $pLogFile): " a1     [ -n "$a1" ] && pLogFile="$a1"     [ "$a1" = "." ] && exit      pShowCmd= # Чтобы не наложился параметр, заданный с командной строки     input1 "Вывести команду синхронизации на экран? (y/n): " "yn."     [ "$cRes" = "." ] && exit     [ "$cRes" = "y" ] && pShowCmd=1      createCmd      echo # Дополнительный отступ для читабельности    showInfo   	 if [ "$pShowCmd" = "1" ] ; then  			echo "Команда rsync:"  			echo "  $RSCmd" "${RSPrm[@]}"  	 fi      myAskYesNo "Запускаем! Вы уверены?" || exit  

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

Здесь также присутствуют функции showInfo и createCmd.

А теперь немного модифицируем функцию input1 (см. в библиотеке) так, чтобы она принимала параметр, который говорит о том, что в случае нажатия точки, нужно выходить из скрипта — «dot-exit». Мы исключим по одной строке на обработку каждого параметра! Сейчас часть кода, отвечающая за это выглядит так:

   input1 "Твой выбор: " "cu." "dot-exit"      pBackgr= # Чтобы не наложился параметр, заданный с командной строки     input1 "Хотите ли Вы выполнить операцию копирования в фоне? (y/n): " "yn." "dot-exit"     [ "$cRes" = "y" ] && pBackgr=1      # А здесь может быть по умолчанию то, что пришло с командной строки     read -p "Введите имя лог-файла (по умлочанию: $pLogFile): " a1     [ -n "$a1" ] && pLogFile="$a1"      pShowCmd= # Чтобы не наложился параметр, заданный с командной строки     input1 "Вывести команду синхронизации на экран? (y/n): " "yn." "dot-exit"     [ "$cRes" = "y" ] && pShowCmd=1  

Можно пойти дальше, и ввести несколько функций для ввода параметров. Но это оставим на следующий раз.

Концовка очевидна:

if [ "$pBackgr" = "1" ] ; then     nohup $RSCmd "${RSPrm[@]}" &  else     $RSCmd "${RSPrm[@]}"  fi  

Мы рассмотрим использование массива чуть позже, а сейчас обратим внимание, что коль скоро в самом конце запускается rsync, результат его выполнения и будет результатом выполнения нашего скрипта. Этим мы добиваемся осуществления правила о том, что любой скрипт должен возвращать результат.

А теперь рассмотрим функции, которые тоже просты и понятны.

showInfo()  {  	 local a1   	 if [ "$pUpdate" = "1" ] ; then  			a1="обновление"  	 else  			a1="клонирование"  	 fi   	 padMid 80 "Режим" "$a1" ; echo $cRes  	 padMid 80 "Источник" "$pSrcDir" ; echo $cRes  	 padMid 80 "Приемник" "$pDstDir" ; echo $cRes  	 padMid 80 "Лог-файл" "$pLogFile" ; echo $cRes   	 transYesNoRu $pBackgr  	 padMid 80 "Выполнять в фоне" "$cRes" ; echo $cRes   	 transYesNoRu $pDryRun  	 padMid 80 "Выполнять в холостом режиме" "$cRes" ; echo $cRes  }  

Здесь мы пользуемся библиотечной функций padMid, чтобы красиво и ровно выводить значения параметров (параметр «80» — ширина строки). Функция transYesNoRu из 1 делает «да», из всего остального «нет».

Вывод при этом примерно таков:

Режим..................................... клонирование  Источник.......................... /data/src/proj/fed16  Приемник........................... /data/src/proj/sync  Лог-файл......................... /var/log/dir-sync.log  Выполнять в фоне................................... Нет  Выполнять в холостом режиме........................ Нет  

И наконец сердце скрипта — генерация команды rsync, где последовательно добавляются ключи в соответствии с заданными параметрами.

createCmd()  {  	 RSCmd="$rsync"   	 if [ "$pUpdate" = "1" ] ; then  			RSCmd="$RSCmd -urlptgoDvEAH"  	 else  			RSCmd="$RSCmd -rlptgoDvEAH --delete"  	 fi   	 # Если в фоне - не надо никакого вывода, и наоборот  	 if [ "$pBackgr" = "1" ] ; then  			RSCmd="$RSCmd -q"  	 else  			RSCmd="$RSCmd --progress -v"  	 fi   	 if [ "$pDryRun" = "1" ] ; then  			RSCmd="$RSCmd -n"  	 fi   	 RSCmd="$RSCmd --super --force"   	 # Дополнительные параметры - элементами массива  	 n=-1  	 ((n++)) ; RSPrm[n]="--log-file=$pLogFile"  	 ((n++)) ; RSPrm[n]="$pSrcDir/"  	 ((n++)) ; RSPrm[n]="$pDstDir/"  }  

То есть createCmd формирует переменную RSCmd, которая потом запускается в оконечной части скрипта.

Особо отметим использование массива RSPrm. Дело в том, что если в именах файлов будут встречаться пробелы (а мы пишем более-менее универсальный скрипт, который этот момент должен учесть), то сборка одной строки RSCmd работать не будет. Помните концовку: $RSCmd "${RSPrm[@]}"? Если бы все набивалось только в строку $RSCmd и концовка выглядела бы как $RSCmd, то имя директории или лог-файла с пробелами было бы разбито интерпретатором bash. Например, при указании директории источника «my dir», вместо копирования «my dir» куда указано, была бы попытка копирования my в dir, а затем еще в это «куда-то».

Попытки собрать строку как

RSCmd="$RSCmd \"$pSrcDir/\" \"$pDstDir/\" " 

, то есть добавить эскапированные кавычки в эту строку, также успехом не увенчаются. Мы получим имена файлов типа «my dir» вместе с кавычками.

Использование массива решает эту проблему. Массив инициализируется также как обычная переменная (RSPrm=), точнее, он и является обычной переменной до тех пор, пока не будет использоваться как массив. И мы именно так и поступаем, когда выполняем ((n++)); RSPrm[n]="—log-file=$pLogFile". Индексы массива в bash начинаются от нуля. Для универсальности и читабельности мы инициализируем n=-1, чтобы потом просто инкрементировать ее и получить новый действительный индекс.

Использование же массива происходит в концовке:

$RSCmd "${RSPrm[@]}" 

Такая конструкция делает следующее — элементы массива вставляются в строку по отдельности, и представляют собой неделимый параметр, что бы в них ни находилось (пробельные символы.) Если же заменить символ @ на * мы получим эффект, аналогичный использованию обычной строки, то есть каждый элемент массива будет подвержен разбору по пробельным символам и именно разбитые таким образом токены предстанут параметрами. Именно этого-то мы и избегали, следовательно нам нужен здесь только @.

Вообще использование массивов таким образом крайне полезно, когда параметрами выступают строки, содержащие пробелы. Например, тот же rsync может принимать параметры фильтрации файлов, типа: ‘-f- *.tmp’, означающее, что при синхронизации игнорируются *.tmp файлы. Так вот, ‘-f- *.tmp’ это единый параметр, который содержит в себе пробел. Если Вы будете собирать строки один раз, то можете указывать эти параметры в кавычках или апострофах типа:

rsync ... '-f- *.tmp' '-f- *.log' ... 

Но если вы такую строку попытаетесь собрать предварительно, а затем выполнить ее — будет караул! Например:

param="-f- *.tmp" param="$param -f- *.log" 

аналогично не сработает и

param="'-f- *.tmp'" param="$param '-f- *.log'" 

И в таких случаях мы вынуждены использовать массив вышеописанным способом.

Резюме

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

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

Обращу внимание читателей, не знакомых с rsync — одну из директорий можно задавать на удаленной машине в виде

[user@][host:]dir-from-root 

, то есть

vova@mycomp:/save/work mycomp:/save/work 

вызов скрипта может быть, например, таким:

dir-sync -u /work mycomp:/save/work 

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

Файлы библиотеки и самого скрипта можно найти здесь.

ссылка на оригинал статьи http://habrahabr.ru/post/163691/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *