💾 Archived View for soviet.circumlunar.space › quarters › collection › Documentation › ed_tutorial.t… captured on 2022-01-09 at 00:57:37.

View Raw

More Information

⬅️ Previous capture (2021-12-03)

-=-=-=-=-=-=-

                   ed(1) is The Right Tool    
                =============================

You know: ed(1) is the standard text editor [1], and indeed you would
find ed(1) installed in any unix-like system. But I guess most Unix
users have never actually tried to open it. The reason is probably that
ed(1) is considered "hard-to-use" and "user-unfriendly". I really see no
reason for such misconceptions and no motivation for their popularity,
so I will try to convince you with concrete examples and scientific
evidence that ed(1) is, almost always, The Right Tool to use. 

This tutorial was originally publised as a series of "phlog" posts (a
blog over gopher), in the hope to serve as an "ed-primer" for beginners.
The eight posts are reported below in the same order as they appeared in
the phlog. The only addition is the last paragraph of Section VII, which
was not present in the initial series of phlogs. The references of each
section are reported at the end of the section.

If you are in a rush and would like to go a proper book, look for the
great "Ed Mastery" by Michael Lucas [2].

KatolaZ

   ed(1) is The Right Tool (I)   
=================================

First things first: ed(1) is a line editor, meaning that it acts on a
line at a time. This is something we are not used to anymore, since our
glass-terminals have allowed full-screen character-based editors to
evolve. But on teletypes line-based editing makes a lot of sense. This
means that we need to tell ed(1) which line we would like to edit and
what to do on that line.  

In this tutorial we will use ed(1) to compile and manage simple TODO
lists. This will allow us to explore almost all the ed(1) commands. 



Let's start by creating the file "TODO.txt" with ed(1):

  $ ed TODO.txt
  TODO.txt: No such file or directory


Well, welcome to ed(1), aka "The Faithful Silent Servant of Unix masters
of old". ed(1) is telling you that it could not find any file named
TODO.txt in the current directory (and this is something we knew
already) and then it prints a new-line to signal that it is ready to
receive commands. Yes, ed(1) has no default "prompt". Actually, it does,
and the prompt is just "no-character-at-all". Now let's add a few items
to our TODO.txt list:

  0a
  TODO LIST
  - phlog about ed(1)
  - put rubbish bins outside
  - make a tea
  - check postgrey hiccup
  .


The first line "0a" tells ed(1) that I want to "a-ppend" lines after the
0-th line. Now, the 0-th line actually indicates the start of the
buffer. ed(1) accepts my command and waits for the lines to be inserted
there. It will keep receiving lines until you input a line containing
only a "." (dot) [3].  Then ed(1) will "print" its prompt (which is
indeed "no-character") and wait for the next command. Let's ask ed(1) to
"p-rint" the whole TODO list then:

  ,p
  TODO LIST
  - phlog about ed(1)
  - put rubbish bins outside
  - make a tea
  - check postgrey hiccup


(notice the ed(1) prompt on the last line!). The command "p" is used to
print lines. It is normally put after the specification of a range of
lines, but in this case "," indicates the range "from the first line
down to the last one". Now what happens if you type "p" alone?

  p
  - check postgrey hiccup


(notice again the ed(1) prompt above). Well, any command in ed(1)
modifies the "current line address", which is the line any ed(1) command
would implicitely work on if you don't provide any line to it. The
command "p" moves the current address to the last line it printed. So
",p" moved the current address to the last line of the buffer, and the
following "p" just printed the current line (the last one). 

Another useful command is "n", which prints lines adding line numbers:

  ,n
  1       TODO LIST
  2       - phlog about ed(1)
  3       - put rubbish bins outside
  4       - make a tea
  5       - check postgrey hiccup
 

Oh now that's convenient! Guess how you ask ed(1) to print the third
line in your buffer:

  3p
  - put rubbish bins outside
  
  
While you can use the special marker "$" to indicate "the last line in
the buffer":

  $n
  5       - check postgrey hiccup
  
 
Notice that there is no space between line ranges and the command that
applies to them. I guess you have had enough ed(1)-ting for today, and I
am not in the right mood to start working at this todo-list now. So
let's just save our work for later:

  w
  94


