Книга: Сценарии командной оболочки. Linux, OS X и Unix. 2-е издание
Назад: Глава 2. Усовершенствование пользовательских команд
Дальше: Глава 4. Тонкая настройка Unix

Глава 3. Создание утилит

Одна из основных целей создания сценариев командной оболочки — перенести сложные команды в файл, где их легко воспроизвести и изменить. Поэтому неудивительно, что на протяжении всей книги рассматриваются пользовательские команды. Но удивительно, что нам не требуется писать обертки для каждой отдельной команды в системах Linux, Solaris и OS X.

Linux/Unix — единственная из основных операционных систем, где можно решить, что флаги по умолчанию не отвечают вашим потребностям, и исправить положение несколькими нажатиями клавиш или сымитировать поведение понравившейся утилиты из другой операционной системы, определив псевдоним или написав сценарий длиной в десяток строк. Именно это делает систему Unix такой дружелюбной, и именно это вдохновило нас написать книгу, которую вы держите в руках!

№ 22. Утилита для напоминания

В распоряжении пользователей Windows и Mac уже много лет имеются превосходные и простые утилиты, такие как Stickies, позволяющие сохранять короткие заметки и выводить напоминания на экран. Они прекрасно подходят для быстрой записи телефонных номеров или другой информации. К сожалению, в командной строке Unix нет аналогичной программы для создания заметок, но эту проблему легко решить парой сценариев.

Первый сценарий, remember (приводится в листинге 3.1), позволяет сохранить заметку в общем файле rememberfile в домашнем каталоге. Если вызвать этот сценарий без аргументов, он будет читать стандартный ввод, пока не встретит символ конца файла (^D), который вводится комбинацией ctrl-D. Если вызвать сценарий с аргументами, он запишет их прямо в файл с данными.

Вторая половина описываемой двоицы — remindme, сопутствующий сценарий, представленный в листинге 3.2, который либо выводит все содержимое файла rememberfile, когда запускается без аргументов, либо отображает результаты поиска, используя аргументы как шаблон.

Код

