ed

[hare] The standard editor
Log | Files | Refs | README | LICENSE

command.ha (15730B)


      1 use errors;
      2 use fmt;
      3 use fs;
      4 use io;
      5 use memio;
      6 use os;
      7 use os::exec;
      8 use regex;
      9 use strings;
     10 
     11 type Command = struct{
     12 	addrs: []Address,
     13 	linenums: []size,
     14 	name: rune,
     15 	suffix: rune,
     16 	printmode: PrintMode,
     17 	delim: rune,
     18 	arg1: str,
     19 	arg2: str,
     20 	arg3: str,
     21 	count: size,
     22 	flag_global: bool,
     23 	textinput: []str,
     24 	subcmds: []Command,
     25 };
     26 
     27 type CommandFn = *fn(*Session, *Command) (void | Error);
     28 
     29 type PrintMode = enum{
     30 	NONE,
     31 	PLAIN,
     32 	NUMBER,
     33 	LIST,
     34 };
     35 
     36 type CmdError = !(
     37 	InvalidAddress
     38 	| UnexpectedAddress
     39 	| InvalidDestination
     40 	| NoFilename
     41 	| WarnBufferModified
     42 	| NoMatch
     43 	| NoPrevRegex
     44 	| NoPrevShCmd
     45 	| NoPrevGlobalSubCmd
     46 	| InvalidGlobalSubCmd
     47 	| NoHistory
     48 	| regex::error
     49 	| fs::error
     50 	| os::exec::error
     51 );
     52 
     53 type UnexpectedAddress = !void;
     54 
     55 type InvalidDestination = !void;
     56 
     57 type NoFilename = !void;
     58 
     59 type WarnBufferModified = !void;
     60 
     61 type NoPrevShCmd = !void;
     62 
     63 type NoPrevGlobalSubCmd = !void;
     64 
     65 type InvalidGlobalSubCmd = !rune;
     66 
     67 fn command_finish(cmd: *Command) void = {
     68 	//debug("command_finish(): delete(cmd.linenums[..])");
     69 	delete(cmd.linenums[..]);
     70 	//debug("command_finish(): free(cmd.arg1)");
     71 	free(cmd.arg1);
     72 	//debug("command_finish(): free(cmd.arg2)");
     73 	free(cmd.arg2);
     74 	//debug("command_finish(): free(cmd.arg3)");
     75 	free(cmd.arg3);
     76 	//debug("command_finish(): delete(cmd.input[..])");
     77 	delete(cmd.textinput[..]);
     78 	//debug("command_finish(): END");
     79 	// TODO: free other fields?
     80 	// TODO: make a separate "fn command_clear()" ? probably not
     81 };
     82 
     83 fn lookupcmd(name: rune) CommandFn = {
     84 	switch (name) {
     85 	case 'a' => return &cmd_append;
     86 	case 'c' => return &cmd_change;
     87 	case 'd' => return &cmd_delete;
     88 	case 'e' => return &cmd_edit;
     89 	case 'E' => return &cmd_edit_forced;
     90 	case 'f' => return &cmd_filename;
     91 	case 'g' => return &cmd_global;
     92 	case 'G' => return &cmd_global_ia;
     93 	case 'h' => return &cmd_help;
     94 	case 'H' => return &cmd_helpmode;
     95 	case 'i' => return &cmd_insert;
     96 	case 'j' => return &cmd_join;
     97 	case 'k' => return &cmd_mark;
     98 	case 'l' => return &cmd_list;
     99 	case 'm' => return &cmd_move;
    100 	case 'n' => return &cmd_number;
    101 	case 'p' => return &cmd_print;
    102 	case 'P' => return &cmd_prompt;
    103 	case 'q' => return &cmd_quit;
    104 	case 'Q' => return &cmd_quit_forced;
    105 	case 'r' => return &cmd_read;
    106 	case 's' => return &cmd_substitute;
    107 	case 't' => return &cmd_copy;
    108 	case 'u' => return &cmd_undo;
    109 	case 'v' => return &cmd_vglobal;
    110 	case 'V' => return &cmd_vglobal_ia;
    111 	case 'w' => return &cmd_write;
    112 	case '=' => return &cmd_linenumber;
    113 	case '!' => return &cmd_shellescape;
    114 	case NUL => return &cmd_null;
    115 	case => abort();
    116 	};
    117 };
    118 
    119 fn cmd_append(s: *Session, cmd: *Command) (void | Error) = {
    120 	s.warned = false;
    121 
    122 	const n = get_linenum(cmd.linenums, s.buf.cursor);
    123 
    124 	if (len(cmd.textinput) > 0)
    125 		hist_newseq(s.buf);
    126 
    127 	for (let i = 0z; i < len(cmd.textinput); i += 1) {
    128 		const line = alloc(Line{ text = cmd.textinput[i], ... });
    129 		buf_insert(s.buf, n + 1 + i, line);
    130 	};
    131 
    132 	if (len(cmd.textinput) > 0)
    133 		hist_tidy(s, s.buf, 0);
    134 
    135 	s.buf.cursor = n + len(cmd.textinput);
    136 	printmode(s, cmd)?;
    137 };
    138 
    139 fn cmd_change(s: *Session, cmd: *Command) (void | Error) = {
    140 	s.warned = false;
    141 
    142 	const (a, b) = get_range(
    143 		s,
    144 		&cmd.linenums,
    145 		s.buf.cursor,
    146 		s.buf.cursor,
    147 	)?;
    148 	if (a == 0) {
    149 		a = 1;
    150 		if (b == 0) {
    151 			b = 1;
    152 		};
    153 	};
    154 
    155 	hist_newseq(s.buf);
    156 	buf_delete(s.buf, a, b);
    157 	hist_tidy(s, s.buf, b - a + 1);
    158 
    159 
    160 	for (let i = 0z; i < len(cmd.textinput); i += 1) {
    161 		const line = alloc(Line{ text = cmd.textinput[i], ... });
    162 		buf_insert(s.buf, a + i, line);
    163 	};
    164 
    165 	s.buf.cursor =
    166 		if (len(cmd.textinput) == 0)
    167 			if (len(s.buf.lines) == a)
    168 				a - 1
    169 			else
    170 				a
    171 		else
    172 			a + len(cmd.textinput) - 1;
    173 	printmode(s, cmd)?;
    174 };
    175 
    176 fn cmd_delete(s: *Session, cmd: *Command) (void | Error) = {
    177 	s.warned = false;
    178 
    179 	const (a, b) = get_range(
    180 		s,
    181 		&cmd.linenums,
    182 		s.buf.cursor,
    183 		s.buf.cursor,
    184 	)?;
    185 	assert_nonzero(s, a)?;
    186 
    187 	hist_newseq(s.buf);
    188 	buf_delete(s.buf, a, b);
    189 	hist_tidy(s, s.buf, b - a + 1);
    190 
    191 	s.buf.cursor =
    192 		if (len(s.buf.lines) == 1)
    193 			0
    194 		else if (len(s.buf.lines) == a)
    195 			a - 1
    196 		else
    197 			a;
    198 	printmode(s, cmd)?;
    199 };
    200 
    201 fn cmd_edit(s: *Session, cmd: *Command) (void | Error) = {
    202 	edit(s, cmd, false)?;
    203 };
    204 
    205 fn cmd_edit_forced(s: *Session, cmd: *Command) (void | Error) = {
    206 	edit(s, cmd, true)?;
    207 };
    208 
    209 fn cmd_filename(s: *Session, cmd: *Command) (void | Error) = {
    210 	s.warned = false;
    211 
    212 	assert_noaddrs(s, cmd.linenums)?;
    213 
    214 	fmt::println(filename(s, cmd, true)?)!;
    215 };
    216 
    217 fn cmd_global(s: *Session, cmd: *Command) (void | Error) = {
    218 	global(s, cmd, true)?;
    219 };
    220 
    221 fn cmd_global_ia(s: *Session, cmd: *Command) (void | Error) = {
    222 	global_ia(s, cmd, true)?;
    223 };
    224 
    225 fn cmd_help(s: *Session, cmd: *Command) (void | Error) = {
    226 	s.warned = false;
    227 
    228 	assert_noaddrs(s, cmd.linenums)?;
    229 
    230 	if (s.lasterror is Error)
    231 		fmt::println(strerror(s.lasterror as Error))!;
    232 
    233 	printmode(s, cmd)?;
    234 };
    235 
    236 fn cmd_helpmode(s: *Session, cmd: *Command) (void | Error) = {
    237 	s.warned = false;
    238 
    239 	assert_noaddrs(s, cmd.linenums)?;
    240 
    241 	s.helpmode = !s.helpmode;
    242 
    243 	if (s.helpmode && s.lasterror is Error)
    244 		fmt::println(strerror(s.lasterror as Error))!;
    245 
    246 	printmode(s, cmd)?;
    247 };
    248 
    249 fn cmd_insert(s: *Session, cmd: *Command) (void | Error) = {
    250 	s.warned = false;
    251 
    252 	const n = get_linenum(cmd.linenums, s.buf.cursor);
    253 	const n = if (n == 0) 1z else n;
    254 
    255 	if (len(cmd.textinput) > 0)
    256 		hist_newseq(s.buf);
    257 
    258 	for (let i = 0z; i < len(cmd.textinput); i += 1) {
    259 		const line = alloc(Line{ text = cmd.textinput[i], ... });
    260 		buf_insert(s.buf, n + i, line);
    261 	};
    262 
    263 	if (len(cmd.textinput) > 0)
    264 		hist_tidy(s, s.buf, 0);
    265 
    266 	s.buf.cursor =
    267 		if (len(cmd.textinput) == 0)
    268 			n
    269 		else
    270 			n + len(cmd.textinput) - 1;
    271 	printmode(s, cmd)?;
    272 };
    273 
    274 fn cmd_join(s: *Session, cmd: *Command) (void | Error) = {
    275 	s.warned = false;
    276 
    277 	const (a, b) = get_range(
    278 		s,
    279 		&cmd.linenums,
    280 		s.buf.cursor,
    281 		addr_nextline(s.buf, s.buf.cursor),
    282 	)?;
    283 	assert_nonzero(s, a)?;
    284 	if (a == b)
    285 		return;
    286 
    287 	let ls: []str = [];
    288 	let mark = NUL;
    289 	for (let n = a; n <= b; n += 1) {
    290 		const line = s.buf.lines[n];
    291 		append(ls, line.text);
    292 		if (mark == NUL)
    293 			mark = line.mark;
    294 	};
    295 
    296 	const newtext = strings::concat(ls...);
    297 	const newline = alloc(Line{
    298 		text = newtext,
    299 		mark = mark,
    300 		...
    301 	});
    302 
    303 	hist_newseq(s.buf);
    304 	buf_delete(s.buf, a, b);
    305 	buf_insert(s.buf, a, newline);
    306 	hist_tidy(s, s.buf, b - a + 1);
    307 
    308 	s.buf.cursor = a;
    309 	printmode(s, cmd)?;
    310 };
    311 
    312 fn cmd_mark(s: *Session, cmd: *Command) (void | Error) = {
    313 	s.warned = false;
    314 
    315 	const n = get_linenum(cmd.linenums, s.buf.cursor);
    316 	assert_nonzero(s, n)?;
    317 
    318 	const mark = cmd.suffix;
    319 
    320 	:clearmark {
    321 		for (let i = 0z; i < len(s.buf.lines); i += 1) {
    322 			if (s.buf.lines[i].mark == mark) {
    323 				s.buf.lines[i].mark = NUL;
    324 				yield :clearmark;
    325 			};
    326 		};
    327 		for (let i = 0z; i < len(s.buf.trash); i += 1) {
    328 			if (s.buf.trash[i].mark == mark) {
    329 				s.buf.trash[i].mark = NUL;
    330 				yield :clearmark;
    331 			};
    332 		};
    333 	};
    334 
    335 	s.buf.lines[n].mark = mark;
    336 
    337 	printmode(s, cmd)?;
    338 };
    339 
    340 fn cmd_list(s: *Session, cmd: *Command) (void | Error) = {
    341 	s.warned = false;
    342 
    343 	const (a, b) = get_range(
    344 		s,
    345 		&cmd.linenums,
    346 		s.buf.cursor,
    347 		s.buf.cursor,
    348 	)?;
    349 	assert_nonzero(s, a)?;
    350 
    351 	printlist(s.buf, a, b)?;
    352 
    353 	s.buf.cursor = b;
    354 };
    355 
    356 fn cmd_move(s: *Session, cmd: *Command) (void | Error) = {
    357 	s.warned = false;
    358 
    359 	const (a, b) = get_range(
    360 		s,
    361 		&cmd.linenums,
    362 		s.buf.cursor,
    363 		s.buf.cursor,
    364 	)?;
    365 	assert_nonzero(s, a)?;
    366 
    367 	// TODO: parse this properly in parse.ha?
    368 	const t = strings::iter(cmd.arg1);
    369 	const addr = match (scan_addr(&t)) {
    370 	case let a: Address =>
    371 		yield a;
    372 	case void =>
    373 		return InvalidAddress;
    374 	};
    375 	const n = match (exec_addr(s, addr)) {
    376 	case let n: size =>
    377 		yield n + 1; // like insert
    378 	case InvalidAddress =>
    379 		return InvalidDestination;
    380 	};
    381 
    382 	if (a < n && n <= b)
    383 		return InvalidDestination;
    384 
    385 	if (n == b + 1)
    386 		return; // no-op
    387 
    388 	const dest =
    389 		if (n > b)
    390 			n - (1 + b - a)
    391 		else
    392 			n;
    393 
    394 	const lines = alloc(s.buf.lines[a..b+1]...); defer free(lines); // TODO: mem?
    395 
    396 	hist_newseq(s.buf);
    397 	buf_delete(s.buf, a, b);
    398 	buf_insert(s.buf, dest, lines...);
    399 	hist_tidy(s, s.buf, b - a + 1);
    400 
    401 	s.buf.cursor = dest - 1 + len(lines);
    402 	printmode(s, cmd)?;
    403 };
    404 
    405 fn cmd_number(s: *Session, cmd: *Command) (void | Error) = {
    406 	s.warned = false;
    407 
    408 	const (a, b) = get_range(
    409 		s,
    410 		&cmd.linenums,
    411 		s.buf.cursor,
    412 		s.buf.cursor,
    413 	)?;
    414 	assert_nonzero(s, a)?;
    415 
    416 	printnumber(s.buf, a, b)?;
    417 
    418 	s.buf.cursor = b;
    419 };
    420 
    421 fn cmd_print(s: *Session, cmd: *Command) (void | Error) = {
    422 	s.warned = false;
    423 
    424 	const (a, b) = get_range(
    425 		s,
    426 		&cmd.linenums,
    427 		s.buf.cursor,
    428 		s.buf.cursor,
    429 	)?;
    430 	assert_nonzero(s, a)?;
    431 
    432 	printplain(s.buf, a, b)?;
    433 
    434 	s.buf.cursor = b;
    435 };
    436 
    437 fn cmd_prompt(s: *Session, cmd: *Command) (void | Error) = {
    438 	s.warned = false;
    439 
    440 	assert_noaddrs(s, cmd.linenums)?;
    441 
    442 	s.promptmode = !s.promptmode;
    443 
    444 	printmode(s, cmd)?;
    445 };
    446 
    447 fn cmd_quit(s: *Session, cmd: *Command) (void | Error) = {
    448 	assert_noaddrs(s, cmd.linenums)?;
    449 
    450 	if (s.buf.modified && !s.warned) {
    451 		s.warned = true;
    452 		return WarnBufferModified;
    453 	};
    454 
    455 	return Quit;
    456 };
    457 
    458 fn cmd_quit_forced(s: *Session, cmd: *Command) (void | Error) = {
    459 	assert_noaddrs(s, cmd.linenums)?;
    460 
    461 	return Quit;
    462 };
    463 
    464 fn cmd_read(s: *Session, cmd: *Command) (void | Error) = {
    465 	s.warned = false;
    466 
    467 	const n = get_linenum(cmd.linenums, addr_lastline(s.buf));
    468 
    469 	const rd: io::handle =
    470 		if (!strings::hasprefix(cmd.arg1, "!")) {
    471 			yield os::open(filename(s, cmd, false)?)?;
    472 		} else {
    473 			let shcmdline = strings::cut(cmd.arg1, "!").1;
    474 			let shcmd = exec::cmd("sh", "-c", shcmdline)?;
    475 			let pipe = exec::pipe();
    476 			exec::addfile(&shcmd, os::stdout_file, pipe.1);
    477 			let proc = exec::start(&shcmd)?;
    478 			io::close(pipe.1)!;
    479 			exec::wait(&proc)?;
    480 			yield pipe.0;
    481 		};
    482 		defer io::close(rd)!;
    483 
    484 	hist_newseq(s.buf);
    485 
    486 	const (sz, linecnt) = buf_read(s.buf, rd, n)?;
    487 
    488 	if (linecnt == 0)
    489 		hist_discardseq(s.buf)
    490 	else
    491 		hist_tidy(s, s.buf, 0);
    492 
    493 	if (!s.suppressmode)
    494 		fmt::println(sz)!;
    495 
    496 	s.buf.cursor = len(s.buf.lines) - 1;
    497 };
    498 
    499 fn cmd_substitute(s: *Session, cmd: *Command) (void | Error) = {
    500 	s.warned = false;
    501 
    502 	const (a, b) = get_range(
    503 		s,
    504 		&cmd.linenums,
    505 		s.buf.cursor,
    506 		s.buf.cursor,
    507 	)?;
    508 	assert_nonzero(s, a)?;
    509 
    510 	// TODO: implement '&', '%'
    511 
    512 	const re = newregex(s, cmd.arg1)?;
    513 		defer regex::finish(&re);
    514 	const replacement = strings::join("\n", cmd.textinput...);
    515 		defer free(replacement);
    516 	const count = if (cmd.count == 0z) 0z else cmd.count - 1z;
    517 
    518 	let changes = 0z;
    519 
    520 	hist_newseq(s.buf);
    521 
    522 	for (let n = a; n <= b; n += 1) {
    523 		const old = s.buf.lines[n].text;
    524 		const results = regex::findall(&re, old);
    525 			defer regex::result_freeall(results);
    526 
    527 		if (len(results) == 0)
    528 			continue;
    529 
    530 		let new = memio::dynamic(); defer io::close(&new)!;
    531 
    532 		let start = 0z;
    533 		let lbound = 0z;
    534 		let rbound = 0z;
    535 		for (let i = count; i < len(results); i += 1) {
    536 			lbound = results[i][0].start;
    537 			rbound = results[i][0].end;
    538 			memio::concat(&new, strings::sub(old, start, lbound))!;
    539 			memio::concat(&new, replacement)!;
    540 			start = rbound;
    541 
    542 			if (!cmd.flag_global)
    543 				break;
    544 		};
    545 		memio::concat(&new, strings::sub(old, rbound, strings::end))!;
    546 		let newtext = memio::string(&new)!;
    547 		let newtexts = strings::split(newtext, "\n");
    548 
    549 		buf_delete(s.buf, n, n);
    550 
    551 		for (let i = 0z; i < len(newtexts); i += 1) {
    552 			let newtext = newtexts[i];
    553 			let newline = alloc(Line{
    554 				text = strings::dup(newtext),
    555 				...
    556 			});
    557 
    558 			buf_insert(s.buf, n + i, newline);
    559 
    560 			s.buf.cursor = n + i;
    561 		};
    562 
    563 		changes += 1;
    564 	};
    565 
    566 	if (changes == 0) {
    567 		hist_discardseq(s.buf);
    568 		return NoMatch;
    569 	} else {
    570 		hist_tidy(s, s.buf, changes);
    571 	};
    572 
    573 	printmode(s, cmd)?;
    574 };
    575 
    576 fn cmd_copy(s: *Session, cmd: *Command) (void | Error) = {
    577 	s.warned = false;
    578 
    579 	const (a, b) = get_range(
    580 		s,
    581 		&cmd.linenums,
    582 		s.buf.cursor,
    583 		s.buf.cursor,
    584 	)?;
    585 	assert_nonzero(s, a)?;
    586 
    587 	// TODO: parse this properly in parse.ha?
    588 	const t = strings::iter(cmd.arg1);
    589 	const addr = match (scan_addr(&t)) {
    590 	case let a: Address =>
    591 		yield a;
    592 	case void =>
    593 		return InvalidAddress;
    594 	};
    595 	const dest = match (exec_addr(s, addr)) {
    596 	case let n: size =>
    597 		yield n + 1;
    598 	case InvalidAddress =>
    599 		return InvalidDestination;
    600 	};
    601 
    602 	const lines = alloc(s.buf.lines[a..b+1]...); defer free(lines); // TODO: ?
    603 
    604 	hist_newseq(s.buf);
    605 	buf_insert(s.buf, dest, lines...);
    606 	hist_tidy(s, s.buf, 0);
    607 
    608 	s.buf.cursor = dest - 1 + len(lines);
    609 	printmode(s, cmd)?;
    610 };
    611 
    612 fn cmd_undo(s: *Session, cmd: *Command) (void | Error) = {
    613 	s.warned = false;
    614 
    615 	assert_noaddrs(s, cmd.linenums)?;
    616 
    617 	switch (s.undomode) {
    618 	case UndoMode::POSIX =>
    619 		if (s.buf.redolastchange) {
    620 			hist_redo(s.buf)?;
    621 			s.buf.redolastchange = false;
    622 		} else {
    623 			hist_undo(s.buf)?;
    624 			s.buf.redolastchange = true;
    625 		};
    626 	case UndoMode::FULL =>
    627 		hist_undo(s.buf)?;
    628 		hist_discardseq(s.buf);
    629 	};
    630 };
    631 
    632 fn cmd_vglobal(s: *Session, cmd: *Command) (void | Error) = {
    633 	global(s, cmd, false)?;
    634 };
    635 
    636 fn cmd_vglobal_ia(s: *Session, cmd: *Command) (void | Error) = {
    637 	global_ia(s, cmd, false)?;
    638 };
    639 
    640 fn cmd_write(s: *Session, cmd: *Command) (void | Error) = {
    641 	s.warned = false;
    642 
    643 	const (a, b) = get_range(
    644 		s,
    645 		&cmd.linenums,
    646 		addr_linenum(s.buf, 1)?,
    647 		addr_lastline(s.buf),
    648 	)?;
    649 	assert_nonzero(s, a)?;
    650 
    651 	let proc: (void | exec::process) = void;
    652 	const wr: io::handle =
    653 		if (!strings::hasprefix(cmd.arg1, "!")) {
    654 			yield os::create(filename(s, cmd, false)?, 0o644)?;
    655 		} else {
    656 			let shcmdline = strings::cut(cmd.arg1, "!").1;
    657 			let shcmd = exec::cmd("sh", "-c", shcmdline)?;
    658 			let pipe = exec::pipe();
    659 			exec::addfile(&shcmd, os::stdin_file, pipe.0);
    660 			proc = exec::start(&shcmd)?;
    661 			yield pipe.1;
    662 		};
    663 
    664 	const sz = buf_write(s.buf, wr, a, b)?;
    665 	io::close(wr)!;
    666 
    667 	// XXX: create a stdout pipe and copy it out here instead?
    668 	if (proc is exec::process)
    669 		exec::wait(&(proc as exec::process))?;
    670 
    671 	if (!s.suppressmode)
    672 		fmt::println(sz)!;
    673 
    674 	if (proc is void && a == 1 && b == len(s.buf.lines) - 1)
    675 		// the entire buffer has been written to a file.
    676 		s.buf.modified = false;
    677 
    678 	if (cmd.suffix == 'q') {
    679 		if (s.buf.modified && !s.warned) {
    680 			s.warned = true;
    681 			return WarnBufferModified;
    682 		};
    683 		return Quit;
    684 	};
    685 };
    686 
    687 fn cmd_linenumber(s: *Session, cmd: *Command) (void | Error) = {
    688 	s.warned = false;
    689 
    690 	const n = get_linenum(cmd.linenums, addr_lastline(s.buf));
    691 	fmt::println(n)!;
    692 
    693 	printmode(s, cmd)?;
    694 };
    695 
    696 fn cmd_shellescape(s: *Session, cmd: *Command) (void | Error) = {
    697 	s.warned = false;
    698 
    699 	assert_noaddrs(s, cmd.linenums)?;
    700 
    701 	let t = strings::iter(cmd.arg1);
    702 	let new = memio::dynamic(); defer io::close(&new)!;
    703 	let preview = false;
    704 
    705 	// handling '!!'
    706 	if (strings::next(&t) == '!') {
    707 		match (s.lastshcmd) {
    708 		case void =>
    709 			return NoPrevShCmd;
    710 		case let s: str =>
    711 			memio::concat(&new, s)!;
    712 		};
    713 		preview = true;
    714 	} else {
    715 		strings::prev(&t);
    716 	};
    717 
    718 	// handling '%' and '\%'
    719 	for (true) {
    720 		match (strings::next(&t)) {
    721 		case void =>
    722 			break;
    723 		case let r: rune =>
    724 			switch (r) {
    725 			case =>
    726 				memio::appendrune(&new, r)!;
    727 			case '%' =>
    728 				if (strings::prev(&t) == '\\') {
    729 					strings::next(&t);
    730 					memio::appendrune(&new, '%')!;
    731 					continue;
    732 				} else {
    733 					strings::next(&t);
    734 				};
    735 
    736 				if (s.buf.filename == "")
    737 					return NoFilename;
    738 
    739 				memio::concat(&new, s.buf.filename)!;
    740 				preview = true;
    741 			};
    742 		};
    743 	};
    744 
    745 	let shcmdline = memio::string(&new)!;
    746 	if (preview)
    747 		fmt::println(shcmdline)!;
    748 
    749 	let rd: io::handle = {
    750 		let shcmd = exec::cmd("sh", "-c", shcmdline)?;
    751 		let pipe = exec::pipe();
    752 		exec::addfile(&shcmd, os::stdout_file, pipe.1);
    753 		let proc = exec::start(&shcmd)?;
    754 		io::close(pipe.1)!;
    755 		exec::wait(&proc)?;
    756 		yield pipe.0;
    757 	};
    758 		defer io::close(rd)!;
    759 
    760 	io::copy(os::stdout, rd)!;
    761 
    762 	if (!s.suppressmode)
    763 		fmt::println("!")!;
    764 
    765 	s.lastshcmd = strings::dup(shcmdline);
    766 };
    767 
    768 fn cmd_null(s: *Session, cmd: *Command) (void | Error) = {
    769 	s.warned = false;
    770 
    771 	const n = get_linenum(
    772 		cmd.linenums,
    773 		addr_nextline(s.buf, s.buf.cursor),
    774 	);
    775 	assert_nonzero(s, n)?;
    776 
    777 	fmt::println(s.buf.lines[n].text)!;
    778 
    779 	s.buf.cursor = n;
    780 };