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