Листинг 3.1. Сценарий remember

  #!/bin/bash

  # remember -- простой блокнот для записи заметок из командной строки

  rememberfile="$HOME/.remember"

  if [ $# -eq 0 ] ; then

    # Предложить пользователю ввести заметку и добавить ее в конец

    #   файла rememberfile.

    echo "Enter note, end with ^D: "

    cat - >> $rememberfile

  else

    # Записать в конец файла .remember все полученные аргументы.

    echo "$@" >> $rememberfile

  fi

  exit 0

В листинге 3.2 приводится сопутствующий сценарий remindme.

Листинг 3.2. Сценарий remindme, сопутствующий сценарию remember из листинга 3.1

#!/bin/bash

# remindme -- ищет в файле с данными совпадения с заданным шаблоном или, если

#   запускается без аргументов, выводит все содержимое файла

rememberfile="$HOME/.remember"

if [ ! -f $rememberfile ] ; then

  echo "$0: You don't seem to have a .remember file. " >&2

  echo "To remedy this, please use 'remember' to add reminders" >&2

  exit 1

fi

if [ $# -eq 0 ] ; then

  # Вывести все содержимое rememberfile, если критерии поиска не заданы.

  more $rememberfile

else

  # Иначе выполнить поиск в файле по заданному критерию и вывести

  #   результаты.

  grep -i -- "$@" $rememberfile | ${PAGER:-more}

fi

exit 0

Как это работает

Сценарий remember в листинге 3.1 может действовать как интерактивная программа, предлагающая пользователю ввести текст заметки для запоминания, или как команда, сохраняющая свои аргументы командной строки. На случай, если пользователь запустит сценарий без аргументов, мы предусмотрели одну хитрость. После вывода сообщения с предложением ввести заметку, мы вызываем команду cat, чтобы прочитать ввод пользователя :

cat - >> $rememberfile

В предыдущих главах нам доводилось использовать команду read, чтобы получить ввод пользователя. Здесь же команда cat читает текст из stdin (дефис - в команде является коротким обозначением stdin или stdout, в зависимости от контекста), пока пользователь не нажмет комбинацию ctrl-D, которая сообщит утилите cat о завершении файла. После этого cat выведет текст, прочитанный из stdin, и добавит его в конец файла rememberfile.

Однако, если сценарий запустить с аргументами, он просто добавит их все в конец rememberfile .

Сценарий remindme в листинге 3.2 не может работать в отсутствие файла rememberfile, поэтому в самом начале, перед попыткой что-либо сделать, он проверяет его наличие. Если файл отсутствует, сценарий завершается с выводом сообщения о причине остановки.

Если сценарий запущен без аргументов, предполагается, что пользователь просто захотел увидеть содержимое rememberfile. Использование утилиты more позволяет организовать постраничный просмотр файла rememberfile .

Если сценарий запущен с аргументами, вызывается утилита grep, чтобы найти совпадения с указанным шаблоном в rememberfile без учета регистра символов, а затем результаты выводятся с помощью утилиты постраничного просмотра .

Запуск сценария

Чтобы воспользоваться утилитой remindme, сначала нужно добавить несколько заметок в файл rememberfile, запустив сценарий remember, как показано в листинге 3.3. После этого можно с помощью remindme выполнить поиск в получившейся базе данных, передав сценарию искомый шаблон.

Результаты

Листинг 3.3. Тестирование сценария remember

$ remember Southwest Airlines: 800-IFLYSWA

$ remember

Enter note, end with ^D:

Find Dave’s film reviews at http://www.DaveOnFilm.com/

^D

Затем, когда спустя несколько месяцев вам потребуется вспомнить текст заметки, вы сможете сделать это с помощью reminder, как показано в лис­тинге 3.4.

Листинг 3.4. Тестирование сценария remindme

$ remindme film reviews

Find Dave’s film reviews at http://www.DaveOnFilm.com/

Или, если вы не можете быстро вспомнить номер телефона, из которого известны только цифры 800, листинг 3.5 демонстрирует, как выполнить поиск по частично известному номеру.

Листинг 3.5. Поиск номера телефона по известной последовательности цифр с помощью сценария remindme

$ remindme 800

Southwest Airlines: 800-IFLYSWA

Усовершенствование сценария

Конечно, не каждый сценарий демонстрирует чудеса программирования, но эти два сценария наглядно показывают, насколько легко расширить возможности командной строки Unix. Чтобы вы себе ни вообразили, наверняка найдется простой способ реализовать это.

В рассмотренные сценарии можно внести много разных усовершенствований. Например, ввести понятие записей: сценарий remember снабжает каждую запись датой и временем, многострочный текст сохраняется как одна запись, а поиск выполняется с использованием регулярных выражений. Такой подход позволит сохранять телефонные номера для групп людей и получать их, помня имя хотя бы одного члена группы. Если вы действительно задумаетесь над усовершенствованием сценария, можете добавить также функцию редактирования и удаления записей. Хотя, с другой стороны, файл ~/.remember легко отредактировать с помощью любого текстового редактора.

№ 23. Интерактивный калькулятор

Если вы помните, scriptbc (сценарий № 9 в главе 1) позволял вызывать калькулятор bc для вычисления выражений, передаваемых в виде аргументов командной строки. Следующий логичный шаг — написать сценарий-обертку, превращающую сценарий scriptbc в интерактивный калькулятор командной строки. Сценарий (приводится в листинге 3.6) получился действительно очень коротким! Но чтобы он заработал, не забудьте поместить сценарий scriptbc в один из каталогов из списка PATH.

Код

Листинг 3.6. Сценарий калькулятора командной строки calc

  #!/bin/bash

  # calc -- калькулятор командной строки, который действует как интерфейс к bc

  scale=2

  show_help()

  {

    cat << EOF

      In addition to standard math functions, calc also supports:

      a % b    remainder of a/b

      a ^ b    exponential: a raised to the b power

      s(x)     sine of x, x in radians

      c(x)     cosine of x, x in radians

      a(x)     arctangent of x, in radians

      l(x)     natural log of x

      e(x)     exponential log of raising e to the x

      j(n,x)   Bessel function of integer order n of x

      scale N  show N fractional digits (default = 2)

  EOF

  }

  if [ $# -gt 0 ] ; then

    exec scriptbc "$@"

  fi

  echo "Calc--a simple calculator. Enter 'help' for help, 'quit' to quit."

  /bin/echo -n "calc> "

  while read command args

  do

    case $command

    in

      quit|exit) exit 0                                ;;

      help|\?)   show_help                             ;;

      scale)     scale=$args                           ;;

      *)         scriptbc -p $scale "$command" "$args" ;;

    esac

    /bin/echo -n "calc> "

  done

  echo ""

  exit 0

