Код отслеживания Google Analytics.

Jul 24, 2010

библиотека shell-framework

Вместо введения
Я пишу скрипты на bash. Это скорее ближе к хобби, хотя иногда нужно и по работе. Конечно, не каждый день, но довольно часто и уже довольно давно. Бывают ситуации, когда либо не хочется пользоваться другими script-языками или когда у заказчика нет специалиста который сможет поддержать ваши скрипты в будущем, если они написаны, например, на perl - в общем, у меня бывало много случаев, когда моими скриптами должен был пользоваться не только я, но и еще кто-то и написаны они должны были быть на bash, а не на чем-то ином. Проанализировав требования к command line interface (CLI) программе, я систематизировал их и написал библиотеку, о которой и пойдет речь ниже.

Что я ожидаю?

Итак, что же мне нужно от CLI программ?
  1. Параметры. CLI программа должна уметь работать с параметрами. Параметры могут быть короткими (-M) и длинными(--multi-volume) . Первые нужны непосредственно для ввода их в командной строке и должны быть краткими для скорости ввода, вторые предназначены для использования программы в других скриптах и потому должны быть минимально читаемыми.
  2. Ожидаемые параметры и поведение. На самом деле я знаю лишь один by default параметр - "-h" и его длинный синоним "--help". Скрипт, будучи запущен с таким параметром, должен распечатать минимальную справку о себе и своих параметрах и закончить выполнение. (Как бы было прекрасно, когда, запуская на удаленной и мало знакомой машине скрипт, я мог бы быть уверен, что всегда смогу получить минимальное описание... :) )
  3. Наличие конфигурационных файлов. Конечно, это не обязательно, но, иногда, крайне желательно, в особенности для скриптов с дюжиной и более возможных параметров.
Пожалуй - это три основных требования. Библиотека  shell-framework была написана чтобы помогать мне писать скрипты, удовлетворяющие таким требованиям.
Еще один Hello world...
Давайте набросаем следующий скрипт:
#!/bin/bash

function printHelp () {
    cat - << END_OF_HELP
It is test script which just print Hello, <name>.
Usage $0 [parameter] ...
Paramter:
    -n <name> - define the <name>. default value is "World"
    -h        - print the help and exit from script
END_OF_HELP
}

declare name="World"

while getopts ":n:h" Option
do
  case $Option in
    n ) name="${OPTARG}" ;;
    h ) printHelp ; exit 1 ;;
  esac
done

echo "Hello, ${name}!"
Это хороший скрипт и он вполне отвечает своим целям. Однако, что будет, если мы захотим добавить еще один параметр - например "-b|--bye" - если он определен, например ‹‹-b "See you"››, то скрипт должен писать, вместо "Hello", "See you"? Нам придется дописать printHelp, добавив в нее описание нового параметра, проинициализировать его default value - "Hello", использовать, вместо "getopt", "getopts" (пример), что несколько изменит цикл и добавить еще строку в case блок. Надо так же заметить, что большинство блоков этого скрипта, таких как "print help", цикл перебора параметров и инициализации переменных, в том или ином виде являются общими для огромного числа скриптов в сети и, на мой взгляд, просто напрашиваются на вынесение их в библиотечные функции.
Посмотрим, как можно решить эту задачу с помощью shell-framework. Для начала скачайте и распакуйте куда-нибудь последнюю версию (ее можно взять здесь). Теперь напишите следующий script
#!/bin/bash

shF_PATH_TO_LIB="./shell-framework/lib"
source ${shF_PATH_TO_LIB}/base

setDescription "Hello world is example script."

#addOption <name> [defaultValue] [shortForm] [longForm] [type] [shortDescription] [longDescription] [priority] [notForConfig]]
#   type can be shF_SIMPLE_OPTION or shF_OPTION_WITH_VALUE
addOption "name" "World" "-n" "--name" "${shF_OPTION_WITH_VALUE}" "you can define your name as ${shF_COMMON_PARAMETER_NAME}." "" "110"


if ! initConfig "$@"  ; then
    exit 1
