commit 62cb528

wf  ·  2026-07-24 18:34:42 +0000 UTC
parent 62cb528
Init
2 files changed,  +1026, -0
A README
A ed
A README
+23, -0
 1@@ -0,0 +1,23 @@
 2+Somewhat POSIX-compliant (see below) ed implementation in pure mksh.
 3+The code is quite heavily commented to also serve as a learning
 4+source.
 5+
 6+To do (x: completed, ~: partially completed, ' ': not completed):
 7+ [~] Regexp.
 8+ [ ] Advanced address syntax.
 9+ [x] Command suffixes (l/n/p).
10+ [x] Marks.
11+ [ ] Check if a file is writable before trying to write something
12+     into it.
13+
14+Bugs and inconsistencies:
15+ - `w !...' doesn't track the amount of bytes that were transferred.
16+   This is a shell scope limitation.
17+ - The 'regular expressions' are in ksh [2] format rather than POSIX
18+   regexp. This is an mksh limitation.
19+ - Most likely more. A lot of the possible edge cases (especially
20+   with the addresses) weren't tested and could be anything from just
21+   not compliant with the spec to outright dangerous.
22+
23+[1]: https://www.man7.org/linux/man-pages/man1/ed.1p.html
24+[2]: https://man.voidlinux.org/mksh#File_name_patterns
A ed
+1003, -0
   1@@ -0,0 +1,1003 @@
   2+#!/bin/mksh
   3+
   4+# Prompt string as specified by -p, pseudo-boolean for prompt mode.
   5+typeset pstring='*' prompt=''
   6+# Booleans for silent, prompt and help modes.
   7+typeset -i1 silent=0 help=0
   8+# Reason for the last error that occured and last shell command
   9+# executed by `!'.
  10+typeset lasterr lastcmd
  11+# Original command line, command character, arguments and optional
  12+# command suffix (l/n/p).
  13+typeset src cmd arg suffix
  14+# Current line and the start/end of the range of addressed lines.
  15+typeset -i cur=0 s=0 e=0
  16+# Temporary variable used for parsing addresses (see getaddr).
  17+typeset addr
  18+# Booleans for whether the buffer was modified and for whether any
  19+# errors occured in the editing session.
  20+typeset -i1 modified=0 error=0
  21+# Editing buffer, buffer marks, line numbers where a regex match was
  22+# found.
  23+typeset -a buffer marks rematch
  24+# Line number of the currently addressed search result.
  25+typeset -i reidx
  26+# Currently loaded file path.
  27+typeset filepath
  28+
  29+# Set up traps for certain signals.
  30+trap 'err "Interrupt"' INT
  31+trap 'savehup' HUP
  32+trap '' QUIT
  33+
  34+# Open a file from `$1' into the editing buffer. No checks for
  35+# modifications to the buffer are done.
  36+open() {
  37+	# The filepath given is not a file, simply set the filepath
  38+	# and cursor position, then abort.
  39+	if [ ! -f "$1" ]; then
  40+		print "$1: No such file" >&2
  41+		filepath="$1"
  42+		cur=0
  43+		return 0
  44+	fi
  45+
  46+	# Read in the contents of the file.
  47+	b=0
  48+	while IFS='' read -r line; do
  49+		buffer+=("$line")
  50+		# Accumulate the amount of bytes read. The extra 1
  51+		# here accounts for the newline.
  52+		[ $silent -eq 0 ] && b=$((b + ${#line} + 1))
  53+	done < "$1"
  54+
  55+	# If silent mode is unset, then print the amount of bytes
  56+	# that were read.
  57+	[ $silent -eq 0 ] && print "$b"
  58+
  59+	# Set the remembered filepath.
  60+	filepath="$1"
  61+
  62+	# Reset the list of marks and the modified flag.
  63+	marks=()
  64+	modified=0
  65+
  66+	# Set the current line to the last line of the buffer.
  67+	cur=$((${#buffer[@]}))
  68+}
  69+
  70+# Extract address(es) from `arg' into `addr'.
  71+getaddr() {
  72+	typeset -i n
  73+	arg="$1"
  74+
  75+	case "$arg" in
  76+		# An empty address.
  77+		'')
  78+		return 1
  79+		;;
  80+
  81+		# Address is the current line.
  82+		'.'*)
  83+		addr=$cur
  84+		arg="${arg#.}"
  85+		return 0
  86+		;;
  87+
  88+		# Address is the last line.
  89+		'$'*)
  90+		addr=${#buffer[@]}
  91+		arg="${arg#\$}"
  92+		return 0
  93+		;;
  94+
  95+		# Address is a mark.
  96+		\'[a-z]*)
  97+		# Find the mark name and index.
  98+		c=${arg#\'}
  99+		c=${c%"${c#?}"}
 100+		i=$(printf '%d' "'$c")
 101+
 102+		# If no such mark exists, then abort.
 103+		if [ -z "${marks[i]}" ]; then
 104+			return 1
 105+		fi
 106+
 107+		# Set the address to the address of the mark.
 108+		addr=${marks[i]}
 109+		arg=${arg#\'$c}
 110+		return 0
 111+		;;
 112+
 113+		# Address is a pattern.
 114+		'/'*|'?'*)
 115+		# First, figure out in which direction we need to
 116+		# search in.
 117+		c=${arg%"${arg#?}"}
 118+		arg="${arg#?}"
 119+
 120+		# Loop over the string (yeah...) to collect the
 121+		# pattern.
 122+		pattern=
 123+		esc=0
 124+		while [ -n "$arg" ]; do
 125+			ch=${arg%"${arg#?}"}
 126+			arg="${arg#?}"
 127+
 128+			if ((esc == 1)); then
 129+				pattern="$pattern$ch"
 130+				esc=0
 131+				continue
 132+			fi
 133+
 134+			# If a backslash was encountered, then treat
 135+			# the next character literally.
 136+			if [ "$ch" = '\' ]; then
 137+				pattern="$pattern$ch"
 138+				esc=1
 139+				continue
 140+			fi
 141+
 142+			# Break out of the loop upon encountering the
 143+			# delimiter.
 144+			[ "$ch" = "$c" ] && break
 145+
 146+			pattern="$pattern$ch"
 147+		done
 148+
 149+		# If we were given a pattern, then perform a search
 150+		# for it.
 151+		if [ -n "$pattern" ]; then
 152+			search "$pattern" "$c"
 153+
 154+			# If no matches were found, abort.
 155+			((${#rematch[@]} == 0)) && return 1
 156+
 157+			# Otherwise, set the found address to the
 158+			# line of the first found match. Also update
 159+			# the current match index to 0.
 160+			addr=${rematch[0]}
 161+			reidx=0
 162+		# If we were given a null pattern, then find the next
 163+		# pattern match in the list of matches.
 164+		else
 165+			# If no matches were found, abort.
 166+			((${#rematch[@]} == 0)) && return 1
 167+
 168+			# Advance to the next match in the list.
 169+			((reidx++))
 170+
 171+			# If we are at the last match of the list,
 172+			# then wrap around to the beginning.
 173+			# Otherwise, advance to the next match.
 174+			((reidx >= ${#rematch[@]})) && reidx=0
 175+
 176+			# Set the found address to the line of the
 177+			# match.
 178+			addr=${rematch[reidx]}
 179+		fi
 180+
 181+		return 0
 182+		;;
 183+
 184+		# Numeric address.
 185+		[0-9]*)
 186+		n=${arg%%[!0-9]*}
 187+		addr=$n
 188+		arg=${arg#"$n"}
 189+		return 0
 190+		;;
 191+	esac
 192+
 193+	# No such address.
 194+	return 1
 195+}
 196+
 197+# Strip address(es) from `src' and update globals.
 198+getline() {
 199+	src="$1" cmd='' arg=''
 200+	s=$cur e=$cur ls=0 rs=0
 201+
 202+	# If a command only contains a newline, then it should be
 203+	# handled as a special case.
 204+	if [ -z "$src" ]; then
 205+		if ((cur + 1 > ${#buffer[@]})); then
 206+			err "Address out of range"
 207+			return 1
 208+		fi
 209+		((cur++))
 210+		s=$cur e=$cur
 211+		cmd='p'
 212+		return 0
 213+	fi
 214+
 215+	if getaddr "$src"; then
 216+		s=$addr
 217+		e=$addr
 218+		ls=1 # The left hand side address is set.
 219+		src="$arg"
 220+	fi
 221+
 222+	# Check if the range of addressed lines starts/ends before
 223+	# the first line or starts/ends after the last line.
 224+	if [ $s -lt 0 ] || [ $s -gt ${#buffer[@]} ] || \
 225+	   [ $e -lt 0 ] || [ $e -gt ${#buffer[@]} ]; then
 226+		err "Invalid address" && return 1
 227+	fi
 228+
 229+	# Figure out if we have a second address to set.
 230+	case "$src" in
 231+	[,\;]*)
 232+		sep=${src%"${src#?}"}
 233+		src=${src#?}
 234+		[ "$sep" = ';' ] && cur=$e
 235+
 236+		if getaddr "$src"; then
 237+			e=$addr
 238+			rs=1
 239+			src="$arg"
 240+		fi
 241+
 242+		# Fill in defaults for unspecified addresses.
 243+		if [ $ls -eq 0 ]; then
 244+			if [ "$sep" = ',' ]; then
 245+				s=1
 246+			else
 247+				s=$cur
 248+			fi
 249+		fi
 250+
 251+		if [ $rs -eq 0 ]; then
 252+			if [ $ls -eq 1 ]; then
 253+				e=$s
 254+			else
 255+				e=$((${#buffer[@]}))
 256+			fi
 257+		fi
 258+		;;
 259+	\'[a-z]*)
 260+		if ! getaddr "$src"; then
 261+			err "Invalid address"
 262+			return 1
 263+		fi
 264+		;;
 265+	esac
 266+
 267+	# Check if the end of the range of addressed lines comes
 268+	# before its start.
 269+	if [ $e -lt $s ]; then
 270+		err "Invalid address"
 271+		return 1
 272+	fi
 273+
 274+	# If a command only contains an address, then it should be
 275+	# handled as a special case.
 276+	if [ -z "$src" ]; then
 277+		# Only one address can be given.
 278+		if [ $rs -eq 1 ]; then
 279+			err "Too many addresses"
 280+			return 1
 281+		# Make sure the address is in the logical range of
 282+		# possible ones.
 283+		elif ((s > ${#buffer[@]} || s <= 0)); then
 284+			err "Invalid address"
 285+			return 1
 286+		fi
 287+		cur=$s
 288+		cmd='p'
 289+		return 0
 290+	fi
 291+
 292+	cmd=${src%"${src#?}"}
 293+	src=${src#?}
 294+
 295+	# Find an (optional) command suffix.
 296+	suffix=
 297+	arg=$src
 298+	case "$cmd" in
 299+		# These commands can't have a suffix.
 300+		[eEfqQrw\!])
 301+		case "$arg" in *[lnp])
 302+			err "Unexpected command suffix"
 303+			return 1
 304+			;;
 305+		esac
 306+		;;
 307+
 308+		# Any other command can, so simply extract the
 309+		# suffix.
 310+		*)
 311+		case "$arg" in *[lnp])
 312+			suffix=${arg#"${arg%?}"}
 313+			arg=${arg%?}
 314+			;;
 315+		esac
 316+		;;
 317+	esac
 318+
 319+	# Trim leading whitespace, if any.
 320+	while [[ "$arg" = [[:space:]]* ]]; do
 321+		arg="${arg#?}"
 322+	done
 323+}
 324+
 325+# Insert the contents of `text' in the buffer, starting from the
 326+# position specified by `at'.
 327+insert() {
 328+	typeset -a new
 329+	typeset -i at i
 330+	at=$1
 331+	shift # Move the text into the $@ variable.
 332+
 333+	# Insert the lines leading up to `at'.
 334+	i=0
 335+	while ((i < at)); do
 336+		new+=("${buffer[i]}")
 337+		((i++))
 338+	done
 339+
 340+	# Insert the new text after `at'.
 341+	for l in "$@"; do
 342+		new+=("$l")
 343+	done
 344+
 345+	# Insert the rest of the lines.
 346+	while ((i < ${#buffer[@]})); do
 347+		new+=("${buffer[i]}")
 348+		((i++))
 349+	done
 350+
 351+	# Update the source buffer and enable the modified flag.
 352+	buffer=("${new[@]}")
 353+	[ $modified -eq 0 ] && modified=1
 354+
 355+	# Update the current line based on how many lines were
 356+	# inserted (if any).
 357+	if (($# > 0)); then
 358+		cur=$((at + $#))
 359+	else
 360+		cur=$at
 361+	fi
 362+}
 363+
 364+# Delete lines in the range `from' through `to'.
 365+delete() {
 366+	typeset -a new
 367+	typeset -i from=$1 to=$2 i=0
 368+
 369+	# Insert the lines leading up to `from'.
 370+	while ((i < from - 1)); do
 371+		new+=("${buffer[i]}")
 372+		((i++))
 373+	done
 374+
 375+	# Skip the range of lines [from, to].
 376+	i=$to
 377+
 378+	# Continue inserting the rest of the buffer.
 379+	while ((i < ${#buffer[@]})); do
 380+		new+=("${buffer[i]}")
 381+		((i++))
 382+	done
 383+
 384+	# Update the buffer and set the modified flag.
 385+	buffer=("${new[@]}")
 386+	[ $modified -eq 0 ] && modified=1
 387+
 388+	# Update the cursor position.
 389+	if ((${#buffer[@]} > 0)); then
 390+		cur=$((from > 1 ? from - 1 : 1))
 391+	else
 392+		cur=0
 393+	fi
 394+}
 395+
 396+# Search the buffer for the pattern `$1'. The line numbers where a
 397+# match was found are stored in the global array `rematch'.
 398+search() {
 399+	pattern="$1" mode="$2"
 400+	rematch=()
 401+
 402+	case "$mode" in
 403+	# Search forwards, starting from the line following the
 404+	# current line.
 405+	'/')
 406+	i=$cur
 407+
 408+	# Loop up to the end of the buffer.
 409+	while ((i < ${#buffer[@]})); do
 410+		case "${buffer[i]}" in *"$pattern"*)
 411+			rematch+=($((i + 1))) ;; esac
 412+		((i++))
 413+	done
 414+
 415+	# Wrap around to the beginning of the buffer and continue
 416+	# looping up to and including the current line.
 417+	i=0
 418+	while ((i <= cur - 1)); do
 419+		case "${buffer[i]}" in *"$pattern"*)
 420+			rematch+=($((i + 1))) ;; esac
 421+		((i++))
 422+	done
 423+	;;
 424+
 425+	# Search backwards, starting from the line preceding the
 426+	# current line.
 427+	'?')
 428+	i=$((cur - 2))
 429+	while ((i >= 0)); do
 430+		case "${buffer[i]}" in *"$pattern"*)
 431+			rematch+=($((i + 1))) ;; esac
 432+		((i--))
 433+	done
 434+
 435+	# Wrap around to the end of the buffer and continue looping
 436+	# up to and including the current line.
 437+	i=$((${#buffer[@]} - 1))
 438+	while ((i >= cur - 1)); do
 439+		case "${buffer[i]}" in *"$pattern"*)
 440+			rematch+=($((i + 1))) ;; esac
 441+		((i--))
 442+	done
 443+	;;
 444+	esac
 445+}
 446+
 447+# Inform the user of an error. If help mode is on, the optional help
 448+# message in `$1' is also printed before the question mark.
 449+err() {
 450+	lasterr="$1"
 451+	[ $help -eq 1 ] && print "$1" >&2
 452+	print "?" >&2
 453+	# Update the global return status.
 454+	[ $error -eq 0 ] && error=1
 455+}
 456+
 457+# Write out a copy of the buffer upon recieving SIGHUP.
 458+savehup() {
 459+	# Make sure we actually need to write anything out.
 460+	if ((${#buffer[@]} == 0 || modified == 0)); then
 461+		return 0
 462+	fi
 463+
 464+	# Figure out what path to write to. First, try to access
 465+	# `<current working dir>/ed.hup'.
 466+	path="$PWD/ed.hup"
 467+	if ! (:< "$path"); then
 468+		# If that fails, try to access `<home dir>/ed.hup'.
 469+		# Since the chances of this failing (naturally) are
 470+		# very low, the only thing left to do is abort.
 471+		path="$HOME/ed.hup"
 472+		(:< "$path") || return 1
 473+	fi
 474+
 475+	# If we were able to get a path, then write out to it.
 476+	for l in "${buffer[@]}"; do
 477+		print -- "$l"
 478+	done >> "$path"
 479+}
 480+
 481+# Print the escaped version of a character. This is only used in the
 482+# `l' command.
 483+printesc() {
 484+	case "$1" in
 485+		'\\')  print -n '\\\\'  ;;
 486+		$'\a') print -n '\\a'   ;;
 487+		$'\b') print -n '\\b'   ;;
 488+		$'\f') print -n '\\f'   ;;
 489+		$'\r') print -n '\\r'   ;;
 490+		$'\t') print -n '\\t'   ;;
 491+		$'\v') print -n '\\v'   ;;
 492+		'$')   print -n '\\$'   ;;
 493+		*)
 494+		# Print as-is if the character is within the
 495+		# printable character range, and print its octal
 496+		# value otherwise.
 497+		if [[ "$1" = [[:print:]] ]]; then
 498+			print -n -- "$1"
 499+		else
 500+			printf '\\%03o' "'$1"
 501+		fi
 502+		;;
 503+	esac
 504+}
 505+
 506+# Print a range of lines. This is used for the [lnp] commands.
 507+rprint() {
 508+	mode="$1"
 509+
 510+	case "$mode" in
 511+	# Print the addressed lines in a visually unambiguous form.
 512+	'l')
 513+	if [ $s -eq 0 ]; then
 514+		err "Invalid address"
 515+		return
 516+	fi
 517+
 518+	i=$s
 519+	while ((i <= e)); do
 520+		l="${buffer[$((i - 1))]}"
 521+		while [ -n "$l" ]; do
 522+			# Get the first character of the line, remove
 523+			# it from the line and print it in its
 524+			# escaped form.
 525+			c=${l%"${l#?}"}
 526+			l=${l#?}
 527+			printesc "$c"
 528+		done
 529+		# Print the end-of-line marker.
 530+		print '$'
 531+		((i++))
 532+	done
 533+	;;
 534+
 535+	# Print the addressed lines (optionally preceded by their
 536+	# line number).
 537+	'n'|'p')
 538+	if [ $s -eq 0 ]; then
 539+		err "Invalid address"
 540+		return
 541+	fi
 542+
 543+	i=$s
 544+	while ((i <= e)); do
 545+		if [ "$cmd" = 'n' ]; then
 546+			print "$i\t${buffer[$((i - 1))]}"
 547+		else
 548+			print "${buffer[$((i - 1))]}"
 549+		fi
 550+		((i++))
 551+	done
 552+	;;
 553+	esac
 554+}
 555+
 556+# Parse and drop command line arguments.
 557+while getopts "p:s" opt; do case "$opt" in
 558+	"p") pstring="$OPTARG"; prompt=1 ;;
 559+	"s") silent=1 ;;
 560+	*) print "Bad flag '-$opt'" >&2; exit 1 ;;
 561+esac done
 562+shift $((OPTIND - 1))
 563+
 564+# If a file name is given, then load it into the editing buffer.
 565+if [ -n "$1" ]; then
 566+	open "$1"
 567+fi
 568+
 569+# Parse and dispatch commands.
 570+while IFS='' read -r src?"${prompt:+$pstring}"; do
 571+	# Extract the address(es) and argument(s) from the command
 572+	# line.
 573+	getline "$src" || continue
 574+
 575+	case "$cmd" in
 576+
 577+	# Append text after the addressed line.
 578+	'a')
 579+	# We only need a single address.
 580+	if [ $e -ne $s ]; then
 581+		err "Too many addresses"
 582+		continue
 583+	fi
 584+
 585+	text=()
 586+	while IFS='' read -r line; do case "$line" in
 587+		'.') break ;;
 588+		*) text+=("$line") ;;
 589+	esac done
 590+
 591+	insert $s "${text[@]}"
 592+	s=$cur e=$cur
 593+	;;
 594+
 595+	# Delete the addressed lines and replace them with new text.
 596+	'c')
 597+	[ $s -eq 0 ] && i=1 || i=$s
 598+
 599+	text=()
 600+	while IFS='' read -r line; do case "$line" in
 601+		'.') break ;;
 602+		*) text+=("$line") ;;
 603+	esac done
 604+
 605+	delete $i $e
 606+	insert $((i - 1)) "${text[@]}"
 607+	s=$cur e=$cur
 608+	;;
 609+
 610+	# Delete the addressed lines.
 611+	'd')
 612+	delete $s $e
 613+	s=$cur e=$cur
 614+	;;
 615+
 616+	# Load a new file into the editing buffer (with E, it is
 617+	# loaded without checking for buffer modifications).
 618+	'e'|'E')
 619+	if [ -n "$arg" ]; then
 620+		file="$arg"
 621+	else
 622+		file="$filepath"
 623+	fi
 624+
 625+	# If acting as `e' and if the buffer was modified, warn about
 626+	# it and reset the modified flag; then abort.
 627+	if [ "$cmd" = 'e' ] && [ $modified -eq 1 ]; then
 628+		err "Warning: unsaved changes"
 629+		modified=0
 630+		continue
 631+	fi
 632+
 633+	# If the file begins with `!', it is a command and its output
 634+	# shall be read into the buffer.
 635+	if [[ "$file" = '!'* ]]; then
 636+		command="${file#!}"
 637+		new=()
 638+
 639+		# Read the output of the command into the buffer.
 640+		b=0
 641+		while IFS='' read -r line; do
 642+			new+=("$line")
 643+			# Accumulate the amount of bytes read. The
 644+			# extra 1 here accounts for the newline.
 645+			[ $silent -eq 0 ] && b=$((b + ${#line} + 1))
 646+		done <<< "$(eval $command)"
 647+
 648+		# If silent mode is off, print the amount of bytes
 649+		# that were read.
 650+		[ $silent -eq 0 ] && print "$b"
 651+
 652+		# Update the buffer, reset marks and set current
 653+		# line number to the end of the buffer.
 654+		buffer=("${new[@]}")
 655+		marks=()
 656+		cur=${#buffer[@]}
 657+
 658+		# Abort.
 659+		continue
 660+	fi
 661+
 662+	open "$file"
 663+	;;
 664+
 665+	# (Update, and) print the currently remembered filepath.
 666+	'f')
 667+	[ -n "$arg" ] && filepath="$arg"
 668+	print "$filepath"
 669+	;;
 670+
 671+	# Print the reason for the last `?' message.
 672+	'h')
 673+	print "$lasterr" >&2
 674+	;;
 675+
 676+	# Toggle help mode.
 677+	'H')
 678+	[ $help -eq 0 ] && help=1 || help=0
 679+	;;
 680+
 681+	# Insert text before the addressed line.
 682+	'i')
 683+	# We only need a single address.
 684+	if [ $e -ne $s ]; then
 685+		err "Too many addresses"
 686+		continue
 687+	fi
 688+
 689+	text=()
 690+	while IFS='' read -r line; do case "$line" in
 691+		'.') break ;;
 692+		*) text+=("$line") ;;
 693+	esac done
 694+
 695+	insert $((s >= 1 ? s - 1 : s)) "${text[@]}"
 696+	s=$cur e=$cur
 697+	;;
 698+
 699+	# Join a range of lines into one.
 700+	'j')
 701+	# Abort if only one address was given.
 702+	if [ $s -ne 0 ] && [ $e -eq 0 ]; then
 703+		continue
 704+	fi
 705+
 706+	# Default to [., .+1] for the joining range.
 707+	if [ $s -eq 0 ]; then
 708+		s=$cur
 709+		e=$((cur + 1))
 710+	fi
 711+
 712+	# Make sure no out-of-bounds joining happens.
 713+	if ((s < 0 || e > ${#buffer[@]})); then
 714+		err "Address out of range"
 715+		continue
 716+	fi
 717+
 718+	i=0
 719+	new=()
 720+
 721+	# Insert the lines leading up to the range.
 722+	while ((i < s - 1)); do
 723+		new+=("${buffer[i]}")
 724+		((i++))
 725+	done
 726+
 727+	# Join the lines in the [i, e] range.
 728+	l=""
 729+	while ((i < e)); do
 730+		l+="${buffer[i]}"
 731+		((i++))
 732+	done
 733+	new+=("$l")
 734+
 735+	# Continue inserting the rest of the buffer.
 736+	while ((i < ${#buffer[@]})); do
 737+		new+=("${buffer[i]}")
 738+		((i++))
 739+	done
 740+
 741+	# Update the source buffer and enable the modified flag.
 742+	buffer=("${new[@]}")
 743+	[ $modified -eq 0 ] && modified=1
 744+
 745+	# Update the current line.
 746+	cur=$((s > 0 ? s : 1))
 747+	s=$cur e=$cur
 748+	;;
 749+
 750+	# Mark the addressed line.
 751+	'k')
 752+	# Check that the mark name is valid.
 753+	if [[ "$arg" != [a-z] ]]; then
 754+		err "Bad mark name"
 755+		continue
 756+	fi
 757+
 758+	# Since mksh doesn't have associative arrays, the hack is to
 759+	# use the index of the character in the ASCII table to
 760+	# reference the mark.
 761+	i=$(printf '%d' "'$arg")
 762+	marks[i]=$s
 763+	;;
 764+
 765+	# Print the addressed lines:
 766+	# l: in a visually unambigous form,
 767+	# n: preceded by their line number,
 768+	# p: without any modifications to the output.
 769+	'l'|'n'|'p')
 770+	rprint "$cmd"
 771+	;;
 772+
 773+	# Move/copy the addressed lines after the specified position.
 774+	'm'|'t')
 775+	# Figure out the address to move the lines to.
 776+	if getaddr "$arg"; then
 777+		to=$addr
 778+	else
 779+		err "Invalid address"
 780+		continue
 781+	fi
 782+
 783+	i=$((s - 1))
 784+	text=()
 785+	new=()
 786+
 787+	# Extract the lines to be moved.
 788+	while ((i <= e)); do
 789+		text+=("${buffer[i]}")
 790+		((i++))
 791+	done
 792+
 793+	# If we are moving and not copying, then delete the lines in
 794+	# the range.
 795+	[ "$cmd" = 'm' ] && delete $s $e
 796+
 797+	# Now begin inserting the lines leading up to the address.
 798+	i=0
 799+	while ((i < to)); do
 800+		new+=("${buffer[i]}")
 801+		((i++))
 802+	done
 803+
 804+	# Now insert the extracted lines.
 805+	for l in "${text[@]}"; do
 806+		new+=("$l")
 807+	done
 808+
 809+	# Continue inserting the rest of the lines.
 810+	while ((i < ${#buffer[@]})); do
 811+		new+=("${buffer[i]}")
 812+		((i++))
 813+	done
 814+
 815+	# Update the editing buffer and set the modified flag.
 816+	buffer=("${new[@]}")
 817+	[ $modified -eq 0 ] && modified=1
 818+
 819+	# Update the cursor position.
 820+	cur=$((to + ${#text[@]} - 1))
 821+	s=$cur e=$cur
 822+	;;
 823+
 824+
 825+	# Toggle prompt mode.
 826+	'P')
 827+	[ -n "$prompt" ] && prompt= || prompt=1
 828+	;;
 829+
 830+	# Quit.
 831+	'q')
 832+	if [ $modified -eq 1 ]; then
 833+		err "Warning: unsaved changes"
 834+		modified=0
 835+		continue
 836+	fi
 837+
 838+	break
 839+	;;
 840+
 841+	# Quit without warning about buffer modifications.
 842+	'Q')
 843+	break
 844+	;;
 845+
 846+	# Read in the contents of a file (or output of a command) and
 847+	# append them to the buffer.
 848+	'r')
 849+	if [ -n "$arg" ]; then
 850+		file="$arg"
 851+	else
 852+		file="$filepath"
 853+	fi
 854+
 855+	# Set the point of insertion of the file.
 856+	if [ $s -eq $cur ]; then
 857+		i=${#buffer[@]}
 858+	elif [ $s -eq 0 ]; then
 859+		i=1
 860+	else
 861+		i=$s
 862+	fi
 863+	text=()
 864+
 865+	# If the filename begins with a `!', it is a command and its
 866+	# output shall be read into the buffer.
 867+	if [[ "$file" = '!'* ]]; then
 868+		command="${file#!}"
 869+		b=0
 870+		while IFS='' read -r line; do
 871+			text+=("$line")
 872+			# Accumulate the amount of bytes read. The
 873+			# extra 1 here accounts for the newline.
 874+			[ $silent -eq 0 ] && b=$((b + ${#line} + 1))
 875+		done <<< "$(eval "$command")"
 876+
 877+		# If silent mode is off, print the amount of bytes
 878+		# that were read.
 879+		[ $silent -eq 0 ] && print "$b"
 880+
 881+		# Insert the text into the buffer and abort.
 882+		insert $i "${text[@]}"
 883+		continue
 884+	fi
 885+
 886+	# Otherwise, open the file and read its contents into the
 887+	# buffer.
 888+	b=0
 889+	while IFS='' read -r line; do
 890+		text+=("$line")
 891+		# Accumulate the amount of bytes read.
 892+		[ $silent -eq 0 ] && b=$((b + ${#line} + 1))
 893+	done < "$file"
 894+
 895+	insert $i "${text[@]}"
 896+	# If silent mode is off, then print the amount of bytes read.
 897+	[ $silent -eq 0 ] && print "$b"
 898+	;;
 899+
 900+	# Search each addressed line, match it against a regexp and
 901+	# replace it. TODO.
 902+	's')
 903+	;;
 904+
 905+	# Undo the last command that changed the buffer. TODO.
 906+	'u')
 907+	;;
 908+
 909+	# Write the addressed lines out to a file.
 910+	'w')
 911+	if [ -n "$arg" ]; then
 912+		file="$arg"
 913+	else
 914+		file="$filepath"
 915+	fi
 916+	i=1 t=${#buffer[@]} b=0
 917+
 918+	[ $s -ne $cur ] && i=$s
 919+	[ $e -ne $cur ] && t=$e
 920+
 921+	# Store the original value of `i' to check against later.
 922+	o=$i
 923+
 924+	# If the output file is a command, then send the range to its
 925+	# stdin. No counting of bytes is done due to the nature of
 926+	# subshells scoping any sort of variable changes.
 927+	if [[ "$arg" = '!'* ]]; then
 928+		while ((i <= t)); do
 929+			l="${buffer[$((i - 1))]}"
 930+			print -r -- "$l"
 931+			((i++))
 932+		done | eval "${arg#!}"
 933+
 934+		# Abort.
 935+		continue
 936+	fi
 937+
 938+	# Truncate the file first. If something went wrong, abort.
 939+	(:> "$file") || continue
 940+
 941+	# We can't write out to things that aren't files.
 942+	if [ ! -f "$file" ]; then
 943+		print "Not a file: '$file'" >&2
 944+		continue
 945+	fi
 946+
 947+	while ((i <= t)); do
 948+		l="${buffer[$((i - 1))]}"
 949+		ll=${#l}
 950+
 951+		print -r -- "$l" >> "$file"
 952+
 953+		# Accumulate the amount of bytes written into `b'.
 954+		if [ $silent -eq 0 ]; then
 955+			# The extra 1 here accounts for the newline.
 956+			b=$((b + ll + 1))
 957+		fi
 958+		((i++))
 959+	done
 960+
 961+	# If silent mode is unset, print the amount of bytes written.
 962+	[ $silent -eq 0 ] && print "$b"
 963+
 964+	# If no known filepath is set, then set one now.
 965+	[ -z "$filepath" ] && filepath="$file"
 966+
 967+	# Clear the modified flag.
 968+	if [ $o == 1 ] && [ $t == ${#buffer[@]} ] && \
 969+	   [ "$file" = "$filepath" ]; then
 970+	   	modified=0
 971+	fi
 972+	;;
 973+
 974+	# Print the current line number.
 975+	'=')
 976+	print $cur
 977+	;;
 978+
 979+	# Interpret a shell command line.
 980+	'!')
 981+	if [ "$arg" = '!' ]; then
 982+		command="$lastcmd"
 983+	else
 984+		command="${arg@/%/${filepath}}"
 985+	fi
 986+	[ "$command" != "$arg" ] && [ $silent -eq 0 ] && print "$command"
 987+
 988+	eval "$command"
 989+	[ "$arg" != '!' ] && lastcmd="$command"
 990+	[ $silent -ne 1 ] && print "!"
 991+	;;
 992+
 993+	# Unknown command.
 994+	*)
 995+	err "Unknown command"
 996+	;;
 997+
 998+	esac
 999+
1000+	# Parse the command suffix (if any).
1001+	[ -n "$suffix" ] && rprint "$suffix"
1002+done
1003+
1004+return $((error))