Как это работает

Самая интересная часть в этом сценарии — инструкция while read , которая образует бесконечный цикл, отображающий приглашение calc>, пока пользователь не завершит работу вводом команды quit или признака конца файла (^D). Лаконичность сценария делает его особенно примечательным: сценарии командной строки должны быть простыми и практичными!

Запуск сценария

Сценарий использует scriptbc, калькулятор, который мы написали в сценарии № 9, поэтому, прежде чем запускать его, не забудьте поместить scriptbc в один из каталогов, перечисленных в списке PATH (или добавьте в сценарий переменную, например $scriptbc, содержащую полный путь к сценарию). По умолчанию данный сценарий выполняется в интерактивном режиме, предлагая пользователю вводить выражения для вычисления. Если запустить его с аргументами, эти аргументы будут переданы непосредственно сценарию scriptbc. В листинге 3.7 показаны оба способа использования сценария.

Результаты

Листинг 3.7. Тестирование сценария calc

$ calc 150 / 3.5

42.85

$ calc

Calc -- a simple calculator. Enter 'help’ for help, 'quit’ to quit.

calc> help

  In addition to standard math functions, calc also supports:

  a % b     remainder of a/b

  a ^ b     exponential: a raised to the b power

  s(x)      sine of x, x in radians

  c(x)      cosine of x, x in radians

  a(x)      arctangent of x, in radians

  l(x)      natural log of x

  e(x)      exponential log of raising e to the x

  j(n,x)    Bessel function of integer order n of x

  scale N   show N fractional digits (default = 2)

calc> 54354 ^ 3

160581137553864

calc> quit

$

ВНИМАНИЕ

Вычисления с вещественными числами, даже простые для человека, могут быть сложными для компьютеров. К сожалению, команда bc иногда реагирует на такие сложности самым неожиданным образом. Например, запустите bc и введите scale=0 и затем 7 % 3. А теперь попробуйте вычислить то же выражение с scale=4. В результате вы получите .0001, что, очевидно, является ошибкой.

Усовершенствование сценария

Все, что можно сделать в bc, можно сделать и в этом сценарии, с той лишь разницей, что calc не имеет памяти команд или состояний. Попробуйте добавить больше математических функций в справочное сообщение. Например, переменные obase и ibase позволяют определить основание системы счисления для вывода и ввода, однако из-за того, что сценарий не имеет памяти команд, вам придется изменить scriptbc (сценарий № 9 в главе 1) или научиться вводить настройки и выражения в одной строке.

№ 24. Преобразование температур

Сценарий в листинге 3.8 — первый в книге, выполняющий сложные математические вычисления, — может преобразовывать значение температуры в градусы Фаренгейта, Цельсия и Кельвина. В нем используется тот же трюк передачи выражений для вычисления калькулятору bc, что и в сценарии № 9, в главе 1.

Код

Листинг 3.8. Сценарий convertatemp

  #!/bin/bash

  # convertatemp -- сценарий преобразования температуры, позволяющий вводить

  #   температуру в градусах Фаренгейта, Цельсия или Кельвина и получать

  #   эквивалентную температуру в двух других шкалах

  if [ $# -eq 0 ] ; then

    cat << EOF >&2

  Usage: $0 temperature[F|C|K]

  where the suffix:

    F      indicates input is in Fahrenheit (default)

    C      indicates input is in Celsius

    K      indicates input is in Kelvin

  EOF

    exit 1

  fi

  unit="$(echo $1|sed -e 's/[-[:digit:]]*//g' | tr '[:lower:]' '[:upper:]' )"

  temp="$(echo $1|sed -e 's/[^-[:digit:]]*//g')"

  case ${unit:=F}

  in

  F ) # Градусы Фаренгейта в градусы Цельсия: Tc = (F - 32) / 1.8

    farn="$temp"

    cels="$(echo "scale=2;($farn - 32) / 1.8" | bc)"

    kelv="$(echo "scale=2;$cels + 273.15" | bc)"

    ;;

  C ) # Градусы Цельсия в градусы Фаренгейта: Tf = (9/5)*Tc+32

    cels=$temp

    kelv="$(echo "scale=2;$cels + 273.15" | bc)"

    farn="$(echo "scale=2;(1.8 * $cels) + 32" | bc)"

    ;;

  K ) # Градусы Цельсия = Kelvin - 273.15,

      #   затем использовать формулу градусы Цельсия -> градусы Фаренгейта

    kelv=$temp

    cels="$(echo "scale=2; $kelv - 273.15" | bc)"

    farn="$(echo "scale=2; (1.8 * $cels) + 32" | bc)"

    ;;

    *)

    echo "Given temperature unit is not supported"

    exit 1

  esac

  echo "Fahrenheit = $farn"

  echo "Celsius = $cels"

  echo "Kelvin = $kelv"

  exit 0