ed(1) prints the number of bytes written in the file TODO.txt. We can
now exit ed(1):

  q
  $

and you are back to the shell prompt. Now you can see your brand-new
TODO list with:

  $ cat TODO.txt
  TODO LIST
  - phlog about ed(1)
  - put rubbish bins outside
  - make a tea
  - check postgrey hiccup
  $

or by using:

  $ printf ",p\n" | ed TODO.txt
  94
  TODO LIST
  - phlog about ed(1)
  - put rubbish bins outside
  - make a tea
  - check postgrey hiccup
  $

;-)
 
 -+-+-+-

ed(1) was included in Unix-V1 (1971) and was used to write the Unix
      kernel and all the programs distributed with Unix at least until
      Unix-V7 (1979)

 -+-+-+-
 
[1] https://www.gnu.org/fun/jokes/ed-msg.html
[2] https://www.tiltedwindmillpress.com/product/ed/
[3] I use a dash "-" to indicate tasks that are still outstanding, a
    plus "+" for tasks I have started working on, and a star "*" for
    completed tasks.


   ed(1) is The Right Tool (II)   
==================================

Let's get on with our mission of learning the basics of ed(1), the
standard text editor of the unix environment.

In the first installment of this mini-series of phlogs [1] we saw a few
basic commands to add some lines ("a"), print lines ("p", "n") and save
a file after you are done ("w"). Using those commands we were able to
compile a simple TODO list:

  $ ed TODO.txt
  94
  ,n
  1       TODO LIST
  2       - phlog about ed(1)
  3       - put rubbish bins outside
  4       - make a tea
  5       - check postgrey hiccup

Now imagine that we want to insert a new item (buy some tea) just before
the line that reminds us to make a tea. We will use the command "i" (for
"insert"):

  4i
  - buy some pea 
  .
  ,n
  1       TODO LIST
  2       - phlog about ed(1)
  3       - put rubbish bins outside
  4       - buy some pea
  5       - make a tea
  6       - check postgrey hiccup

Easy, right? While "a" is used to "append" lines after a given line, "i"
is instead used to "insert" lines before a given line. As with "a", the 
"i" commands is ended by a line containing only a ".". If you have ever
used vi(1) or one of its many clones, you know that "i" is also the
command needed to move to "insert" mode in vi(1). Well, this is not a
coincidence.

Notice that when we inserted the new line we also introduced a typo: we
definitely want to buy some "tea" (not a "pea"). We can "change" the
content of a line using the command "c":

  4c
  - buy some tea
  .
  ,n
  1       TODO LIST
  2       - phlog about ed(1)
  3       - put rubbish bins outside
  4       - buy some tea
  5       - make a tea
  6       - check postgrey hiccup

Notice that ed(1) acts consistently: the three commands used to append,
insert, or change lines are all terminated by a line containing a single
".". Actually, the command "c" in general allows us to replace one line
(or a range of lines) with another line (or with a range of lines). So
if you prefer coffee instead of tea you might use:

  4,5c
  - buy some coffee
  - buy sugar
  - make a coffee
  .
  ,n
  1       TODO LIST
  2       - phlog about ed(1)
  3       - put rubbish bins outside
  4       - buy some coffee
  5       - buy sugar
  6       - make a coffee
  7       - check postgrey hiccup

Well, it's evident that having to retype a line just to correct a typo
is not exactly what one would call an efficient editing session. Indeed,
the command "c" is mostly used when the content of a range of lines has
to be substantially modified. Luckily, ed(1) has another command ("s",
for "substitute") which allows to easily correct typos, as well as to
perform complicate substitutions across any range of lines. That will be
the focus of the next phlog in this series :)

 -+-+-+-

[1] gopher://republic.circumlunar.space/0/~katolaz/phlog/20190505_ed_lists.txt



   ed(1) is The Right Tool (III)
===================================

A todo-list is useful if you start working on the items and you are
eventually able to remove some items from the list, sooner or later...

Our current list looks like this:

  ,n
  1       TODO LIST
  2       - phlog about ed(1)
  3       - put rubbish bins outside
  4       - buy some coffee
  5       - buy sugar
  6       - make a coffee
  7       - check postgrey hiccup

