Редактор TECO: EMACS, я твой отец

в 5:42, , рубрики: emacs, teco, интерпретаторы, КодоБред, ненормальное программирование, старожилы помнят, текстовые редакторы

Впервые про TECO я прочитал в пародийной статье Real Programmers Don't Use Pascal, написанной незадолго до моего рождения. Там было написано, что настоящие программисты не используют новомодные редакторы EMACS и VI:

Нет, Настоящий Программист хочет редактор вида «Просил? Так получай!» — сложный, загадочный, мощный, не прощающий ошибок, опасный. TECO, если быть точным.

Оригинал

No, the Real Programmer wants a `you asked for it, you got it' text editor — complicated, cryptic, powerful, unforgiving, dangerous. TECO, to be precise.

Это меня заинтриговало. Что за зверь такой, можно ли его пощупать? Википедия рассказала, что TECO — это Text Editor & COrrector, создан он в 1962-м году в DEC и использовался на компьютерах семейства PDP, а позже на системах OpenVMS. Оказалось, что существует порт на Си, который поддерживается энтузиастами в актуальном состоянии и собирается под современными операционными системами. Вот я и решил почувствовать себя настоящим программистом хотя бы немножко.

Редактор TECO: EMACS, я твой отец - 1

Компиляция под Linux никаких сложностей не вызвала (надо только поставить libncurses-dev). И вот мы запускаем tecoc и видим мощный интерфейс редактора:

*

Да, одна звёздочка. Это приглашение ко вводу команд. Похоже, без инструкции не обойтись. Хорошие новости — инструкция легко ищется в интернете. Это книжка почти на 300 страниц под названием «Standard TECO Text Editor and Corrector for the VAX, PDP-11, PDP-10, and PDP-8». Вот здесь можно почитать в PDF последнюю версию 1990-го года, в это время TECO уже практически был всеми забыт.

Для начала стоит выяснить, как же выйти из этого чуда. Оказывается, нужно ввести EX, а затем два раза нажать Escape. Вообще двойное нажатие Escape приводит к выполнению введённой команды. Клавиша Enter используется просто как перевод строки. Escape отображается на экране как доллар, но введя доллар, вы не получите эффекта Escape. Тем не менее далее если я пишу $, подразумевается, что надо нажать Escape. Так что выходим мы с помощью EX$$ (при этом файлы сохраняются). Отлично, входить и выходить научились — полдела сделано.

Вообще формат команд TECO примерно такой:

[[<число1>,] <число2>] [:] <команда> [<текст1> [ $ <текст2> ] $ ]

Довольно непривычно, что аргументы могут идти и слева, и справа. Впрочем, лучше воспринимать это по-другому: числа слева — это не часть команды, а ещё одна команда, которая возвращает число (или пару чисел) в качестве результата. При этом последующая команда может использовать результат предыдущей.

С эскейпами неудобно, потому что если его скопируешь и вставишь, получится просто доллар, который не воспринимается как разделитель. К счастью, есть альтернативный синтаксис для команд с текстовым аргументом: @ <команда> <разделитель> <текст> <разделитель>, где разделитель — любой символ. Например, вставить текст 'Habr' в текущую позицию курсора можно с помощью IHabr$ (I, текст, Escape — ничего не напоминает?), а можно с помощью @I/Habr/. Чтобы упростить себе жизнь, я перешёл на второй синтаксис.

Прекрасно. Хорошо бы научиться вводить какой-нибудь текст в файл. Чтобы указать выходной файл, используется команда EW, а входной файл — команда ER. По-хорошему они не должны совпадать. Оказывается, в те времена было популярно многостраничное представление текстовых файлов с разделением на страницы с помощью символа <FF> или form-feed. Это символ с кодом 12 (0xC), который вводился через Ctrl+L (L — двенадцатая буква английского алфавита). Это не только инструктировало принтер выплюнуть страницу и начать новую, но и позволяло редактировать длинный файл при скромных объёмах оперативной памяти. TECO грузит в память только одну страницу входного файла (до символа <FF>) и позволяет отредактировать её, записать в выходной файл и перейти к следующей. Также есть операции склейки и разделения страниц. Если файл будет один и тот же, понятно, что ничего хорошего не выйдет. Ну ладно, мы со страницами играться не будем, в наши дни это совсем дикость. Наши файлы будут одностраничными. Итак, создадим файл с нуля:

*@EW/habr.txt/$$
*@I/Hello, Habrahabr!

This is TECO, the most powerful editor in the world.
Stop using your fancy IDEs, TECO for the win!

Bye.
/$$
*EX$$

Это сработало, действительно появился файл habr.txt с нашим текстом. Я каждую команду завершал двумя Escape, но вообще-то это необязательно. Ну хорошо, а как же всё-таки редактировать существующий файл? Неохота каждый раз выдумывать новое имя. Для этого есть специальная команда EB, которая при первой записи переименует входной файл в *.bak, таким образом соблюдая то, что входной и выходной файл различны. После открытия файла надо не забыть перелистнуть на первую страницу (команда Y), а затем можно вывести всё содержимое файла командой HT:

*@EB/habr.txt/YHT$$
Hello, Habrahabr!

This is TECO, the most powerful editor in the world.
Stop using your fancy IDEs, TECO for the win!

Bye.
*

Строго говоря, HT — это две команды. Команда H возвращает пару чисел — 0 и длину текстового буфера, то есть страницы, которая сейчас в памяти. А команда T печатает фрагмент файла в заданном диапазоне смещений:

*0,5T$$
Hello*7,17T$$
Habrahabr!*

Да, перевод строки никто вам не добавит лишний раз, попросили пять символов — получайте. Зато всё чётко и ясно. Если команде T передать на вход только одно число, оно интерпретируется как количество строк, которое надо напечатать, считая от позиции курсора вперёд или назад. При этом если курсор в середине строки, то 0T печатает фрагмент от начала текущей строки до курсора, а просто T без параметров — от курсора до конца строки:

*5J$$
*0T$$
Hello*T$$
, Habrahabr!
*

Команда J, которой мы воспользовались выше, перемещает курсор на заданное абсолютное смещение (курсор расположен всегда между символами). Ну так как-то некрасиво смотреть, хотелось бы увидеть этот самый курсор. Можно между 0T и T просто напечатать палочку? Да, можно. Команда печати — это ^A (можно ввести Ctrl+A, а можно прямо галочку и букву A по очереди):

*0T@^A/|/T$$
Hello|, Habrahabr!
*

Ура, мы теперь умеем видеть позицию курсора. Хорошо бы записать эту команду и при необходимости выполнять её. Если сразу после выполнения команды набрать * и потом букву или цифру, то текст предыдущей команды запишется в соответствующий текстовый Q-регистр (я так и не выяснил, почему не просто регистр, а Q-регистр). Наберём, например, *Z. Теперь содержимое регистра Z можно выполнить как команду с помощью команды MZ:

*MZ$$
Hello|, Habrahabr!
*

Отлично, мы записали первый TECO-макрос. Ну бегать по позициям скучно, хорошо бы уметь что-нибудь искать. Поищем, например, слово IDE. Справка говорит, что для этого есть команда S:

*@S/IDE/$$
*

И что? Ничего не выдал. Нашёл или не нашёл? А если нашёл, то где? Да, интерактивные редакторы развращают. Раз ничего не выдал, значит, нашёл. TECO просто переставил курсор после найденного текста. Давайте повторим и сразу нарисуем строчку с курсором:

*@S/IDE/MZ$$
?SRH   Search failure "IDE"

Ой, а теперь-то что? Ага, ищет-то он с текущей позиции курсора, вот второго IDE и не нашлось. Надо сперва перейти к началу файла (можно просто J):

*J@S/IDE/MZ$$
Stop using your fancy IDE|s, TECO for the win!

Во, красота. А как насчёт выделить найденный текст с обеих сторон? Тут пришлось изрядно порыть документацию. Пригодились такие штуки:

  • ^S — возвращает длину результата последнего поиска или последней вставки (со знаком минус, чтобы веселее было)
  • . — точка возвращает текущую позицию курсора в буфере.
  • C — перемещает курсор вправо на количество символов, возвращённое предыдущей командой (есть и противоположная команда, которая идёт влево, — R).

Соответственно с помощью ^SC можно перейти к началу найденной строки, потом знакомое 0T распечатает префикс, затем выведем [. Далее надо напечатать фрагмент от позиции . до .-^S (помните, что ^S — отрицательное число). Затем напечатать ], вернуть курсор на место с помощью -^SC и вывести остаток строки с помощью T. Вот вся программа:

*^SC0T@^A/[/.,.-^ST@^A/]/-^SCT$$
Stop using your fancy [IDE]s, TECO for the win!
*

Отлично, мы уже начали уделывать Перл. Самое время для следующей цитаты из статьи про реальных программистов:

Замечено, что последовательность команд TECO скорее напоминает передачу шума, чем читаемый текст. Весёлое развлечение — набрать в TECO своё имя как команду и попытаться угадать, что произойдёт. Практически любая опечатка при разговоре с TECO может уничтожить вашу программу или, что ещё хуже, внести неуловимый и таинственный баг в когда-то рабочую процедуру.

Оригинал

It has been observed that a TECO command sequence more closely resembles transmission line noise than readable text. One of the more entertaining games to play with TECO is to type your name in as a command line and try to guess what it does. Just about any possible typing error while talking with TECO will probably destroy your program, or even worse — introduce subtle and mysterious bugs in a once working subroutine.

Кстати, некоторое подобие регулярных выражений в TECO тоже имеется. Например, аналогом [A-Z]d+ будет ^EW^EM^ED. Если вы не любите обычные регулярные выражения, поработайте немного в TECO. После этого будете любить.

Теперь хотелось бы каких-нибудь управляющих структур. Скажем, такая задача: считая, что курсор стоит на начале строки, взять текст строки в красивую рамочку. Кстати, двигаться по строкам вниз — команда L, а вверх — -L. Также вы можете нажимать Ctrl+H и Ctrl+J для быстрого выполнения команд -LT и LT и бегать по тексту туда-сюда.

Для этой задачи нам потребуется вставить столько минусиков, сколько букв в текущей строке. Как это измерить? Можно вызвать . два раза, до и после L, и посчитать разность. Нам пригодится запись и чтение чисел в Q-регистры (число и текст хранятся в Q-регистре с одним и тем же именем независимо). UA записывает число в регистр A, а QA — считывает его. Простой цикл на n итераций — это n<...>. Например, если мы хотим вставить минусик A раз, мы напишем QA<@I/-/>. Весь макрос будет выглядеть так:

.UAL.-QA-2UA-L@I/+/QA<@I/-/>@I/+
|/L2R@I/|
+/QA<@I/-/>@I/+
/-LC

Возможно, для кого-то самое непонятное в этом макросе — это -2. А ещё потом 2R. Всё очень просто: перевод строки в те времена всегда занимал два символа, 'nr'. Никаких разногласий не было, это было прекрасно. Нам надо его вычесть из разности координат начал строк, а для отрисовки правой рамочки надо сходить на два символа влево.

Сохраним этот макрос его в регистр Y. Это, кстати, можно сделать не только через *Y после выполнения команды, а с помощью команды ^U записи строки в регистр: @^UY/текст макроса/. Выполним его, находясь в начале буфера, и получим:

*MY$$
*HT$$
+-----------------+
|Hello, Habrahabr!|
+-----------------+

This is TECO, the most powerful editor in the world.
Stop using your fancy IDEs, TECO for the win!

Bye.
*

Супер! Переменные, циклы — это уже похоже на настоящее программирование. Можно идти на собеседование на должность Senior TECO Developer. Кстати, о собеседованиях. Давайте напишем макрос FizzBuzz на TECO. Не знаю, делал ли это кто-то до меня.

Тут пригодилось бы деление с остатком на 15, но операции деления с остатком, к сожалению, нет. Зато есть деление нацело, поэтому можно выразить через x-x/15*15. Правда приоритетов операций тоже нет, поэтому придётся писать -x/15*15+x. Далее в зависимости от остатка надо сделать разные вещи. Если остаток 0, напечатать FizzBuzz, если 3, 6, 9 или 12, то Fizz, если 5 или 10, то Buzz, а иначе — входное число. Для этого пригодится команда O. Без числового аргумента это безусловный прыжок (то есть GOTO), а с числовым аргументом — это вроде switch: nO прыгает на n-ную метку, перечисленную через запятую, если она есть. Метки выглядят как !метка! (так же пишут и комментарии — это просто метка, на которую никто не прыгает). Создадим метки !f! (для Fizz), !b! (для Buzz) и !fb! (для FizzBuzz), а также !e! — конец для выпрыгивания наружу, а-ля break. Вот весь макрос, включая команду его записи в Q-регистр F:

*^UF
UA-QA/15*15+QA@O/fb,,,f,,b,f,,,f,b,,f/QA=@O/e/!fb!@^A/Fizz/!b!@^A/Buzz
/@O/e/!f!@^A/Fizz
/!e!$$

Заметьте, что в команду ^A я передаю перевод строки, чтобы вывести его на экран. Представляете, в Java до сих пор нет многострочных литералов, а в TECO они уже были больше полувека назад! Ещё я немного сэкономил, сделав "fallthrough" после ветки !fb! и напечатав "FizzBuzz" из двух половинок. Совсем как в обычном switch-case операторе.

Интересно, что макрос начинается с UA: записать в регистр A число. А какое число? Очень просто — аргумент этого самого макроса. Его надо указать просто перед вызовом. Проверяем:

*4MF$$
4
*5MF$$
Buzz
*105MF$$
FizzBuzz
*87MF$$
Fizz
*44MF$$
44
*

Супер, мы прошли собеседование! Последний мой эксперимент потребовался, чтобы сделать КДПВ к этой статье. Как написать программу, которая выведет решётками нужные буквы? Похоже, что в языке нет никаких массивов и структур данных. Но зато есть текстовый буфер! Я загнал знакогенератор туда в виде <символ><первая строка битовой маски><символ><вторая строка битовой маски>... (Команда HK очистит текущее содержимое буфера):

*HK@I/H130H130H124H130H130A62A66A254A130A130B254B128B252B130B252R252R130R252R128R128R/$$

Спозиционироваться на нужное число можно с помощью J<номер строки>@S/<символ>/, потому что числовой параметр S позволяет найти энное вхождение заданной строки. Я не нашёл как выполнить команду, параметризованную произвольным текстовым параметром, поэтому сформировал команду в Q-регистре D (:^UD дописывает в конец, а просто ^UD заменяет текст в Q-регистре) и выполнил её как макрос. Затем распарсить число можно с помощью команды обратный слэш . Ещё нам потребуются условные операторы: "N<команда>' — выполнить, если числовой аргумент не ноль, а "E<команда>' — выполнить, если числовой аргумент ноль. Ветку «иначе» тоже можно создать, отделив её трубой. Таким образом, вывод пробела или решётки делается через "E32^T|35^T', где ^T печатает символ с соответствующим ASCII-кодом. Ещё полезная команда %<регистр>, увеличивающая численное значение в соответствующем Q-регистре на единицу.

Регистры я использовал так (чтобы не запутаться, каждый использовался только для числа или для строки):

  • A — текущая строка текущей буквы в виде битовой маски
  • B — 2^N, где N — текущий бит
  • C — строчка, которую надо вывести, все символы должны быть в знакогенераторе
  • D — автогенерируемый макрос для поиска битовой маски в знакогенераторе
  • E — счётчик текущей буквы
  • F — счётчик строк (1-6)

@^UC/HABRAHABR/1UF!bl!0UE!bm!J@^UD/@S|/QEQC:^UD@:^UD/|/QFMD$
UAC128UB!br!QB&QA"E32^T|35^T'QB/2UBQB"N@O/br/'%E^[QE-:QC"N@O/bm/'@^A/
/%F^[QF-6"N@O/bl/'$$

На TECO было написано множество макросов, причём довольно сложных. Разумеется, чтобы хоть что-то понять, хорошо бы структурировать макрос, вставлять переводы строки, отступы и комментарии. Однако выясняется, что это сильно замедляет обработку макроса. Поэтому придумали минимайзеры. Вот код такого минимайзера до минификации, вот он же после. Что-то похожее мы наблюдаем сегодня в мире JavaScript.

Интересно, что имеются команды чтения символа с клавиатуры, в связи с чем макрос можно сделать интерактивным. Используя возможности вывода терминалов через Escape-последовательности, легко позиционировать курсор на экране, обновлять текст фрагментами и переключать цвета. Таким образом с помощью макроса можно обработать каждый вводимый символ или специальную клавишу и сделать интерактивный режим редактирования. Примерно таким образом родился Emacs, который изначально действительно был макросом к TECO, и только потом был переписан как отдельное приложение.

Автор: Тагир Валеев

Источник

Поделиться

* - обязательные к заполнению поля