Как это работает

Большая часть сценария, вероятно, ясна, но давайте внимательнее рассмотрим математические вычисления и регулярные выражения, выполняющие основную работу. Многие плохо воспринимают математические формулы в таком виде, поэтому ниже приводится формула преобразования температуры по Фаренгейту в температуру по Цельсию:

275206.png.

Преобразованную в последовательность для передачи калькулятору bc и вычисления, эту формулу можно видеть в строке . Обратное преобразование из градусов Цельсия в градусы Фаренгейта реализовано в строке . Этот сценарий также переводит температуру из градусов Цельсия в градусы Кельвина . Он наглядно демонстрирует одну важную причину использовать мнемонические имена для переменных: код становится проще для чтения и отладки.

Еще один интересный аспект сценария — регулярные выражения, наиболее замысловатое из которых находится в строке . Понять эту строку проще, если развернуть операцию подстановки, выполняемую sed. Подстановка всегда имеет вид s/old/new/; в данном случае шаблон old описывает строку, начинающуюся с ноля или более дефисов (-), за которыми следует любое количество цифр (как вы помните, [:digit:] — это форма записи класса символов в ANSI, представляющего собой произвольную цифру, а звездочка (*) обозначает ноль или более вхождений предыдущего шаблона). Шаблон new описывает, чем заменить совпадение с шаблоном old, и в данном случае это всего лишь //, то есть пустой шаблон. Его удобно использовать, когда требуется просто удалить совпадения с шаблоном old. Данная операция подстановки фактически удаляет все цифры и дефисы так, что ввод -31f превращается в f и мы получаем возможность определить шкалу измерения температуры. После этого команда tr нормализует результат, преобразуя его в верхний регистр, то есть строка -31f, например, превращается в F.

Другое выражение sed выполняет противоположную операцию : оно удаляет все, что не является частью числа, используя оператор ^ для инвертирования совпадения с любым символом в классе [:digit:]. (В большинстве языков программирования инвертирование выполняет оператор !.) В результате получается значение для преобразования с применением соответствующей формулы.

Запуск сценария

Сценарий имеет простой и понятный формат входных данных, хотя и необычный для команд Unix. Сценарию передается числовое значение с необязательным символом в конце, обозначающим шкалу; в отсутствие этого символа предполагается, что значение температуры представлено в градусах Фаренгейта.

Чтобы узнать температуру в градусах Цельсия и Кельвина, эквивалентную 0° Фаренгейта, введите 0F. Чтобы узнать температуру в градусах Цельсия и Фаренгейта, эквивалентную 100° Кельвина, введите 100K. А чтобы узнать температуру в градусах Кельвина и Фаренгейта, эквивалентную 100° Цельсия, введите 100C.

Похожий прием использования односимвольного обозначения в конце мы увидим в главе 7, в сценарии № 60, который выполняет преобразования между валютами.

Результаты

В листинге 3.9 показано несколько примеров преобразования температур.

Листинг 3.9. Тестирование сценария convertatemp несколькими преобразованиями

$ convertatemp 212

Fahrenheit = 212

Celsius = 100.00

Kelvin = 373.15

$ convertatemp 100C

Fahrenheit = 212.00

Celsius = 100

Kelvin = 373.15

$ convertatemp 100K

Fahrenheit = -279.67

Celsius = -173.15

Kelvin = 100

Усовершенствование сценария