Now it is clear I have started phlogging about ed(1), so the first item
should somehow be updated. I have come up with a simple convention to
identify the status of items in a todo-list, by using the first
character of the item: a "-" indicates an outstanding item, a "+"
indicates something I have started working on, and a "*" marks a
completed item. In this case, I would like to change the "-" in the
first item with a "+". Let's start searching for the line where I
mentioned "phlog":

/phlog/
- phlog about ed(1)

Yep. An expression like "/expr/" is used in ed(1) to search for a
pattern. ed(1) search for the first occurrence of that pattern and
prints it. Most versions of ed(1) will also wrap the search around and
start from the beginning if they reach the end of the file. Now the
current line (the so-called ".") is the one containing the item I want
to edit. Let's "substitute" the first "-" in the line with a "+":

s/-/+/
p
+ phlog about ed(1)

Yeah, it's that simple. The command "s" (for "substitute") replaces the
first occurrence of the pattern between the first pair of "/" following
it with the pattern between the second pair of "/" following it. In this
case, we simply replaced "-" with "+". 

The command "s" accepts a line (or a range of lines) to work on. So if
we are indeed working on all the items in the todo-list, you can change
their state with a single command:

1,$s/-/+/
,p
TODO LIST
+ phlog about ed(1)
+ put rubbish bins outside
+ buy some coffee
+ buy sugar
+ make a coffee
+ check postgrey hiccup

Well, that's powerful, right? But in my case, well, it's not true, since
I have not put the rubbish outside and some other items are in a
different state. Let's revert out last change:

u
,p
TODO LIST
+ phlog about ed(1)
- put rubbish bins outside
- buy some coffee
- buy sugar
- make a coffee
- check postgrey hiccup

The command "u" (for "undo") will do that (i.e., revert the effect of
the last command). Try to see what happens if you keep pressing "u" :)

The cool thing about "s" is that the line it has to act upon can be
expressed with a pattern. For instance, I have solved the hiccup in
postgrey, so I can change the status of the line containing "postgrey"
to "done" by replacing the leading "-" with a "*". But I don't remember
which line is it, so I can use the command:

