«Где найти информацию, и как научиться нормально работать с shell?». В самом деле, shell-программирование считается достаточно очевидным, но когда предлагаешь на учебных курсах какие-то задачи, то оказывается, что в тупик могут поставить достаточно простые вопросы. Если ответы на такие вопросы не знает будущий администратор, то это достаточно плохо. Как правило, с программированием на shell знакомятся на первом этапе изучения ОС Unix, поскольку одновременно его можно использовать как продолжение разговора об «основах», т.е. о принципах организации файловой системы, о правах доступа к файлам, о средствах управления процессами и т.д. А дальше?Сколько-нибудь подробное изложение shell можно найти только в кирпичах наподобие [1] или [2]. Однако это также пособия для начинающих или продолжающих.

Посмотрим на вопрос с другой стороны. Зачем администратору знание shell? Для него shell — это орудие для каждодневной работы, средство упрощения трудоемких работ, автоматизации часто выполняемых процедур обслуживания, но одновременно и средство для обнаружения, исправления, а иногда и для создания проблем. Большую часть проблем на машине создает именно администратор. Вот, например, пара банальных описок для команды tar:

tar -cf -v abc | ...
tar -cvf /dev/nul abc

Маленький шаг в сторону, и в вашей системе может не остаться свободного места или появиться странный файл. Создать что-то дикое легко. Как исправить? Именно из наблюдения за ошибками подобного рода и появилась первая статья этой рубрики [3], в которой предлагалась задача с удалением всех файлов в текущем каталоге. Одна из проблем заключалась в возможности существования имен со спецсимволами (т.е. символами, которые имеют специфическое значение в shell и могут просто не дойти до команды при наборе командной строки). Достаточно создать файл с именем наподобие -f или -r, и небольшая проблема обеспечена.

Спецсимволов оказывается несколько больше, чем «нормальных». Даже если исключить управляющие символы и символы, не попавшие в число стандартных 128, то почти все «обычные» символы кроме букв, оказывается, имеют специальное значение.

Вот пример использования % (если вдруг кажется, что этот символ не используется):

[patterns]# find / -print > /dev/null

Попробуем нажать на что-нибудь наподобие CTRL-Z:

[1]+ Stopped find / -print
>/dev/null
Перезапуск в фоне:
[patterns]# %1 &
[1]+ find / -print >/dev/null
 &
[patterns]#
[patterns]# kill %1

Любой из указанных во врезке символов может входить в имя файла — иногда с достаточно неприятными последствиями. Для того чтобы слегка развлечься, можно поэкспериментировать со следующим скриптом (написан в расчете на bash, стандартный для Linux интерпретатор команд):

typeset -i index
index=1
while ((index < 256))
do
ochar=`(echo obase=8;echo 
$index) | bc`
echo -en «./$ochar» | xargs -0 
touch
#   char=$(echo -e «$ochar»)
#   > ./»$char»
index=index+1
done

Вместо строчки с xargs можно попробовать снять комментарии с двух других, но, увы, в этом случае создать файл с именем в виде символа «переход на другую строку» не удастся, а это, согласитесь, обидно. (Этот цикл по вполне понятным причинам не может создать только файлы «00», «.» и «/», хотя команда touch и будет выдана для двух последних символов).

Теперь желающие могут попробовать удалять созданные таким образом файлы по отдельности. Первый его этап заключается в выяснении, существуют ли подобные файлы. Обычная команда ls может не предоставить достаточной информации. В каталоге, где был запущен данный скрипт, вам попросту позвонят в ухо (файл с именем CTRL-G) и покажут нечто диковатое. Необходимы дополнительные ключи; изучить имена позволяет ключ -b.

Следующим этапом является набор команды удаления. Чаще всего можно явно начать набирать команду rm ?./...?. Одинарные кавычки спасают от большинства проблем с защитой отдельных символов. При необходимости добавить саму кавычку следует воспользоваться ???. Обеспечить ввод спецсимволов помогает комбинация CTRL-V. Уже само по себе начало rm ./ позволяет использовать имена файлов, которые начинаются с символа «-». Впрочем, в особо сложных случаях перечисленных средств может не хватить или покажется, что подобные символы долго искать на клавиатуре. В таком случае проще создать именно комбинацию echo ... | xargs .... — тем более, что для этого часто можно воспользоваться копированием мышью ранее выведенной по команде ls строки. Если же пришло время удалить весь зверинец целиком, то достаточно набрать rm -f *, поскольку «ключевых» файлов типа -r мы все же не создавали. При наличии длинных имен (мы-то экспериментировали с односимвольными) не следует забывать, что rm * не удаляет файлы, которые начинаются с «.», и попытается проинтерпретировать «ключевые». От второй напасти избавляет ./.