В сценарий можно добавить поддержку нескольких флагов, чтобы ограничить вывод единственным результатом. Например, команда convertatemp -c 100F выводила бы только значение в градусах Цельсия, эквивалентное 100° Фаренгейта. Это помогло бы также упростить использование данного сценария внутри других.

№ 25. Вычисление платежей по кредиту

Другой распространенный вид вычислений, который наверняка пригодится пользователям — оценка платежей по кредиту. Сценарий в листинге 3.10 помогает также ответить на вопрос: «Куда потратить премию?», — и еще один, связанный с ним: «Могу ли я наконец позволить себе купить новую Tesla?».

Формула вычисления платежей, основанная на сумме кредита, процентах и его продолжительности, выглядит непростой, тем не менее грамотное использование переменных может помочь обуздать этого математического зверя и сделать вычисления на удивление простыми и понятными.

Код

Листинг 3.10. Сценарий loancalc

  #!/bin/bash

  # loancalc -- По заданной сумме кредита, процентной ставке

  #   и продолжительности (в годах), вычисляет суммы платежей

  # Формула: M = P * ( J / (1 - (1 + J) ^ -N)),

  #   где P = сумма кредита, J = месячная процентная ставка, N = протяженность   #   (месяцев).

  # Обычно пользователи вводят P, I (годовая процентная ставка) и L (протяженность   #   в годах).

  . library.sh # Подключить библиотечный сценарий.

  if [ $# -ne 3 ] ; then

    echo "Usage: $0 principal interest loan-duration-years" >&2

    exit 1

  fi

P=$1 I=$2 L=$3

  J="$(scriptbc -p 8 $I / \( 12 \* 100 \) )"

  N="$(( $L * 12 ))"

  M="$(scriptbc -p 8 $P \* \( $J / \(1 - \(1 + $J\) \^ -$N\) \) )"

  # Выполнить необходимые преобразования значений:

  dollars="$(echo $M | cut -d. -f1)"

  cents="$(echo $M | cut -d. -f2 | cut -c1-2)"

  cat << EOF

  A $L-year loan at $I% interest with a principal amount of $(nicenumber $P 1 )

  results in a payment of \$$dollars.$cents each month for the duration of

  the loan ($N payments).

  EOF

  exit 0

Как это работает

Рассмотрение самих вычислений выходит за рамки этой книги, но обратите внимание, как сложную математическую формулу можно реализовать непосредственно в сценарии командной оболочки.

Другой способ выполнить все вычисления — передать один большой поток входных данных программе bc, потому что она поддерживает переменные. Однако возможность манипулировать промежуточными значениями внутри самого сценария доказывает, что он позволяет произвести часть вычислений без привлечения команды bc. Кроме того, деление формулы на несколько промежуточных вычислений упрощает отладку. Например, следующий код разбивает вычисленные месячные платежи на доллары и центы и гарантирует правильное форматирование денежных сумм:

dollars="$(echo $M | cut -d. -f1)"

cents="$(echo $M | cut -d. -f2 | cut -c1-2)"

Команда cut оказывается здесь особенно полезной . Вторая строка в этом коде извлекает из суммы месячного платежа ту часть, которая следует за десятичной точкой, и затем отсекает все, что следует за вторым символом. Если вы пожелаете округлить число до центов в большую сторону, просто прибавьте 0,005 к результату вычислений перед усечением центов до двух цифр.

Обратите внимание, как в строке командой . library.sh подключается библиотечный сценарий, созданный в главе 1, что обеспечивает доступность всех функций (в данном сценарии используется функция nicenumber() из главы 1).

Запуск сценария

Этот коротенький сценарий принимает три параметра: сумма кредита, процентная ставка и срок кредита (в годах).

Результаты

Представьте, что вы узнали о выходе новой модели Tesla Model S и вам интересно узнать, сколько придется заплатить, если купить ее в кредит. Стоимость модели Model S начинается примерно с 69 900 долларов, а ставка по кредиту составляет 4,75% годовых. Допустим, что у вас уже есть автомобиль, за который вы выручите 25 000 долларов на вторичном рынке, и вам остается добавить 44 900. Недолго думая, вы можете сравнить суммы выплат по четырех- и пятилетнему автокредиту, просто воспользовавшись сценарием, показанным в листинге 3.11.

Листинг 3.11. Тестирование сценария loancalc

$ loancalc 44900 4.75 4

A 4-year loan at 4.75% interest with a principal amount of 44,900

results in a payment of $1028.93 each month for the duration of

the loan (48 payments).

$ loancalc 44900 4.75 5

A 5-year loan at 4.75% interest with a principal amount of 44,900

results in a payment of $842.18 each month for the duration of

the loan (60 payments).

Если вы в состоянии потянуть выплаты по четырехлетнему автокредиту, вы погасите его быстрее, и общая сумма выплат (произведение суммы месячного платежа на количество месяцев) значительно уменьшится. Чтобы подсчитать экономию, можно воспользоваться интерактивным калькулятором из сценария № 23, как показано ниже:

$ calc '(842.18 * 60) - (1028.93 * 48)'

1142.16

1142,16 доллара — хорошая экономия, этих денег хватит на отличный ноутбук!

Усовершенствование сценария

Этот сценарий мог бы запрашивать необходимые данные при запуске без параметров. Еще более полезная версия сценария могла бы предлагать пользователю ввести любые три параметра из четырех (сумма кредита, процентная ставка, срок и сумма месячных платежей) и автоматически вычислять четвертое значение. В этом случае, зная, что вы способны выплачивать только 500 долларов в месяц и максимальная ставка по пятилетнему автокредиту составляет 6%, вы сумели бы определить максимальную сумму доступного для вас кредита. Подобные вычисления можно выполнять, реализовав поддержку разных флагов, которые пользователи передавали бы сценарию.

№ 26. Слежение за событиями

Следующая пара сценариев реализует простую программу-календарь, похожую на утилиту напоминания из сценария № 22. Первый сценарий, addagenda (представлен в листинге 3.12), позволяет определить событие, повторяющееся (в определенные дни недели, месяца или года) или однократное (в конкретный день, месяц и год). Все даты проверяются и сохраняются вместе с однострочным описанием события в файле .agenda, в домашнем каталоге пользователя. Второй сценарий, agenda (представлен в листинге 3.13), просматривает все сохраненные события и отыскивает запланированные на текущую дату.

Этот инструмент особенно удобно использовать для запоминания дней рождений и годовщин. Если вы забываете про важные события, приведенная ниже пара сценариев поможет вам избежать конфуза!

Код

Листинг 3.12. Сценарий addagenda

  #!/bin/bash

  # addagenda -- предлагает пользователю добавить новое событие для сценария agenda

  agendafile="$HOME/.agenda"

  isDayName()

  {

    # Возвращает 0, если все в порядке, 1 -- в случае ошибки.

    case $(echo $1 | tr '[[:upper:]]' '[[:lower:]]') in

      sun*|mon*|tue*|wed*|thu*|fri*|sat*) retval=0 ;;

      * ) retval=1 ;;

    esac

    return $retval

  }

  isMonthName()

  {

    case $(echo $1 | tr '[[:upper:]]' '[[:lower:]]') in

      jan*|feb*|mar*|apr*|may|jun*)  return 0 ;;

      jul*|aug*|sep*|oct*|nov*|dec*) return 0 ;;

      * ) return 1 ;;

    esac

  }

  normalize()

  {

    # Возвращает строку с первым символом в верхнем регистре

    #   и другими двумя -- в нижнем.

    /bin/echo -n $1 | cut -c1 | tr '[[:lower:]]' '[[:upper:]]'

    echo $1 | cut -c2-3| tr '[[:upper:]]' '[[:lower:]]'

  }

  if [ ! -w $HOME ] ; then

    echo "$0: cannot write in your home directory ($HOME)" >&2

    exit 1

  fi

  echo "Agenda: The Unix Reminder Service"

  /bin/echo -n "Date of event (day mon, day month year, or dayname): "

  read word1 word2 word3 junk

  if isDayName $word1 ; then

    if [ ! -z "$word2" ] ; then

      echo "Bad dayname format: just specify the day name by itself." >&2

      exit 1

    fi

    date="$(normalize $word1)"

  else

    if [ -z "$word2" ] ; then

      echo "Bad dayname format: unknown day name specified" >&2

      exit 1

    fi

    if [ ! -z "$(echo $word1|sed 's/[[:digit:]]//g')" ] ; then

      echo "Bad date format: please specify day first, by day number" >&2

        exit 1

    fi

    if [ "$word1" -lt 1 -o "$word1" -gt 31 ] ; then

      echo "Bad date format: day number can only be in range 1-31" >&2

      exit 1

    fi

    if [ ! isMonthName $word2 ] ; then

      echo "Bad date format: unknown month name specified." >&2

      exit 1

    fi

    word2="$(normalize $word2)"

    if [ -z "$word3" ] ; then

      date="$word1$word2"

    else

      if [ ! -z "$(echo $word3|sed 's/[[:digit:]]//g')" ] ; then

        echo "Bad date format: third field should be year." >&2

        exit 1

      elif [ $word3 -lt 2000 -o $word3 -gt 2500 ] ; then

        echo "Bad date format: year value should be 2000-2500" >&2

        exit 1

      fi

      date="$word1$word2$word3"

    fi

  fi

  /bin/echo -n "One-line description: "

  read description

  # Данные готовы к записи в файл

  echo "$(echo $date|sed 's/ //g')|$description" >> $agendafile

  exit 0

