Приемы написания скриптов в 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" ] &amp;&amp; pBackgr=1

   # А здесь может быть по умолчанию то, что пришло с командной строки
   read -p "Введите имя лог-файла (по умлочанию: $pLogFile): " a1
   [ -n "$a1" ] &amp;&amp; pLogFile="$a1"

   pShowCmd= # Чтобы не наложился параметр, заданный с командной строки
   input1 "Вывести команду синхронизации на экран? (y/n): " "yn." "dot-exit"
   [ "$cRes" = "y" ] &amp;&amp; pShowCmd=1

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

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

if [ "$pBackgr" = "1" ] ; then
   nohup $RSCmd "${RSPrm[@]}" &amp;
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



См. примеры кода для статьи.

  • Soft Space SIA
    Latvia
  • Этот адрес электронной почты защищен от спам-ботов. У вас должен быть включен JavaScript для просмотра.
  • +371 25 46 42 49
Big-IP Edge Client openconnect client cisco secure client download netextender download for mac wisenet viewer download admanager plus download globalprotect download citrix secure access watchguard ssl vpn download Qfinder Pro fortinet vpn forticlient mac