TECO Editor: EMACS, I am your father

For the first time about TECO I read in a parody article Real Programmers Don't Use Pascal , written shortly before my birth. It was written there that real programmers do not use the newfangled editors EMACS and 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.

It intrigued me. What kind of animal is this, can it be touched? Wikipedia said that TECO - is T the ext E ditor & of CO rrector, he created in 1962 and was used in the DEC PDP on the family computer, and later on OpenVMS systems. It turned out that there is a port in C , which is maintained by enthusiasts in an up-to-date state and is assembled under modern operating systems. So I decided to feel like a real programmer at least a little bit.



Compilation under Linux did not cause any difficulties (you just need to install libncurses-dev). And so we launch tecoc and see the powerful editor interface:


*

Yes, one star. This is an invitation to enter commands. It seems that you can not do without instructions. The good news is that the manual is easy to find on the Internet. This is a nearly 300-page book called Standard TECO Text Editor and Corrector for the VAX, PDP-11, PDP-10, and PDP-8. Here you can read in PDF the latest version of 1990, at which time TECO was almost forgotten by everyone.


To begin with, it is worth figuring out how to get out of this miracle. It turns out that you need to enter EX , and then press Escape two times. In general, double-clicking Escape leads to the execution of the entered command. The Enter key is used simply as a line feed. The Escape is displayed as a dollar on the screen, but entering a dollar will not give you the Escape effect. However, further if I write $ , it is implied that I must press Escape. So we exit with EX$$ (while the files are saved). Well, we learned to enter and exit - half the job is done.


In general, the TECO command format is something like this:


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

It is rather unusual that arguments can go both on the left and on the right. However, it’s better to take it differently: the numbers on the left are not part of the team, but another team that returns a number (or a pair of numbers) as a result. In this case, the subsequent command can use the result of the previous one.


Escapes are inconvenient, because if you copy and paste it, you’ll just get a dollar, which is not perceived as a separator. Fortunately, there is an alternative syntax for commands with a text argument:, @ <команда> <разделитель> <текст> <разделитель> where the separator is any character. For example, you can insert the text 'Habr' at the current cursor position with IHabr$ (I, text, Escape - doesn’t resemble anything?), But you can with @I/Habr/ . To simplify my life, I switched to the second syntax.


Perfectly. It would be nice to learn how to enter some text into a file. To specify the output file, the command is used EW , and the input file is the command ER . In a good way, they should not match. It turns out that in those days, multi-page presentation of text files with pagination using a symbol <FF> or form-feed was popular <FF> . This is a character with code 12 (0xC), which was entered via Ctrl + L (L is the twelfth letter of the English alphabet). This not only instructed the printer to spit out the page and start a new one, but also made it possible to edit a long file with modest amounts of RAM. TECO loads only one page of the input file into memory (up to the character <FF> ) and allows you to edit it, write to the output file and go to the next. There are also operations for gluing and splitting pages. If the file is the same, it is clear that nothing good will come of it. Well, we won’t play with the pages, these days it’s absolutely wild. Our files will be single page. So, create a file from scratch:


*@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$$

It worked, a file habr.txt with our text really appeared habr.txt . I completed each command with two Escape, but actually this is not necessary. Well, but how can I edit an existing file? Reluctance to invent a new name every time. To do this, there is a special command EB that, when you first record, renames the input file to *.bak , thus observing that the input and output files are different. After opening the file, do not forget to flip to the first page (command Y ), and then you can display the entire contents of the file with the command 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.
*

Strictly speaking, HT these are two teams. The command H returns a pair of numbers - 0 and the length of the text buffer, that is, the page that is now in memory. And the command T prints a fragment of the file in a given range of offsets:


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

Yes, no one will add the line feed again, they asked for five characters - get it. But everything is clear and clear. If a command is T passed only one number to the input, it is interpreted as the number of lines to be printed, counting from the cursor position forward or backward. Moreover, if the cursor is in the middle of the line, it 0T prints a fragment from the beginning of the current line to the cursor, but simply T without parameters - from the cursor to the end of the line:


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

The command J that we used above moves the cursor to the specified absolute offset (the cursor is always located between characters). Well, since it’s ugly to look, I would like to see this very cursor. Is it just print a wand? Yes you can. The print command is this ^A (you can enter Ctrl + A, or you can directly tick and the letter A in turn):


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

Hooray, we can now see the cursor position. It would be nice to record this command and, if necessary, execute it. If you immediately type a * letter or a number after the command is executed * , the text of the previous command will be written in the corresponding Q-register (I still have not figured out why it is not just a register, but a Q-register). Naberom example *Z . Now the contents of the register Z can be executed as a command using the command MZ :


*MZ$$
Hello|, Habrahabr!
*

Well, we recorded the first TECO macro. Well, running through positions is boring, it would be nice to be able to look for something. Let's look for, for example, the word IDE. The help says that there is a command for this S :


*@S/IDE/$$
*

So what? Didn’t betray anything. Found or not found? And if found, then where? Yes, interactive editors are corrupting. If I didn’t give anything, then I found it. TECO simply moved the cursor after the text found. Let's repeat and immediately draw a line with the cursor:


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

Oh, and now what? Yeah, he is looking for something from the current cursor position, but there was no second IDE. You must first go to the beginning of the file (you can simply J ):


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

In, beauty. What about highlighting the text found on both sides? Then I had to prettyly rummage the documentation. Such things came in handy:


  • ^S - returns the length of the result of the last search or the last insertion (with a minus sign to make it more fun)
  • . - point returns the current cursor position in the buffer.
  • C - moves the cursor to the right by the number of characters returned by the previous command (there is also the opposite command, which goes to the left, - R ).