Второй сценарий, в листинге 3.13, короче, но используется чаще.

Листинг 3.13. Сценарий agenda, сопутствующий сценарию addagenda из листинга 3.12

  #!/bin/sh

  # agenda -- сканирует файл .agenda в поисках записей, относящихся

  #   к текущей дате

  agendafile="$HOME/.agenda"

  checkDate()

  {

    # Создать значения по умолчанию для сопоставления с текущей датой.

    weekday=$1 day=$2 month=$3 year=$4

    format1="$weekday" format2="$day$month" format3="$day$month$year"

    # И выполнить поиск среди записей в файле...

    IFS="|" # Команда read автоматически разбивает

            #   прочитанные строки по символам в IFS.

    echo "On the agenda for today:"

    while read date description ; do

      if [ "$date" = "$format1" -o "$date" = "$format2" -o \

           "$date" = "$format3" ]

      then

        echo " $description"

      fi

    done < $agendafile

  }

  if [ ! -e $agendafile ] ; then

    echo "$0: You don't seem to have an .agenda file. " >&2

    echo "To remedy this, please use 'addagenda' to add events" >&2

    exit 1

  fi

  # Получить текущую дату...

  eval $(date '+weekday="%a" month="%b" day="%e" year="%G"')

  day="$(echo $day|sed 's/ //g’)" # Удалить возможные пробелы в начале.

  checkDate $weekday $day $month $year

  exit 0