/postgrey/s/-/*/
,p
TODO LIST
+ phlog about ed(1)
- put rubbish bins outside
- buy some coffee
- buy sugar
- make a coffee


Remember that the "." is set at the last edited line:

.


Now that we have completed an item, we can finally celebrate: hurray!
Well, there is still a bunch of stuff to do anyway. How do we jump to
the next outstanding item in the list? Easy: we just look for a "-":

/-/
- put rubbish bins outside

Yeah, I know, but this is not rubbish-day. Let's look for the next
outstanding task:

//
- buy some coffee

Yep! The quite quick combination "//" repeats the last search you did!
And we can keep going:

//
- buy sugar
//
- make a coffee

That's handy, especially since "/" is normally placed in a quite
comfortable position in many keyboard layouts. The last example: I have
to be honest with you: I can't drink any coffee, so I would like to
replace any occurrence of "coffee" with "tea":

/coffe/s/coffee/tea/
p
- buy some tea

The command "/coffee/s/coffee/tea/" is to be read as "look for the next
line containing 'coffee' and replace 'coffee' with 'tea' on that line.
So what happened here? Let's have a look at the whole file:

,n
1       TODO LIST
2       + phlog about ed(1)
3       - put rubbish bins outside
4       - buy some tea
5       - buy sugar
6       - make a coffee
7       * check postgrey hiccup

So, the last search we performed ("//") had set the dot to line 6
("-make a coffee"). When we issued the substitution command, ed(1) had
to look for the next occurrence of "coffee". It wrapped the search
around (siince there is no line containing "coffee" after line 6), found
"coffee" on line 4, and replaced "coffee" with "tea" on that line. We
would like to repeat the last "s" command then, to replace the other
occurrence of "coffee"

/coffe/s/coffee/tea/
.
- make a tea

OK, that's powerful, but re-entering the command is not fun. Anyway, we
can't learn everything in a single phlog, and this one is already quite
long. Let's not forget to save our work:

w
121

To be honest with you, I really would like to get rid of that line with
"sugar", since I never put any sugar in my tea. But that's a quest for
another day :)


   ed(1) is The Right Tool (IV)
==================================

Let's have a look at our current TODO list:

  $ ed TODO.txt
  121
  ,p
  TODO LIST
  + phlog about ed(1)
  - put rubbish bins outside
  - buy some tea
  - buy sugar
  - make a tea
  * check postgrey hiccup

I need to add a couple more items to the list. Let's do it:

  a
  - find the sugar pot
  - configure Unix V7 outpost
  - read Shismatrix
  .

First, there is a typo:

  s/Shi/Schi/
  .
  - read Schismatrix

Then, as I said in the last phlog, I really would like to get rid of
sugar. Really. I need to "delete" all the lines containing "sugar" (and
I have just inserted another one!). If "i" is for "insert", "a" is for
"append", "c" is for change, "p" is for "print", "s" is for
"substitute", what do you expect to be the command to "delete"? Yes,
indeed, the ed(1) command for "delete" is "d", and it requires either an
address or a pattern. Let's give it a try:

  /sugar/d
  ,n
  1       TODO LIST
  2       + phlog about ed(1)
  3       - put rubbish bins outside
  4       - buy some tea
  5       - make a tea
  6       * check postgrey hiccup
  7       - find the sugar pot
  8       - configure Unix V7 outpost
  9       - read Schismatrix

Wow. The line saying "- buy sugar" has disappeared. Let's undo, and try
something else:

  u
  5d
  ,n
  1       TODO LIST
  2       + phlog about ed(1)
  3       - put rubbish bins outside
  4       - buy some tea
  5       - make a tea
  6       * check postgrey hiccup
  7       - find the sugar pot
  8       - configure Unix V7 outpost
  9       - read Schismatrix

Same outcome! So you can delete a line by using the command "d" and
specifying either a line number or the pattern that the line should
match. Cool, but sometimes impractical, especially if you have lots of
"sugar" you want to get rid of. There is obviously an efficient
ed(1)-way of dealing with that. In order to have something non-trivial
to work on (i.e., more than one line containing "sugar"), we first
"undo" the last change, so that all the "sugar" reappears:

  u
  ,n
  1       TODO LIST
  2       + phlog about ed(1)
  3       - put rubbish bins outside
  4       - buy some tea
  5       - buy sugar
  6       - make a tea
  7       * check postgrey hiccup
  8       - find the sugar pot
  9       - configure Unix V7 outpost
  10      - read Schismatrix

What if we want to print all and only the lines containing "sugar"? We
know that the simple search command "/sugar/" won't work, since it
prints only the next matching line and then stops. Let's try someting
more powerful:

  g/sugar/p
  - buy sugar
  - find the sugar pot

We have just used the "global" ed(1) command, indicated by "g". This
command looks for matches of the pattern immediately following it (in
this case, the pattern is "sugar") and then executes the command
indicated right after the pattern itself (in this case, the command is
"p" for "print"). So what we have just done is a specific instance of
"g/RE/p", which looks for all the lines matching the regular expression
"RE" and prints them. Yes, this is exactly where the Unix command
grep(1) comes from! [1]

The nice thing about the global command is that it can be followed by
literally any other ed(1) command. So if we need to delete all the
lines containing "sugar", we just give:

  g/sugar/d
  ,n
  1       TODO LIST
  2       + phlog about ed(1)
  3       - put rubbish bins outside
  4       - buy some tea
  5       - make a tea
  6       * check postgrey hiccup
  7       - configure Unix V7 outpost
  8       - read Schismatrix

Now we are talking. This could have become the "gred" command, but the
sage dwarves at Murray Hill noticed that it would have been better to
have a generic tool to handle generic stream-oriented editing, and the
Unix command sed(1) was born instead [2]. Before we save the todo-list,
I need to note that I have started working on two items, namely those on
line 7 and line 8 (yes, I am indeed putting up an outpost with a working
Unix V7 machine for general use, and I need a good name for it, possibly
from the Schismatrix universe :P):

  g/ix/s/-/+/
  ,n
  1       TODO LIST
  2       + phlog about ed(1)
  3       - put rubbish bins outside
  4       - buy some tea
  5       - make a tea
  6       * check postgrey hiccup
  7       + configure Unix V7 outpost
  8       + read Schismatrix

Can you see why the global command did exactly what we expected it to
do? Let's dissect it. We asked ed(1) to look globally for the pattern
"ix" (g/ix/), and to substitute a "-" with a "+" in the matching lines
(s/-/+/). Easy, right? Let's save our work now.

w
156
q

 -+-+-+-

[1] http://www.catb.org/~esr/jargon/html/G/grep.html
[2] http://www.cs.dartmouth.edu/~doug/reader.pdf

   ed(1) is the right tool (V)
=================================

This is a relatively quick one. I recently used ed(1) to write my entry
to the ROOPHLOCH 2020 contest [1], and I did so from the ed(1) version
available in FUZIX [2], which is somehow close to the original ed(1)
available in early Research Unix systems. No need to say that ed(1) did
its job as expected (obviously, an editor does not know anything about
the amount of nonsense you put in a file, so I refer here to the
mechanics of editing, not the content of the post :P). 

After I finished my post, I piped it through fold(1) to make sure that
all the lines were at most 68 chars long. I went for fold(1) because
that is the only formatter I had on FUZIX (and I actually had to port it
myself to FUZIX from sbase [3], because FUZIX does not have fold(1)
yet). And then I remembered that someting like: 

  $ fold -w 68 -s file.txt

would indeed make sure that none of the lines is wider than 68 chars,
also avoiding to break words (-s), but it would *not* join lines
belonging to the same paragraph, as for instance fmt(1) or par(1) would
do. This means that my paragraphs were all messed up, with lots of
unwanted newlines where the original newlines stood. 

The solution? Just join all the lines of a paragraph in a single line!
The direct way of doing that is to use the ed(1) command "j" (yes, "j"
is for join), which will join the current line and the next one, and
leave the current address at the resulting line. I did this by repeating
the "j" command as many times as needed in order to have each paragraph
in a single line (there is indeed a much smarter way of doing that, but
we need some more advanced ed-fu). Then saved the file, gave it to fold,
and then cat(1) it to a serial port connected to my laptop, ready to be
sent over the Internet.

We will learn a much quicker way to use fold(1) from within ed(1), but
before that we need to have a look at a few more ed(1) commands.

 -+-+-+-

[1] gopher://zaibatsu.circumlunar.space:70/1/~solderpunk/roophloch/2020
[2] http://www.fuzix.org
[3] http://git.suckless.org/sbase/

   ed(1) is the right tool (VI)
==================================

So, let's keep going from where we left it. We have put together a
TODO-list with ed(1):

  $ cat TODO.txt
  TODO LIST
  + phlog about ed(1)
  - put rubbish bins outside
  - buy some tea
  - make a tea
  * check postgrey hiccup
  + configure Unix V7 outpost
  + read Schismatrix

and we have learned a bunch of simple ed(1) commands so far. Indeed, a
lot of time has passed since we put that TODO-list together, and many
items have been addressed and can be marked as "done". In particular, I
have definitely phlogged (a bit) about ed(1), I have put the rubbish
outside (at least a couple of times since we started this series of
phlogs), and made myself many teas. We want our TODO-list to reflect
this progress:

  $ ed TODO.txt
  156
  /phlog/s/^+/*/
  3s/^-/*/
  /make/s/^-/*/
  1,$p
  TODO LIST
  * phlog about ed(1)
  * put rubbish bins outside
  - buy some tea
  * make a tea
  * check postgrey hiccup
  + configure Unix V7 outpost
  + read Schismatrix

