Подготовлено по материалам рассылки «программирование на shell и awk»
http://win.subscribe.ru/catalog/comp.soft.prog.shellandawk
Big Shadow
Киев, 2003


Оформление вывода в shell-сценариях

В этой заметке будут обсуждаться:

Коварное эхо

Как вы знаете, команда echo помещает в стандартный вывод свои аргументы, разделенные пробелами, и завершаемые символом перевода строки.

Однако, в 70-е годы прошлого века не все было так одназначно. Существовал один филосовский вопрос. Суть его была такова: что должна делать команда echo, если ей не передали аргументов, в частности, следует ли ей выдавать пустую строку или вообще ничего не предпринимать? По этому поводу велись большие дебаты. Такие, что вдохновили Дуга МакИлроя /Doug McIlroy/ сочинить притчу, которая приведена ниже.

UNIX и эхо.

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

Сама природа покровительствовала UNIX и вторила ей более охотно, чем кому-либо из смертных. Простые люди поражались ее эхом, таким оно было точным и кристально чистым. Они не могли поверить, что ей отвечают те же леса и скалы, которые так искажают их собственные голоса. Когда один нетерпеливый пастушок попросил UNIX: "Пусть эхо ответит ничего", и она послушно открыла рот, эхо промолчало. "Зачем ты открываешь рот? — спросил пастушок. — Отныне никогда не открывай его, если эхо должно ответить ничего!". — и UNIX подчинилась.

"Но я хочу совершенного исполнения, даже если эхо отвечает ничего, — потребовал другой, обидчивый, юноша, — а никакого совершенного эха не получится при закрытом рте". Не желая обидеть никого из них, UNIX согласилась говорить разные "ничего" для нетерпеливого и обидчивого юношей. Она называла "ничего" для обидчивого как '\n'. Однако теперь, когда она говорила '\n', на самом деле она не произносила ничего, поэтому ей приходилось открывать рот дважды: один раз, чтобы сказать '\n', и второй раз, чтобы не сказать ничего. Это не понравилось обидчивому юноше, который тотчас сказал: "Для меня '\n' звучит, как настоящее "ничего", но когда ты открываешь рот второй раз, то все портишь. Возьми второе "ничего" назад". Услужливая UNIX согласилась отказаться от некоторых эхо и обозначила это как '\с'. С тех пор обидчивый юноша мог услышать совершенное эхо "ничего", если он задавал '\n' и '\с' вместе, но говорят, что он так и не услышал его, поскольку умер от излишеств в обозначениях. [1]

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

Уже в 7-ой редакции UNIX echo распознавала флаг -n, который подавлял вывод завершающего символа перевода строки:
$ echo -n Seven Edition
Seven Edition$

В System V, где команда echo умеет интерпретировать упраляющие последовательности вида \Х, для подавления вывода завершающего символа перевода строки используют обозначение . Поэтому вызвав echo с аргументом -n вы можете получить не совсем то, что ожидали:
$ echo -n System V
-n System V
$

В Linux же, echo не желает обрабатывать последовательности вида \Х:
$ echo Linux\\с
Linux \с
$

до тех пор, пока вы не зададите ей флаг -e:
$ echo -e Linux\\с
Linux$

Можно воспользоваться и флагом -n, echo из Linux его понимает.

Добавляет путаницы и то, что почти всегда shell имеет встроенную команду echo, поведение которой может отличаться от внешней /bin/echo. Кроме того, в системе может оказаться не одна echo, а парочка. В таком случае, второе echo обычно находится где-нибудь в районе /usr/ucb/. Из-за таких различий в реализации echo возникают определенные трудности при переносе сценариев из системы в систему. Есть ли способ их избежать? Конечно, но это уже другая история...

Да будет цвет!

