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

Глава 4. Тонкая настройка Unix

Со стороны может показаться, что разные версии Unix обеспечивают единообразный способ использования командной строки, во многом благодаря их совместимости со стандартами POSIX. Но любой, кому доводилось пользоваться несколькими разными системами Unix, знает, насколько сильно они могут различаться по множеству параметров. Вам придется очень постараться, чтобы найти систему Unix или Linux, в которой, к примеру, отсутствует стандартная команда ls, но... поддерживает ли ваша версия команды флаг --color? Поддерживает ли ваша версия командной оболочки Bourne извлечение фрагментов из переменных (например, с помощью конструкции вида: ${var:0:2})?

Одной из наиболее широких, пожалуй, областей применения сценариев командной оболочки является настройка конкретной разновидности Unix, чтобы сделать ее более похожей на другие системы. Большинство современных GNU-версий утилит прекрасно работают во многих разновидностях Unix, не являющихся Linux (например, старую и неудобную версию tar можно заменить более новой GNU-версией), однако чаще настройка Unix не связана со столь радикальными обновлениями, что позволяет избежать потенциальных проблем с добавлением новых двоичных файлов в поддерживаемые системы. Вместо этого с помощью сценариев можно преобразовать популярные флаги в их локальные эквиваленты, чтобы использовать основные особенности Unix для создания более удобных версий существующих команд или даже решить старые проблемы отсутствия некоторых возможностей.

№ 27. Вывод содержимого файлов с нумерацией строк

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

awk '{ print NR": "$0 }’ < inputfile

В некоторых реализациях Unix команда cat поддерживает флаг -n, в других команда more (less или pg) имеет флаг, позволяющий указать ей на необходимость вывести номера строк. Но в некоторых разновидностях Unix ни один из предложенных способов не будет работать, и тогда для решения поставленной задачи можно использовать простой сценарий из листинга 4.1.

Код

Листинг 4.1. Сценарий numberlines

#!/bin/bash

# numberlines -- простая альтернатива команде cat -n и др.

for filename in "[email protected]"

do

  linecount="1"

  while IFS="\n" read line

  do

    echo "${linecount}: $line"

    linecount="$(( $linecount + 1 ))"

  done < $filename

done

exit 0

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

Главный цикл в этой программе имеет небольшую хитрость: он выглядит как обычный цикл while, но самой важной его частью является строка done < $filename . Как оказывается, основные блочные конструкции действуют как бы в своих виртуальных подоболочках. То есть такое перенаправление файла не только допустимо, но и упрощает выполнение итераций по строкам в $filename. Добавление инструкции read — в каждой итерации загружающей новую строку в переменную line — дает простую возможность вывести номер строки с ее содержимым и увеличить переменную linecount .

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

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

Результаты

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

Листинг 4.2. Тестирование сценария numberlines на выдержке из сказки «Alice in Wonderland» (Алиса в Стране Чудес).

$ numberlines alice.txt

1: Alice was beginning to get very tired of sitting by her sister on the

2: bank, and of having nothing to do: once or twice she had peeped into the

3: book her sister was reading, but it had no pictures or conversations in

4: it, 'and what is the use of a book,' thought Alice 'without pictures or

5: conversations?'

6:

7: So she was considering in her own mind (as well as she could, for the

8: hot day made her feel very sleepy and stupid), whether the pleasure

9: of making a daisy-chain would be worth the trouble of getting up and

10: picking the daisies, when suddenly a White Rabbit with pink eyes ran

11: close by her.

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

Получив содержимое файла с пронумерованными строками, вы легко сможете изменить порядок их следования на противоположный, как показано ниже:

cat -n filename | sort -rn | cut -c8-

Такая команда будет работать в системах, где команда cat поддерживает флаг -n. Для чего это может пригодиться? Например, для вывода содержимого файла журнала в обратном порядке следования записей — от новых к старым.

№ 28. Перенос длинных строк

Одно из ограничений команды fmt и эквивалентного ей сценария № 14 из главы 2 состоит в том, что они переносят и оформляют отступы во всех строках, которые встретятся им на пути, даже если в этом нет никакого смысла. В результате текст электронного письма может превратиться в абракадабру (например, перенос слова .signature — не самое лучшее решение), как и содержимое любого другого файла, где переносы строк играют важную роль.

