Продолжим начатое, и на реальном примере дополним подход к разбору параметров и унификации функционала скриптов.
Итак, давайте напишем скрипт, который синхронизирует некую директорию в другую: 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/
Добавить комментарий