Перевод заметки Дейва Гауэра о руководстве к языку sam
Это перевод публикации Дейва Гауэра (Dave Gauer) по поводу документа "A tutorial for the sam command language" от Роба Пайка.
Оригинал текста на английском:
Текст ссылается на документ 1987 года:
Если вы не знаете кто такой Роберт Пайк, то можете заглянуть в википедию:
Введение
Sam это любопытный текстовый редактор, созданный Робом Пайком. А рассматриваемый документ представляет собой краткое руководство по языку команд, используемых в этом текстовом редакторе.
Я довольно много вынес для себя из этого 14-станичного документа. В основном это касается дизайна языка.
Идиомы языка
Первое, что можно почерпнуть из этого документа: идея языковых идиом. Конечно, во всех языках есть идиомы и мы часто говорим об идиамотическом решении проблемы, например "В Ruby идиоматически правильно сделать это так ...". Но Пайк прямо указывает на двухсимвольную последовательность и говорит:
Адрес +- это идиома
(В частности: +- это сокращённая запись +1-1, то есть "выбери следующую строку, а затем вернись к предыдущей строке", что приводит к выбору текущей строки.)
Почему это важно? Ну вот если бы я разрабатывал язык и мне нужна была бы возможность выбирать текущую строку, то я, вероятно, подался бы искушению создать для этого новую команду (L или что-то вроде того). Но именно из-за таких решений инструмент в итоге становится не очень удобными, поскольку приходится изучать огромное количество отдельных команд. (Все мы знаем хотя бы пару таких примеров.)
В идеале нужно сделать так, что бы элементы языка работали вместе, принося максимальную пользу. А затем минимизировать их количество.
Циклы и условия без мишуры
Команда x, на мой взгляд, является ключом к тому, что делает язык команд sam таким великолепным. Хотя это сокращение от "извлечь" ("extract"), но я привык думать о ней как о цикле "для каждого" ("for each").
Вот так, например, можно заменить все "hello" на "bye":
,x/hello/ c/bye/
Адрес "," (а это сокращение от "0,$") для начала выбирает весь документ целиком (ещё одна идиома sam!). Команда "x/hello/" означает "для каждой подстроки 'hello' ...", а "c/bye/" - "замени её на 'bye'".
Конечно, это обычный найти-и-заменить. Вот эквивалентная команда для Vim:
:%s/hello/bye/g
Но если команда s в ex/vi/Vim специализирована и выполняет только замену, то команда x в sam может выполнить любую другую команду, например d (delete, удаление).
Вот так можно удалить все комментарии в формате сценариев командной оболочки (shell-style):
,x/^#.*\n/ d
То есть, что бы сделать что-то с каждой строкой файла в sam нужно выполнить:
,x/.*\n/
Ага, скажете вы, но ведь это же как команда g ("global") в ex/vi/Vim, которая так же может выполнять произвольные команды для каждого совпадения с шаблоном...
И да, вы правы. Но это мы пока ещё не копнули глубже. А что по поводу условий?
Вот так можно задать условие с помощью команды g ("guard") в sam:
,x/.*\n/ g/foo/ d
(Прошу прощения за путаницу: я понимаю, что упоминание команды g в ex/vi/Vim, а за затем такой же команды в sam, которая означает другое, сбивает с толку. Это совершенно разные команды.)
Я воспринимаю g в sam, как выражение "if" ("если").
Пример выше я читаю так: "для каждой строки, если она содержит 'foo', удали эту строку".
Обратите внимание, что команда d удаляет всю строку целиком. Это происходит потому, что g/foo/ означает "если содержит 'foo'". Как и условный оператор ("if"), g не выбирает совпавшую подстроку, а просто определяет логику работы.
Композиция
Вот что волнует меня по настоящему, ведь этот документ позволил мне сформулировать то, что беспокоило меня долгое время.
Проблема с sed
Мне нравится sed за свою лаконичность. Хотя я ценю и удобочитаемость исходного кода: тут в качестве примера могу предложить RubyLit [4]. Но в командной строке лаконичность важнее. Я помню, когда появился PowerShell, хотя в нём было много хороших идей, я увидел длинные названия команд с Заглавными-Буквами. И я сразу подумал: "вы что, с ума сошли?". Меньше всего в командной строке я хочу набирать текстовые последовательности длинной в абзац! (И да, я знаком с сокращениями в PowerShell).
Но sed расстраивает тем, что дразнит своей программируемостью. Будучи технически "полным по Тьюрингу", он никогда не предназначался для выражения сложной логики программирования, что видно.
Проблема с Awk
Мне нравится и Awk за его неявный настраиваемый цикл разбора строк/записей и простую структуру правил "pattern {action}". Это элегантно, даёт широкие возможности использования и весьма инновационно для своего времени (и, я полагаю, вдохновлено языком программирования Снобол/Snobol [5]). Кроме того, книга "The AWK Programming Language" от самих A, W и K очень-очень хороша!
Но Awk расстраивает меня даже больше чем sed, именно потому, что это язык программирования. В рамках одного уровня Awk демонстрирует элегантность, но у него нет возможности выразить правила внутри других правил, поэтому вы не можете использовать всю эту выразительную силу за пределами верхнего уровня "записи". Правила Awk нельзя вкладывать друг в друга:
# Вы не можете сделать в Awk как-то так:{ , { } }
А после сопоставления данных на верхнем уровне, остальная часть вашей Awk-программы представляет собой императивное C-подобное программирование в духе "for(i=0; i<3; i++)". Это какой-то позор!
Великолепие sam
У sam есть то, чего так не хватает Awk: Композиция.
Вот пример Пайка:
В качестве простого примера рассмотрим команду замены всех вхождений Emacs на emacs:
,x/Emacs/ c/emacs/
Это, конечно, будет работать. Но мы можем использовать команды x, что бы переписать только первую букву Emacs, а не слово целиком:
,x/Emacs/ x/E/ c/e/
Внимательно посмотрите на этот второй пример. И, возможно, он покажется вам таким же захватывающим, как и мне.
Я прочитал бы его следующим образом: "по всему документу найди все подстроки 'Emacs' и для каждой замени 'E' 'e'."
Команда x комбинируется как сама с собой, так и с любой другой командой. Поэтому можно использовать этот простой в освоении механизм для краткого и точного выполнения сколь угодно сложного выбора без необходимости ломать голову в попытке выполнить всё это с помощью одного регулярного выражения.
Конечно, примеры выше это простейшая задача найти-и-заменить, но я надеюсь, что вы легко сможете представить себе и нетривиальные случаи.
Вся вторая половина документа в основном состоит из составных примеров с участием x.
И меня больше всего завораживает то, насколько естественно составляются такие "циклы" без какой-либо необходимости в явных именах переменных или разделителях кода.
Группировка
Все вышеперечисленные примеры, вероятно, можно отнести к конкатенативному программированию (Forth [6], Meow5 [7]). Подразумевается, что каждое последующее выражение неявно влияет на результаты работы предыдущего. Но также вы можете запускать выражения в группе, что существенно меняет их взаимосвязь:
{
command1
command2
}
Пайк описывает сгруппированные выражения, как "применяемые параллельно".
На практике это означает, что каждое выражение обрабатывает входные данные, какими они были на момент начала работы группы:
,x/foo|bar/ g/foo/c/bar/ g/bar/c/foo/
В вышеприведённом примере сначала все "foo" заменятся на "bar", а потом в полученном тексте все "bar" заменятся на "foo" (то есть во всём тексте вообще больше не останется "bar" после второго выражения). Но, вероятно, подразумевалось поменять местами "foo" и "bar". Как этого добиться? Это похоже на обмен содержимого двух переменных без третьего временного хранилища.
Что же, с помощью группировки в sam вы можете это реализовать так:
,x/foo|bar/ {
g/foo/c/bar/
g/bar/c/foo/
}
Первое выражение в группе заменит все "foo" на "bar", но второе выражение всё равно получит на вход исходный текст, а значит заменит все "bar" на "foo", как и задумывалось.
Для меня это звучит довольно дико и у меня нет полного понимания как такое может быть реализовано. Легко представить, как каждое выражение в группе работает со своей копией входных строк, но как объединить результаты обратно в конечный результат?
Единственная подсказка, оставленная Пайком в документе:
Как уже упоминалось, это означает, что каждая команда в рамках составной команды видит состояние файла до каких-либо изменений. [...] Косвенным следствием этого является то, что изменения должны выполняться в прямом порядке по файлу и не должны перекрываться.
Я думаю, что могу такое себе представить, но конкретные детали мне не понятны.
Заключение
Как я уже говорил в начале, для себя я вынес массу полезного из этого краткого документа по языку команд для текстового редактора, которым, скорее всего, никогда не буду пользоваться.
Язык sam концептуально мал и прост. Это хорошо демонстрирует силу нескольких удачно подобранных элементов, которые хорошо сочетаются вместе.
В результате получилось то, что я считаю самой большой упущенной возможностью Awk: отсутствие возможности компоновки его главной особенности: правил `pattern {action}`.
Язык sam настолько похож на ed или sed, что поначалу кажется довольно знакомым. Но sam показывает, что можно добиться гораздо большей гибкости и выразительности, но без дополнительной сложности - просто благодаря интуитивно понятной концепции совместной работы команд.
Удобство это субъективная мера и для меня некоторые примеры Пайка читаются лучше, чем другие. Но поскольку он разбивает проблему на небольшие кусочки, на мой взгляд, примеры команд x гораздо более легкие для понимания, чем типичные сложные выражения в ex/vi/Vim.
Вот, например, что я дописал в мой .vimrc пару недель назад:
vnoremaptd =gv:g/^$/d gv:s/^\s*/* [ ] /g :noh
Это выражение берет выделенный список и превращает его в список дел VimWiki. Было бы интересно реализовать такое на sam!
Большое спасибо Will Clardy (quexxon.net) [8] за наводку на этот документ!