А что, если вам потребуется реализовать перенос только очень длинных строк в документе, оставив все остальное нетронутым? С набором команд, доступным пользователю Unix по умолчанию, остается только одно: вручную просмотреть все строки в редакторе, по отдельности передавая длинные команде fmt. (В редакторе vi для этого достаточно установить курсор на требуемую строку и выполнить команду !$fmt.)

Сценарий в листинге 4.3 автоматизирует задачу, используя конструкцию ${#varname}, которая возвращает длину строки, хранящейся в переменной varname.

Код

Листинг 4.3. Сценарий toolong

  #!/bin/bash

  # toolong -- передает команде fmt только строки из потока ввода,

  #   которые длиннее указанного предела

  width=72

  if [ ! -r "$1" ] ; then

    echo "Cannot read file $1" >&2

    echo "Usage: $0 filename" >&2

    exit 1

  fi

while read input

  do

    if [ ${#input} -gt $width ] ; then

      echo "$input" | fmt

    else

      echo "$input"

    fi

done < $1

  exit 0

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

Обратите внимание, что простая конструкция < $1 в конце цикла while подает на его вход указанный файл. Каждая строка из этого файла читается командой read input и сохраняется в переменной input для дальнейшего анализа.

Если ваша командная оболочка не поддерживает конструкцию ${#var}, ее поведение можно сымитировать очень удобной командой «word count» (счетчик слов) wc:

varlength="$(echo "$var" | wc -c)"

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

varlength="$(echo "$var" | wc -c | sed 's/[^[:digit:]]//g')"

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

Этот сценарий принимает единственное имя файла, как показано в листинге 4.4.

Результаты

Листинг 4.4. Тестирование сценария toolong

$ toolong 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.

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

№ 29. Вывод файла с дополнительной информацией

Многие распространенные команды Unix и Linux первоначально создавались для работы с медленными, преимущественно неинтерактивными средствами вывода (мы уже упоминали, что Unix — это довольно древняя ОС?) и потому выводят минимум информации и не поддерживают интерактивного режима работы. Примером может служить команда cat: когда она используется для просмотра коротких файлов, она не выводит никакой полезной информации о файле. Однако было бы нелишне иметь такую информацию, так давайте получим ее! В листинге 4.5 приводится реализация команды showfile, альтернативы команде cat.

Код

Листинг 4.5. Сценарий showfile

  #!/bin/bash

  # showfile -- выводит содержимое файла и дополнительную информацию

  width=72

  for input

  do

    lines="$(wc -l < $input | sed 's/ //g')"

    chars="$(wc -c < $input | sed 's/ //g')"

    owner="$(ls -ld $input | awk '{print $3}')"

    echo "-----------------------------------------------------------------"

    echo "File $input ($lines lines, $chars characters, owned by $owner):"

    echo "-----------------------------------------------------------------"

    while read line

    do

      if [ ${#line} -gt $width ] ; then

        echo "$line" | fmt | sed -e '1s/^/ /' -e '2,$s/^/+ /'

      else

        echo " $line"

      fi

    done < $input

    echo "-----------------------------------------------------------------"

  done | ${PAGER:more}

  exit 0

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

Чтобы вместе с содержимым файла вывести заголовок и заключительную информацию, этот сценарий использует интересный трюк, доступный в командной оболочке: ближе к концу сценария,с помощью конструкции done < $input , выполняется перенаправление входного файла в цикл while. Но самым сложным, пожалуй, элементом сценария является вызов sed для вывода строк длиннее указанной величины:

echo "$line" | fmt | sed -e '1s/^/ /' -e '2,$s/^/+ /'

Строки, имеющие длину больше указанного максимального значения, переносятся с помощью команды fmt (вместо нее можно использовать эквивалентный сценарий № 14 из главы 2). Чтобы визуально отличать строки, которые продолжаются на следующей строке в выводе, от строк, оставшихся нетронутыми, перед первой строкой намеренно добавляются два пробела, а перед последующими — знак «плюс» и один пробел. В конце вывод передается через конвейер команде ${PAGER:more} постраничного просмотра, заданной в переменной окружения $PAGER, или, если эта переменная не настроена, программе more .

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

Сценарию можно передать одно или несколько имен файлов, как показано в лис­тинге 4.6.

Результаты

Листинг 4.6. Тестирование сценария showfile

$ showfile ragged.txt

-----------------------------------------------------------------

File ragged.txt (7 lines, 639 characters, owned by taylor):

-----------------------------------------------------------------

  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.

№ 30. Имитация флагов в стиле GNU с помощью quota

Непоследовательная поддержка флагов командами в разных системах Unix и Linux — источник бесконечных проблем для пользователей, которым приходится переключаться между основными разновидностями этих систем, особенно между коммерческими версиями Unix (SunOS/Solaris, HP-UX и другие) и открытой системой Linux. Одна из таких команд — quota. В одних системах Unix она поддерживает длинные флаги, а в других только однобуквенные.

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

Код

Листинг 4.7. The newquota script

  #!/bin/bash

  # newquota -- интерфейс к команде quota, принимающий длинные флаги в стиле GNU

  # quota поддерживает три флага, -g, -v и -q, но этот сценарий

  #   позволяет передавать также флаги '--group', '--verbose' и '--quiet'.

  flags=""

  realquota="$(which quota)"

  while [ $# -gt 0 ]

  do

    case $1

    in

      --help)      echo "Usage: $0 [--group --verbose --quiet -gvq]" >&2

                         exit 1 ;;

      --group)    flags="$flags -g";  shift ;;

      --verbose)  flags="$flags -v";  shift ;;

      --quiet)    flags="$flags -q";  shift ;;

      --)         shift;              break ;;

      *)          break;         # Завершить цикл 'while'!

    esac

  done

  exec $realquota $flags "[email protected]"

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

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

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

Существует два способа интеграции подобных оберток в систему. Самый простой: переименовать файл сценария, дав ему имя quota, скопировать его в локальный каталог (например, /usr/local/bin) и добавить этот каталог в начало списка в переменной PATH, чтобы поиск в нем выполнялся раньше, чем в других стандартных для Linux каталогах (/bin и /usr/bin). Другой способ: добавить общесистемный псевдоним, чтобы команда quota, введенная пользователем, в действительности вызывала сценарий newquota. (В некоторых дистрибутивах Linux имеется встроенная утилита для управления общесистемными псевдонимами, как, например, alternatives в Debian.) Однако в последнем случае возникает некоторый риск при включении команды quota с новыми флагами в пользовательские сценарии: если такие сценарии не задействуют интерактивную оболочку входа пользователя, они могут не увидеть настроенный псевдоним и в результате вызовут оригинальную команду quota вместо newquota.

Результаты

В листинге 4.8 приводятся результаты вызовов сценария newquota с флагами --verbose и --quiet.

Листинг 4.8. Тестирование сценария newquota

$ newquota --verbose

Disk quotas for user dtint (uid 24810):

     Filesystem   usage   quota   limit   grace   files   quota   limit   grace

           /usr  338262  614400  675840           10703  120000   126000

$ newquota --quiet

В режиме --quiet информация выводится, только если пользователь превысил выделенные ему квоты. Как показывают результаты, все работает правильно. И кстати, мы не превысили квоты. Уф-ф!

№ 31. Делаем sftp более похожей на ftp

В составе пакета ssh (Secure Shell) имеется безопасная версия программы ftp (для работы с протоколом File Transfer Protocol), но ее интерфейс может показаться неудобным для тех, кто привык пользоваться старым, замшелым клиентом ftp. Основная проблема в том, что ftp вызывается как ftp remotehost и затем предлагает ввести имя учетной записи и пароль. Программа sftp, напротив, требует передать учетные данные и имя удаленного хоста в командной строке и не работает как должно (или как ожидается), если ей передать только имя хоста.

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

Код

Листинг 4.9. Сценарий mysftp, более дружественная версия sftp

  #!/bin/bash

  # mysftp--Makes sftp start up more like ftp

  /bin/echo -n "User account: "

  read account

  if [ -z $account ] ; then

    exit 0; # Видимо, пользователь передумал

  fi

  if [ -z "$1" ] ; then

    /bin/echo -n "Remote host: "

    read host

    if [ -z $host ] ; then

      exit 0

    fi

  else

    host=$1

  fi

  # Конец сценария и переключение на sftp.

  #   Флаг -C разрешает использовать сжатие.

  exec sftp -C [email protected]$host

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

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

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

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

Результаты

Давайте посмотрим, что случится, если вызвать этот сценарий и программу sftp без аргументов командной строки. В листинге 4.10 показана попытка запустить программу sftp.

Листинг 4.10. Попытка запустить утилиту sftp без аргументов приводит к появлению малопонятной справочной информации

$ sftp

usage: sftp [-1246Cpqrv] [-B buffer_size] [-b batchfile] [-c cipher]

          [-D sftp_server_path] [-F ssh_config] [-i identity_file] [-l limit]

          [-o ssh_option] [-P port] [-R num_requests] [-S program]

          [-s subsystem | sftp_server] host

       sftp [[email protected]]host[:file ...]

       sftp [[email protected]]host[:dir[/]]

       sftp -b batchfile [[email protected]]host

В целом это правильно, но выглядит непонятно. Напротив, сценарий mysftp позволяет продолжить и установить соединение, как показано в листинге 4.11.

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

$ mysftp

User account: taylor

Remote host: intuitive.com

Connecting to intuitive.com...

[email protected]'s password:

sftp> quit

Вызовите сценарий, указав имя удаленного хоста, как при использовании обычной программы ftp, и он предложит ввести только учетные данные (как показано в листинге 4.12), а затем скрытно вызовет sftp.

Листинг 4.12. Запуск сценария mysftp с единственным аргументом: именем хоста для подключения

$ mysftp intuitive.com

User account: taylor

Connecting to intuitive.com...

[email protected]'s password:

sftp> quit

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

Когда есть такой сценарий, неизбежно возникает вопрос, можно ли создать на его основе инструмент автоматизированного резервного копирования или синхронизации. И действительно, mysftp — отличный кандидат на эту роль. В рамках такого усовершенствования можно было бы определить каталог в вашей системе, затем написать сценарий-обертку, создающий ZIP-архив важных файлов в этом каталоге, и использовать mysftp для копирования архива на сервер или в облачное хранилище. Все перечисленное мы и попробуем реализовать в сценарии № 72, в главе 9.

№ 32. Исправление grep

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

К счастью, если реализовать эти функции в сценарии командной оболочки, они будут доступны даже в старых коммерческих системах Unix с относительно примитивной командой grep. Чтобы определить количество строк контекста выше и ниже совпадения, передайте сценарию флаг -c value и шаблон для поиска. Этот сценарий (представлен в листинге 4.13) также заимствует ANSI-последовательности управления цветом из сценария № 11 в главе 1 для подсветки совпавшего фрагмента.

Код

Листинг 4.13. Сценарий cgrep

  #!/bin/bash

  # cgrep -- grep с поддержкой вывода контекста и подсветкой совпадения

  context=0

  esc="^["

  boldon="${esc}[1m" boldoff="${esc}[22m"

  sedscript="/tmp/cgrep.sed.$$"

  tempout="/tmp/cgrep.$$"

  function showMatches

  {

    matches=0

    echo "s/$pattern/${boldon}$pattern${boldoff}/g" > $sedscript

    for lineno in $(grep -n "$pattern" $1 | cut -d: -f1)

    do

      if [ $context -gt 0 ] ; then

        prev="$(( $lineno - $context ))"

        if [ $prev -lt 1 ] ; then

          # Чтобы исключить ошибку "invalid usage of line address 0."

          prev="1"

        fi

        next="$(( $lineno + $context ))"

        if [ $matches -gt 0 ] ; then

          echo "${prev}i\\" >> $sedscript

          echo "----" >> $sedscript

        fi

        echo "${prev},${next}p" >> $sedscript

      else

        echo "${lineno}p" >> $sedscript

      fi

      matches="$(( $matches + 1 ))"

    done

    if [ $matches -gt 0 ] ; then

      sed -n -f $sedscript $1 | uniq | more

    fi

  }

  trap "$(which rm) -f $tempout $sedscript" EXIT

  if [ -z "$1" ] ; then

    echo "Usage: $0 [-c X] pattern {filename}" >&2

    exit 0

  fi

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

    context="$2"

    shift; shift

  elif [ "$(echo $1|cut -c1-2)" = "-c" ] ; then

    context="$(echo $1 | cut -c3-)"

    shift

  fi

  pattern="$1"; shift

  if [ $# -gt 0 ] ; then

    for filename ; do

      echo "----- $filename -----"

      showMatches $filename

    done

  else

    cat - > $tempout # Записать поток во временный файл.

    showMatches $tempout

  fi

  exit 0

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

Этот сценарий задействует команду grep -n, чтобы получить номера всех совпавших строк в файле , и затем, используя заданное число строк контекста, определяет номера начальной и конечной строк для включения в контекст. Эти номера выводятся во временный сценарий для sed, объявленный в , который выполняет команду поиска с заменой, чтобы добавить к найденному совпадению ANSI-последовательности включения и выключения вывода жирным шрифтом. Перечисленные операции составляют почти 90% сценария.

Также следует отметить использование команды trap , которая позволяет включать обработку событий в цикл выполнения сценария командной оболочкой. В первом аргументе ей передается последовательность команд, которую следует выполнить, а в остальных — имена сигналов (событий). В данном случае мы сообщаем оболочке, что в момент выхода из сценария она должна вызвать команду rm, чтобы удалить два временных файла.

Самое примечательное в команде trap — она сработает в любом случае, независимо от того, в какой точке сценария произойдет выход. В последующих сценариях вы увидите, что с помощью trap можно обработать самые разные сигналы, а не только SIGEXIT (или EXIT, или числовой эквивалент сигнала SIGEXIT, который равен 0). Фактически несколькими вызовами команды trap можно определить последовательности команд для обработки нескольких разных сигналов, то есть реализовать вывод сообщения «временные файлы стерты», если кто-то пошлет сценарию сигнал SIGQUIT (ctrl-C), которое не будет выводиться в случае обычного события выхода (SIGEXIT).

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

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

Результаты

Листинг 4.14. Тестирование сценария cgrep

$ cgrep -c 1 teacup ragged.txt

----- ragged.txt -----

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

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

После некоторого усовершенствования этот сценарий мог бы добавлять номера к выводимым строкам с совпадениями.

№ 33. Работа со сжатыми файлами

За годы разработки Unix немногие программы пересматривались и переделывались чаще, чем compress. В большинстве систем Linux доступны три основные программы сжатия: compress, gzip и bzip2. Каждая создает файлы со своим расширением (.z, .gz и .bz2, соответственно), и степень сжатия может отличаться, в зависимости от формы данных в файлах.

Независимо от степени сжатия и от используемых для него программ, работа со сжатыми файлами во многих системах Unix требует распаковать их вручную, выполнить желаемые операции с данными и повторно упаковать их по завершении. Это довольно утомительное занятие, которое точно стоит автоматизировать! Сценарий, представленный в листинге 4.15, действует как удобная обертка для выполнения трех распространенных операций со сжатыми файлами: cat, more и grep.

Код

Листинг 4.15: Сценарий zcat/zmore/zgrep

#!/bin/bash

# zcat, zmore и zgrep -- сценарию следует присвоить три имени

#   с помощью символических или жестких ссылок. Это позволит прозрачно

#   работать со сжатыми файлами.

Z="compress"; unZ="uncompress" ; Zlist=""

gz="gzip"    ; ungz="gunzip"    ; gzlist=""

bz="bzip2"   ; unbz="bunzip2"   ; bzlist=""

# Первый шаг: попытаться изолировать имена файлов в командной строке.

#   Сделаем это последовательно, перебирая аргументы по одному и проверяя,

#   являются ли они именами файлов. Если очередное имя соответствует файлу и имеет

#   расширение, характеризующее программу сжатия, распакуем файл, запишем имя

#   файла и повторим итерацию.

#   По окончании повторно сожмем все, что было распаковано.

for arg

do

  if [ -f "$arg" ] ; then

    case "$arg" in

      *.Z) $unZ "$arg"

           arg="$(echo $arg | sed 's/\.Z$//')"

           Zlist="$Zlist \"$arg\""

           ;;

      *.gz) $ungz "$arg"

            arg="$(echo $arg | sed 's/\.gz$//')"

            gzlist="$gzlist \"$arg\""

            ;;

      *.bz2) $unbz "$arg"

             arg="$(echo $arg | sed 's/\.bz2$//')"

             bzlist="$bzlist \"$arg\""

             ;;

    esac

  fi

  newargs="${newargs:-""} \"$arg\""