Каким образом происходит задание цвета символов и фона, а также переключение различных режимов отображения терминала в UNIX (и во множестве других ОС)? Да также, как и в легендарные времена телетайпных терминалов — посылкой на устройство отображения управляющих команд в виде esc-последовательностей (esc сокр. от англ. escape, произносится как эскейп). Такая последовательность состоит из двух или более символов, первый из которых будет esc (десятичный код равен 27, восьмеричный — 033, шестнадцатеричный — 0x1B; во многих программах и конфигурационных файлах обозначается как \e, \E или ^[).

Перед тем как перейти к детальному рассмотрению esc-последовательностей, давайте вначале научимся вводить символ esc с клавиатуры (если вы умеете это делать, то можете переходить к выполению примеров из следующего абзаца). Чтобы ввести с клавиатуры код любой "непечатной", т. е. управляющей клавиши (функциональной, стрелки и т. п.), необходимо нажать Ctrl+V, а затем интересующую вас "кнопку". Этот прием будет работать не только в командной строке, но и в текстовом редакторе vi.

Проверим на практике. Выполним команду echo "Ctrl+V,Esc[30;40m";clear (на экране символ esc будет показан как ^[):
$ echo "^[[30;40m";clear

Если вы все сделали правильно, то приобретайте прибор ночного видения :)! Пожалуй я не ошибусь, предположив, что у вас возникли вопросы:
— А что это за числа? К чему они ведут? "А вдруг они не курят, а вдруг они не пьют?" [2]
— Числа 30 и 40 это аргументы команды esc[ n m. При необходимости указать несколько аргументов, их перечисляют через точку с запятой: esc[ n1 ; n2 ;...m. В зависимости от значения n результаты будут следующими: ================================================================== k | n=k - установка | n=k+30 - установка | цвет символов | режима | цвета символов | в режиме | отображения | n=k+40 - установка | повышенной | символов | цвета фона | яркости --|----------------------|-------------------------|-------------- 0 | сброс всех атрибутов | черный | серый 1 | яркий (утолщенный) | красный | розовый 2 | тусклый | зеленый | салатовый 3 | | коричневый | желтый 4 | подчеркнутый | синий | светло-синий 5 | мигающий | фиолетовый | лиловый 6 | | бирюзовый | голубой 7 | реверсный | белый | ярко-белый ================================================================== Теперича, имея в своем распоряжении такую табличку, можете приступать к получению "синих экранов", "красных квадратов" и прочей творческой работе. В которой желаю вам всяческих успехов (тем кто предпочитает другие слова — удачи)!!!

Что еще могут esc-последовательности?

Управлять перемещением курсора по экрану:
esc[s или
esc7
запомнить положение курсора
esc[u или
esc8
восстановить запомненное положение курсора
escc
очистить экран и установить курсор в левый верхний угол
esc[ n A
вверх на n строк
esc[ n B или
esc[ n e
вниз на n строк
esc[ n C или
esc[ n a
вправо на n позиций
esc[ n D
влево на n позиций
esc[ n E
в начало строки и на n строк вниз
esc[ n F
в начало строки и на n строк вверх
esc[ n1;n2 H или
esc[ n1;n2 f
переместить в позицию n2 строки n1
esc[ n Z
на n табуляций назад (как tab, но в обратную сторону)
esc[ n `
в той же строке в позицию n
esc[ n d
в той же позиции в строку n
escM
сдвинуть курсор на строчку вверх, если он находится в самой верхней строке, то сдвинуть содержимое экрана на строчку вниз

Очищать части экрана:
esc[0J
от курсора до конца экрана
esc[1J
от начала экрана до курсора
esc[2J
весь экран
esc[0K
от курсора до конца строки
esc[1K
от начала строки до курсора
esc[2K
всю строку
esc[ n X
очистить n знаков справа от позиции курсора

Раздвигать и сдвигать строки на экране:
esc[ n L
вставить n пустых строк ниже текущей строки
esc[ n M
удалить n строк ниже текущей строки
esc[ n P
удалить n знаков справа от курсора (в пределах строки)
esc[ n @
вставить n знаков справа от курсора (в пределах строки)

Проведем испытания. Начнем с подготовки рабочего пространства. Выполните команду clear (или просто нажмите Ctrl+L). Затем — более хитрую (напомню, что символы ^[ появляются в результате нажатия клавиш Ctrl+L,Esc):

$ echo '^[[10;10Habc^[[5dXYZ' XYZ $ abc
Переходим к "разбору полетов". Как вы видите, esc-последовательность ^[[10;10H переместила курсор в 10-ю позицию 10-й строки экрана и начиная с этой позиции был осуществлен вывод символов abc. Потом esc-последовательность ^[[5d переместила курсор немного назад в 5-ю строку, неизменив его позиции в строке. После этого echo напечатала символы XYZ и вывела завершающий символ перевода строки. Поэтому-то приглашение командного интерпретатора в виде у.е. появилось в следующей (шестой) строке.

tput, termcap и terminfo

Довольно просто, не так ли? Однако, если вспомнить, что в UNIX для ввода/вывода можно использовать множество различных терминалов, отлищающихся и размером экрана (количество строк и символов в строке), и командами управления экраном, и кодами, которые генерируют клавиши и т. п., то возникает вопрос: как одни и те же программы умудряются корректно взаимодействовать со всем этим "зоопарком"?

Наведение порядка в этом "зоосаде" обеспечивают два компонента UNIX: база данных и библиотека подпрограмм. В первом описаны поддерживаемые терминалом функции, а второй используется для выполнения запросов к базе данных. Каждый тип терминала в этой базе имеет свое название и перечень его свойств. Изначально эта информация хранилась в файле termcap (от terminal capabilities — возможности терминала). C годами termcap очень вырос и в семействе AT&T юниксов превратился в базу данных terminfo. В ней, чтобы не хранить всю информацию в одном большом файле, описание каждого терминала компилируется и сохраняется в отдельном файле.

Свойствами терминала могут быть:

Названия свойств стандартизированы и неизменны (почти:), а их значения наоборот изменяются в зависимости от типа терминала. В качестве примера ниже приведено несколько стандартных обозначений характеристик терминалов:

  termcap terminfo
число символов в строке co cols
число строк на экране li lines
очистить экран сl clear
включить повышенную яркость md bold
включить мигание mb blink
включить реверсное отображение mr rev
установить цвет символов AF setaf
установить цвет фона AB setab
переместить курсор cm cup
подать звуковой сигнал bl bel

Более подробную информацию можно получить в манах по termcap и terminfo.

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

Что же делать, если у вас возникло желание написать сценарий, который работал бы с любым, поддерживаемым операцинной системой, терминалом с одинаковой (или уменьшенной) функциональностью? Попробуйте воспользоваться утилитой tput. Синтаксис у нее следующий:

tput [-T тип_терминала] название_свойства [параметр(ы) ...]

В качестве значения аргумента название_свойства может быть задано любое имя из списка свойств базы данных termcap и/или terminfo для терминала указанного в тип_терминала. Если этот аргумент опущен, то tput будет использовать значение переменной TERM.

Для кодирования цветов используются следующие числа (в скобках указываются цвета для режима повышенной яркости): 0 - черный (серый) 1 - красный (розовый) 2 - зеленый (салатовый) 3 - коричневый (желтый) 4 - синий (светло-синий) 5 - фиолетовый (лиловый) 6 - бирюзовый (голубой) 7 - белый (ярко-белый)

Однако, не все так просто в нашем королевстве. Будьте готовы к тому, что в одних системах (BSD) tput будет ожидать название_свойства в стиле termcap, а в других (System V) — в стиле terminfo. Примеры: $ tput AF 2 # теперь буковки будут зелененькие $ tput setaf 2 # если сразу не сработало, то теперь уж точно $ tput cm 24 35 # переместить курсор в 24-ю строку, # а про позицию догадайтесь сами $ tput cup 24 35 # аналогично $ tput li # сколько у меня строк на эране? 30 $ tput lines # а у вас?

Всем, кто хочет изучить материальную часть более предметно, рекомендую поэкспериментировать с утилитами, которые конвертируют записи базы terminfo в termcap и наоборот. В BSD-системах это tconv, а в Linux и юниксах ветви AT&T — infocmp. Вот вам еще примерчики: $ infocmp -C|grep cl #вывод в формате termcap :cl=\E[H\E[J:cm=\E[%i%d;%dH:cr=^M:cs=\E[%i%d;%dr:\ $ infocmp -I|grep cl #вывод в формате terminfo clear=\E[H\E[J, el1=\E[1K, el=\E[K, ed=\E[J,

Конечно же, существует много случаев, когда использование esc-последовательностей напрямую оправдано. Приведу в качестве примера фрагмент файла /etc/rc.status дистрибутива SuSE Linux (комментарии мои): if test "$TERM" != "raw" && stty size > /dev/null 2>&1 ; then # если наш терминал понимает, что такое ширина и высота, # то назначаем кодам esc-последовательностей понятные названия esc=`echo -en "\033"` extd="${esc}[1m" warn="${esc}[1;31m" done="${esc}[1;32m" attn="${esc}[1;33m" norm=`echo -en "${esc}[m\017"` stat=`echo -en "\015${esc}[${COLUMNS}C${esc}[10D"` # ... неинтересное пропускаем rc_done_up="${esc}[1A${rc_done}" rc_failed_up="${esc}[1A${rc_failed}" rc_reset="${norm}" rc_save="${esc}7" rc_restore="${esc}8" function rc_cuu () { echo -en "\033[${1}A"; } # и даже определяем функцию # для перемещения по экрану вверх на заданное число строк else # ну а если терминал не знает где у него верх, а где низ, # то ведем себя проще и выдаем информацию построчно без выкрутасов, # чтобы люди к нам тянулись :) ...

И небольшое замечание относительно ДОСа. Помните такую ОС? Так вот, если к ней прикрутить стандартный драйвер ANSI.SYS, то все вышеперечисленные esc-последовательности будут работать и там.


1. Брайан Керниган, Роб Пайк
UNIX — универсальная среда программирования
М.: Финансы и статистика, 1992

2. Слова из песни, которые как известно не выбросишь...