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.