done

case $0 in

   *zcat* ) eval cat $newargs              ;;

  *zmore* ) eval more $newargs             ;;

  *zgrep* ) eval grep $newargs             ;;

        * ) echo "$0: unknown base name. Can't proceed." >&2

            exit 1

esac

# Теперь сожмем все.

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

  eval $Z $Zlist

fi

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

  eval $gz $gzlist

fi

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

  eval $bz $bzlist

fi

# Вот и все!

exit 0

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

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

Наиболее интересный трюк в этом сценарии — использование директивы eval для повторного сжатия файлов . Она необходима для правильной интерпретации имен файлов, содержащих пробелы. Когда производится заполнение переменных Zlist, gzlist и bzlist, каждый аргумент заключается в кавычки, так что типичным примером значений этих переменных может служить строка ""sample.c" "test.pl" "penny.jar"". Поскольку список включает вложенные кавычки, команда, такая как cat $Zlist, может сообщить, что файл "sample.c" не найден. Чтобы заставить командную оболочку действовать, как если бы эта команда была введена в командной строке (когда кавычки автоматически удаляются после анализа arg), используется директива eval.

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

Для правильной работы сценарий должен иметь три имени. Как это сделать в Linux? Просто: вам помогут ссылки. Можно использовать символические ссылки — специальные файлы, хранящие имена файлов, на которые они ссылаются, или жесткие ссылки, фактически являющиеся индексными узлами inode, ссылающимися на файл. Мы предпочитаем использовать символические ссылки. Их легко создавать, как показано ниже, в листинге 4.16 (здесь предполагается, что сам сценарий сохранен в файле с именем zcat).