fi

echo "Hello, ${name}!"

Рассмотрим подробнее некоторые его части:
shF_PATH_TO_LIB="./shell-framework/lib"
source ${shF_PATH_TO_LIB}/base
shF_PATH_TO_LIB определяет, где находится библиотека. Нужно обязательно правильно проинициализировать эту переменную, потому что ее используют все библиотечные скрипты, чтобы подгружать друг-друга. К сожалению, из-за особенностей bash мне пришлось зарезервировать префикс "shF_" для всех переменных этой библиотеки и, понятное дело, использовать его в ваших скриптах не рекомендуется, если вы хотите избежать коллизий.
Строка с source подключает файл base, который по сути является набором стандартных подключений - чтобы не писать каждый раз "подключить библиотеку help, config и тд", я сделал этот файл.
setDescription "Hello world is example script."
Определяет краткое описание нашего скрипта. Оно будет использовано потом при показе help-а
addOption "name" "World" "-n" "--name" "${shF_OPTION_WITH_VALUE}" "you can define your name as ${shF_COMMON_PARAMETER_NAME}." "" "110"
Добавляет новый параметр(option) со следующими атрибутами:
  • переменная name, которая будет хранить его значение
  • значение по умолчанию: World
  • краткая форма -n
  • длинная --name
  • после него ожидается значение (${shF_OPTION_WITH_VALUE}),
  • описание: "you can define your name as ${shF_COMMON_PARAMETER_NAME}."
  • При распечатке в help-е список параметров будет отсортирован в соответствии с весом заданным при инициализации - 110.
if ! initConfig "$@"  ; then
    exit 1
fi
initConfig "$@" пытается "распарсить" строку параметров в соответствии с конфигурацией.
Давайте теперь посмотрим, как он будет работать:
$ ./helloWorld.sh
Hello, World!
$ ./helloWorld.sh -n Nick
Hello, Nick!
$ ./helloWorld.sh --name Nick
<current time="">: ERROR ./shell-framework/lib/opts.parseOpts:122 The value for option (--name) must be defined.
В последнем примере, скрипт нам сообщает, что после параметра (--name) ожидается значение. Дело в том, что стандарта на то, как должны быть оформлены параметры и значения нет и я избрал следующий подход - короткие имена отделяются от значений пробельными символами, а длинные - символом "=", что, по-моему, повышает наглядность:
$ ./helloWorld.sh --name=Nick
Hello, Nick!
Ну хорошо, а где же help? Ведь наш первый скрипт к этому моменту уже умел распечатывать подсказку по самому себе. Ну что же - давайте попробуем:
$ ./helloWorld.sh -h 
Hello world is example script.
Usage: ./helloWorld.sh [option(s)] [command]
Options:
        -h|--help       print this help
        --help-options  print detailed description of options using
        -l <parameter>|--shF_logLevel=<parameter>   define the current log level (<parameter>). You can use the following number - 0(shF_EVERYTHING), 1(shF_DEBUG), 2(shF_INFO), 3(shF_HIGH), 4(shF_WARN), 5(shF_ERROR). Config name is shF_currentLogLevel. Default value is "2".
        --logFile=<parameter>     define the <parameter> as log stream. You can use stdError, stdOut or file name. Config name is shF_currentLogFile. Default value is "stdError".
        -c <parameter>|--config=<parameter> define the <parameter> as property file which will be loaded.
        -p|--print-config       print the current configuration.
        -n <parameter>|--name=<parameter>   you can define your name as <parameter>. Config name is name. Default value is "World"
Т.е. у нас уже есть не только автогенерация help, но и множество дополнительных параметров(опций) - уровень логгинга, использование лог файла, поддержка конфигурационных файлов. Про логи, я пожалуй здесь рассказывать не буду, а вот работу конфигурационной системы вполне можно описать.
Когда мы создаем параметр, то мы задаем так же и имя переменной, которая будет проинициализирована значением этого параметра. Порядок инициализации переменной будет следующим:
  1. значение по умолчанию, если оно есть
  2. значение из конфигурационного файла(далее "конфиг"), если оно есть
  3. значение командной строки,  если оно есть
