Моя прошлая статья Приемы написания скриптов на 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
Если найдутся читатели, кто хотел бы продолжить развивать этот скрипт — напишите в комментариях.
Файлы библиотеки и самого скрипта можно найти здесь.
Автор: justAdmin