Листинг 4.16. Создание символических ссылок zmore и zgrep на сценарий zcat

$ ln -s zcat zmore

$ ln -s zcat zgrep

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

Результаты

Вездесущая утилита compress быстро сожмет файл ragged.txt и присвоит ему расширение .z:

$ compress ragged.txt

Сжатый файл ragged.txt можно просмотреть командой zcat, как показано в лис­тинге 4.17.

Листинг 4.17. Использование zcat для вывода содержимого сжатого файла

$ zcat ragged.txt.Z

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.

Еще раз выполнить в нем поиск строки teacup.

$ zgrep teacup ragged.txt.Z

rattling teacups would change to tinkling sheep-bells, and the

При этом файл сохранится в сжатом состоянии, как показывает листинг 4.18.

Листинг 4.18. Вывод команды ls показывает, что имеется только один файл с таким именем, и это сжатый файл

$ ls -l ragged.txt*

-rw-r--r--  1 taylor  staff  443 Jul  7 16:07 ragged.txt.Z

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

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

№ 34. Гарантия максимальной степени сжатия файла

Как было отмечено в рецепте № 33, большинство реализаций Linux включает несколько утилит сжатия, но решать, какая из них наиболее эффективно сожмет конкретный файл, приходится пользователю. Однако пользователи обычно привыкают к одной программе, не подозревая, что другие утилиты дали бы лучшие результаты. Еще большую сумятицу вносит тот факт, что некоторые файлы лучше сжимаются с использованием одного алгоритма, а другие — с использованием другого, и нет никакой возможности выявить лучший вариант без прямых экспериментов.

