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))