commit 62cb528
wf
·
2026-07-24 18:34:42 +0000 UTC
parent 62cb528
Init
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))