Логичное решение проблемы — написать сценарий, который сожмет файл с применением каждого из инструментов и оставит наименьший файл как наилучший. Именно это делает сценарий bestcompress, представленный в лис­тинге 4.19!

Код

Листинг 4.19. Сценарий bestcompress

#!/bin/bash

# bestcompress -- пытается сжать файл всеми доступными инструментами

#   сжатия и сохраняет наименьший сжатый файл, сообщая результат

#   пользователю. Если флаг -a не указан, bestcompress пропускает

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

Z="compress"     gz="gzip"    bz="bzip2"

Zout="/tmp/bestcompress.$$.Z"

gzout="/tmp/bestcompress.$$.gz"

bzout="/tmp/bestcompress.$$.bz"

skipcompressed=1

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

  skipcompressed=0 ; shift

fi

if [ $# -eq 0 ]; then

  echo "Usage: $0 [-a] file or files to optimally compress" >&2

  exit 1

fi

trap "/bin/rm -f $Zout $gzout $bzout" EXIT

for name in "[email protected]"

do

  if [ ! -f "$name" ] ; then

    echo "$0: file $name not found. Skipped." >&2

    continue

  fi

  if [ "$(echo $name | egrep '(\.Z$|\.gz$|\.bz2$)')" != "" ] ; then

    if [ $skipcompressed -eq 1 ] ; then

      echo "Skipped file ${name}: It's already compressed."

      continue

    else

      echo "Warning: Trying to double-compress $name"

    fi

  fi

  # Запустить параллельное сжатие файла тремя инструментами.

  $Z < "$name" > $Zout &

  $gz < "$name" > $gzout &

  $bz < "$name" > $bzout &

  wait # ждать, пока все три инструмента завершат сжатие.

  # Выявить файл, сжатый лучше всех.

  smallest="$(ls -l "$name" $Zout $gzout $bzout | \

    awk '{print $5"="NR}' | sort -n | cut -d= -f2 | head -1)"

  case "$smallest" in

    1 ) echo "No space savings by compressing $name. Left as is."

        ;;

    2 ) echo Best compression is with compress. File renamed ${name}.Z

        mv $Zout "${name}.Z" ; rm -f "$name"

        ;;

    3 ) echo Best compression is with gzip. File renamed ${name}.gz

        mv $gzout "${name}.gz" ; rm -f "$name"

        ;;

    4 ) echo Best compression is with bzip2. File renamed ${name}.bz2

        mv $bzout "${name}.bz2" ; rm -f "$name"

  esac