Итак, мы научились работать с отдельными файлами в наших джунглях. А как быть с массовыми операциями? Как видим, даже задача перечисления всех файлов в текущем каталоге не так уж и тривиальна. Обычной «*» не хватает. В каждой каталоге имеется два особых файла «.» и «..», другие «скрытые» файлы, которые начинаются с точки, и остальные, которые не имеют точки на первом месте. Формально все файлы попадают в шаблон «.* *», однако, этот шаблон для некоторых операций слишком велик. Если каталог пуст, то даже ls обругает отсутствие файла «*», а при попытке удалить файлы .* будет включать в себя и «.», и «..», чего хотелось бы избежать.

Оказывается, bash (и не только он) поддерживает шаблоны. Комбинации вида повторитель (шаблон) позволяют работать с некоторыми группами. В качестве шаблона может быть и обычный шаблон, а может и группа шаблонов, разделенных |, — фактически это операция «или». В качестве повторителя используется один из символов: * — шаблон может встретиться любое число раз (в частности, нулевое); ? — шаблон может встретиться 0-1 раз; + — шаблон встречается не менее одного раза; @ — ровно один раз; ! — ни один из.

Попытаемся воспользоваться этими соглашениями:

[patterns]# touch a aa aaa 
b ab aab
[patterns]# ls
a aa aaa aab ab b
[patterns]# ls ?(a)b
bash: syntax error near 
unexpected token `?(a?

Проблема, как оказывается, заключена в опциях bash:

[patterns]# shopt extglob
extglob off
[patterns]# shopt -s extglob

Вот теперь уже можно предложить «рабочие» примеры:

[patterns]#ls ?(a)b
ab b
[patterns]# ls *(a)b
aab ab b
[patterns]# ls +(a)b
aab ab
[patterns]# ls @(a)b
ab
[patterns]# ls !(a)b
aab b
[patterns]# ls a@(a|b)
aa ab
[patterns]# ls a@(a|b)
aa ab
[patterns]# ls !(a)
aa aaa aab ab b
[patterns]# touch .xx
[patterns]# ls !(a)
aa aaa aab ab b

Увы, шаблоны не дают нам запросто посмотреть «скрытые» файлы, а шаблон «.* *», толстоват. А позволят ли шаблоны его «укоротить»? Вот тут им можно помочь. Кроме опции extglob имеется еще, например, такая, как nullglob. Она говорит о том, что в случае отсутствия файлов с данным шаблоном следует его отбрасывать. Правда, иногда это может привести к интересным последствиям:

[root@localhost patterns]# 
shopt -s nullglob
[root@localhost patterns]# 
ls .y*
a aa aaa aab ab b

В данном случае файлов, которые бы начинались на .y, нет. Вместо этого выдается список всех «обычных» файлов в каталогов. Однако мы-то как раз и хотим породить список всех файлов за исключением «.» и «..». Пробуем:

[root@localhost patterns]# 
ls -d * .!(.)
. .xx a aa aaa aab ab b
[root@localhost patterns]# 
ls -d * .!(.|)
.xx a aa aaa aab ab b

Увы, до конца пути добраться не удалось:

[root@localhost patterns]# 
>-d
[root@localhost patterns]# 
ls -d * .!(.|)
.xx a aa aaa aab ab b

Файла -d мы не видим. Тут можно пойти либо путем использования «./», либо используя стандартный ключ «--» (вы не забыли добавить его к своей программе?), который обозначает конец списка опций:

[root@localhost patterns]# 
ls -d -- * .!(.|)
-d .xx a aa aaa aab ab b

По необходимости ключ «--» можно использовать и с rm и рядом других команд. Уф, кажется, победа. Домашнее задание для особо любопытных:

  • когда данная команда не работает?
  • как при помощи программы grep найти все комбинации -l в некотором файле?

Имя

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

Имеют хождение два основных способа передачи параметров: по значению и по ссылке. Фактически в одном случае передается именно значение без какой-либо связи с параметром, а во втором случае — адрес переменной. Скажем, в Фортране все параметры по умолчанию передаются по ссылке. Иногда это ведет к интересным последствиям. Так, выполнение фрагмента

call SOMETHING(2)
j = j +2
…
subroutine SOMETHING(i)
i = 6
end

при использовании некоторых компиляторов могло привести к тому, что значение j увеличивалось на 6. Это было возможно в том случае, когда «2» представлялось какой-то переменной в памяти и эта переменная (константа!) менялась в памяти, а далее использовалась для суммирования. В Си, как известно, способ передачи параметров фактически выбирается в точке вызова. Например, a(b, &c) указывает на то, что b надо передать по значению (и не важно, что b может быть огромной структурой), а с — по ссылке. Для C++ это уже становится не так очевидно, поскольку из явного вызова a(b,c) сразу не ясен способ передачи параметров. В Аде вообще появляется явная возможность указать стиль передачи данных in, out, in out и т.п.

На самом деле способов передачи параметров несколько больше. Один из них заключается в передаче параметра «по имени». Хороший пример работы «по имени» дает следующая процедура для Алгола (пример взят из [4]):

real procedure SUM(expr, index,
 lb, ub); value lb, ub;
real expr; integer index, lb, ub;
begin real temp; temp:=0;
for index:=lb step 1 until ub do 
temp:=temp+expr;
SUM:=temp;
end proc SUM;

Если подходить к данной процедуре с точки зрения Си или Фортрана, то можно увидеть процедуру, которая просто пытается умножить значение expr на (ub-lb+1) сложением. Однако фокус состоит в том, что обращение к expr может включать вычисление на основании значения index. Поэтому, например, SUM(A[i]*B[i], i, 1, 25) фактически является скалярным произведением 25-элементных векторов A и B, а SUM(C[k,2], k, -100, 100) — суммой элементов определенной вырезки из массива. Для других языков программирования подобную конструкцию просто так не реализовать. В Си и Фортране придется иметь дополнительную процедуру. При этом в процедуре подсчета придется использовать явное обращение к функции с параметром, скажем:

SUBROUTINE SUM(EXPR)
REAL EXPR, TEMP
...
TEMP = TEMP+EXPR(X)
...
END

Если же брать скриптовые языки типа TCL, то для них почти любое обращение превращается в обращение «по имени». Интересен также схожий вариант использования блоков в Smalltalk [5]:

2 to:100 by:2 do:[:each|sun<-sum+each]

Данный цикл имеет достаточно интересное формальное определение:

to:limit by:step do:block
self<=limit iftrue:
[block value:self.
(self+step) to:limit by:step 
do:block]

Аналогии достаточно прозрачны.

Теперь рассмотрим callback. Этот прием достаточно популярен для обработки событий в языках моделирования или в ОС, например, при задании процедур обработки прерывания, разбора пакетов, разработке графического интерфейса программы, например, под Motif [6] и т.д.

Более того, некоторые документы почти исключительно состоят из описаний этого вида интерфейса. Например, именно так выглядит стандарт IEEE на процедурный интерфейс к языку моделирования VHDL.

Игра в классики

Передача параметра-функции в Си, его запоминание и использование достаточно прозрачны:

void (*myfunc)(void *data);
void *mydata;
...
void set_func(void (*func)(void *data), 
void *data)
{
myfunc = func;
mydata = data;
}
....
void do_it()
{
(*myfunc)(mydata);
}

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

А что делать, если язык не содержит ссылок? Наличие ссылок всегда порождает потенциальные проблемы с защитой, целостностью (например, из-за наличия «потерянных» ссылок) и т.п. В Java ссылок нет, с другой стороны фактически почти все присваивания выполняются через своеобразные неявные ссылки. Однако «почти» не означает явной возможности передать, запомнить и использовать конкретную функцию. Передавать и запоминать можно объекты, но не функции. Из этого потенциального тупика, однако находится достаточно изящный выход. Никто не мешает создать новый класс исключительно для передачи требуемой функции. Правда, возникает некоторое ограничение на именование метода класса, но и оно не фатально.

При этом есть две возможности: создание самостоятельного именованного класса и создание неименованного класса. Вот иллюстрация использования именованного класса (слегка сокращенный пример из [7]):

interface Incrementable{
void increment();
}
class Callee1 implements 
Incrementable{
private int i = 0;
public void increment(){
i++;
}
....
}
...
class Callee2 ....{
...
private void incr(){ ...}
private class Closure implements 
Incrementable{
public void increment() { 
incr(); };
}
Incrementable getCallbackreference(){
return new Closure();
}
}
...
class Caller{
private Incrementable callbackReference;
Caller(Incrementable cbn){
CallbackReference = cbn;
}
void go(){
callbackReference.increment(); }
}
...
public class Callback{
public static void main(String[]
 args){
Callee1 c1 = new Callee1();
Callee1 c2 = new Callee2();
...
Caller caller1 = new Caller(c1);
Caller caller2 = new Caller(c2. 
GetCallbackreference());
caller1.go();
caller2.go();
...
}
}

Для того чтобы можно было запомнить и использовать какую-либо процедуру в рамках go класса Caller, процедура должна реализовывать какой-то унифицированный по имени метод стандартного класса или интерфейса. Слово «интерфейс» важно, так как множественного наследования в Java нет. Однако никто не мешает в Java иметь класс с реализацией группы интерфейсов. При этом класс может быть не самостоятелен (как Callee1), а являться внутренним классом другого класса (Closure в рамках Callee2). Обратите внимание, что процедура getCallbackreference порождает новый элемент класса Closure и этот элемент имеет полный доступ к методам Callee2 (в данном случае это приватный incr). Это возможно только в одном случае, когда реально объект Closure фактически имеет ссылку не только на собственные данные, но и на порождающий его элемент (тут можно провести аналогию с C++).

Класс Caller уже не требует необходимости запоминать функцию, запоминается объект, который готов выполнять методы интерфейса Incrementable. При создании очередного элемента объект запоминается и по мере необходимости используется. Естественно, запоминание может быть и не столь «статичным» (в момент создания), его вполне можно переопределять. В принципе здесь везде говорилось о запоминании одной функции, но реально-то запоминание объекта с определенным интерфейсом эквивалентно передаче группы функций и данных.

Второй же вариант (с неименованными классами) вполне в духе передачи параметров по имени. Вот иллюстрирующий его фрагмент (также скомпонован из [8]):

public interface Contents() {
int value();
}
....
public class Parcel6{
public Contents cont(){
return new Contents(){
private int i = 11;
public int value(){ return i;}
};
}
...
public static void main(String[] 
args){
Parcel6 p = new Parcel6();
Contents c = p.cont();
}
}

Изюминка здесь содержится в строке с new Contents. Породить просто Contents нельзя — он не имеет никакой реализации («чисто виртуальный проект») — однако в этом же месте немедленно делается и определение реализации, поэтому результат вполне пригоден к употреблению.

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

Литература

[1] Крис Хейр и др. Внутренний мир Unix: Пер. с англ. К.: ДиаСофт, 1998

[2] Робин Брук, Дэвид Б. Хорват и др. Unix для системных администраторов. Энциклопедия пользователя: Пер. с англ. К.: ДиаСофт, 1998

[3] Игорь Облаков. Мелочи жизни. «Открытые системы», 1999, № 1

[4] Т. Пратт. Языки программирования. Разработка и реализация: Пер. с англ. М.: Мир, 1979

[5] Фути К., Судзуки Н. Языки программирования и схемотехника СБИС: Пер. с япон. М.: Мир, 1988

[7] Доценко А.В., Исаков А.Б., Рябов А.Ю. Unix. X Window. Motif. Основы программирования. В двух томах. М.: АО «Аналитик», 1995

[8] Брюс Эккель. Философия Java. Библиотека программиста. СПб.: Питер, 2001

Игорь Облаков (oblakov@rapas.ru), технический директор ЗАО «Рапас» (Москва), более 10 лет преподает вопросы администрирования ОС Unix.