Так что если мы создадим файл config.txt с таким содержимым
name="Config"
то вполне можем его использовать:
$ ./helloWorld.sh
Hello, World!
$ ./helloWorld.sh --config=./config.txt
Hello, Config!
$ ./helloWorld.sh -c ./config.txt --name=Virens
Hello, Virens!
На самом деле скрипт и сам умеет генерировать свои конфиги
$ ./helloWorld.sh -p
#define the current log level (). You can use the following number - 0(shF_EVERYTHING), 1(shF_DEBUG), 2(shF_INFO), 3(shF_HIGH), 4(shF_WARN), 5(shF_ERROR). Default value is "2".
#shF_currentLogLevel="2"

#define the  as log stream. You can use stdError, stdOut or file name. Default value is "stdError".
#shF_currentLogFile="stdError"

#you can define your name as . Default value is "World".
#name="World"

Вам достаточно просто перенаправить этот поток в файл, чтобы получить новый конфиг. Скрипт генерит конфиг с комментариями перед каждой переменной (они начинаются со знака #), которые вы можете сами определить при создании нового параметра (если не были заданы, то будет использована та же строка, что и для help). Как вы видите, все строки в этом config файле закомментированы - это произошло потому, что ни одна из переменных не отличалась на момент генерации конфига  от своего значения по умолчанию. Можно определить параметры и в этом случае конфигурация будет несколько иной:
$ ./helloWorld.sh -p -n Olga
#define the current log level (). You can use the following number - 0(shF_EVERYTHING), 1(shF_DEBUG), 2(shF_INFO), 3(shF_HIGH), 4(shF_WARN), 5(shF_ERROR). Default value is "2".
#shF_currentLogLevel="2"

#define the  as log stream. You can use stdError, stdOut or file name. Default value is "stdError".
#shF_currentLogFile="stdError"

#you can define your name as . Default value is "World".
name="Olga"
Таким образом, вы можете записать текущие настройки в файл и использовать их в будущем.
Как же решается задача с новым параметром "-b|--bye" описанная ранее? Довольно просто - добавлением строки с новым параметром:
#!/bin/bash

shF_PATH_TO_LIB="./shell-framework/lib"
source ${shF_PATH_TO_LIB}/base

setDescription "Hello world is example script."

#addOption <name> [defaultValue] [shortForm] [longForm] [type] [shortDescription] [longDescription] [priority] [notForConfig]]
#   type can be shF_SIMPLE_OPTION or shF_OPTION_WITH_VALUE
addOption "name" "World" "-n" "--name" "${shF_OPTION_WITH_VALUE}" "you can define your name as ${shF_COMMON_PARAMETER_NAME}." "" "110"
addOption "greeting" "Hello" "-b" "--bye" "${shF_OPTION_WITH_VALUE}" "you can define greeting word as ${shF_COMMON_PARAMETER_NAME}." "" "120"


if ! initConfig "$@"  ; then
        exit 1
    fi

echo "${greeting}, ${name}!"
$ ./helloWorld.sh -b "That's all" -n "folks"
That's all, folks!
Заключение
Эта библиотека не ограничивается работой с параметрами коммандной строки, конфигурационными файлами и автоматической генерации описания вашего скрипта. В ней так же есть ассоциативные массивы (из не было в bash до версии 4), некоторое расширение работы с trap и unit тестирование. Если будет желание, я расскажу как работать и с этими разделами.
Конечно же у нее есть недостатки - например скорость работы или сложность кода - все же bash плохо приспособлен для написания таких объемных скриптов. Но, несмотря на это, я ее уже успешно использую и буду рад, если кто-то найдет ее полезной.

3 comments:

Anonymous said...

Is it still available? It seems that links are broken.

Beggy said...

There are some issues with hosting. If you like I can resend last version to you by mail

Anonymous said...

Yes, please - mail4me2k @ gmail.com. Thank you in advance.