done

exit 0

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

Самая интересная строка в сценарии — . Команда ls в этой строке выводит размеры каждого файла (исходного и трех сжатых, в определенном порядке), команда awk выделяет размеры файлов, команда sort сортирует результаты в числовом порядке, и в конце остается номер строки в выводе ls с наименьшим файлом. Если все сжатые версии получились больше оригинала, результат будет равен 1, и на экране появится соответствующее сообщение . Иначе число покажет, какая из утилит — compress, gzip или bzip2 — лучше справилась с задачей. Затем остается только переместить соответствующий файл в текущий каталог и удалить оригинал.

Обратите также внимание на строку , где производится запуск всех трех утилит сжатия. Утилиты запускаются параллельно, благодаря использованию завершающего символа &, который перемещает запущенную программу в подоболочку. Последующая команда wait приостанавливает сценарий, пока все запущенные программы не завершатся. В однопроцессорной системе этот прием может не дать существенного прироста производительности, но в многопроцессорной задача будет распределена между несколькими процессами, и ее выполнение теоретически должно завершиться быстрее.

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

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

Результаты

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

Листинг 4.20. Вывод команды ls показывает, что в каталоге присутствует файл со сказкой «Алиса в Стране Чудес». Обратите внимание, что файл имеет размер 154872 байт

$ ls -l alice.txt

-rw-r--r-- 1 taylor staff 154872 Dec 4 2002 alice.txt

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

Листинг 4.21. Запуск сценария bestcompress для сжатия файла alice.txt

$ bestcompress alice.txt

Best compression is with compress. File renamed alice.txt.Z

Как показано в листинге 4.22, сжатый файл получился намного меньше оригинала.

Листинг 4.22. Размер сжатого файла (66287 байт) значительно уменьшился с размером оригинала, как было показано в листинге 4.20

$ ls -l alice.txt.Z

-rw-r--r-- 1 taylor wheel 66287 Jul 7 17:31 alice.txt.Z

Назад: Глава 3. Создание утилит
Дальше: Глава 5. Системное администрирование: управление пользователями