Accordingly, with the help ^SC you can go to the beginning of the found line, then the familiar one will 0T print the prefix, then print it [ . Next, you need to print the fragment from position . to .-^S (remember that it ^S is a negative number). Then print ] , return the cursor to the place with -^SC and print the rest of the line with T . Here is the whole program:


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

Alright, we already started doing Pearl. It's time for the following quote from an article about real programmers:


Замечено, что последовательность команд 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.

By the way, some similarity of regular expressions in TECO is also available. For example, it [A-Z]\d+ will be an analogue ^EW^EM^ED . If you don't like regular regular expressions, work a little at TECO . After that you will love.


Now I would like some control structures. Let's say this problem: assuming that the cursor is at the beginning of a line, take the text of the line in a beautiful frame. By the way, move down the lines - the command L , and up - -L . You can also press Ctrl + H and Ctrl + J to quickly execute commands -LT and LT and run through the text back and forth.


For this task, we need to insert as many minus signs as there are letters in the current line. How to measure it? You can call . twice, before and after L , and calculate the difference. Writing and reading numbers in Q-registers comes in handy (the number and text are stored in the Q-register with the same name independently). UA writes a number to the register A , and QA - reads it. A simple loop at n iterations is this n<...> . For example, if we want to insert a minus sign A once, we will write QA<@I/-/> . The whole macro will look like this:


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

Perhaps for someone the most incomprehensible thing in this macro is -2. And then later 2R . It's very simple: newline in those days always held two characters '\r\n' . There was no disagreement; it was wonderful. We need to subtract it from the difference in the coordinates of the beginning of the lines, and to draw the right frame we need to go two characters to the left.


Save this macro to register Y . Incidentally, this can be done not only through *Y after executing the command, and a command ^U line entry in the register: @^UY/текст макроса/ . Let's execute it, being at the beginning of the buffer, and we will receive:


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

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

Bye.
*

Super! Variables, loops - this is already like real programming. You can go for an interview for the position of Senior TECO Developer. Speaking of interviews. Let's write a FizzBuzz macro on TECO. I don’t know if anyone did this before me.


Here division with the remainder by 15 would be useful, but the operation of division with the remainder, unfortunately, is not. But there is a division entirely, therefore it can be expressed through x-x/15*15 . True, there are no priorities for operations either, so I have to write -x/15*15+x . Next, depending on the remainder, you need to do different things. If the remainder is 0, print FizzBuzz if 3, 6, 9 or 12, then Fizz if 5 or 10, then Buzz , otherwise, the input number. A command is useful for this O . Without a numerical argument, this is an unconditional jump (i.e. GOTO), and with a numerical argument it is like a switch: nO jumps to the nth mark, separated by commas, if any. Tags look like !метка! (comments are also written - this is just a label that no one jumps on). Create labels !f! (for Fizz), !b! (for Buzz) and !fb! (for FizzBuzz), as well as the !e! end for jumping out, a la break . Here is the whole macro, including the command to write it to the Q-register 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!$$

Notice that ^A I pass the line feed to the command in ^A order to display it on the screen. Imagine, in Java there are still no multiline literals, but in TECO they were already more than half a century ago! I also saved a bit by doing “fallthrough” after the branch !fb! and typing “FizzBuzz” from two halves. Just like in a regular switch-case statement.


Interestingly, the macro begins with UA : write a A number UA in the register A . And what number? Very simple - the argument of this macro itself. It must be specified just before the call. We check:


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

Great, we went through an interview! My last experiment was required to make KDPV for this article. How to write a program that displays the letters you need? It seems that there are no arrays and data structures in the language. But there is a text buffer! I drove the character generator there in the form <символ><первая строка битовой маски><символ><вторая строка битовой маски>... (The command HK will clear the current contents of the buffer):


*HK@I/H130H130H124H130H130A62A66A254A130A130B254B128B252B130B252R252R130R252R128R128R/$$

You can position yourself on the desired number with J<номер строки>@S/<символ>/ , because the numerical parameter S allows you to find the nth occurrence of a given string. I did not find how to execute a command parameterized by an arbitrary text parameter, so I formed a command in the Q-register D ( :^UD appends to the end, but simply ^UD replaces the text in the Q-register) and executed it as a macro. You can then parse the number using the backslash command \ . We also need conditional operators: "N<команда>' - execute if the numeric argument is not zero, but "E<команда>' - execute if the numeric argument is zero. A branch “otherwise” can also be created by separating it with a pipe. Thus, the conclusion of a space or a lattice is done through "E32^T|35^T' , where ^T prints a character with the appropriate ASCII code. Another useful command %<регистр> that increases the numerical value in the corresponding Q-register by one.


I used registers like this (in order not to get confused, each was used only for a number or for a string):


  • A - current line of the current letter in the form of a bit mask
  • B - 2 ^ N, where N is the current bit
  • C - line to be printed, all characters must be in the character generator
  • D - auto-generated macro to search for a bit mask in the character generator
  • E - current letter counter
  • F - row counter (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/'$$

A lot of macros were written on TECO, and quite complex ones. Of course, in order to at least understand something, it would be nice to structure the macro, insert line feeds, indents and comments. However, it turns out that this greatly slows down macro processing. Therefore, they came up with minimizers. Here is the code of such a minimizer before minification, here it is after. Something similar we see today in the world of JavaScript.


Interestingly, there are commands for reading a character from the keyboard, and therefore the macro can be made interactive. Using the ability to display terminals through Escape sequences, it is easy to position the cursor on the screen, update text with fragments and switch colors. Thus, using a macro, you can process each input character or special key and make an interactive editing mode. In approximately this way, Emacs was born, which was originally really a macro for TECO, and only then was rewritten as a separate application.