Как это работает

Сценарии addagenda и agenda поддерживают три типа событий: еженедельные («каждую среду»), ежегодные («каждого 3 августа») и однократные («1 января 2017»). В процессе добавления записей в файл событий их даты нормализуются и сжимаются так, что 3 August превращается в 3Aug, а Thursday превращается в Thu. Эта операция выполняется функцией normalize в сценарии addagenda .

Данная функция отсекает все, что следует за третьим символом, и преобразует первый символ в верхний регистр, а два остальных — в нижний. Такой формат соответствует стандартным сокращенным названиям дней недели и месяцев в выводе команды date, что необходимо для правильной работы сценария agenda. Остальная часть сценария addagenda не содержит ничего сложного; большую его часть занимает проверка формата введенных данных.

Наконец, в строке , он сохраняет нормализованные данные в скрытый файл. Отношение кода, связанного с проверкой ошибок, к коду, выполняющему фактическую работу, довольно типично для хорошо написанных программ: проверка и первичная обработка входных данных позволят сделать уверенные предположения об их формате в последующих приложениях.

Сценарий agenda проверяет события, преобразуя текущую дату в три возможных строковых представления (день недели, число+месяц и день+месяц+год) . Затем он сравнивает каждую из этих строк с датами из записей в файле .agenda. Найденные совпадения выводятся на экран.

Самый, пожалуй, интересный прием в этой паре сценариев — использование команды eval для присваивания четырем переменным четырех значений, определяющих дату :

eval $(date "+weekday=\"%a\" month=\"%b\" day=\"%e\" year=\"%G\"")

Можно было бы получить значения по одному (например, weekday="$(date +%a)"), но в очень редких случаях этот способ дает ошибочные результаты, если в ходе выполнения четырех вызовов date произойдет смена даты, так что краткая форма с единственным вызовом предпочтительнее. Плюс, это просто круто выглядит.

