Я пишу скрипты на bash. Это скорее ближе к хобби, хотя иногда нужно и по работе. Конечно, не каждый день, но довольно часто и уже довольно давно. Бывают ситуации, когда либо не хочется пользоваться другими script-языками или когда у заказчика нет специалиста который сможет поддержать ваши скрипты в будущем, если они написаны, например, на perl - в общем, у меня бывало много случаев, когда моими скриптами должен был пользоваться не только я, но и еще кто-то и написаны они должны были быть на bash, а не на чем-то ином. Проанализировав требования к command line interface (CLI) программе, я систематизировал их и написал библиотеку, о которой и пойдет речь ниже.
Что я ожидаю?
Итак, что же мне нужно от CLI программ?
- Параметры. CLI программа должна уметь работать с параметрами. Параметры могут быть короткими (-M) и длинными(--multi-volume) . Первые нужны непосредственно для ввода их в командной строке и должны быть краткими для скорости ввода, вторые предназначены для использования программы в других скриптах и потому должны быть минимально читаемыми.
- Ожидаемые параметры и поведение. На самом деле я знаю лишь один by default параметр - "-h" и его длинный синоним "--help". Скрипт, будучи запущен с таким параметром, должен распечатать минимальную справку о себе и своих параметрах и закончить выполнение. (Как бы было прекрасно, когда, запуская на удаленной и мало знакомой машине скрипт, я мог бы быть уверен, что всегда смогу получить минимальное описание... :) )
- Наличие конфигурационных файлов. Конечно, это не обязательно, но, иногда, крайне желательно, в особенности для скриптов с дюжиной и более возможных параметров.
Еще один 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}/baseshF_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 fiinitConfig "$@" пытается "распарсить" строку параметров в соответствии с конфигурацией.
Давайте теперь посмотрим, как он будет работать:
$ ./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, но и множество дополнительных параметров(опций) - уровень логгинга, использование лог файла, поддержка конфигурационных файлов. Про логи, я пожалуй здесь рассказывать не буду, а вот работу конфигурационной системы вполне можно описать.
Когда мы создаем параметр, то мы задаем так же и имя переменной, которая будет проинициализирована значением этого параметра. Порядок инициализации переменной будет следующим:
- значение по умолчанию, если оно есть
- значение из конфигурационного файла(далее "конфиг"), если оно есть
- значение командной строки, если оно есть
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 (Вам достаточно просто перенаправить этот поток в файл, чтобы получить новый конфиг. Скрипт генерит конфиг с комментариями перед каждой переменной (они начинаются со знака #), которые вы можете сами определить при создании нового параметра (если не были заданы, то будет использована та же строка, что и для help). Как вы видите, все строки в этом config файле закомментированы - это произошло потому, что ни одна из переменных не отличалась на момент генерации конфига от своего значения по умолчанию. Можно определить параметры и в этом случае конфигурация будет несколько иной:). 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"
$ ./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:
Is it still available? It seems that links are broken.
There are some issues with hosting. If you like I can resend last version to you by mail
Yes, please - mail4me2k @ gmail.com. Thank you in advance.
Post a Comment