OK, that's a bit better. I have marked all the "completed" tasks with a
"*", using different ways to address the corresponding lines. The main
issue is that I would like to keep all the outstanding tasks at the
beginning of the TODO list, for convenience. How to do that?  What we
would like to do is to "move" line 4 (- buy some tea) at the top of the
list, i.e., after the line with "TODO LIST". Easy:

  4m1
  1,$p
  TODO LIST
  - buy some tea
  * phlog about ed(1)
  * put rubbish bins outside
  * make a tea
  * check postgrey hiccup
  + configure Unix V7 outpost
  + read Schismatrix

Wow. That's it, but what's the magic? No magic involved, at all. If you
want to "move" a line somewhere else, you use the "m" command. The
command "m" can be preceded by an address, a range, or a regular
expression that specifies which lines you want to move, and is followed
by an address. It will put the lines specified before "m" right after
the address specified after "m". So

   4m1

means move line 4 after line 1. Which is exactly what "m" did. Now, it
would also make sense to put all the completed tasks at the end of
the file, since we don't have to work on them any more. Well, since
all the completed tasks are lines starting with "*", this is quite easy
to achieve:

  g/^\*/m$
  1,$p
  TODO LIST
  - buy some tea
  + configure Unix V7 outpost
  + read Schismatrix
  * phlog about ed(1)
  * put rubbish bins outside
  * make a tea
  * check postgrey hiccup

