Со стороны может показаться, что разные версии Unix обеспечивают единообразный способ использования командной строки, во многом благодаря их совместимости со стандартами POSIX. Но любой, кому доводилось пользоваться несколькими разными системами Unix, знает, насколько сильно они могут различаться по множеству параметров. Вам придется очень постараться, чтобы найти систему Unix или Linux, в которой, к примеру, отсутствует стандартная команда ls, но... поддерживает ли ваша версия команды флаг --color? Поддерживает ли ваша версия командной оболочки Bourne извлечение фрагментов из переменных (например, с помощью конструкции вида: ${var:0:2})?
Одной из наиболее широких, пожалуй, областей применения сценариев командной оболочки является настройка конкретной разновидности Unix, чтобы сделать ее более похожей на другие системы. Большинство современных GNU-версий утилит прекрасно работают во многих разновидностях Unix, не являющихся Linux (например, старую и неудобную версию tar можно заменить более новой GNU-версией), однако чаще настройка Unix не связана со столь радикальными обновлениями, что позволяет избежать потенциальных проблем с добавлением новых двоичных файлов в поддерживаемые системы. Вместо этого с помощью сценариев можно преобразовать популярные флаги в их локальные эквиваленты, чтобы использовать основные особенности Unix для создания более удобных версий существующих команд или даже решить старые проблемы отсутствия некоторых возможностей.
Существует несколько способов вывода номеров строк вместе с содержимым файлов, и большинство из этих способов имеют простую и короткую реализацию. Например, ниже приводится решение с использованием 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 "$@"
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. Для чего это может пригодиться? Например, для вывода содержимого файла журнала в обратном порядке следования записей — от новых к старым.
Одно из ограничений команды 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, которое в исходном файле находится в отдельной строке, осталось в отдельной строке и в полученном выводе.
Многие распространенные команды 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.
Непоследовательная поддержка флагов командами в разных системах 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 "$@"
Фактически весь сценарий состоит из цикла 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 информация выводится, только если пользователь превысил выделенные ему квоты. Как показывают результаты, все работает правильно. И кстати, мы не превысили квоты. Уф-ф!
В составе пакета 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 $account@$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 [user@]host[:file ...]
sftp [user@]host[:dir[/]]
sftp -b batchfile [user@]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.
Некоторые версии 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
После некоторого усовершенствования этот сценарий мог бы добавлять номера к выводимым строкам с совпадениями.
За годы разработки 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 и функции сжатия, выполняющей проверку на наличие ошибок.
Как было отмечено в рецепте № 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 "$@"
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