Книга: Сценарии командной оболочки. Linux, OS X и Unix. 2-е издание
Назад: Глава 1. Отсутствующая библиотека
Дальше: Глава 3. Создание утилит

Глава 2. Усовершенствование пользовательских команд

Типичная система Unix или Linux по умолчанию включает сотни команд, которые, с учетом многообразия флагов и способов сочетания команд посредством каналов, дают миллионы разных вариантов работы в командной строке.

Прежде чем двинуться дальше, взгляните на листинг 2.1, в котором приводится премиальный сценарий, подсчитывающий количество команд, доступных в списке каталогов PATH.

Листинг 2.1. Подсчет количества выполняемых и невыполняемых файлов в текущем списке PATH

#!/bin/bash

# Подсчет количества команд: простой сценарий для подсчета количества выполняемых

#   команд в каталогах из списка PATH

IFS=":"

count=0 ; nonex=0

for directory in $PATH ; do

  if [ -d "$directory" ] ; then

    for command in "$directory"/* ; do

      if [ -x "$command" ] ; then

        count="$(( $count + 1 ))"

      else

        nonex="$(( $nonex + 1 ))"

      fi

    done

  fi

done

echo "$count commands, and $nonex entries that weren't executable"

exit 0

Этот сценарий подсчитывает не просто файлы, а выполняемые файлы, и может использоваться для оценки количества команд и невыполняемых файлов в каталогах из списка PATH в разных системах (табл. 2.1).

Таблица 2.1. Типичное количество команд в разных ОС

Операционная система

Команд

Невыполняемых файлов

Ubuntu 15.04 (включая все библиотеки для разработки)

3156

5

OS X 10.11 (со всеми установленными инструментами для разработки)

1663

11

FreeBSD 10.2

954

4

Solaris 11.2

2003

15

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

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

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

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

№ 14. Форматирование длинных строк

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

Однако в некоторых системах Unix команда fmt отсутствует. В особенности это относится к устаревшим системам, часто имевшим минимальную реализацию.

Как оказывается, команда nroff, входившая в состав Unix с самого начала, является сценарием-оберткой и может использоваться для переноса длинных строк и заполнения коротких строк для их выравнивания, как показано в листинге 2.2.

Код

Листинг 2.2. Сценарий fmt для форматирования длинных текстовых строк

  #!/bin/bash

  # fmt -- утилита форматирования текста, действующая как обертка для nroff

  #   Добавляет два флага: -w X, для задания ширины строк,

  #   и -h, для расстановки переносов и улучшения выравнивания

  while getopts "hw:" opt; do

    case $opt in

      h ) hyph=1           ;;

      w ) width="$OPTARG"  ;;

    esac

  done

  shift $(($OPTIND - 1))

  nroff << EOF

  .ll ${width:-72}

  .na

  .hy ${hyph:-0}

  .pl 1

  $(cat "$@")

  EOF

  exit 0

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

Этот короткий сценарий реализует поддержку двух дополнительных флагов: -w X, для ограничения ширины строк X символами (по умолчанию 72), и -h, разрешающий разрывать слова и расставлять переносы. Обратите внимание на проверку флагов в . Цикл while вызывает getopts, чтобы прочитать каждый параметр, переданный сценарию, а внутренний блок case решает, что делать с ними. После анализа флагов сценарий вызывает shift в строке , чтобы отбросить проанализированные параметры, для чего используется переменная $OPTIND (хранящая индекс следующего аргумента, который должна была бы прочитать функция getopts), и оставляет прочие аргументы для последующей обработки.

В сценарии также используется встроенный документ (обсуждался в сценарии № 9, в главе 1) — особый блок кода, который можно использовать для передачи нескольких строк на вход команды. Используя это удобное средство, сценарий в передает сценарию nroff все команды, необходимые для получения желаемого результата. В этом документе используется типичный для bash прием подстановки значения вместо неопределенной переменной , чтобы передать разумное значение по умолчанию, если пользователь не указал свое. Наконец, сценарий вызывает команду cat с именами файлов, подлежащих обработке. Для выполнения поставленной задачи вывод команды cat передается команде nroff . Этот прием часто будет встречаться в данной книге.

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

Этот сценарий можно запустить непосредственно из командной строки, но вероятнее всего он станет частью внешнего конвейера, запускаемого редактором, таким как vi или vim (например, !}fmt), для форматирования абзаца текста.

Результаты

Команда в листинге 2.3 разрешает расстановку переносов и задает максимальную ширину 50 символов.

Листинг 2.3. Форматирование текста с помощью сценария fmt путем расстановки переносов и ограничения ширины текста 50 символами

$ fmt -h -w 50 014-ragged.txt

So she sat on, with closed eyes, and half believed

herself in Wonderland, though she knew she had but

to open them again, and all would change to dull

reality--the grass would be only rustling in the

wind, and the pool rippling to the waving of the

reeds--the rattling teacups would change to tin-

kling sheep-bells, and the Queen’s shrill cries

to the voice of the shepherd boy--and the sneeze

of the baby, the shriek of the Gryphon, and all

the other queer noises, would change (she knew) to

the confused clamour of the busy farm-yard--while

the lowing of the cattle in the distance would

take the place of the Mock Turtle’s heavy sobs.

Сравните содержимое в листинге 2.3 (обратите внимание, как был выполнен перенос слова tinkling, выделенного жирным в строках 6 и 7) с выводом в листинге 2.4, полученным с использованием ширины по умолчанию и запрещенными переносами.

Листинг 2.4. Форматирование по умолчанию без переносов, осуществляемое сценарием fmt

$ fmt 014-ragged.txt

So she sat on, with closed eyes, and half believed herself in

Wonderland, though she knew she had but to open them again, and all

would change to dull reality--the grass would be only rustling in the

wind, and the pool rippling to the waving of the reeds--the rattling

teacups would change to tinkling sheep-bells, and the Queen’s shrill

cries to the voice of the shepherd boy--and the sneeze of the baby, the

shriek of the Gryphon, and all the other queer noises, would change (she

knew) to the confused clamour of the busy farm-yard--while the lowing of

the cattle in the distance would take the place of the Mock Turtle’s

heavy sobs.

№ 15. Резервное копирование файлов при удалении

Одна из распространенных проблем, с которыми часто сталкиваются пользователи Unix, — сложность восстановления удаленных по ошибке файлов или каталогов. В Unix нет приложения, такого же удобного, как Undelete 360, WinUndelete или утилита для OS X, которое позволяло бы просматривать и восстанавливать удаленные файлы щелчком на кнопке. Как только вы нажмете клавишу enter после ввода команды rm filename, файл станет историей.

Чтобы решить эту проблему, нужно организовать тайное и автоматическое архивирование файлов и каталогов в архив .deleted-files. Немного подумав, можно написать сценарий (представленный в листинге 2.5), который сделает все это почти незаметно для пользователя.

Код

Листинг 2.5. Сценарий newrm, копирующий файлы перед удалением с диска

  #!/bin/bash

  # newrm -- замена существующей команды rm.

  #   Этот сценарий предоставляет простую возможность восстановления, создавая и

  #   используя новый каталог в домашнем каталоге пользователя. Может обрабатывать

  #   каталоги и отдельные файлы. Если пользователь добавляет флаг -f, файлы

  #   удаляются БЕЗ архивирования.

  # Важное предупреждение: возможно, вам понадобится создать задание для cron или

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

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

  #   дисковое пространство!

  archivedir="$HOME/.deleted-files"

  realrm="$(which rm)"

  copy="$(which cp) -R"

  if [ $# -eq 0 ] ; then # Позволить 'rm’ вывести сообщение о порядке использования.

    exec $realrm # Our shell is replaced by /bin/rm.

  fi

  # Проверить все параметры на наличие флага '-f’

  flags=""

  while getopts "dfiPRrvW" opt

  do

    case $opt in

      f ) exec $realrm "$@"    ;; # exec позволяет покинуть сценарий немедленно.

      * ) flags="$flags -$opt" ;; # Другие флаги предназначены команде rm.

    esac

  done

  shift $(( $OPTIND - 1 ))

  # НАЧАЛО ОСНОВНОГО СЦЕНАРИЯ

  # =================

  # Гарантировать наличие каталога $archivedir.

  if [ ! -d $archivedir] ; then

    if [ ! -w $HOME ] ; then

      echo "$0 failed: can't create $archivedir in $HOME" >&2

      exit 1

    fi

    mkdir $archivedir

    chmod 700 $archivedir # Ограничить доступ к каталогу.

  fi

  for arg

  do

    newname="$archivedir/$(date "+%S.%M.%H.%d.%m").$(basename "$arg")"

    if [ -f "$arg" -o -d "$arg" ] ; then

      $copy "$arg" "$newname"

    fi

  done

  exec $realrm $flags "$@" # Текущий сценарий будет вытеснен командой realrm.

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

В этом сценарии есть много интересных аспектов, в основном связанных с необходимостью скрыть его работу от пользователя. Например, сценарий не генерирует сообщений об ошибках в ситуациях, когда обнаруживает, что не может продолжить работу; он просто позволяет команде realrm самой сгенерировать такое сообщение, вызывая (обычно) /bin/rm с иногда ошибочными параметрами. Вызов realrm производится с помощью команды exec, которая замещает текущий процесс новым, выполняющим указанную команду. Сразу после вызова команды exec realrm текущий сценарий фактически прекращает работу, и в вызывающую командную оболочку передается код возврата, генерируемый процессом realrm.

Поскольку сценарий втайне создает в домашнем каталоге пользователя новый каталог , он должен гарантировать, что хранимые в нем файлы не окажутся доступны для других только из-за неправильно настроенного значения umask. (Значение umask определяет привилегии доступа по умолчанию для создаваемых файлов и каталогов.) Чтобы избежать непреднамеренного открытия доступа к резервируемым файлам, сценарий вызывает в строке команду chmod, дающую право на доступ к каталогу только для текущего пользователя.

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

newname="$archivedir/$(date "+"%S.%M.%H.%d.%m").$(basename "$arg")"

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

Но зачем усложнять реализацию добавлением даты и времени в имя резервируемого файла? Чтобы дать возможность сохранять несколько копий удаляемого файла с одним и тем же именем. После архивирования файла сценарием нельзя будет отличить /home/oops.txt от /home/subdir/oops .txt иначе как по времени удаления. Если стирание одноименных файлов произойдет одновременно (или в течение одной секунды), резервные копии файлов, удаленных первыми, будут затерты. Для решения этой проблемы можно организовать добавление абсолютных путей к оригинальным файлам в имена резервных копий.

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

Чтобы установить сценарий, добавьте псевдоним — тогда при вводе команды rm действительно будет вызываться этот сценарий, а не команда /bin/rm. В командных оболочках bash и ksh псевдонимы определяются так:

alias rm=yourpath/newrm

Результаты

Результаты работы этого сценария преднамеренно скрыты (как показывает лис­тинг 2.6), так что обратим все внимание на каталог .deleted-files.

Листинг 2.6. Тестирование сценария newrm

$ ls ~/.deleted-files

ls: /Users/taylor/.deleted-files/: No such file or directory

$ newrm file-to-keep-forever

$ ls ~/.deleted-files/

51.36.16.25.03.file-to-keep-forever

Что и требовалось получить. Файл был удален из локального каталога и скрытно перемещен в каталог .deleted-files. Добавление префикса с временем удаления позволяет сохранять в каталоге одноименные файлы, удаленные в разное время, не затирая их.

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

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

newname="$archivedir/$(date "+"%S.%M.%H.%d.%m").$(basename "$arg")"

Можно изменить порядок следования компонентов в новом имени на противоположный, чтобы исходное имя файла следовало первым, а за ним — дата удаления в секундах. Далее, поскольку время измеряется с точностью до секунды, может так получиться, что при одновременном удалении одноименных файлов из разных каталогов (например, rm test testdir/test) произойдет затирание одной копии удаленного файла другой. Поэтому, как еще одно полезное усовершенствование, можно добавить в имя архивируемого файла его прежнее местоположение, чтобы в результате получить, например, файлы timestamp.test и timestamp.testdir.test, явно отличающиеся друг от друга.

№ 16. Работа с архивом удаленных файлов

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

Код

Листинг 2.7. Сценарий unrm для восстановления файлов из резервных копий

  #!/bin/bash

  # unrm -- отыскивает в архиве удаленных файлов требуемый файл или

  #   каталог. Если найдено более одного совпадения, выводит список

  #   результатов поиска, упорядоченных по времени, и предлагает

  #   пользователю выбрать нужный для восстановления.

  archivedir="$HOME/.deleted-files"

  realrm="$(which rm)"

  move="$(which mv)"

  dest=$(pwd)

  if [ ! -d $archivedir ] ; then

    echo "$0: No deleted files directory: nothing to unrm" >&2

    exit 1

  fi

  cd $archivedir

  # Если сценарий запущен без аргументов, просто вывести список

  #   удаленных файлов.

  if [ $# -eq 0 ] ; then

    echo "Contents of your deleted files archive (sorted by date):"

    ls -FC | sed -e 's/\([[:digit:]][[:digit:]]\.\)\{5\}//g' \

      -e 's/^/ /’

    exit 0

  fi

  # Иначе принять шаблон для поиска, предложенный пользователем.

  #   Проверить наличие в архиве нескольких совпадений с шаблоном

  matches="$(ls -d *"$1" 2> /dev/null | wc -l)"

  if [ $matches -eq 0 ] ; then

    echo "No match for \"$1\" in the deleted file archive." >&2

    exit 1

  fi

  if [ $matches -gt 1 ] ; then

    echo "More than one file or directory match in the archive:"

    index=1

    for name in $(ls -td *"$1")

    do

      datetime="$(echo $name | cut -c1-14| \

        awk -F. '{ print $5"/"$4" at "$3":"$2":"$1 }')"

      filename="$(echo $name | cut -c16-)"

      if [ -d $name ] ; then

        filecount="$(ls $name | wc -l | sed 's/[^[:digit:]]//g')"

        echo " $index) $filename (contents = ${filecount} items," \

             " deleted = $datetime)"

      else

        size="$(ls -sdk1 $name | awk '{print $1}')"

        echo " $index) $filename (size = ${size}Kb, deleted = $datetime)"

      fi

      index=$(( $index + 1))

    done

    echo ""

    /bin/echo -n "Which version of $1 should I restore ('0' to quit)? [1] : "

    read desired

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

      echo "$0: Restore canceled by user: invalid input." >&2

      exit 1

    fi

    if [ ${desired:=1} -ge $index ] ; then

      echo "$0: Restore canceled by user: index value too big." >&2

      exit 1

    fi

    if [ $desired -lt 1 ] ; then

      echo "$0: Restore canceled by user." >&2

      exit 1

    fi

    restore="$(ls -td1 *"$1" | sed -n "${desired}p")"

    if [ -e "$dest/$1" ] ; then

      echo "\"$1\" already exists in this directory. Cannot overwrite." >&2

      exit 1

    fi

    /bin/echo -n "Restoring file \"$1\" ..."

    $move "$restore" "$dest/$1"

    echo "done."

    /bin/echo -n "Delete the additional copies of this file? [y] "

    read answer

    if [ ${answer:=y} = "y" ] ; then

      $realrm -rf *"$1"

      echo "Deleted."

    else

      echo "Additional copies retained."

    fi

  else

    if [ -e "$dest/$1" ] ; then

      echo "\"$1\" already exists in this directory. Cannot overwrite." >&2

      exit 1

    fi

    restore="$(ls -d *"$1")"

    /bin/echo -n "Restoring file \"$1\" ... "

    $move "$restore" "$dest/$1"

    echo "Done."

  fi

  exit 0

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

Первый фрагмент кода в , блок в условной инструкции if [$# -eq 0], выполняется, если сценарий запущен без аргументов. Он выводит содержимое архива удаленных файлов. Однако тут есть одна загвоздка: нам нужно вывести имена файлов без префикса со временем удаления, потому что он предназначен только для внутреннего использования. Префикс только ухудшил бы читаемость списка. Для решения этой задачи применяется команда sed в , которая удаляет первые пять вхождений шаблона «цифра цифра точка» из каждой строки в выводе команды ls.

Пользователь может указать в аргументе имя файла или каталога для восстановления. Следующий шаг в — проверка количества совпадений с именем, указанным пользователем.

Необычное применение вложенных двойных кавычек в этой строке (вокруг $1) позволяет команде ls находить совпадения с именами файлов, содержащими пробелы, а шаблонный символ * разрешает совпадения с именами, включающими произвольные префиксы с временем удаления. Последовательность 2> /dev/null нужна, чтобы скрыть любые сообщения об ошибках от пользователя, выводимые командой. С наибольшей вероятностью будет скрыто сообщение об ошибке «No such file or directory» («Нет такого файла или каталога»), которое выводит команда ls, когда не может найти файл с указанным именем.

При наличии нескольких совпадений с указанным именем файла или каталога выполняется самая сложная часть сценария — блок в инструкции if [ $matches -gt 1 ] , который выводит все результаты. Флаг -t в команде ls, вызываемой в главном цикле for, обеспечивает перебор файлов в архиве в обратном хронологическом порядке — от более новых к более старым, а вызов команды awk в преобразует префикс в имени файла в дату и время удаления в круглых скобках. В строке определяется размер файла в килобайтах, для чего вызывается команда ls с флагом -k.

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

Когда пользователь выберет одно из совпадений, команда в получит точное имя файла для восстановления. Эта команда чуть иначе использует sed. Здесь с помощью флага -n строчному редактору sed передается номер строки (${desired}) и команда p (print — печать), что позволяет быстро извлечь из потока ввода указанную строку. Хотите увидеть только строку с номером 37? Команда sed -n 37p сделает это.

Далее, в строке , сценарий unrm проверяет, не затрет ли он существующий файл, и затем восстанавливает файл или каталог вызовом команды /bin/mv. После этого в пользователю дается возможность удалить все остальные (вероятно, избыточные) копии файла, и сценарий завершается.

Обратите внимание, что команда ls с шаблоном *"$1" найдет все файлы, имена которых оканчиваются значением параметра $1, поэтому список с «совпавшими файлами» может содержать не только файл, который пользователь хотел бы восстановить. Например, если удаляемый каталог содержал файлы 11.txt и 111.txt, команда unrm 11.txt сообщит, что найдено несколько совпадений и вернет список с обоими файлами, 11.txt и 111.txt. На первый взгляд в этом нет ничего страшного, но как только пользователь выберет файл для восстановления (11.txt) и ответит утвердительно на предложение удалить другие копии, сценарий удалит также файл 111.txt. Такое поведение по умолчанию в некоторых случаях может оказаться нежелательным. Однако это легко исправить, использовав шаблон ??.??.??.??.??."$1", если в сценарии newrm сохранен формат префикса в именах копий.

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

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

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

Результаты

При запуске без аргументов сценарий выведет список всех файлов и каталогов в архиве удаленных файлов, как показано в листинге 2.8.

Листинг 2.8. При запуске без аргументов сценарий unrm выведет список файлов и каталогов, доступных для восстановления

$ unrm

Contents of your deleted files archive (sorted by date):

  detritus                  this is a test

  detritus                  garbage

Получив аргумент с именем файла, сценарий выведет больше информации о файлах, если найдет несколько совпадений с указанным именем, как показано в листинге 2.9.

Листинг 2.9. При запуске с единственным аргументом сценарий unrm попытается восстановить файл

$ unrm detritus

More than one file or directory match in the archive:

  1) detritus (size = 7688Kb, deleted = 11/29 at 10:00:12)

  2) detritus (size = 4Kb, deleted = 11/29 at 09:59:51)

Which version of detritus should I restore ('0' to quit)? [1] : 0

unrm: Restore canceled by user.

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

Используйте этот сценарий внимательно, потому что в нем не выполняется никаких проверок и отсутствуют всякие ограничения. Объем архива с удаленными файлами будет расти без всяких ограничений. Чтобы избежать исчерпания дискового пространства, создайте задание для cron, вызывающее команду find, для очистки удаленных файлов, с флагом -mtime, чтобы выявить файлы, остававшиеся невостребованными в течение нескольких недель. 14-дневного срока хранения в архиве, вероятно, будет вполне достаточно и для большинства пользователей, и для того, чтобы предотвратить исчерпание дискового пространства.

Можно также внести ряд других усовершенствований, которые сделают сценарий более дружественным для пользователя. Например, добавить флаг -l для восстановления последней (latest) копии и флаг -D для удаления дополнительных копий файла. Подумайте, какие еще флаги вы добавили бы, чтобы упростить работу со сценарием?

№ 17. Журналирование операций удаления файлов

Вместо архивирования удаляемых файлов иногда достаточно просто фиксировать факты удаления. В листинге 2.10 приводится сценарий, который журналирует вызовы команды rm в отдельном файле, ни о чем не извещая пользователя.

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

ПРИМЕЧАНИЕ

Обертки — мощная концепция, и в этой книге вы еще не раз встретитесь с ней.

Код

Листинг 2.10. Сценарий logrm

  #!/bin/bash

  # logrm -- журналирует все операции удаления файлов, если вызывается без флага -s

  removelog="/var/log/remove.log"

  if [ $# -eq 0 ] ; then

    echo "Usage: $0 [-s] list of files or directories" >&2

    exit 1

  fi

  if [ "$1" = "-s" ] ; then

    # Запрошена операция без журналирования...

    shift

  else

    echo "$(date): ${USER}: $@" >> $removelog

  fi

  /bin/rm "$@"

  exit 0

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

Первая условная инструкция в проверяет ввод пользователя и показывает сообщение, описывающее порядок использования сценария, если он вызван без аргументов. Затем, в строке , сценарий проверяет, не содержит ли аргумент $1 флаг -s; если содержит, сценарий пропустит операцию журналирования. В заключение сценарий записывает текущее время, имя пользователя и текст команды в файл $removelog , и передает свои параметры фактической программе /bin/rm .

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

Обычно при установке программ-оберток, таких как сценарий logrm, обертываемые команды переименовываются, а оберткам присваиваются имена оригинальных команд. Если вы решите пойти этим путем, убедитесь, что обертка вызывает переименованную программу, а не саму себя! Например, если вы переименовали /bin/rm в /bin/rm.old, а сценарий сохранили с именем /bin/rm, тогда в предпоследней строке сценария замените вызов /bin/rm на /bin/rm.old.

Как вариант, можно определить псевдоним, чтобы заменить стандартный вызов rm вызовом команды logrm:

alias rm=logrm

В любом случае вам потребуются права доступа к каталогу /var/log на выполнение и запись, что может не соответствовать настройкам системы по умолчанию.

Результаты

Давайте создадим несколько файлов, удалим их и затем заглянем в журнал remove.log, как показано в листинге 2.11.

Листинг 2.11. Тестирование сценария logrm

$ touch unused.file ciao.c /tmp/junkit

$ logrm unused.file /tmp/junkit

$ logrm ciao.c

$ cat /var/log/remove.log

Thu Apr  6 11:32:05 MDT 2017: susan: /tmp/central.log

Fri Apr  7 14:25:11 MDT 2017: taylor: unused.file /tmp/junkit

Fri Apr  7 14:25:14 MDT 2017: taylor: ciao.c

Отлично! Обратите внимание, что пользователь susan удалил файл /tmp/central.log во вторник.

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

В сценарии может возникнуть проблема с правами доступа к файлу журнала. Файл remove.log либо будет доступен всем для записи, и тогда любой пользователь сможет удалить его содержимое, например, командой cat /dev/null > /var/log/remove.log, или он вообще не будет доступен для записи, и тогда сценарий просто не станет журналировать события. Можно, конечно, попробовать установить привилегию setuid, чтобы сценарий запускался с правами суперпользователя root, открывающими доступ к файлу журнала. Но тут есть две проблемы. Во-первых, это очень плохая идея! Никогда не давайте сценариям привилегию setuid! Она позволяет выполнить команду с правами определенного пользователя, независимо от того, кто ее вызывает, что ухудшает безопасность системы. Во-вторых, можно оказаться в ситуации, когда пользователи имеют право удалять свои файлы, но сценарий не дает сделать этого, потому что действующий идентификатор пользователя, установленный привилегией setuid, будет унаследован командой rm, что нарушит ее работу. Может возникнуть большой конфуз, если обнаружится, что пользователи не имеют права удалять даже свои собственные файлы!

Для файловых систем ext2, ext3 и ext4 (используются по умолчанию в большинстве дистрибутивов Linux), существует другое решение — с помощью команды chattr установить на файл журнала специальное разрешение «только для добавления», что сделает его доступным для записи всем пользователям без всякой опасности. Еще одно решение: записывать сообщения в системный журнал с помощью замечательной команды logger. Журналирование операций с командой rm в этом случае будет выглядеть так:

logger -t logrm "${USER:-LOGNAME}: $*"

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

ПРИМЕЧАНИЕ

Если вы решите использовать команду logger, прочитайте страницу справочного руководства syslogd(8), где написано, как убедиться, что ваша конфигурация не отбрасывает события с приоритетом user.notice. Обычно эта настройка находится в файле /etc/syslogd.conf.

№ 18. Вывод содержимого каталогов

Нам всегда казался бессмысленным один из аспектов команды ls: для каталогов она либо выводит список содержащихся в них файлов, либо показывает количество блоков по 1024 байта, необходимых для хранения данных. Ниже показано, как выглядит типичный элемент списка, возвращаемого командой ls -l:

drwxrwxr-x   2 taylor   taylor   4096 Oct 28 19:07 bin

Но в этой информации мало проку! В действительности нам хотелось бы знать, сколько файлов находится в каталоге. Именно это делает сценарий в листинге 2.12. Он генерирует многоколоночный список файлов и каталогов, показывая для файлов их размеры, а для каталогов — количество содержащихся в них записей.

Код

Листинг 2.12. Сценарий formatdir для получения более информативных списков каталогов

  #!/bin/bash

  # formatdir -- выводит содержимое каталога в дружественном и информативном виде

  # Обратите внимание: необходимо, чтобы "scriptbc" (сценарий № 9) находился

  #   в одном из каталогов, перечисленных в PATH, потому что он неоднократно

  #   вызывается в данном сценарии.

  scriptbc=$(which scriptbc)

  # Функция для преобразования размеров из KB в KB, MB или GB для

  #   большей удобочитаемости вывода

  readablesize()

  {

    if [ $1 -ge 1048576 ] ; then

      echo "$($scriptbc -p 2 $1 / 1048576)GB"

    elif [ $1 -ge 1024 ] ; then

      echo "$($scriptbc -p 2 $1 / 1024)MB"

    else

      echo "${1}KB"

    fi

  }

  #################

  ## КОД ОСНОВНОГО СЦЕНАРИЯ

  if [ $# -gt 1 ] ; then

    echo "Usage: $0 [dirname]" >&2

    exit 1

  elif [ $# -eq 1 ] ; then # Указан определенный каталог, не текущий?

    cd "$@"                # Тогда перейти в него.

    if [ $? -ne 0 ] ; then # Или выйти, если каталог не существует.

      exit 1

    fi

  fi

  for file in *

  do

    if [ -d "$file" ] ; then

      size=$(ls "$file" | wc -l | sed 's/[^[:digit:]]//g')

      if [ $size -eq 1 ] ; then

        echo "$file ($size entry)|"

      else

        echo "$file ($size entries)|"

      fi

    else

      size="$(ls -sk "$file" | awk '{print $1}')"

      echo "$file ($(readablesize $size))|"

    fi

  done | \

    sed 's/ /^^^/g' | \

    xargs -n 2 | \

    sed 's/\^\^\^/ /g' | \

    awk -F\| '{ printf "%-39s %-39s\n", $1, $2 }'

  exit 0

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

Одним из наиболее интересных элементов сценария является функция readablesize , которая принимает число в килобайтах и выводит килобайты, мегабайты или гигабайты, в зависимости от наиболее подходящей единицы измерения. Например, для файла очень большого размера она выведет 2.08GB вместо 2,083,364KB. Обратите внимание, что readablesize вызывается с применением конструкции $( ) :

echo "$file ($(readablesize $size))|"

Подоболочки автоматически наследуют все функции, объявленные в родительской оболочке, поэтому подоболочка, запущенная конструкцией $(), получит доступ к функции readablesize. Очень удобно.

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

Основная логика сценария занимается организацией вывода в две колонки, выровненные по вертикали. Одна из проблем, возникающих при этом, состоит в том, что пробелы в потоке вывода нельзя просто заменить символами перевода строки, потому что имена файлов и каталогов сами могут содержать пробелы. Чтобы решить эту проблему, сценарий в сначала замещает каждый пробел последовательностью из трех «крышек» (^^^). Затем с помощью команды xargs объединяет строки попарно, чтобы каждая пара строк превратилась в одну, разделенную вертикальной чертой на два поля. Наконец, в вызывается команда awk для вывода полей с требуемым выравниванием.

Обратите внимание, как просто в подсчитывается количество (не скрытых) элементов внутри каталога с помощью команд wc и sed:

size=$(ls "$file" | wc -l | sed 's/[^[:digit:]]//g')

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

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

Результаты

Листинг 2.13. Тестирование сценария formatdir

$ formatdir ~

Applications (0 entries)              Classes (4KB)

DEMO (5 entries)                      Desktop (8 entries)

Documents (38 entries)                Incomplete (9 entries)

IntermediateHTML (3 entries)          Library (38 entries)

Movies (1 entry)                      Music (1 entry)

NetInfo (9 entries)                   Pictures (38 entries)

Public (1 entry)                      RedHat 7.2 (2.08GB)

Shared (4 entries)                    Synchronize! Volume ID (4KB)

X Desktop (4KB)                       automatic-updates.txt (4KB)

bin (31 entries)                      cal-liability.tar.gz (104KB)

cbhma.tar.gz (376KB)                  errata (2 entries)

fire aliases (4KB)                    games (3 entries)

junk (4KB)                            leftside navbar (39 entries)

mail (2 entries)                      perinatal.org (0 entries)

scripts.old (46 entries)              test.sh (4KB)

testfeatures.sh (4KB)                 topcheck (3 entries)

tweakmktargs.c (4KB)                  websites.tar.gz (18.85MB)

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

С данным сценарием может возникнуть проблема, если в системе имеется пользователь, обожающий последовательности из трех «крышек» в именах файлов. Конечно, это весьма маловероятно — из 116 696 файлов в нашей тестовой системе Linux не нашлось ни одного, имя которого содержало хотя бы один символ крышки, — но если такое случится, вывод сценария окажется испорченным. Если вас волнует эта проблема, попробуйте преобразовывать пробелы в другую последовательность символов, еще менее вероятную в именах файлов. Четыре «крышки»? Пять?

№ 19. Поиск файлов по именам

В системах Linux имеется очень практичная команда locate, которая не всегда присутствует в других разновидностях Unix. Эта команда выполняет поиск в предварительно созданной базе данных имен файлов по регулярному выражению, указанному пользователем. Нужно быстро найти мастер-файл .cshrc? Ниже показано, как это сделать с помощью locate:

$ locate .cshrc

/.Trashes/501/Previous Systems/private/etc/csh.cshrc

/OS9 Snapshot/Staging Archive/:home/taylor/.cshrc

/private/etc/csh.cshrc

/Users/taylor/.cshrc

/Volumes/110GB/WEBSITES/staging.intuitive.com/home/mdella/.cshrc

Как видите, в системе OS X мастер-файл .cshrc находится в каталоге /private/etc. Версия locate, которую мы напишем, будет просматривать все файлы на диске и конструировать их внутренний список для быстрого поиска, где бы они ни находились — в корзине, на отдельном томе. В списке окажутся даже скрытые файлы, имена которых начинаются с точки. Как вы вскоре поймете, это одновременно достоинство и недостаток новой команды.

Код

Описываемый метод поиска файлов прост в реализации и предполагает создание двух сценариев. Первый (в листинге 2.14) создает базу данных всех имен файлов, вызывая команду find, а второй (в листинге 2.15) — просто вызывает команду grep для поиска в новой базе данных.

Листинг 2.14. Сценарий mklocatedb

  #!/bin/bash

  # mklocatedb -- создает базу данных для locate с использованием find.

  #   Для запуска этого сценария пользователь должен обладать привилегиями

  #   суперпользователя root.

  locatedb="/var/locate.db"

  if [ "$(whoami)" != "root" ] ; then

    echo "Must be root to run this command." >&2

    exit 1

  fi

  find / -print > $locatedb

  exit 0

Второй сценарий еще короче.

Листинг 2.15. Сценарий locate

#!/bin/sh

# locate -- выполняет поиск в базе данных по заданному шаблону

locatedb="/var/locate.db"

exec grep -i "$@" $locatedb

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

Сценарий mklocatedb должен запускаться с привилегиями суперпользователя root, чтобы он смог увидеть все файлы во всей системе, поэтому в строке он проверяет свои привилегии с помощью команды whoami. Однако запуск сценария с привилегиями root влечет за собой проблему безопасности, потому что, если каталог закрыт для рядовых пользователей, база данных locate не должна хранить информацию о нем или его содержимом. Эта проблема будет решена в главе 5, в новом, более безопасном сценарии locate, который учитывает правила защищенности и безопасности (сценарий № 39). А пока данный сценарий просто имитирует поведение стандартной команды locate из Linux, OS X и других дистрибутивов.

Не удивляйтесь, если сценарию mklocatedb потребуется несколько минут или больше; он выполняет обход всей файловой системы, что требует значительного времени, даже для систем среднего размера. Результат также может получиться весьма впечатляющим. В одной из наших тестовых систем OS X файл locate.db содержал более 1,5 миллиона записей и занимал 1874,5 Мбайт дискового пространства.

После создания базы данных сам сценарий locate выглядит очень простым; он просто вызывает команду grep со всеми аргументами, полученными от пользователя.

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

Прежде чем воспользоваться сценарием locate, необходимо запустить mklocatedb. Когда он завершит работу, вызов locate почти мгновенно будет находить совпадения в файловой системе с любыми заданными шаблонами.

Результаты

Сценарий mklocatedb не принимает аргументов и ничего не выводит, как показано в листинге 2.16.

Листинг 2.16. Запуск сценария mklocatedb с помощью команды sudo для получения привилегий root

$ sudo mklocatedb

Password:

...

Много времени спустя

...

$

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

$ ls -l /var/locate.db

-rw-r--r--  1 root  wheel  174088165 Mar 26 10:02 /var/locate.db

Теперь все готово к поиску файлов с помощью locate:

$ locate -i solitaire

/Users/taylor/Documents/AskDaveTaylor image folders/0-blog-pics/vista-search-solitaire.png

/Users/taylor/Documents/AskDaveTaylor image folders/8-blog-pics/windows-play-solitaire-1.png

/usr/share/emacs/22.1/lisp/play/solitaire.el.gz

/usr/share/emacs/22.1/lisp/play/solitaire.elc

/Volumes/MobileBackups/Backups.backupdb/Dave’s MBP/2014-04-03-163622/BigHD/Users/taylor/Documents/AskDaveTaylor image folders/0-blog-pics/vista-search-solitaire.png

/Volumes/MobileBackups/Backups.backupdb/Dave’s MBP/2014-04-03-163622/BigHD/Users/taylor/Documents/AskDaveTaylor image folders/8-blog-pics/windows-play-solitaire-3.png

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

$ locate '\.c$' | wc -l

1479

ПРИМЕЧАНИЕ

Обратите внимание на использованное здесь регулярное выражение. Команда grep требует экранировать символ точки (.), иначе она будет соответствовать любому одному символу. Кроме того, символ $ обозначает конец строки или, в данном случае, конец имени файла.

Приложив чуть больше усилий, мы могли бы передать каждый из найденных файлов команде wc и подсчитать общее количество строк исходного кода на языке C в системе, но это будет, пожалуй, перебор.

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

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

Еще одно усовершенствование, которое можно добавить в сценарий locate, — проверка и завершение с сообщением об ошибке при попытке запустить его без шаблона для поиска или в отсутствие файла базы данных locate.db. В текущей реализации сценарий просто выведет стандартное сообщение об ошибке от команды grep, которое может оказаться неинформативным для обычного пользователя. Еще более важной, как обсуждалось выше, является проблема безопасности: доступность рядовым пользователям имен всех файлов в системе, включая те, что должны быть скрыты от их глаз. Усовершенствования, касающиеся безопасности, мы добавим в сценарии № 39, в главе 5.

№ 20. Имитация других окружений: MS-DOS

Хотя в повседневной практике это едва ли понадобится, но с точки зрения освоения некоторых понятий командной оболочки будет интересно и показательно попробовать создать версии классических команд MS-DOS, таких как DIR, в виде сценариев, совместимых с Unix. Конечно, можно просто определить псевдоним и отобразить команду DIR в Unix-команду ls:

alias DIR=ls

Но такое отображение не имитирует фактического поведения команды; оно просто помогает забывчивым пользователям заучить новые названия команд. Если вам доводилось использовать древние способы взаимодействий с компьютером, вы наверняка вспомните, что флаг /W требует использовать широкий формат вывода. Но если передать флаг /W команде ls, она сообщит, что каталог /W не найден. Следующий сценарий DIR, представленный в листинге 2.17, напротив, написан так, что принимает и обрабатывает флаги, начинающиеся с символа слеша.

Код

Листинг 2.17. Сценарий DIR, имитирующий DOS-команду DIR в Unix

#!/bin/bash

# DIR -- имитирует поведение команды DIR в DOS, принимает некоторые

#   стандартные флаги команды DIR и выводит содержимое указанного каталога

function usage

{

cat << EOF >&2

  Usage: $0 [DOS flags] directory or directories

  Where:

    /D sort by columns

    /H show help for this shell script

    /N show long listing format with filenames on right

    /OD sort by oldest to newest

    /O-D sort by newest to oldest

    /P pause after each screenful of information

    /Q show owner of the file

    /S recursive listing

    /W use wide listing format

EOF

  exit 1

}

#####################

### ОСНОВНОЙ СЦЕНАРИЙ

postcmd=""

flags=""

while [ $# -gt 0 ]

do

  case $1 in

    /D      ) flags="$flags -x"  ;;

    /H      ) usage              ;;

    /[NQW]  ) flags="$flags -l"  ;;

    /OD     ) flags="$flags -rt" ;;

    /O-D    ) flags="$flags -t"  ;;

    /P      ) postcmd="more"     ;;

    /S      ) flags="$flags -s"  ;;

          * ) # Неизвестный флаг: возможно, признак конца команды DIR;

              #   поэтому следует прервать цикл while.

  esac

  shift       # Флаг обработан; проверить -- есть ли что-то еще.

done

# Обработка флагов завершена; теперь выполнить саму команду:

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

  ls $flags "$@" | $postcmd

else

  ls $flags "$@"

fi

exit 0

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

Этот сценарий демонстрирует, что инструкция case в языке командной оболочки фактически проверяет регулярное выражение. Как можно видеть в строке , DOS-флаги /N, /Q и /W отображаются в один и тот же Unix-флаг -l в окончательном вызове команды ls, и все это достигается с помощью простого регулярного выражения /[NQW].

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

Сохраните сценарий в файле с именем DIR (также желательно создать псевдоним dir=DIR, потому что командный интерпретатор DOS не различает регистр символов, в отличие от Unix). Теперь, вводя команду DIR с флагами, типичными для команды DIR в MS-DOS, пользователи будут получать осмысленные результаты (как показано в листинге 2.18), а не сообщение о том, что команда не найдена.

Результаты

Листинг 2.18. Тестирование сценария DIR со списком файлов

$ DIR /OD /S ~/Desktop

total 48320

7720 PERP - Google SEO.pdf             28816 Thumbs.db

    0 Traffic Data                      8 desktop.ini

    8 gofatherhood-com-crawlerrors.csv  80 change-lid-close-behavior-win7-1.png

   16 top-100-errors.txt                176 change-lid-close-behavior-win7-2.png

    0 $RECYCLE.BIN                      400 change-lid-close-behavior-win7-3.png

    0 Drive Sunshine                    264 change-lid-close-behavior-win7-4.png

   96 facebook-forcing-pay.jpg           32 change-lid-close-behavior-win7-5.png

10704 WCSS Source Files

Это список с содержимым указанного каталога, отсортированный в обратном хронологическом порядке, от более новых к более старым, и размерами файлов (для каталогов всегда выводится размер 0).

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

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

№ 21. Вывод времени в разных часовых поясах

Основное требование, предъявляемое к команде date, — отображение даты и времени для часового пояса, настроенного в системе. Но как быть пользователям в дальней поездке, пересекающим несколько часовых поясов? Или тем, у кого есть друзья и коллеги, живущие в других уголках планеты, и им хотелось бы знать, который сейчас час, например, в Касабланке, Ватикане или Сиднее?

Как оказывается, команда date в большинстве современных разновидностей Unix опирается в своей работе на базу данных часовых поясов. Обычно хранящаяся в каталоге /usr/share/zoneinfo эта база данных содержит информацию о более чем 600 регионах и соответствующих им смещениях относительно универсального скоординированного времени (Universal Coordinated Time, UTC — часто также называется средним временем по Гринвичу, Greenwich Mean Time или GMT). Команда date учитывает значение переменной окружения TZ, определяющей часовой пояс, которой можно присвоить любой регион из базы данных, например:

$ TZ="Africa/Casablanca" date

Fri Apr 7 16:31:01 WEST 2017

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

Большая часть сценария в листинге 2.19 связана с базой данных часовых поясов (которая обычно хранится в виде нескольких файлов в каталоге zonedir), точнее, с попыткой найти файл, соответствующий указанному шаблону. После обнаружения файла сценарий устанавливает найденный часовой пояс как текущий (в виде TZ="Africa/Casablanca" в данном примере) и с этими настройками вызывает команду date в подоболочке. Команда date определит часовой пояс по значению переменной TZ, и ей совершенно безразлично, хранит ли она временное значение или это тот часовой пояс, в котором вы проводите большую часть времени.

Код

Листинг 2.19. Сценарий timein для вывода времени в определенном часовом поясе

  #!/bin/bash

  # timein -- выводит текущее время в указанном часовом поясе или

  #   географической области. При вызове без аргументов выводит время

  #   UTC/GMT. Используйте слово "list", чтобы вывести список всех известных

  #   географических областей.

  #   Обратите внимание, что сценарий может находить совпадения с каталогами

  #   часовых поясов (областей), но действительными спецификациями являются

  #   только файлы (города).

  #   Ссылка на базу данных часовых поясов: http://www.twinsun.com/tz/tz-link.htm

  zonedir="/usr/share/zoneinfo"

  if [ ! -d $zonedir ] ; then

    echo "No time zone database at $zonedir." >&2

    exit 1

  fi

  if [ -d "$zonedir/posix" ] ; then

    zonedir=$zonedir/posix # Modern Linux systems

  fi

  if [ $# -eq 0 ] ; then

    timezone="UTC"

    mixedzone="UTC"

  elif [ "$1" = "list" ] ; then

    ( echo "All known time zones and regions defined on this system:"

      cd $zonedir

      find -L * -type f -print | xargs -n 2 | \

        awk '{ printf " %-38s %-38s\n", $1, $2 }'

    ) | more

    exit 0

  else

    region="$(dirname $1)"

    zone="$(basename $1)"

    # Заданный часовой пояс имеет прямое соответствие? Если да, можно продолжать.

    #   Иначе следует продолжить поиск. Для начала подсчитать совпадения.

    matchcnt="$(find -L $zonedir -name $zone -type f -print |

        wc -l | sed 's/[^[:digit:]]//g' )"

    # Проверить наличие хотя бы одного совпадения.

    if [ "$matchcnt" -gt 0 ] ; then

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

      if [ $matchcnt -gt 1 ] ; then

        echo "\"$zone\" matches more than one possible time zone record." >&2

        echo "Please use 'list' to see all known regions and time zones." >&2

        exit 1

      fi

      match="$(find -L $zonedir -name $zone -type f -print)"

      mixedzone="$zone"

    else # Может быть, удастся найти совпадение с регионом, а не

         #   с конкретным часовым поясом.

      # Первый символ в названии области/пояса преобразовать в верхний

      #   регистр, остальные -- в нижний

      mixedregion="$(echo ${region%${region#?}} \

                   | tr '[[:lower:]]' '[[:upper:]]')\

                   $(echo ${region#?} | tr '[[:upper:]]' '[[:lower:]]')"

      mixedzone="$(echo ${zone%${zone#?}} | tr '[[:lower:]]' '[[:upper:]]') \

                 $(echo ${zone#?} | tr '[[:upper:]]' '[[:lower:]]')"

      if [ "$mixedregion" != "." ] ; then

        # Искать только указанный часовой пояс в заданной области,

        #   чтобы позволить пользователям указывать уникальные пары, когда

        #   возможны другие варианты (например, "Atlantic").

        match="$(find -L $zonedir/$mixedregion -type f -name $mixedzone -print)"

      else

        match="$(find -L $zonedir -name $mixedzone -type f -print)"

      fi

      # Если найден файл, точно соответствующий заданному шаблону

      if [ -z "$match" ] ; then

        # Проверить, не является ли шаблон слишком неоднозначным.

        if [ ! -z $(find -L $zonedir -name $mixedzone -type d -print) ] ; then

          echo "The region \"$1\" has more than one time zone. " >&2

        else # Или полное отсутствие совпадений

          echo "Can't find an exact match for \"$1\". " >&2

        fi

        echo "Please use 'list' to see all known regions and time zones." >&2

        exit 1

      fi

    fi

    timezone="$match"

  fi

  nicetz=$(echo $timezone | sed "s|$zonedir/||g") # Отформатировать вывод.

  echo It\'s $(TZ=$timezone date '+%A, %B %e, %Y, at %l:%M %p') in $nicetz

  exit 0

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

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

В основном сложность данного сценария обусловлена желанием определить часовой пояс по введенному пользователем названию области, для которого не найдено прямого совпадения в базе данных часовых поясов. Данные хранятся в ней в виде столбцов timezonename и region/locationname, и сценарий старается отобразить полезные сообщения об ошибках для наиболее типичных проблем, связанных с вводом, например, когда часовой пояс не может быть определен, потому что пользователь указал страну, которая делится на несколько часовых поясов (например, Бразилию).

Даже при том, что присваивание TZ="Casablanca" приводит к неудаче поиска географической области, город Casablanca (Касабланка) действительно существует в базе данных. Проблема в том, что для успешного определения часового пояса необходимо использовать правильное сочетание названия области и города Africa/Casablanca, как было показано во введении к этому сценарию.

С другой стороны, данный сценарий способен самостоятельно найти файл Casablanca в каталоге Africa и точно определить часовой пояс. Но одной только области Africa будет недостаточно, потому что сценарий найдет несколько подобластей в каталоге Africa и выведет сообщение, указывающее, что предоставленной информации недостаточно для уникальной идентификации часового пояса . Можно также воспользоваться полным списком всех часовых поясов или передать сценарию точное название часового пояса (например, UTC или WET).

ПРИМЕЧАНИЕ

Отличный справочник по часовым поясам можно найти по адресу: http://www.twinsun.com/tz/tz-link.htm.

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

Чтобы узнать текущее время в географической области или в городе, передайте сценарию timein аргумент с названием области или города. Если вы знаете и область, и город, передайте их в формате region/city (например, Pacific/Honolulu). При вызове без аргументов сценарий timein выведет время UTC/GMT. В листинге 2.20 показаны примеры вызова сценария timein с разными часовыми поясами.

Результаты

Листинг 2.20. Тестирование сценария timein с разными часовыми поясами

$ timein

It’s Wednesday, April 5, 2017, at 4:00 PM in UTC

$ timein London

It’s Wednesday, April 5, 2017, at 5:00 PM in Europe/London

$ timein Brazil

The region "Brazil" has more than one time zone. Please use 'list’

to see all known regions and time zones.

$ timein Pacific/Honolulu

It’s Wednesday, April 5, 2017, at 6:00 AM in Pacific/Honolulu

$ timein WET

It’s Wednesday, April 5, 2017, at 5:00 PM in WET

$ timein mycloset

Can’t find an exact match for "mycloset". Please use 'list'

to see all known regions and time zones.

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

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

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

Назад: Глава 1. Отсутствующая библиотека
Дальше: Глава 3. Создание утилит