and was accomplished by telling ed(1) to take all (g) the lines that
start with "*" (/^\*/), and move them (m) after the last line ($). Note
that we had to "quote" the "*" character, since it is also used as a
wildcard in regular expressions, while we are referrin to a literal "*"
here.

Now what if we would like to copy a line instead of moving it? If you
thought that the "copy" command would be "c" then think again, because
we have already seen that "c" is the command to "change" an existing
line [1]. The command to copy lines is "t", for "transfer", and its
syntax is identical to that of "m". I just remembered that I need to buy
some tomatoes:

  /tea/t
  1,$p
  TODO LIST
  - buy some tea
  + configure Unix V7 outpost
  + read Schismatrix
  * phlog about ed(1)
  * put rubbish bins outside
  * make a tea
  * check postgrey hiccup
  - buy some tea

what happened here? I gave the command "/tea/t" which means "look for
the first line that matches 'tea' and copy it...WHERE?". Well, remember
that consistency in ed(1) is key: if an address is missing, then ed(1)
will assume you are referring to the "current line". In this case, the
current line when we typed the "t" command was the last line in the file
(since we had just given the "1,$p" command...). So "t" did the right
thing, and copied the matching line after the last line of the file. Now
we just replace "tea" with tomatoes:

   s/ea/omatoes/

and then move all the lines with outstanding items to the top of the
file:

   g/^-/m1
   1,$p
   TODO LIST
   - buy some tomatoes
   - buy some tea
   + configure Unix V7 outpost
   + read Schismatrix
   * phlog about ed(1)
   * put rubbish bins outside
   * make a tea
   * check postgrey hiccup

Quite simple, right? As usual, let's save our TODO-list, until next
time:
   
   w
   176
   q

 -+-+-+-

[1] gopher://republic.circumlunar.space/0/~katolaz/phlog/20190830_ed_lists_2.txt

   ed(1) is the right tool (VII)
===================================

The nice thing about TODO-lists is that you will eventually be able to
get through them and pencil-out all the completed tasks. But sometimes
it is good to keep track of the many things you have accomplished by
going through a TODO-list. So I normally just save the finised tasks in
a "WELL-DONE" file, to give myself a reassuring (although fictitious)
pat in the shoulder.  Let's have a look at our current TODO-list:

  $ ed TODO.txt
  176
  1,$p
  TODO LIST
  - buy some tomatoes
  - buy some tea
  + configure Unix V7 outpost
  + read Schismatrix
  * phlog about ed(1)
  * put rubbish bins outside
  * make a tea
  * check postgrey hiccup

As per our choice, all the lines starting with "*" denote completed
tasks. If we want to copy all of them to a file named "WELL-DONE", ed(1)
knows a useful trick or two:

  6,9w WELL-DONE
  84

We should be able to figure out what might have happened here using our
current ed-fu. "6,9" specifies a set of lines (all the lines between 6
and 9, both included), while "w" is the command to "write" ed(1) buffers
to a file. We have also seen than "w" followed by a file name will
actually dump all the current content of ed(1)'s buffer on that file.
Well, this is exactly what happened here, only we told ed(1) to dump a
range of lines (6-to-9) instead of the whole buffer, and the destination
file is called WELL-DONE. We can check if our intuition is correct by
exiting ed(1) and using cat(1) on the file WELL-DONE. The good news is
that we don't need to exit ed(1) at all:

  !cat WELL-DONE
  * phlog about ed(1)
  * put rubbish bins outside
  * make a tea
  * check postgrey hiccup
  !