Так как date может вернуть день как число с нежелательным начальным пробелом, следующая строка удаляет его. А теперь посмотрим, как все это работает!

Запуск сценария

Сценарий addagenda предлагает пользователю ввести дату нового события. Затем, если дата имеет допустимый формат, сценарий предлагает ввести однострочное описание события.

Сопутствующий сценарий agenda не имеет параметров и, когда вызывается, выводит список всех событий, запланированных на текущую дату.

Результаты

Чтобы увидеть, как работает эта пара сценариев, добавим несколько новых событий, как показано в листинге 3.14.

Листинг 3.14. Тестирование сценария addagenda и добавление нескольких событий

$ addagenda

Agenda: The Unix Reminder Service

Date of event (day mon, day month year, or dayname): 31 October

One-line description: Halloween

$ addagenda

Agenda: The Unix Reminder Service

Date of event (day mon, day month year, or dayname): 30 March

One-line description: Penultimate day of March

$ addagenda

Agenda: The Unix Reminder Service

Date of event (day mon, day month year, or dayname): Sunday

One-line description: sleep late (hopefully)

$ addagenda

Agenda: The Unix Reminder Service

Date of event (day mon, day month year, or dayname): march 30 17

Bad date format: please specify day first, by day number

$ addagenda

Agenda: The Unix Reminder Service

Date of event (day mon, day month year, or dayname): 30 march 2017

One-line description: Check in with Steve about dinner

Теперь с помощью сценария agenda можно быстро вспомнить, что должно произойти сегодня, как показано в листинге 3.15.

Листинг 3.15. Использование сценария agenda для поиска событий на сегодня

$ agenda

On the agenda for today:

  Penultimate day of March

  sleep late (hopefully)

  Check in with Steve about dinner

Обратите внимание, что даты в совпавших событиях представлены в форматах: день недели, число+месяц и день+месяц+год. Для полноты картины в листинге 3.16 показано содержимое файла .agenda со всеми дополнительными записями:

Листинг 3.16. Содержимое файла .agenda со всеми записями

$ cat ~/.agenda

14Feb|Valentine’s Day

25Dec|Christmas

3Aug|Dave’s birthday

4Jul|Independence Day (USA)

31Oct|Halloween

30Mar|Penultimate day of March

Sun|sleep late (hopefully)

30Mar2017|Check in with Steve about dinner

Усовершенствование сценария

Этот сценарий лишь слегка затронул сложную и интересную тему. Было бы неплохо включить в него возможность заглядывать на несколько дней вперед, добавив в сценарий agenda арифметические операции с датой. Если в системе используется GNU-версия команды date, выполнить такие операции будет проще простого. Если нет, тогда для операций с датой средствами командной оболочки придется написать довольно сложный код. Далее в книге мы еще вернемся к арифметике с датами, особенно в сценариях № 99, № 100 и № 101 в главе 15.

В качестве еще одного простого усовершенствования в сценарий agenda можно было бы добавить вывод сообщения «Nothing scheduled for today» («На сегодня ничего не запланировано») при отсутствии совпадений с текущей датой, вместо сбивающего с толку сообщения «On the agenda for today:» («В списке событий сегодня:»), за которым ничего не следует.

Этот сценарий можно было бы использовать на компьютере с ОС Unix для вывода общесистемных напоминаний о таких событиях, как запланированное создание резервных копий, корпоративные праздники и дни рождений сотрудников. Для этого нужно сначала установить на компьютеры пользователей сценарий agenda и убедиться, что общий файл .agenda доступен только для чтения. А затем добавить вызов сценария agenda в файл .login каждого пользователя или в аналогичный файл, запускаемый в момент входа.

ПРИМЕЧАНИЕ

Просто удивительно, насколько сильно могут различаться реализации date в разных системах Unix и Linux, поэтому, попробовав реализовать что-то более сложное со своей командой date и потерпев неудачу, загляните в страницу справочного руководства man, чтобы увидеть, поддерживает ли она то, чего вы желаете добиться.

Назад: Глава 2. Усовершенствование пользовательских команд
Дальше: Глава 4. Тонкая настройка Unix