Date: 2026-02-09

Daily Source Reading: ed [Part 3 - Commands]

ed commands

So, this post was a bit delayed and I apologize. I accidentally hosed one of my subnet configs in my homelab while not being at home and rendered the entire stack inaccessible. I should really invest in a KVM to access it remotely if needed.

Anyhoo, back to the world of ed! We are going to look at it's heart of hearts, the commands. We won't explain each command, that's what the ed(1) man page is for. So let's dive in:

/* exec_command: execute the next command in command buffer; return print
   request, if any */
int
exec_command(void)
{
    /* a ton of variable declarations that we will skip here */

    SKIP_BLANKS();
    switch ((c = (unsigned char)*ibufp++)) {
    case 'a':
        GET_COMMAND_SUFFIX();
        if (!isglobal) clear_undo_stack();
        if (append_lines(second_addr) < 0)
            return ERR;
        break;
    case 'c':
        if (check_addr_range(current_addr, current_addr) < 0)
            return ERR;
        GET_COMMAND_SUFFIX();
        if (!isglobal) clear_undo_stack();
        if (delete_lines(first_addr, second_addr) < 0 ||
            append_lines(current_addr) < 0)
            return ERR;
        break;
    case 'd':
        if (check_addr_range(current_addr, current_addr) < 0)
            return ERR;
        GET_COMMAND_SUFFIX();
        if (!isglobal) clear_undo_stack();
        if (delete_lines(first_addr, second_addr) < 0)
            return ERR;
        else if ((addr = INC_MOD(current_addr, addr_last)) != 0)
            current_addr = addr;
        break;
    /* and a lot more commands following this */

As per usual, keeping it simple here. No fancy callbacks or automatic dispatch system. Switch on the command letter, and go.

To make it a bit easier, let's remove the error checks, breaks and GET_COMMAND_SUFFIX and other helpers. Unless marked with a FALLTHROUGH comment, assume there is an implicit break.

switch ((c = (unsigned char)*ibufp++)) {
case 'a':
    append_lines(second_addr);
case 'c':
    delete_lines(first_addr, second_addr);
    append_lines(current_addr);
case 'd':
    delete_lines(first_addr, second_addr);
case 'e':
    /* FALLTHROUGH */
case 'E':
    fnp = get_filename(1));
    delete_lines(1, addr_last);
    close_sbuf();
    open_sbuf();
    read_file(fnp, 0);
case 'f':
    fnp = get_filename(1));
    puts(strip_escapes(fnp));
case 'g': /* FALLTHROUGH */
case 'v': /* FALLTHROUGH */
case 'G': /* FALLTHROUGH */
case 'V': /* FALLTHROUGH */
    exec_global(n, gflag);
case 'h':
    if (*errmsg) fprintf(stderr, "%s\n", errmsg);
case 'H':
    if ((garrulous = 1 - garrulous) && *errmsg)
        fprintf(stderr, "%s\n", errmsg);
case 'i':
    append_lines(second_addr - 1);
case 'j':
    join_lines(first_addr, second_addr);
case 'k':
    mark_line_node(get_addressed_line_node(second_addr), c);
case 'l':
    display_lines(first_addr, second_addr, gflag | GLS);
case 'm':
    move_lines(addr);
case 'n':
    display_lines(first_addr, second_addr, gflag | GNP);
case 'p':
    display_lines(first_addr, second_addr, gflag | GPR);
case 'P':
    prompt = prompt ? NULL : optarg ? optarg : dps;
case 'q': /* FALLTHROUGH */
case 'Q':
    gflag =  (modified && !scripted && c == 'q') ? EMOD : EOF;
case 'r':
    fnp = get_filename(0));
    read_file(fnp, second_addr));
case 's':
    /* ------------------- */
    /* TBD In Another Post */
    /* ------------------- */  
case 't':
    copy_lines(addr);
case 'u':
    pop_undo_stack();
case 'w': /* FALLTHROUGH */
case 'W':
    fnp = get_filename(0));
    write_file(fnp, (c == 'W') ? "a" : "w",
        first_addr, second_addr);
case 'x':
    seterrmsg("crypt unavailable");
    return ERR;
case 'z':
    display_lines(second_addr, min(addr_last,
        second_addr + rows), gflag);
case '=':
    printf("%d\n", addr_cnt ? second_addr : addr_last);
case '!':
    sflags = get_shell_command());
    if (sflags) printf("%s\n", shcmd + 1);
    fflush(NULL); /* flush any buffered I/O */
    system(shcmd + 1);
    if (!scripted) printf("!\n");
case '\n':
    display_lines(second_addr, second_addr, 0);
}

Let's start with what we can see in the first parts of this snippet.

Append

So, maybe just by being first in the list, append_lines is going to be our first victim.

/* append_lines: insert text from stdin to after line n; stop when either a
   single period is read or EOF; return status */
static int
append_lines(int n)
{
    /* more variables */

    for (current_addr = n;;) {
        /* error handling... */
        SPL1();
        do {
            if ((lp = put_sbuf_line(lp)) == NULL) {
                SPL0();
                return ERR;
            } else if (up)
                up->t = get_addressed_line_node(current_addr);
            else if ((up = push_undo_stack(UADD, current_addr,
                current_addr)) == NULL) {
                SPL0();
                return ERR;
            }
        } while (lp != eot);
        modified = 1;
        SPL0();
    }
    /* NOTREACHED */
}

So, due to both, time constraints and removing noise, we will skip past any of the boilerplate error handling and declarations.

In its core, append_lines merely disables interrupts (mostly because put_sbuf_line expects it) and add the new line into the list.

Delete

/* delete_lines: delete a range of lines */
int
delete_lines(int from, int to)
{
    line_t *n, *p;

    SPL1();
    if (push_undo_stack(UDEL, from, to) == NULL) {
        SPL0();
        return ERR;
    }
    n = get_addressed_line_node(INC_MOD(to, addr_last));
    p = get_addressed_line_node(from - 1);
                    /* this get_addressed_line_node last! */
    if (isglobal)
        unset_active_nodes(p->q_forw, n);
    REQUE(p, n);
    addr_last -= to - from + 1;
    current_addr = from - 1;
    modified = 1;
    SPL0();
    return 0;
}

Deleting lines is pretty much the same. We just reque the list from the start to end of the deletion range, effectively removing the lines that were in the middle.

Change

if (delete_lines(first_addr, second_addr) < 0 ||
    append_lines(current_addr) < 0)

Now here we just re-use deletion and appending to pretty much implement the change command for free.

We will not have time in this post to go through all functions. Copying lines, moving lines, marking lines, etc. are not wizardry and I'll leave those to the reader (and I'm also being a bit lazy here to be honest. Only so much time in a day.).

Conclusion

I think at this point we have pulled away enough layers to see that it doesn't take much to implement a text editor so simple and yet powerful that it was literally used to implement entire compilers and operation systems.

One function though we will take a look at, but this is for the next post. That is the s command and how the regular expressions are handled.