Pull to refresh

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

Reading time 11 min
Views 28K
Моя прошлая статья Приемы написания скриптов на 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


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

Файлы библиотеки и самого скрипта можно найти здесь.
Tags:
Hubs:
+19
Comments 8
Comments Comments 8

Articles