Wait, what was that? Let's give it another shot:

  !pwd
  /home/katolaz/tmp
  !

this can't be what we think it is, right?

  !ls -l
  total 8
  -rw-r--r--  1 katolaz  katolaz  176 Oct 10 10:39 TODO.txt
  -rw-r--r--  1 katolaz  katolaz   84 Oct 11 05:35 WELL-DONE
  !

OK, OK, we got it! ed(1) uses the command "!" to "shell out" another
command, i.e., to execute a command of our choice as if we were typing
it at the shell's prompt. The "!" command will execute the command you
type, show its output on the screed (beware, ONLY on the screen, without
affecting your current buffer), and then finish with a single "!" on a
line. At that point, we know that the external command has finished, and
ed(1) is ready to receive more commands:

  1,$p
  TODO LIST
  - buy some tomatoes
  - buy some tea
  + configure Unix V7 outpost
  + read Schismatrix
  * phlog about ed(1)
  * put rubbish bins outside
  * make a tea
  * check postgrey hiccup

Quite handy, right? Notice again that the invocation of external
commands through "!" does *not* modify the current buffer.

So we have successfully written lines 6-9 to the file called "WELL-DONE"
in the current directory. Now we remove them from TODO.txt:

  g/^\* /d
  1,$p
  TODO LIST
  - buy some tomatoes
  - buy some tea
  + configure Unix V7 outpost
  + read Schismatrix

to keep our TODO-list in order. I also did my shopping yesterday, so:

  g/buy/s/^-/\*/
  1,$p
  TODO LIST
  * buy some tomatoes
  * buy some tea
  + configure Unix V7 outpost
  + read Schismatrix

but now I would like to append the newly-completed tasks to the file
"WELL-DONE". If we used (don't do it!):

  2,3w WELL-DONE

ed(1) would silently destroy the previous content of WELL-DONE, and put
there the two tasks we just completed. But this is not what we want:
remember, keeping track of the stuff you have done is a good boost for
yout morale! The ed(1) command to "append" lines at the end of an
existing file is instead "W":

  2,3W WELL-DONE
  35
  !cat WELL-DONE
  * phlog about ed(1)
  * put rubbish bins outside
  * make a tea
  * check postgrey hiccup
  * buy some tomatoes
  * buy some tea
  !

We can now remove the completed tasks and save our current TODO.txt
file:

  2,3d
  w
  57
  1,$p
  TODO LIST
  + configure Unix V7 outpost
  + read Schismatrix

What if we want to re-insert our old (completed) tasks somewhere in our
TODO.txt file, e.g., after line 1? Simple:

  1r WELL-DONE
  119
  1,$p
  TODO LIST
  * phlog about ed(1)
  * put rubbish bins outside
  * make a tea
  * check postgrey hiccup
  * buy some tomatoes
  * buy some tea
  + configure Unix V7 outpost
  + read Schismatrix

And our old (completed) tasks are back. But no, we really don't need to
see them here: 
  
  u
  1,$p
  TODO LIST
  + configure Unix V7 outpost
  + read Schismatrix
  w
  57
  q

Note that in the versions of ed(1) currently available in most unix
operating systems (including Linux, OpenBSD, FreeBSD, NetBSD), the "r"
command can also read lines from an external command into an ed(1)
buffer.  For instance

  $ ed filelist
  filelist: No such file or directory
  r ! ls -l
  125
  1,$p
  total 8
  -rw-r--r--  1 katolaz  katolaz   57 Oct 11 06:01 TODO.txt
  -rw-r--r--  1 katolaz  katolaz  119 Oct 11 06:00 WELL-DONE

which is sometimes very handy. This option was not available to the
original versions of ed(1) up to UNIXv7. In that case, you would use a
simple trick:

  $ ed filelist
  filelist: No such file or directory
  ! ls -l > tmp
  !
  r tmp
  178
  1,$p
  total 8
  -rw-r--r--  1 katolaz  katolaz   57 Oct 11 06:01 TODO.txt
  -rw-r--r--  1 katolaz  katolaz  119 Oct 11 06:00 WELL-DONE
  -rw-r--r--  1 katolaz  katolaz    0 Oct 11 06:06 tmp

which (almost) does the job.

Now it should not be hard for you to figure out how to use the "!"
command to fold(1) a buffer. It actually requires only a little bit of
ed-fu. Let us assume you have written an email with ed(1) and you now
want to fold(1) it to 72 characters:

   w
   e ! fold -s -w 72 %

You must be able to decypher this command easily. We are writing the
current buffer first ("w"), and then we load in the current buffer ("e")
the output of the command "fold -s -w 72 %". Obviously, there is some
ed(1)-magic involved: indeed, the character "%" is replaced by ed(1)
with the name of the current file. So the result of the command is to
replace the current buffer with the fold(1)-ed one. Cool, right?

  -+-+-+-

   ed(1) is the right tool (VIII)
====================================

If you are reading this tutorial, it means that you had the patience and
determination to go through the quite bumpy ride of learning the basics
of ed(1). You did well, but I won't congratulate you, though.

Indeed, by learning the basics of ed(1) you have not achieved anything
special: for almost two decades, learning ed(1) was the very first thing
any newbie willing to seriously play with unix had to do. You learned
ed(1) first, and then you could mess around with source code and start
writing your own programs. Then, glassy terminals made the minimalist
terseness of ed(1) unnecessary, and more powerful full-screen visual
editors made their appearance in unix systems, such as emacs and vi
(yes, vi(1), not vim(1). vim(1) came much later).

At that point, ed(1) started being forgotten, since newcomers to the
unix world did not need it for their cool projects, and this little
useful tool acquired the (unjustified) fame of being hard to use and
hard to learn.

I hope this tutorial has shown you that ed(1) is a potentially very
useful tool, which is not that hard to use or to learn. It is obviously
a tool from another era of computing, when minimalism was a necessity
and terseness was a virtue. But its influence on the unix ecosystem is
indeed deep and wide, and by learning its basics we have had the
opportunity to see how the spirit of ed(1) still survives in hundreds of
tools that have inherited its syntax, its commands, its idioms, its
idiosyncrasies.

We have not had the opportunity to talk about all the commands available
in ed(1), but at this point RTFM (i.e., reading the famous man-page)
should be enough for you to get along. I provide here some pointers to
useful stuff:

  - we haven't talked about regular expressions, which make ed(1) so
    much more powerful. Go learn them. I will write a post or two about
    the basics, but in the meanwhile, go learn them.

  - 'k' is for labelling rows, which makes life easier sometimes

  - 'G' is like 'g', but for interactively editing lines

  - 'v' is like 'g', but works on lines NOT matching a pattern

  - 'V' is for... (read the two previous points and use your brain)

  - 'h' prints an explanation of the last error

  - 'H' toggles explation of errors

  - in some implementations, 'z' provides a print-by-screen function

  - in some implementations, 'y' allows to transform a set of characters
    into another, more or less as tr(1) would do, but in some other
    implementations it 'yanks' lines in a temporary buffer

  - in some implementations, 'x' allows to encrypt a buffer before
    writing it to disk, but in other implementations it is used to
    'paste' lines previously 'yanked' with 'y'.
   

I would also encourage you to have a look at the following books and
articles about ed(1):

  - Ed Mastery, by Michael W. Lucas (possibly the best book on ed(1)
    currently available)
    https://mwl.io/nonfiction/tools#ed

  - ed(1) man page in the Single Unix Specification
    https://pubs.opengroup.org/onlinepubs/9699919799/utilities/ed.html

  - ed(1) man page from UNIXv1, ca. 1971
    http://man.cat-v.org/unix-1st/1/ed

  - ed(1) man page from UNIXv7, 1979
    http://man.cat-v.org/unix_7th/1/ed

  - ed(1) at gopherpedia
    gopher://gopherpedia.com/0/Ed (text editor)

  - A history of UNIX before Berkeley, Ian Darwin & Geoffrey Collyer,
    section 3.1
    http://www.darwinsys.com/history/hist.html
  
  - ed(1) is Turing-complete 
    https://nixwindows.wordpress.com/2018/03/13/ed1-is-turing-complete/

  - ed(1) is the standard text editor
    https://www.gnu.org/fun/jokes/ed-msg.txt

That's all, folks! :)

 -+-+-+-