summaryrefslogtreecommitdiff
path: root/corebinutils
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:25:19 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:25:19 +0300
commitf0f174696b38adfd6063bb74e5b48d2b864cb650 (patch)
tree445ec6db16bfd8ecd36dfcb8907c852c8c104b10 /corebinutils
parent0e49ba0e7b8b9bc8bdc61f8c80228252392e758e (diff)
parent93528dc40c12704e0f9ca16475e97e68b4317fb9 (diff)
downloadProject-Tick-f0f174696b38adfd6063bb74e5b48d2b864cb650.tar.gz
Project-Tick-f0f174696b38adfd6063bb74e5b48d2b864cb650.zip
Add 'corebinutils/ed/' from commit '93528dc40c12704e0f9ca16475e97e68b4317fb9'
git-subtree-dir: corebinutils/ed git-subtree-mainline: 0e49ba0e7b8b9bc8bdc61f8c80228252392e758e git-subtree-split: 93528dc40c12704e0f9ca16475e97e68b4317fb9
Diffstat (limited to 'corebinutils')
-rw-r--r--corebinutils/ed/.gitignore25
-rw-r--r--corebinutils/ed/GNUmakefile73
-rw-r--r--corebinutils/ed/LICENSE26
-rw-r--r--corebinutils/ed/LICENSES/BSD-2-Clause.txt9
-rw-r--r--corebinutils/ed/README.md152
-rw-r--r--corebinutils/ed/buf.c331
-rw-r--r--corebinutils/ed/compat.c31
-rw-r--r--corebinutils/ed/compat.h10
-rw-r--r--corebinutils/ed/ed.1999
-rw-r--r--corebinutils/ed/ed.h280
-rw-r--r--corebinutils/ed/glbl.c218
-rw-r--r--corebinutils/ed/io.c345
-rw-r--r--corebinutils/ed/main.c1421
-rw-r--r--corebinutils/ed/re.c129
-rw-r--r--corebinutils/ed/sub.c254
-rw-r--r--corebinutils/ed/test/=.err1
-rw-r--r--corebinutils/ed/test/Makefile25
-rw-r--r--corebinutils/ed/test/README31
-rw-r--r--corebinutils/ed/test/TODO15
-rw-r--r--corebinutils/ed/test/a.d5
-rw-r--r--corebinutils/ed/test/a.r8
-rw-r--r--corebinutils/ed/test/a.t9
-rw-r--r--corebinutils/ed/test/a1.err3
-rw-r--r--corebinutils/ed/test/a2.err3
-rw-r--r--corebinutils/ed/test/addr.d9
-rw-r--r--corebinutils/ed/test/addr.r2
-rw-r--r--corebinutils/ed/test/addr.t5
-rw-r--r--corebinutils/ed/test/addr1.err1
-rw-r--r--corebinutils/ed/test/addr2.err1
-rw-r--r--corebinutils/ed/test/ascii.d.uu9
-rw-r--r--corebinutils/ed/test/ascii.r.uu9
-rw-r--r--corebinutils/ed/test/ascii.t0
-rw-r--r--corebinutils/ed/test/bang1.d0
-rw-r--r--corebinutils/ed/test/bang1.err1
-rw-r--r--corebinutils/ed/test/bang1.r1
-rw-r--r--corebinutils/ed/test/bang1.t5
-rw-r--r--corebinutils/ed/test/bang2.err1
-rw-r--r--corebinutils/ed/test/c.d5
-rw-r--r--corebinutils/ed/test/c.r4
-rw-r--r--corebinutils/ed/test/c.t12
-rw-r--r--corebinutils/ed/test/c1.err3
-rw-r--r--corebinutils/ed/test/c2.err3
-rw-r--r--corebinutils/ed/test/ckscripts.sh36
-rw-r--r--corebinutils/ed/test/d.d5
-rw-r--r--corebinutils/ed/test/d.err1
-rw-r--r--corebinutils/ed/test/d.r1
-rw-r--r--corebinutils/ed/test/d.t3
-rw-r--r--corebinutils/ed/test/e1.d1
-rw-r--r--corebinutils/ed/test/e1.err1
-rw-r--r--corebinutils/ed/test/e1.r1
-rw-r--r--corebinutils/ed/test/e1.t1
-rw-r--r--corebinutils/ed/test/e2.d1
-rw-r--r--corebinutils/ed/test/e2.err1
-rw-r--r--corebinutils/ed/test/e2.r1
-rw-r--r--corebinutils/ed/test/e2.t1
-rw-r--r--corebinutils/ed/test/e3.d1
-rw-r--r--corebinutils/ed/test/e3.err1
-rw-r--r--corebinutils/ed/test/e3.r1
-rw-r--r--corebinutils/ed/test/e3.t1
-rw-r--r--corebinutils/ed/test/e4.d1
-rw-r--r--corebinutils/ed/test/e4.r1
-rw-r--r--corebinutils/ed/test/e4.t1
-rw-r--r--corebinutils/ed/test/f1.err1
-rw-r--r--corebinutils/ed/test/f2.err1
-rw-r--r--corebinutils/ed/test/g1.d5
-rw-r--r--corebinutils/ed/test/g1.err1
-rw-r--r--corebinutils/ed/test/g1.r15
-rw-r--r--corebinutils/ed/test/g1.t6
-rw-r--r--corebinutils/ed/test/g2.d5
-rw-r--r--corebinutils/ed/test/g2.err1
-rw-r--r--corebinutils/ed/test/g2.r1
-rw-r--r--corebinutils/ed/test/g2.t2
-rw-r--r--corebinutils/ed/test/g3.d5
-rw-r--r--corebinutils/ed/test/g3.err1
-rw-r--r--corebinutils/ed/test/g3.r5
-rw-r--r--corebinutils/ed/test/g3.t4
-rw-r--r--corebinutils/ed/test/g4.d5
-rw-r--r--corebinutils/ed/test/g4.r7
-rw-r--r--corebinutils/ed/test/g4.t13
-rw-r--r--corebinutils/ed/test/g5.d3
-rw-r--r--corebinutils/ed/test/g5.r9
-rw-r--r--corebinutils/ed/test/g5.t2
-rw-r--r--corebinutils/ed/test/h.err1
-rw-r--r--corebinutils/ed/test/i.d5
-rw-r--r--corebinutils/ed/test/i.r8
-rw-r--r--corebinutils/ed/test/i.t9
-rw-r--r--corebinutils/ed/test/i1.err3
-rw-r--r--corebinutils/ed/test/i2.err3
-rw-r--r--corebinutils/ed/test/i3.err3
-rw-r--r--corebinutils/ed/test/j.d5
-rw-r--r--corebinutils/ed/test/j.r4
-rw-r--r--corebinutils/ed/test/j.t2
-rw-r--r--corebinutils/ed/test/k.d5
-rw-r--r--corebinutils/ed/test/k.r5
-rw-r--r--corebinutils/ed/test/k.t10
-rw-r--r--corebinutils/ed/test/k1.err1
-rw-r--r--corebinutils/ed/test/k2.err1
-rw-r--r--corebinutils/ed/test/k3.err1
-rw-r--r--corebinutils/ed/test/k4.err6
-rw-r--r--corebinutils/ed/test/l.d0
-rw-r--r--corebinutils/ed/test/l.r0
-rw-r--r--corebinutils/ed/test/l.t0
-rw-r--r--corebinutils/ed/test/m.d5
-rw-r--r--corebinutils/ed/test/m.err4
-rw-r--r--corebinutils/ed/test/m.r5
-rw-r--r--corebinutils/ed/test/m.t7
-rw-r--r--corebinutils/ed/test/mkscripts.sh74
-rw-r--r--corebinutils/ed/test/n.d0
-rw-r--r--corebinutils/ed/test/n.r0
-rw-r--r--corebinutils/ed/test/n.t0
-rw-r--r--corebinutils/ed/test/nl.err1
-rw-r--r--corebinutils/ed/test/nl1.d5
-rw-r--r--corebinutils/ed/test/nl1.r8
-rw-r--r--corebinutils/ed/test/nl1.t8
-rw-r--r--corebinutils/ed/test/nl2.d5
-rw-r--r--corebinutils/ed/test/nl2.r6
-rw-r--r--corebinutils/ed/test/nl2.t4
-rw-r--r--corebinutils/ed/test/p.d0
-rw-r--r--corebinutils/ed/test/p.r0
-rw-r--r--corebinutils/ed/test/p.t0
-rw-r--r--corebinutils/ed/test/q.d0
-rw-r--r--corebinutils/ed/test/q.r0
-rw-r--r--corebinutils/ed/test/q.t5
-rw-r--r--corebinutils/ed/test/q1.err1
-rw-r--r--corebinutils/ed/test/r1.d5
-rw-r--r--corebinutils/ed/test/r1.err1
-rw-r--r--corebinutils/ed/test/r1.r7
-rw-r--r--corebinutils/ed/test/r1.t3
-rw-r--r--corebinutils/ed/test/r2.d5
-rw-r--r--corebinutils/ed/test/r2.err1
-rw-r--r--corebinutils/ed/test/r2.r10
-rw-r--r--corebinutils/ed/test/r2.t1
-rw-r--r--corebinutils/ed/test/r3.d1
-rw-r--r--corebinutils/ed/test/r3.r2
-rw-r--r--corebinutils/ed/test/r3.t1
-rw-r--r--corebinutils/ed/test/s1.d5
-rw-r--r--corebinutils/ed/test/s1.err1
-rw-r--r--corebinutils/ed/test/s1.r5
-rw-r--r--corebinutils/ed/test/s1.t6
-rw-r--r--corebinutils/ed/test/s10.err4
-rw-r--r--corebinutils/ed/test/s2.d5
-rw-r--r--corebinutils/ed/test/s2.err4
-rw-r--r--corebinutils/ed/test/s2.r5
-rw-r--r--corebinutils/ed/test/s2.t4
-rw-r--r--corebinutils/ed/test/s3.d0
-rw-r--r--corebinutils/ed/test/s3.err1
-rw-r--r--corebinutils/ed/test/s3.r1
-rw-r--r--corebinutils/ed/test/s3.t6
-rw-r--r--corebinutils/ed/test/s4.err1
-rw-r--r--corebinutils/ed/test/s5.err1
-rw-r--r--corebinutils/ed/test/s6.err1
-rw-r--r--corebinutils/ed/test/s7.err5
-rw-r--r--corebinutils/ed/test/s8.err4
-rw-r--r--corebinutils/ed/test/s9.err4
-rw-r--r--corebinutils/ed/test/t.d5
-rw-r--r--corebinutils/ed/test/t.r16
-rw-r--r--corebinutils/ed/test/t1.d5
-rw-r--r--corebinutils/ed/test/t1.err1
-rw-r--r--corebinutils/ed/test/t1.r16
-rw-r--r--corebinutils/ed/test/t1.t3
-rw-r--r--corebinutils/ed/test/t2.d5
-rw-r--r--corebinutils/ed/test/t2.err1
-rw-r--r--corebinutils/ed/test/t2.r6
-rw-r--r--corebinutils/ed/test/t2.t1
-rw-r--r--corebinutils/ed/test/u.d5
-rw-r--r--corebinutils/ed/test/u.err1
-rw-r--r--corebinutils/ed/test/u.r9
-rw-r--r--corebinutils/ed/test/u.t31
-rw-r--r--corebinutils/ed/test/v.d5
-rw-r--r--corebinutils/ed/test/v.r11
-rw-r--r--corebinutils/ed/test/v.t6
-rw-r--r--corebinutils/ed/test/w.d5
-rw-r--r--corebinutils/ed/test/w.r10
-rw-r--r--corebinutils/ed/test/w.t2
-rw-r--r--corebinutils/ed/test/w1.err1
-rw-r--r--corebinutils/ed/test/w2.err1
-rw-r--r--corebinutils/ed/test/w3.err1
-rw-r--r--corebinutils/ed/test/x.err1
-rw-r--r--corebinutils/ed/test/z.err2
-rw-r--r--corebinutils/ed/tests/test.sh180
-rw-r--r--corebinutils/ed/tests/uu_decode.c132
-rw-r--r--corebinutils/ed/undo.c150
182 files changed, 5549 insertions, 0 deletions
diff --git a/corebinutils/ed/.gitignore b/corebinutils/ed/.gitignore
new file mode 100644
index 0000000000..a74d30b48c
--- /dev/null
+++ b/corebinutils/ed/.gitignore
@@ -0,0 +1,25 @@
+*.a
+*.core
+*.lo
+*.nossppico
+*.o
+*.orig
+*.pico
+*.pieo
+*.po
+*.rej
+*.so
+*.so.[0-9]*
+*.sw[nop]
+*~
+.*DS_Store
+.cache
+.clangd
+.ccls-cache
+.depend*
+compile_commands.json
+compile_commands.events.json
+tags
+build/
+out/
+.linux-obj/
diff --git a/corebinutils/ed/GNUmakefile b/corebinutils/ed/GNUmakefile
new file mode 100644
index 0000000000..e12f22e600
--- /dev/null
+++ b/corebinutils/ed/GNUmakefile
@@ -0,0 +1,73 @@
+.DEFAULT_GOAL := all
+
+CC ?= cc
+CPPFLAGS += -D_GNU_SOURCE -I$(CURDIR)
+CFLAGS ?= -O2
+CFLAGS += -std=c17 -g -Wall -Wextra -Werror -Wno-unused-parameter
+LDFLAGS ?=
+LDLIBS ?=
+
+OBJDIR := $(CURDIR)/build
+OUTDIR := $(CURDIR)/out
+TARGET := $(OUTDIR)/ed
+SOURCES := \
+ $(CURDIR)/buf.c \
+ $(CURDIR)/compat.c \
+ $(CURDIR)/glbl.c \
+ $(CURDIR)/io.c \
+ $(CURDIR)/main.c \
+ $(CURDIR)/re.c \
+ $(CURDIR)/sub.c \
+ $(CURDIR)/undo.c
+OBJS := \
+ $(OBJDIR)/buf.o \
+ $(OBJDIR)/compat.o \
+ $(OBJDIR)/glbl.o \
+ $(OBJDIR)/io.o \
+ $(OBJDIR)/main.o \
+ $(OBJDIR)/re.o \
+ $(OBJDIR)/sub.o \
+ $(OBJDIR)/undo.o
+
+.PHONY: all clean dirs status test
+
+all: $(TARGET)
+
+dirs:
+ @mkdir -p "$(OBJDIR)" "$(OUTDIR)"
+
+$(TARGET): $(OBJS) | dirs
+ $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS)
+
+$(OBJDIR)/buf.o: $(CURDIR)/buf.c $(CURDIR)/ed.h $(CURDIR)/compat.h | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/buf.c" -o "$@"
+
+$(OBJDIR)/compat.o: $(CURDIR)/compat.c $(CURDIR)/compat.h | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/compat.c" -o "$@"
+
+$(OBJDIR)/glbl.o: $(CURDIR)/glbl.c $(CURDIR)/ed.h $(CURDIR)/compat.h | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/glbl.c" -o "$@"
+
+$(OBJDIR)/io.o: $(CURDIR)/io.c $(CURDIR)/ed.h $(CURDIR)/compat.h | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/io.c" -o "$@"
+
+$(OBJDIR)/main.o: $(CURDIR)/main.c $(CURDIR)/ed.h $(CURDIR)/compat.h | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/main.c" -o "$@"
+
+$(OBJDIR)/re.o: $(CURDIR)/re.c $(CURDIR)/ed.h $(CURDIR)/compat.h | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/re.c" -o "$@"
+
+$(OBJDIR)/sub.o: $(CURDIR)/sub.c $(CURDIR)/ed.h $(CURDIR)/compat.h | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/sub.c" -o "$@"
+
+$(OBJDIR)/undo.o: $(CURDIR)/undo.c $(CURDIR)/ed.h $(CURDIR)/compat.h | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/undo.c" -o "$@"
+
+test: $(TARGET)
+ ED_BIN="$(TARGET)" CC="$(CC)" sh "$(CURDIR)/tests/test.sh"
+
+status:
+ @printf '%s\n' "$(TARGET)"
+
+clean:
+ @rm -rf "$(OBJDIR)" "$(OUTDIR)"
diff --git a/corebinutils/ed/LICENSE b/corebinutils/ed/LICENSE
new file mode 100644
index 0000000000..130ba43b5e
--- /dev/null
+++ b/corebinutils/ed/LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 1993
+ Andrew Moore, Talke Studio. All rights reserved.
+
+Copyright (c) 2026
+ Project Tick. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE.
diff --git a/corebinutils/ed/LICENSES/BSD-2-Clause.txt b/corebinutils/ed/LICENSES/BSD-2-Clause.txt
new file mode 100644
index 0000000000..5f662b354c
--- /dev/null
+++ b/corebinutils/ed/LICENSES/BSD-2-Clause.txt
@@ -0,0 +1,9 @@
+Copyright (c) <year> <owner>
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/corebinutils/ed/README.md b/corebinutils/ed/README.md
new file mode 100644
index 0000000000..389e63ef50
--- /dev/null
+++ b/corebinutils/ed/README.md
@@ -0,0 +1,152 @@
+# ed
+
+Standalone musl-libc-based Linux port of FreeBSD `ed` for Project Tick BSD/Linux Distribution.
+
+## Build
+
+```sh
+gmake -f GNUmakefile
+gmake -f GNUmakefile CC=musl-gcc
+```
+
+## Test
+
+```sh
+gmake -f GNUmakefile test
+gmake -f GNUmakefile test CC=musl-gcc
+```
+
+## Notes
+
+- Port strategy is direct Linux-native cleanup of the FreeBSD source, not a BSD ABI shim.
+- The original multi-file editor core and FreeBSD regression corpus are preserved, but the Linux build surface is standalone: `GNUmakefile`, shell test entrypoint, and musl-safe libc usage.
+- Scratch-buffer storage uses `mkstemp(3)` in `TMPDIR` or `/tmp`, then `fdopen(3)` for the editor's temp file stream.
+- Shell escapes and filter I/O (`!`, `r !cmd`, `w !cmd`) use `system(3)`, `popen(3)`, and `pclose(3)`.
+- Terminal resize handling uses Linux `ioctl(TIOCGWINSZ)` on stdin.
+- Regex handling stays on POSIX `regcomp(3)` / `regexec(3)` from the active libc.
+- FreeBSD `strlcpy(3)` usage was replaced with a local implementation so the port does not depend on glibc extensions or `libbsd`.
+
+## Linux Semantics
+
+- Supported: core editing commands, shell escapes, global commands, undo, binary buffer handling, and the upstream FreeBSD regression scripts.
+- Supported: restricted `red` mode path checks from the original source.
+- Unsupported: historic crypt mode. Startup `-x` exits with an explicit Linux error, and the interactive `x` command reports `crypt mode is not supported on Linux`.
+- Known inherited quirk: the implicit newline print command still accepts the historic `,1` address form from this `ed` lineage. The Linux test harness allows only that single upstream deviation so other parser regressions still fail hard.
+
+## What is ed?
+
+ed is an 8-bit-clean, POSIX-compliant line editor. It should work with
+any regular expression package that conforms to the POSIX interface
+standard, such as GNU regex(3).
+
+If reliable signals are supported (e.g., POSIX sigaction(2)), it should
+compile with little trouble. Otherwise, the macros SPL1() and SPL0()
+should be redefined to disable interrupts.
+
+The following compiler directives are recognized:
+NO_REALLOC_NULL - if realloc(3) does not accept a NULL pointer
+BACKWARDS - for backwards compatibility
+NEED_INSQUE - if insque(3) is missing
+
+The file `POSIX' describes extensions to and deviations from the POSIX
+standard.
+
+The ./test directory contains regression tests for ed. The README
+file in that directory explains how to run these.
+
+For a description of the ed algorithm, see Kernighan and Plauger's book
+"Software Tools in Pascal," Addison-Wesley, 1981.
+
+## ed POSIX message
+
+
+This version of ed(1) is not strictly POSIX compliant, as described in
+the POSIX 1003.2 document. The following is a summary of the omissions,
+extensions and possible deviations from POSIX 1003.2.
+
+OMISSIONS
+---------
+1) For backwards compatibility, the POSIX rule that says a range of
+ addresses cannot be used where only a single address is expected has
+ been relaxed.
+
+2) To support the BSD `s' command (see extension [1] below),
+ substitution patterns cannot be delimited by numbers or the characters
+ `r', `g' and `p'. In contrast, POSIX specifies any character expect
+ space or newline can used as a delimiter.
+
+EXTENSIONS
+----------
+1) BSD commands have been implemented wherever they do not conflict with
+ the POSIX standard. The BSD-ism's included are:
+ i) `s' (i.e., s[n][rgp]*) to repeat a previous substitution,
+ ii) `W' for appending text to an existing file,
+ iii) `wq' for exiting after a write,
+ iv) `z' for scrolling through the buffer, and
+ v) BSD line addressing syntax (i.e., `^' and `%') is recognized.
+
+2) The POSIX interactive global commands `G' and `V' are extended to
+ support multiple commands, including `a', `i' and `c'. The command
+ format is the same as for the global commands `g' and `v', i.e., one
+ command per line with each line, except for the last, ending in a
+ backslash (\).
+
+3) An extension to the POSIX file commands `E', `e', `r', `W' and `w' is
+ that <file> arguments are processed for backslash escapes, i.e., any
+ character preceded by a backslash is interpreted literally. If the
+ first unescaped character of a <file> argument is a bang (!), then the
+ rest of the line is interpreted as a shell command, and no escape
+ processing is performed by ed.
+
+4) For SunOS ed(1) compatibility, ed runs in restricted mode if invoked
+ as red. This limits editing of files in the local directory only and
+ prohibits shell commands.
+
+DEVIATIONS
+----------
+1) Though ed is not a stream editor, it can be used to edit binary files.
+ To assist in binary editing, when a file containing at least one ASCII
+ NUL character is written, a newline is not appended if it did not
+ already contain one upon reading. In particular, reading /dev/null
+ prior to writing prevents appending a newline to a binary file.
+
+ For example, to create a file with ed containing a single NUL character:
+ $ ed file
+ a
+ ^@
+ .
+ r /dev/null
+ wq
+
+ Similarly, to remove a newline from the end of binary `file':
+ $ ed file
+ r /dev/null
+ wq
+
+2) Since the behavior of `u' (undo) within a `g' (global) command list is
+ not specified by POSIX, it follows the behavior of the SunOS ed:
+ undo forces a global command list to be executed only once, rather than
+ for each line matching a global pattern. In addition, each instance of
+ `u' within a global command undoes all previous commands (including
+ undo's) in the command list. This seems the best way, since the
+ alternatives are either too complicated to implement or too confusing
+ to use.
+
+ The global/undo combination is useful for masking errors that
+ would otherwise cause a script to fail. For instance, an ed script
+ to remove any occurrences of either `censor1' or `censor2' might be
+ written as:
+ ed - file <<EOF
+ 1g/.*/u\
+ ,s/censor1//g\
+ ,s/censor2//g
+ ...
+
+3) The `m' (move) command within a `g' command list also follows the SunOS
+ ed implementation: any moved lines are removed from the global command's
+ `active' list.
+
+4) If ed is invoked with a name argument prefixed by a bang (!), then the
+ remainder of the argument is interpreted as a shell command. To invoke
+ ed on a file whose name starts with bang, prefix the name with a
+ backslash.
diff --git a/corebinutils/ed/buf.c b/corebinutils/ed/buf.c
new file mode 100644
index 0000000000..9b41c066ff
--- /dev/null
+++ b/corebinutils/ed/buf.c
@@ -0,0 +1,331 @@
+/* buf.c: This file contains the scratch-file buffer routines for the
+ ed line editor. */
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 1993 Andrew Moore, Talke Studio.
+ * Copyright (c) 2026 Project Tick.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/stat.h>
+
+#include "ed.h"
+
+
+static FILE *sfp; /* scratch file pointer */
+static off_t sfseek; /* scratch file position */
+static int seek_write; /* seek before writing */
+static line_t buffer_head; /* incore buffer */
+
+/* get_sbuf_line: get a line of text from the scratch file; return pointer
+ to the text */
+char *
+get_sbuf_line(line_t *lp)
+{
+ static char *sfbuf = NULL; /* buffer */
+ static size_t sfbufsz; /* buffer size */
+
+ size_t len;
+
+ if (lp == &buffer_head)
+ return NULL;
+ seek_write = 1; /* force seek on write */
+ /* out of position */
+ if (sfseek != lp->seek) {
+ sfseek = lp->seek;
+ if (fseeko(sfp, sfseek, SEEK_SET) < 0) {
+ fprintf(stderr, "%s\n", strerror(errno));
+ errmsg = "cannot seek temp file";
+ return NULL;
+ }
+ }
+ len = lp->len;
+ REALLOC(sfbuf, sfbufsz, len + 1, NULL);
+ if (fread(sfbuf, sizeof(char), len, sfp) != len) {
+ fprintf(stderr, "%s\n", strerror(errno));
+ errmsg = "cannot read temp file";
+ return NULL;
+ }
+ sfseek += len; /* update file position */
+ sfbuf[len] = '\0';
+ return sfbuf;
+}
+
+
+/* put_sbuf_line: write a line of text to the scratch file and add a line node
+ to the editor buffer; return a pointer to the end of the text */
+const char *
+put_sbuf_line(const char *cs)
+{
+ line_t *lp;
+ size_t len;
+ const char *s;
+
+ if ((lp = (line_t *) malloc(sizeof(line_t))) == NULL) {
+ fprintf(stderr, "%s\n", strerror(errno));
+ errmsg = "out of memory";
+ return NULL;
+ }
+ /* assert: cs is '\n' terminated */
+ for (s = cs; *s != '\n'; s++)
+ ;
+ if (s - cs >= LINECHARS) {
+ errmsg = "line too long";
+ free(lp);
+ return NULL;
+ }
+ len = s - cs;
+ /* out of position */
+ if (seek_write) {
+ if (fseeko(sfp, (off_t)0, SEEK_END) < 0) {
+ fprintf(stderr, "%s\n", strerror(errno));
+ errmsg = "cannot seek temp file";
+ free(lp);
+ return NULL;
+ }
+ sfseek = ftello(sfp);
+ seek_write = 0;
+ }
+ /* assert: SPL1() */
+ if (fwrite(cs, sizeof(char), len, sfp) != len) {
+ sfseek = -1;
+ fprintf(stderr, "%s\n", strerror(errno));
+ errmsg = "cannot write temp file";
+ free(lp);
+ return NULL;
+ }
+ lp->len = len;
+ lp->seek = sfseek;
+ add_line_node(lp);
+ sfseek += len; /* update file position */
+ return ++s;
+}
+
+
+/* add_line_node: add a line node in the editor buffer after the current line */
+void
+add_line_node(line_t *lp)
+{
+ line_t *cp;
+
+ cp = get_addressed_line_node(current_addr); /* this get_addressed_line_node last! */
+ INSQUE(lp, cp);
+ addr_last++;
+ current_addr++;
+}
+
+
+/* get_line_node_addr: return line number of pointer */
+long
+get_line_node_addr(line_t *lp)
+{
+ line_t *cp = &buffer_head;
+ long n = 0;
+
+ while (cp != lp && (cp = cp->q_forw) != &buffer_head)
+ n++;
+ if (n && cp == &buffer_head) {
+ errmsg = "invalid address";
+ return ERR;
+ }
+ return n;
+}
+
+
+/* get_addressed_line_node: return pointer to a line node in the editor buffer */
+line_t *
+get_addressed_line_node(long n)
+{
+ static line_t *lp = &buffer_head;
+ static long on = 0;
+
+ SPL1();
+ if (n > on)
+ if (n <= (on + addr_last) >> 1)
+ for (; on < n; on++)
+ lp = lp->q_forw;
+ else {
+ lp = buffer_head.q_back;
+ for (on = addr_last; on > n; on--)
+ lp = lp->q_back;
+ }
+ else
+ if (n >= on >> 1)
+ for (; on > n; on--)
+ lp = lp->q_back;
+ else {
+ lp = &buffer_head;
+ for (on = 0; on < n; on++)
+ lp = lp->q_forw;
+ }
+ SPL0();
+ return lp;
+}
+
+static char *sfn; /* scratch file name */
+
+static int
+prepare_scratch_name(void)
+{
+ static const char suffix[] = "ed.XXXXXX";
+ const char *tmpdir;
+ size_t dirlen;
+ size_t suffixlen;
+ size_t need;
+ int needslash;
+ char *path;
+
+ tmpdir = getenv("TMPDIR");
+ if (tmpdir == NULL || tmpdir[0] == '\0')
+ tmpdir = "/tmp";
+ dirlen = strlen(tmpdir);
+ suffixlen = sizeof(suffix);
+ needslash = dirlen > 0 && tmpdir[dirlen - 1] != '/';
+ need = dirlen + (size_t)needslash + suffixlen;
+ if (need > PATH_MAX) {
+ fprintf(stderr, "%s\n", "temporary directory path too long");
+ errmsg = "temporary directory path too long";
+ return ERR;
+ }
+ path = malloc(need);
+ if (path == NULL) {
+ fprintf(stderr, "%s\n", strerror(errno));
+ errmsg = "out of memory";
+ return ERR;
+ }
+ memcpy(path, tmpdir, dirlen);
+ if (needslash)
+ path[dirlen++] = '/';
+ memcpy(path + dirlen, suffix, suffixlen);
+ free(sfn);
+ sfn = path;
+ return 0;
+}
+
+/* open_sbuf: open scratch file */
+int
+open_sbuf(void)
+{
+ int fd = -1;
+ int u;
+
+ isbinary = newline_added = 0;
+ if (prepare_scratch_name() < 0)
+ return ERR;
+ u = umask(077);
+ if ((fd = mkstemp(sfn)) == -1 ||
+ (sfp = fdopen(fd, "w+")) == NULL) {
+ if (fd != -1) {
+ close(fd);
+ unlink(sfn);
+ }
+ perror(sfn != NULL ? sfn : "mkstemp");
+ errmsg = "cannot open temp file";
+ umask(u);
+ return ERR;
+ }
+ umask(u);
+ return 0;
+}
+
+
+/* close_sbuf: close scratch file */
+int
+close_sbuf(void)
+{
+ if (sfp) {
+ if (fclose(sfp) < 0) {
+ fprintf(stderr, "%s: %s\n", sfn != NULL ? sfn : "ed",
+ strerror(errno));
+ errmsg = "cannot close temp file";
+ return ERR;
+ }
+ sfp = NULL;
+ if (sfn != NULL)
+ unlink(sfn);
+ }
+ free(sfn);
+ sfn = NULL;
+ sfseek = seek_write = 0;
+ return 0;
+}
+
+
+/* quit: remove_lines scratch file and exit */
+void
+quit(int n)
+{
+ if (sfp) {
+ fclose(sfp);
+ if (sfn != NULL)
+ unlink(sfn);
+ }
+ free(sfn);
+ sfn = NULL;
+ exit(n);
+}
+
+
+static unsigned char ctab[256]; /* character translation table */
+
+/* init_buffers: open scratch buffer; initialize line queue */
+void
+init_buffers(void)
+{
+ int i = 0;
+
+ /* Keep stdin unbuffered to avoid i/o contention with shell escapes
+ invoked by nonterminal input, e.g.,
+ ed - <<EOF
+ !cat
+ hello, world
+ EOF */
+ setvbuf(stdin, NULL, _IONBF, 0);
+
+ /* Ensure stdout is line buffered. This avoids bogus delays
+ of output if stdout is piped through utilities to a terminal. */
+ setvbuf(stdout, NULL, _IOLBF, 0);
+ if (open_sbuf() < 0)
+ quit(2);
+ REQUE(&buffer_head, &buffer_head);
+ for (i = 0; i < 256; i++)
+ ctab[i] = i;
+}
+
+
+/* translit_text: translate characters in a string */
+char *
+translit_text(char *s, int len, int from, int to)
+{
+ static int i = 0;
+
+ unsigned char *us;
+
+ ctab[i] = i; /* restore table to initial state */
+ ctab[i = from] = to;
+ for (us = (unsigned char *) s; len-- > 0; us++)
+ *us = ctab[*us];
+ return s;
+}
diff --git a/corebinutils/ed/compat.c b/corebinutils/ed/compat.c
new file mode 100644
index 0000000000..d034179584
--- /dev/null
+++ b/corebinutils/ed/compat.c
@@ -0,0 +1,31 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "compat.h"
+
+size_t
+ed_strlcpy(char *dst, const char *src, size_t dstsize)
+{
+ const char *s;
+ size_t srclen;
+
+ s = src;
+ while (*s != '\0')
+ s++;
+ srclen = (size_t)(s - src);
+
+ if (dstsize != 0) {
+ size_t copylen;
+ size_t i;
+
+ copylen = srclen;
+ if (copylen >= dstsize)
+ copylen = dstsize - 1;
+ for (i = 0; i < copylen; i++)
+ dst[i] = src[i];
+ dst[copylen] = '\0';
+ }
+
+ return srclen;
+}
diff --git a/corebinutils/ed/compat.h b/corebinutils/ed/compat.h
new file mode 100644
index 0000000000..c12fdc6a94
--- /dev/null
+++ b/corebinutils/ed/compat.h
@@ -0,0 +1,10 @@
+#ifndef ED_COMPAT_H
+#define ED_COMPAT_H
+
+#include <stddef.h>
+
+size_t ed_strlcpy(char *dst, const char *src, size_t dstsize);
+
+#define strlcpy ed_strlcpy
+
+#endif
diff --git a/corebinutils/ed/ed.1 b/corebinutils/ed/ed.1
new file mode 100644
index 0000000000..0db9f8d76b
--- /dev/null
+++ b/corebinutils/ed/ed.1
@@ -0,0 +1,999 @@
+.Dd April 9, 2021
+.Dt ED 1
+.Os
+.Sh NAME
+.Nm ed ,
+.Nm red
+.Nd text editor
+.Sh SYNOPSIS
+.Nm
+.Op Fl
+.Op Fl s
+.Op Fl p Ar string
+.Op Ar file
+.Nm red
+.Op Fl
+.Op Fl s
+.Op Fl p Ar string
+.Op Ar file
+.Sh DESCRIPTION
+The
+.Nm
+utility is a line-oriented text editor.
+It is used to create, display, modify and otherwise manipulate text
+files.
+When invoked as
+.Nm red ,
+the editor runs in
+.Qq restricted
+mode, in which the only difference is that the editor restricts the
+use of filenames which start with
+.Ql \&!
+(interpreted as shell commands by
+.Nm )
+or contain a
+.Ql \&/ .
+Note that editing outside of the current directory is only prohibited
+if the user does not have write access to the current directory.
+If a user has write access to the current directory, then symbolic
+links can be created in the current directory, in which case
+.Nm red
+will not stop the user from editing the file that the symbolic link
+points to.
+.Pp
+If invoked with a
+.Ar file
+argument, then a copy of
+.Ar file
+is read into the editor's buffer.
+Changes are made to this copy and not directly to
+.Ar file
+itself.
+Upon quitting
+.Nm ,
+any changes not explicitly saved with a
+.Em w
+command are lost.
+.Pp
+Editing is done in two distinct modes:
+.Em command
+and
+.Em input .
+When first invoked,
+.Nm
+is in command mode.
+In this mode commands are read from the standard input and
+executed to manipulate the contents of the editor buffer.
+A typical command might look like:
+.Pp
+.Sm off
+.Cm ,s No / Em old Xo
+.No / Em new
+.No / Cm g
+.Xc
+.Sm on
+.Pp
+which replaces all occurrences of the string
+.Em old
+with
+.Em new .
+.Pp
+When an input command, such as
+.Em a
+(append),
+.Em i
+(insert) or
+.Em c
+(change), is given,
+.Nm
+enters input mode.
+This is the primary means
+of adding text to a file.
+In this mode, no commands are available;
+instead, the standard input is written
+directly to the editor buffer.
+Lines consist of text up to and
+including a
+.Em newline
+character.
+Input mode is terminated by
+entering a single period
+.Pq Em .\&
+on a line.
+.Pp
+All
+.Nm
+commands operate on whole lines or ranges of lines; e.g.,
+the
+.Em d
+command deletes lines; the
+.Em m
+command moves lines, and so on.
+It is possible to modify only a portion of a line by means of replacement,
+as in the example above.
+However even here, the
+.Em s
+command is applied to whole lines at a time.
+.Pp
+In general,
+.Nm
+commands consist of zero or more line addresses, followed by a single
+character command and possibly additional parameters; i.e.,
+commands have the structure:
+.Pp
+.Sm off
+.Xo
+.Op Ar address Op , Ar address
+.Ar command Op Ar parameters
+.Xc
+.Sm on
+.Pp
+The address(es) indicate the line or range of lines to be affected by the
+command.
+If fewer addresses are given than the command accepts, then
+default addresses are supplied.
+.Sh OPTIONS
+The following options are available:
+.Bl -tag -width indent
+.It Fl s
+Suppress diagnostics.
+This should be used if
+.Nm Ns 's
+standard input is from a script.
+.It Fl p Ar string
+Specify a command prompt.
+This may be toggled on and off with the
+.Em P
+command.
+.It Ar file
+Specify the name of a file to read.
+If
+.Ar file
+is prefixed with a
+bang (!), then it is interpreted as a shell command.
+In this case,
+what is read is
+the standard output of
+.Ar file
+executed via
+.Xr sh 1 .
+To read a file whose name begins with a bang, prefix the
+name with a backslash (\\).
+The default filename is set to
+.Ar file
+only if it is not prefixed with a bang.
+.El
+.Sh LINE ADDRESSING
+An address represents the number of a line in the buffer.
+The
+.Nm
+utility maintains a
+.Em current address
+which is
+typically supplied to commands as the default address when none is specified.
+When a file is first read, the current address is set to the last line
+of the file.
+In general, the current address is set to the last line
+affected by a command.
+.Pp
+A line address is
+constructed from one of the bases in the list below, optionally followed
+by a numeric offset.
+The offset may include any combination
+of digits, operators (i.e.,
+.Em + ,
+.Em -
+and
+.Em ^ )
+and whitespace.
+Addresses are read from left to right, and their values are computed
+relative to the current address.
+.Pp
+One exception to the rule that addresses represent line numbers is the
+address
+.Em 0
+(zero).
+This means "before the first line,"
+and is legal wherever it makes sense.
+.Pp
+An address range is two addresses separated either by a comma or
+semi-colon.
+The value of the first address in a range cannot exceed the
+value of the second.
+If only one address is given in a range, then
+the second address is set to the given address.
+If an
+.Em n Ns -tuple
+of addresses is given where
+.Em "n\ >\ 2" ,
+then the corresponding range is determined by the last two addresses in
+the
+.Em n Ns -tuple .
+If only one address is expected, then the last address is used.
+.Pp
+Each address in a comma-delimited range is interpreted relative to the
+current address.
+In a semi-colon-delimited range, the first address is
+used to set the current address, and the second address is interpreted
+relative to the first.
+.Pp
+The following address symbols are recognized:
+.Bl -tag -width indent
+.It .
+The current line (address) in the buffer.
+.It $
+The last line in the buffer.
+.It n
+The
+.Em n Ns th
+line in the buffer
+where
+.Em n
+is a number in the range
+.Em [0,$] .
+.It - or ^
+The previous line.
+This is equivalent to
+.Em -1
+and may be repeated with cumulative effect.
+.It -n or ^n
+The
+.Em n Ns th
+previous line, where
+.Em n
+is a non-negative number.
+.It +
+The next line.
+This is equivalent to
+.Em +1
+and may be repeated with cumulative effect.
+.It +n
+The
+.Em n Ns th
+next line, where
+.Em n
+is a non-negative number.
+.It , or %
+The first through last lines in the buffer.
+This is equivalent to
+the address range
+.Em 1,$ .
+.It ;
+The current through last lines in the buffer.
+This is equivalent to
+the address range
+.Em .,$ .
+.It /re/
+The next line containing the regular expression
+.Em re .
+The search wraps to the beginning of the buffer and continues down to the
+current line, if necessary.
+// repeats the last search.
+.It ?re?
+The
+previous line containing the regular expression
+.Em re .
+The search wraps to the end of the buffer and continues up to the
+current line, if necessary.
+?? repeats the last search.
+.It 'lc
+The
+line previously marked by a
+.Em k
+(mark) command, where
+.Em lc
+is a lower case letter.
+.El
+.Sh REGULAR EXPRESSIONS
+Regular expressions are patterns used in selecting text.
+For example, the command:
+.Pp
+.Sm off
+.Cm g No / Em string Xo
+.No /
+.Xc
+.Sm on
+.Pp
+prints all lines containing
+.Em string .
+Regular expressions are also
+used by the
+.Em s
+command for selecting old text to be replaced with new.
+.Pp
+In addition to a specifying string literals, regular expressions can
+represent
+classes of strings.
+Strings thus represented are said to be matched
+by the corresponding regular expression.
+If it is possible for a regular expression
+to match several strings in a line, then the left-most longest match is
+the one selected.
+.Pp
+The following symbols are used in constructing regular expressions:
+.Bl -tag -width indent
+.It c
+Any character
+.Em c
+not listed below, including
+.Ql \&{ ,
+.Ql \&} ,
+.Ql \&( ,
+.Ql \&) ,
+.Ql <
+and
+.Ql > ,
+matches itself.
+.It Pf \e c
+Any backslash-escaped character
+.Em c ,
+except for
+.Ql \&{ ,
+.Ql \&} ,
+.Ql \&( ,
+.Ql \&) ,
+.Ql <
+and
+.Ql > ,
+matches itself.
+.It .
+Match any single character.
+.It Op char-class
+Match any single character in
+.Em char-class .
+To include a
+.Ql \&]
+in
+.Em char-class ,
+it must be the first character.
+A range of characters may be specified by separating the end characters
+of the range with a
+.Ql - ,
+e.g.,
+.Ql a-z
+specifies the lower case characters.
+The following literal expressions can also be used in
+.Em char-class
+to specify sets of characters:
+.Pp
+.Bl -column "[:alnum:]" "[:cntrl:]" "[:lower:]" "[:xdigit:]" -compact
+.It [:alnum:] Ta [:cntrl:] Ta [:lower:] Ta [:space:]
+.It [:alpha:] Ta [:digit:] Ta [:print:] Ta [:upper:]
+.It [:blank:] Ta [:graph:] Ta [:punct:] Ta [:xdigit:]
+.El
+.Pp
+If
+.Ql -
+appears as the first or last
+character of
+.Em char-class ,
+then it matches itself.
+All other characters in
+.Em char-class
+match themselves.
+.Pp
+Patterns in
+.Em char-class
+of the form:
+.Pp
+.Bl -item -compact -offset 2n
+.It
+.Op \&. Ns Ar col-elm Ns .\&
+or,
+.It
+.Op = Ns Ar col-elm Ns =
+.El
+.Pp
+where
+.Ar col-elm
+is a
+.Em collating element
+are interpreted according to the current locale settings
+(not currently supported).
+See
+.Xr regex 3
+and
+.Xr re_format 7
+for an explanation of these constructs.
+.It Op ^char-class
+Match any single character, other than newline, not in
+.Em char-class .
+.Em Char-class
+is defined
+as above.
+.It ^
+If
+.Em ^
+is the first character of a regular expression, then it
+anchors the regular expression to the beginning of a line.
+Otherwise, it matches itself.
+.It $
+If
+.Em $
+is the last character of a regular expression, it
+anchors the regular expression to the end of a line.
+Otherwise, it matches itself.
+.It Pf \e <
+Anchor the single character regular expression or subexpression
+immediately following it to the beginning of a word.
+(This may not be available)
+.It Pf \e >
+Anchor the single character regular expression or subexpression
+immediately following it to the end of a word.
+(This may not be available)
+.It Pf \e (re\e)
+Define a subexpression
+.Em re .
+Subexpressions may be nested.
+A subsequent backreference of the form
+.Pf \e Em n ,
+where
+.Em n
+is a number in the range [1,9], expands to the text matched by the
+.Em n Ns th
+subexpression.
+For example, the regular expression
+.Ql \e(.*\e)\e1
+matches any string
+consisting of identical adjacent substrings.
+Subexpressions are ordered relative to
+their left delimiter.
+.It *
+Match the single character regular expression or subexpression
+immediately preceding it zero or more times.
+If
+.Em *
+is the first
+character of a regular expression or subexpression, then it matches
+itself.
+The
+.Em *
+operator sometimes yields unexpected results.
+For example, the regular expression
+.Ql b*
+matches the beginning of
+the string
+.Ql abbb
+(as opposed to the substring
+.Ql bbb ) ,
+since a null match
+is the only left-most match.
+.It \e{n,m\e} or \e{n,\e} or \e{n\e}
+Match the single character regular expression or subexpression
+immediately preceding it at least
+.Em n
+and at most
+.Em m
+times.
+If
+.Em m
+is omitted, then it matches at least
+.Em n
+times.
+If the comma is also omitted, then it matches exactly
+.Em n
+times.
+.El
+.Pp
+Additional regular expression operators may be defined depending on the
+particular
+.Xr regex 3
+implementation.
+.Sh COMMANDS
+All
+.Nm
+commands are single characters, though some require additional parameters.
+If a command's parameters extend over several lines, then
+each line except for the last
+must be terminated with a backslash (\\).
+.Pp
+In general, at most one command is allowed per line.
+However, most commands accept a print suffix, which is any of
+.Em p
+(print),
+.Em l
+(list),
+or
+.Em n
+(enumerate),
+to print the last line affected by the command.
+.Pp
+An interrupt (typically ^C) has the effect of aborting the current command
+and returning the editor to command mode.
+.Pp
+The
+.Nm
+utility
+recognizes the following commands.
+The commands are shown together with
+the default address or address range supplied if none is
+specified (in parenthesis).
+.Bl -tag -width indent
+.It (.)a
+Append text to the buffer after the addressed line.
+Text is entered in input mode.
+The current address is set to last line entered.
+.It (.,.)c
+Change lines in the buffer.
+The addressed lines are deleted
+from the buffer, and text is appended in their place.
+Text is entered in input mode.
+The current address is set to last line entered.
+.It (.,.)d
+Delete the addressed lines from the buffer.
+If there is a line after the deleted range, then the current address is set
+to this line.
+Otherwise the current address is set to the line
+before the deleted range.
+.It e Ar file
+Edit
+.Ar file ,
+and sets the default filename.
+If
+.Ar file
+is not specified, then the default filename is used.
+Any lines in the buffer are deleted before
+the new file is read.
+The current address is set to the last line read.
+.It e Ar !command
+Edit the standard output of
+.Ar !command ,
+(see
+.Ar !command
+below).
+The default filename is unchanged.
+Any lines in the buffer are deleted before the output of
+.Ar command
+is read.
+The current address is set to the last line read.
+.It E Ar file
+Edit
+.Ar file
+unconditionally.
+This is similar to the
+.Em e
+command,
+except that unwritten changes are discarded without warning.
+The current address is set to the last line read.
+.It f Ar file
+Set the default filename to
+.Ar file .
+If
+.Ar file
+is not specified, then the default unescaped filename is printed.
+.It (1,$)g/re/command-list
+Apply
+.Ar command-list
+to each of the addressed lines matching a regular expression
+.Ar re .
+The current address is set to the
+line currently matched before
+.Ar command-list
+is executed.
+At the end of the
+.Em g
+command, the current address is set to the last line affected by
+.Ar command-list .
+.Pp
+Each command in
+.Ar command-list
+must be on a separate line,
+and every line except for the last must be terminated by a backslash
+(\\).
+Any commands are allowed, except for
+.Em g ,
+.Em G ,
+.Em v ,
+and
+.Em V .
+A newline alone in
+.Ar command-list
+is equivalent to a
+.Em p
+command.
+.It (1,$)G/re/
+Interactively edit the addressed lines matching a regular expression
+.Ar re .
+For each matching line,
+the line is printed,
+the current address is set,
+and the user is prompted to enter a
+.Ar command-list .
+At the end of the
+.Em G
+command, the current address
+is set to the last line affected by (the last)
+.Ar command-list .
+.Pp
+The format of
+.Ar command-list
+is the same as that of the
+.Em g
+command.
+A newline alone acts as a null command list.
+A single
+.Ql &
+repeats the last non-null command list.
+.It H
+Toggle the printing of error explanations.
+By default, explanations are not printed.
+It is recommended that ed scripts begin with this command to
+aid in debugging.
+.It h
+Print an explanation of the last error.
+.It (.)i
+Insert text in the buffer before the current line.
+Text is entered in input mode.
+The current address is set to the last line entered.
+.It (.,.+1)j
+Join the addressed lines.
+The addressed lines are
+deleted from the buffer and replaced by a single
+line containing their joined text.
+The current address is set to the resultant line.
+.It (.)klc
+Mark a line with a lower case letter
+.Em lc .
+The line can then be addressed as
+.Em 'lc
+(i.e., a single quote followed by
+.Em lc )
+in subsequent commands.
+The mark is not cleared until the line is
+deleted or otherwise modified.
+.It (.,.)l
+Print the addressed lines unambiguously.
+If a single line fills more than one screen (as might be the case
+when viewing a binary file, for instance), a
+.Dq Li --More--
+prompt is printed on the last line.
+The
+.Nm
+utility waits until the RETURN key is pressed
+before displaying the next screen.
+The current address is set to the last line
+printed.
+.It (.,.)m(.)
+Move lines in the buffer.
+The addressed lines are moved to after the
+right-hand destination address, which may be the address
+.Em 0
+(zero).
+The current address is set to the
+last line moved.
+.It (.,.)n
+Print the addressed lines along with
+their line numbers.
+The current address is set to the last line
+printed.
+.It (.,.)p
+Print the addressed lines.
+The current address is set to the last line
+printed.
+.It P
+Toggle the command prompt on and off.
+Unless a prompt was specified by with command-line option
+.Fl p Ar string ,
+the command prompt is by default turned off.
+.It q
+Quit
+.Nm .
+.It Q
+Quit
+.Nm
+unconditionally.
+This is similar to the
+.Em q
+command,
+except that unwritten changes are discarded without warning.
+.It ($)r Ar file
+Read
+.Ar file
+to after the addressed line.
+If
+.Ar file
+is not specified, then the default
+filename is used.
+If there was no default filename prior to the command,
+then the default filename is set to
+.Ar file .
+Otherwise, the default filename is unchanged.
+The current address is set to the last line read.
+.It ($)r Ar !command
+Read
+to after the addressed line
+the standard output of
+.Ar !command ,
+(see the
+.Ar !command
+below).
+The default filename is unchanged.
+The current address is set to the last line read.
+.It (.,.)s/re/replacement/
+.It (.,.)s/re/replacement/g
+.It (.,.)s/re/replacement/n
+Replace text in the addressed lines
+matching a regular expression
+.Ar re
+with
+.Ar replacement .
+By default, only the first match in each line is replaced.
+If the
+.Em g
+(global) suffix is given, then every match is to be replaced.
+The
+.Em n
+suffix, where
+.Em n
+is a positive number, causes only the
+.Em n Ns th
+match to be replaced.
+It is an error if no substitutions are performed on any of the addressed
+lines.
+The current address is set the last line affected.
+.Pp
+.Ar \&Re
+and
+.Ar replacement
+may be delimited by any character other than space and newline
+(see the
+.Em s
+command below).
+If one or two of the last delimiters is omitted, then the last line
+affected is printed as though the print suffix
+.Em p
+were specified.
+.Pp
+An unescaped
+.Ql &
+in
+.Ar replacement
+is replaced by the currently matched text.
+The character sequence
+.Em \em ,
+where
+.Em m
+is a number in the range [1,9], is replaced by the
+.Em m th
+backreference expression of the matched text.
+If
+.Ar replacement
+consists of a single
+.Ql % ,
+then
+.Ar replacement
+from the last substitution is used.
+Newlines may be embedded in
+.Ar replacement
+if they are escaped with a backslash (\\).
+.It (.,.)s
+Repeat the last substitution.
+This form of the
+.Em s
+command accepts a count suffix
+.Em n ,
+or any combination of the characters
+.Em r ,
+.Em g ,
+and
+.Em p .
+If a count suffix
+.Em n
+is given, then only the
+.Em n Ns th
+match is replaced.
+The
+.Em r
+suffix causes
+the regular expression of the last search to be used instead of the
+that of the last substitution.
+The
+.Em g
+suffix toggles the global suffix of the last substitution.
+The
+.Em p
+suffix toggles the print suffix of the last substitution
+The current address is set to the last line affected.
+.It (.,.)t(.)
+Copy (i.e., transfer) the addressed lines to after the right-hand
+destination address, which may be the address
+.Em 0
+(zero).
+The current address is set to the last line
+copied.
+.It u
+Undo the last command and restores the current address
+to what it was before the command.
+The global commands
+.Em g ,
+.Em G ,
+.Em v ,
+and
+.Em V .
+are treated as a single command by undo.
+.Em u
+is its own inverse.
+.It (1,$)v/re/command-list
+Apply
+.Ar command-list
+to each of the addressed lines not matching a regular expression
+.Ar re .
+This is similar to the
+.Em g
+command.
+.It (1,$)V/re/
+Interactively edit the addressed lines not matching a regular expression
+.Ar re .
+This is similar to the
+.Em G
+command.
+.It (1,$)w Ar file
+Write the addressed lines to
+.Ar file .
+Any previous contents of
+.Ar file
+are lost without warning.
+If there is no default filename, then the default filename is set to
+.Ar file ,
+otherwise it is unchanged.
+If no filename is specified, then the default
+filename is used.
+The current address is unchanged.
+.It (1,$)wq Ar file
+Write the addressed lines to
+.Ar file ,
+and then executes a
+.Em q
+command.
+.It (1,$)w Ar !command
+Write the addressed lines to the standard input of
+.Ar !command ,
+(see the
+.Em !command
+below).
+The default filename and current address are unchanged.
+.It (1,$)W Ar file
+Append the addressed lines to the end of
+.Ar file .
+This is similar to the
+.Em w
+command, expect that the previous contents of file is not clobbered.
+The current address is unchanged.
+.It Pf (.+1)z n
+Scroll
+.Ar n
+lines at a time starting at addressed line.
+If
+.Ar n
+is not specified, then the current window size is used.
+The current address is set to the last line printed.
+.It !command
+Execute
+.Ar command
+via
+.Xr sh 1 .
+If the first character of
+.Ar command
+is
+.Ql \&! ,
+then it is replaced by text of the
+previous
+.Ar !command .
+The
+.Nm
+utility does not process
+.Ar command
+for backslash (\\) escapes.
+However, an unescaped
+.Em %
+is replaced by the default filename.
+When the shell returns from execution, a
+.Ql \&!
+is printed to the standard output.
+The current line is unchanged.
+.It ($)=
+Print the line number of the addressed line.
+.It (.+1)newline
+Print the addressed line, and sets the current address to
+that line.
+.El
+.Sh FILES
+.Bl -tag -width /tmp/ed.* -compact
+.It Pa /tmp/ed.*
+buffer file
+.It Pa ed.hup
+the file to which
+.Nm
+attempts to write the buffer if the terminal hangs up
+.El
+.Sh DIAGNOSTICS
+When an error occurs,
+.Nm
+prints a
+.Ql \&?
+and either returns to command mode
+or exits if its input is from a script.
+An explanation of the last error can be
+printed with the
+.Em h
+(help) command.
+.Pp
+Since the
+.Em g
+(global) command masks any errors from failed searches and substitutions,
+it can be used to perform conditional operations in scripts; e.g.,
+.Pp
+.Sm off
+.Cm g No / Em old Xo
+.No / Cm s
+.No // Em new
+.No /
+.Xc
+.Sm on
+.Pp
+replaces any occurrences of
+.Em old
+with
+.Em new .
+If the
+.Em u
+(undo) command occurs in a global command list, then
+the command list is executed only once.
+.Pp
+If diagnostics are not disabled, attempting to quit
+.Nm
+or edit another file before writing a modified buffer
+results in an error.
+If the command is entered a second time, it succeeds,
+but any changes to the buffer are lost.
+.Sh SEE ALSO
+.Xr sed 1 ,
+.Xr sh 1 ,
+.Xr vi 1 ,
+.Xr regex 3
+.Pp
+USD:12-13
+.Rs
+.%A B. W. Kernighan
+.%A P. J. Plauger
+.%B Software Tools in Pascal
+.%O Addison-Wesley
+.%D 1981
+.Re
+.Rs
+.\" 4.4BSD USD:9
+.%A B. W. Kernighan
+.%T A Tutorial Introduction to the UNIX Text Editor
+.Re
+.Rs
+.\" 4.4BSD USD:10
+.%A B. W. Kernighan
+.%T Advanced Editing on UNIX
+.Re
+.Sh LIMITATIONS
+The
+.Nm
+utility processes
+.Ar file
+arguments for backslash escapes, i.e., in a filename,
+any characters preceded by a backslash (\\) are
+interpreted literally.
+.Pp
+If a text (non-binary) file is not terminated by a newline character,
+then
+.Nm
+appends one on reading/writing it.
+In the case of a binary file,
+.Nm
+does not append a newline on reading/writing.
+.Pp
+per line overhead: 4 ints
+.Sh HISTORY
+An
+.Nm
+command appeared in
+.At v1 .
+.Sh BUGS
+The
+.Nm
+utility does not recognize multibyte characters.
diff --git a/corebinutils/ed/ed.h b/corebinutils/ed/ed.h
new file mode 100644
index 0000000000..4874bd38c0
--- /dev/null
+++ b/corebinutils/ed/ed.h
@@ -0,0 +1,280 @@
+/* ed.h: type and constant definitions for the ed editor. */
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 1993 Andrew Moore
+ * Copyright (c) 2026 Project Tick.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <errno.h>
+#include <limits.h>
+#include <regex.h>
+#include <signal.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "compat.h"
+
+#define ERR (-2)
+#define EMOD (-3)
+#define FATAL (-4)
+
+#define MINBUFSZ 512 /* minimum buffer size - must be > 0 */
+#define SE_MAX 30 /* max subexpressions in a regular expression */
+#ifdef INT_MAX
+# define LINECHARS INT_MAX /* max chars per line */
+#else
+# define LINECHARS 2147483647 /* max chars per line */
+#endif
+
+/* gflags */
+#define GLB 001 /* global command */
+#define GPR 002 /* print after command */
+#define GLS 004 /* list after command */
+#define GNP 010 /* enumerate after command */
+#define GSG 020 /* global substitute */
+
+typedef regex_t pattern_t;
+
+#if defined(__GNUC__) || defined(__clang__)
+#define ED_NORETURN __attribute__((__noreturn__))
+#else
+#define ED_NORETURN
+#endif
+
+/* Line node */
+typedef struct line {
+ struct line *q_forw;
+ struct line *q_back;
+ off_t seek; /* address of line in scratch buffer */
+ int len; /* length of line */
+} line_t;
+
+
+typedef struct undo {
+
+/* type of undo nodes */
+#define UADD 0
+#define UDEL 1
+#define UMOV 2
+#define VMOV 3
+
+ int type; /* command type */
+ line_t *h; /* head of list */
+ line_t *t; /* tail of list */
+} undo_t;
+
+#ifndef max
+# define max(a,b) ((a) > (b) ? (a) : (b))
+#endif
+#ifndef min
+# define min(a,b) ((a) < (b) ? (a) : (b))
+#endif
+
+#define INC_MOD(l, k) ((l) + 1 > (k) ? 0 : (l) + 1)
+#define DEC_MOD(l, k) ((l) - 1 < 0 ? (k) : (l) - 1)
+
+/* SPL1: disable some interrupts (requires reliable signals) */
+#define SPL1() mutex++
+
+/* SPL0: enable all interrupts; check sigflags (requires reliable signals) */
+#define SPL0() \
+if (--mutex == 0) { \
+ if (sigflags & (1 << (SIGHUP - 1))) handle_hup(SIGHUP); \
+ if (sigflags & (1 << (SIGINT - 1))) handle_int(SIGINT); \
+}
+
+/* STRTOL: convert a string to long */
+#define STRTOL(i, p) { \
+ if (((i = strtol(p, &p, 10)) == LONG_MIN || i == LONG_MAX) && \
+ errno == ERANGE) { \
+ errmsg = "number out of range"; \
+ i = 0; \
+ return ERR; \
+ } \
+}
+
+#if defined(sun) || defined(NO_REALLOC_NULL)
+/* REALLOC: assure at least a minimum size for buffer b */
+#define REALLOC(b,n,i,err) \
+if ((i) > (n)) { \
+ size_t ti = (n); \
+ char *ts; \
+ SPL1(); \
+ if ((b) != NULL) { \
+ if ((ts = (char *) realloc((b), ti += max((i), MINBUFSZ))) == NULL) { \
+ fprintf(stderr, "%s\n", strerror(errno)); \
+ errmsg = "out of memory"; \
+ SPL0(); \
+ return err; \
+ } \
+ } else { \
+ if ((ts = (char *) malloc(ti += max((i), MINBUFSZ))) == NULL) { \
+ fprintf(stderr, "%s\n", strerror(errno)); \
+ errmsg = "out of memory"; \
+ SPL0(); \
+ return err; \
+ } \
+ } \
+ (n) = ti; \
+ (b) = ts; \
+ SPL0(); \
+}
+#else /* NO_REALLOC_NULL */
+/* REALLOC: assure at least a minimum size for buffer b */
+#define REALLOC(b,n,i,err) \
+if ((i) > (n)) { \
+ size_t ti = (n); \
+ char *ts; \
+ SPL1(); \
+ if ((ts = (char *) realloc((b), ti += max((i), MINBUFSZ))) == NULL) { \
+ fprintf(stderr, "%s\n", strerror(errno)); \
+ errmsg = "out of memory"; \
+ SPL0(); \
+ return err; \
+ } \
+ (n) = ti; \
+ (b) = ts; \
+ SPL0(); \
+}
+#endif /* NO_REALLOC_NULL */
+
+/* REQUE: link pred before succ */
+#define REQUE(pred, succ) (pred)->q_forw = (succ), (succ)->q_back = (pred)
+
+/* INSQUE: insert elem in circular queue after pred */
+#define INSQUE(elem, pred) \
+{ \
+ REQUE((elem), (pred)->q_forw); \
+ REQUE((pred), elem); \
+}
+
+/* REMQUE: remove_lines elem from circular queue */
+#define REMQUE(elem) REQUE((elem)->q_back, (elem)->q_forw);
+
+/* NUL_TO_NEWLINE: overwrite ASCII NULs with newlines */
+#define NUL_TO_NEWLINE(s, l) translit_text(s, l, '\0', '\n')
+
+/* NEWLINE_TO_NUL: overwrite newlines with ASCII NULs */
+#define NEWLINE_TO_NUL(s, l) translit_text(s, l, '\n', '\0')
+
+
+/* Local Function Declarations */
+void add_line_node(line_t *);
+int append_lines(long);
+int apply_subst_template(const char *, regmatch_t *, int, int);
+int build_active_list(int);
+int cbc_decode(unsigned char *, FILE *);
+int cbc_encode(unsigned char *, int, FILE *);
+int check_addr_range(long, long);
+void clear_active_list(void);
+void clear_undo_stack(void);
+int close_sbuf(void);
+int copy_lines(long);
+int delete_lines(long, long);
+int display_lines(long, long, int);
+line_t *dup_line_node(line_t *);
+int exec_command(void);
+long exec_global(int, int);
+int extract_addr_range(void);
+char *extract_pattern(int);
+int extract_subst_tail(int *, long *);
+char *extract_subst_template(void);
+int filter_lines(long, long, char *);
+line_t *get_addressed_line_node(long);
+pattern_t *get_compiled_pattern(void);
+char *get_extended_line(int *, int);
+char *get_filename(void);
+int get_keyword(void);
+long get_line_node_addr(line_t *);
+long get_matching_node_addr(pattern_t *, int);
+long get_marked_node_addr(int);
+char *get_sbuf_line(line_t *);
+int get_shell_command(void);
+int get_stream_line(FILE *);
+int get_tty_line(void);
+void handle_hup(int);
+void handle_int(int);
+void handle_winch(int);
+int has_trailing_escape(char *, char *);
+int hex_to_binary(int, int);
+void init_buffers(void);
+int is_legal_filename(char *);
+int join_lines(long, long);
+int mark_line_node(line_t *, int);
+int move_lines(long);
+line_t *next_active_node(void);
+long next_addr(void);
+int open_sbuf(void);
+char *parse_char_class(char *);
+int pop_undo_stack(void);
+undo_t *push_undo_stack(int, long, long);
+const char *put_sbuf_line(const char *);
+int put_stream_line(FILE *, const char *, int);
+int put_tty_line(const char *, int, long, int);
+ED_NORETURN void quit(int);
+long read_file(char *, long);
+long read_stream(FILE *, long);
+int search_and_replace(pattern_t *, int, int);
+int set_active_node(line_t *);
+void signal_hup(int);
+void signal_int(int);
+char *strip_escapes(char *);
+int substitute_matching_text(pattern_t *, line_t *, int, int);
+char *translit_text(char *, int, int, int);
+void unmark_line_node(line_t *);
+void unset_active_nodes(line_t *, line_t *);
+long write_file(char *, const char *, long, long);
+long write_stream(FILE *, long, long);
+
+/* global buffers */
+extern char *ibuf;
+extern char *ibufp;
+extern int ibufsz;
+
+/* global flags */
+extern int isbinary;
+extern int isglobal;
+extern int modified;
+extern int mutex;
+extern int sigflags;
+
+/* global vars */
+extern long addr_last;
+extern long current_addr;
+extern const char *errmsg;
+extern long first_addr;
+extern int lineno;
+extern long second_addr;
+extern long u_addr_last;
+extern long u_current_addr;
+extern long rows;
+extern int cols;
+extern int newline_added;
+extern int scripted;
+extern int patlock;
diff --git a/corebinutils/ed/glbl.c b/corebinutils/ed/glbl.c
new file mode 100644
index 0000000000..ddfca1c68a
--- /dev/null
+++ b/corebinutils/ed/glbl.c
@@ -0,0 +1,218 @@
+/* glob.c: This file contains the global command routines for the ed line
+ editor */
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 1993 Andrew Moore, Talke Studio.
+ * Copyright (c) 2026 Project Tick.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/types.h>
+#include <sys/ioctl.h>
+#include <sys/wait.h>
+
+#include "ed.h"
+
+
+/* build_active_list: add line matching a pattern to the global-active list */
+int
+build_active_list(int isgcmd)
+{
+ pattern_t *pat;
+ line_t *lp;
+ long n;
+ char *s;
+ char delimiter;
+
+ if ((delimiter = *ibufp) == ' ' || delimiter == '\n') {
+ errmsg = "invalid pattern delimiter";
+ return ERR;
+ } else if ((pat = get_compiled_pattern()) == NULL)
+ return ERR;
+ else if (*ibufp == delimiter)
+ ibufp++;
+ clear_active_list();
+ lp = get_addressed_line_node(first_addr);
+ for (n = first_addr; n <= second_addr; n++, lp = lp->q_forw) {
+ if ((s = get_sbuf_line(lp)) == NULL)
+ return ERR;
+ if (isbinary)
+ NUL_TO_NEWLINE(s, lp->len);
+ if (!(regexec(pat, s, 0, NULL, 0) == isgcmd) &&
+ set_active_node(lp) < 0)
+ return ERR;
+ }
+ return 0;
+}
+
+
+/* exec_global: apply command list in the command buffer to the active
+ lines in a range; return command status */
+long
+exec_global(int interact, int gflag)
+{
+ static char *ocmd = NULL;
+ static int ocmdsz = 0;
+
+ line_t *lp = NULL;
+ int status;
+ int n;
+ char *cmd = NULL;
+
+#ifdef BACKWARDS
+ if (!interact)
+ if (!strcmp(ibufp, "\n"))
+ cmd = "p\n"; /* null cmd-list == `p' */
+ else if ((cmd = get_extended_line(&n, 0)) == NULL)
+ return ERR;
+#else
+ if (!interact && (cmd = get_extended_line(&n, 0)) == NULL)
+ return ERR;
+#endif
+ clear_undo_stack();
+ while ((lp = next_active_node()) != NULL) {
+ if ((current_addr = get_line_node_addr(lp)) < 0)
+ return ERR;
+ if (interact) {
+ /* print current_addr; get a command in global syntax */
+ if (display_lines(current_addr, current_addr, gflag) < 0)
+ return ERR;
+ while ((n = get_tty_line()) > 0 &&
+ ibuf[n - 1] != '\n')
+ clearerr(stdin);
+ if (n < 0)
+ return ERR;
+ else if (n == 0) {
+ errmsg = "unexpected end-of-file";
+ return ERR;
+ } else if (n == 1 && !strcmp(ibuf, "\n"))
+ continue;
+ else if (n == 2 && !strcmp(ibuf, "&\n")) {
+ if (cmd == NULL) {
+ errmsg = "no previous command";
+ return ERR;
+ } else cmd = ocmd;
+ } else if ((cmd = get_extended_line(&n, 0)) == NULL)
+ return ERR;
+ else {
+ REALLOC(ocmd, ocmdsz, n + 1, ERR);
+ memcpy(ocmd, cmd, n + 1);
+ cmd = ocmd;
+ }
+
+ }
+ ibufp = cmd;
+ for (; *ibufp;)
+ if ((status = extract_addr_range()) < 0 ||
+ (status = exec_command()) < 0 ||
+ (status > 0 && (status = display_lines(
+ current_addr, current_addr, status)) < 0))
+ return status;
+ }
+ return 0;
+}
+
+
+static line_t **active_list; /* list of lines active in a global command */
+static long active_last; /* index of last active line in active_list */
+static long active_size; /* size of active_list */
+static long active_ptr; /* active_list index (non-decreasing) */
+static long active_ndx; /* active_list index (modulo active_last) */
+
+/* set_active_node: add a line node to the global-active list */
+int
+set_active_node(line_t *lp)
+{
+ if (active_last + 1 > active_size) {
+ size_t ti = active_size;
+ line_t **ts;
+ SPL1();
+#if defined(sun) || defined(NO_REALLOC_NULL)
+ if (active_list != NULL) {
+#endif
+ if ((ts = (line_t **) realloc(active_list,
+ (ti += MINBUFSZ) * sizeof(line_t *))) == NULL) {
+ fprintf(stderr, "%s\n", strerror(errno));
+ errmsg = "out of memory";
+ SPL0();
+ return ERR;
+ }
+#if defined(sun) || defined(NO_REALLOC_NULL)
+ } else {
+ if ((ts = (line_t **) malloc((ti += MINBUFSZ) *
+ sizeof(line_t **))) == NULL) {
+ fprintf(stderr, "%s\n", strerror(errno));
+ errmsg = "out of memory";
+ SPL0();
+ return ERR;
+ }
+ }
+#endif
+ active_size = ti;
+ active_list = ts;
+ SPL0();
+ }
+ active_list[active_last++] = lp;
+ return 0;
+}
+
+
+/* unset_active_nodes: remove a range of lines from the global-active list */
+void
+unset_active_nodes(line_t *np, line_t *mp)
+{
+ line_t *lp;
+ long i;
+
+ for (lp = np; lp != mp; lp = lp->q_forw)
+ for (i = 0; i < active_last; i++)
+ if (active_list[active_ndx] == lp) {
+ active_list[active_ndx] = NULL;
+ active_ndx = INC_MOD(active_ndx, active_last - 1);
+ break;
+ } else active_ndx = INC_MOD(active_ndx, active_last - 1);
+}
+
+
+/* next_active_node: return the next global-active line node */
+line_t *
+next_active_node(void)
+{
+ while (active_ptr < active_last && active_list[active_ptr] == NULL)
+ active_ptr++;
+ return (active_ptr < active_last) ? active_list[active_ptr++] : NULL;
+}
+
+
+/* clear_active_list: clear the global-active list */
+void
+clear_active_list(void)
+{
+ SPL1();
+ active_size = active_last = active_ptr = active_ndx = 0;
+ free(active_list);
+ active_list = NULL;
+ SPL0();
+}
diff --git a/corebinutils/ed/io.c b/corebinutils/ed/io.c
new file mode 100644
index 0000000000..cbef3b3d03
--- /dev/null
+++ b/corebinutils/ed/io.c
@@ -0,0 +1,345 @@
+/* io.c: This file contains the i/o routines for the ed line editor */
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 1993 Andrew Moore, Talke Studio.
+ * Copyright (c) 2026 Project Tick.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include "ed.h"
+
+/* read_file: read a named file/pipe into the buffer; return line count */
+long
+read_file(char *fn, long n)
+{
+ FILE *fp;
+ long size;
+ int cs;
+
+ fp = (*fn == '!') ? popen(fn + 1, "r") : fopen(strip_escapes(fn), "r");
+ if (fp == NULL) {
+ fprintf(stderr, "%s: %s\n", fn, strerror(errno));
+ errmsg = "cannot open input file";
+ return ERR;
+ }
+ if ((size = read_stream(fp, n)) < 0) {
+ fprintf(stderr, "%s: %s\n", fn, strerror(errno));
+ errmsg = "error reading input file";
+ }
+ if ((cs = (*fn == '!') ? pclose(fp) : fclose(fp)) < 0) {
+ fprintf(stderr, "%s: %s\n", fn, strerror(errno));
+ errmsg = "cannot close input file";
+ }
+ if (size < 0 || cs < 0)
+ return ERR;
+ if (!scripted)
+ fprintf(stdout, "%lu\n", size);
+ return current_addr - n;
+}
+
+static char *sbuf; /* file i/o buffer */
+static int sbufsz; /* file i/o buffer size */
+int newline_added; /* if set, newline appended to input file */
+
+/* read_stream: read a stream into the editor buffer; return status */
+long
+read_stream(FILE *fp, long n)
+{
+ line_t *lp = get_addressed_line_node(n);
+ undo_t *up = NULL;
+ unsigned long size = 0;
+ int o_newline_added = newline_added;
+ int o_isbinary = isbinary;
+ int appended = (n == addr_last);
+ int len;
+
+ isbinary = newline_added = 0;
+ for (current_addr = n; (len = get_stream_line(fp)) > 0; size += len) {
+ SPL1();
+ if (put_sbuf_line(sbuf) == NULL) {
+ SPL0();
+ return ERR;
+ }
+ lp = lp->q_forw;
+ if (up)
+ up->t = lp;
+ else if ((up = push_undo_stack(UADD, current_addr,
+ current_addr)) == NULL) {
+ SPL0();
+ return ERR;
+ }
+ SPL0();
+ }
+ if (len < 0)
+ return ERR;
+ if (appended && size && o_isbinary && o_newline_added)
+ fputs("newline inserted\n", stderr);
+ else if (newline_added && (!appended || (!isbinary && !o_isbinary)))
+ fputs("newline appended\n", stderr);
+ if (isbinary && newline_added && !appended)
+ size += 1;
+ if (!size)
+ newline_added = 1;
+ newline_added = appended ? newline_added : o_newline_added;
+ isbinary = isbinary | o_isbinary;
+ return size;
+}
+
+
+/* get_stream_line: read a line of text from a stream; return line length */
+int
+get_stream_line(FILE *fp)
+{
+ int c;
+ int i = 0;
+
+ while (((c = getc(fp)) != EOF || (!feof(fp) && !ferror(fp))) &&
+ c != '\n') {
+ REALLOC(sbuf, sbufsz, i + 1, ERR);
+ if (!(sbuf[i++] = c))
+ isbinary = 1;
+ }
+ REALLOC(sbuf, sbufsz, i + 2, ERR);
+ if (c == '\n')
+ sbuf[i++] = c;
+ else if (ferror(fp)) {
+ fprintf(stderr, "%s\n", strerror(errno));
+ errmsg = "cannot read input file";
+ return ERR;
+ } else if (i) {
+ sbuf[i++] = '\n';
+ newline_added = 1;
+ }
+ sbuf[i] = '\0';
+ return (isbinary && newline_added && i) ? --i : i;
+}
+
+
+/* write_file: write a range of lines to a named file/pipe; return line count */
+long
+write_file(char *fn, const char *mode, long n, long m)
+{
+ FILE *fp;
+ long size;
+ int cs;
+
+ fp = (*fn == '!') ? popen(fn+1, "w") : fopen(strip_escapes(fn), mode);
+ if (fp == NULL) {
+ fprintf(stderr, "%s: %s\n", fn, strerror(errno));
+ errmsg = "cannot open output file";
+ return ERR;
+ }
+ if ((size = write_stream(fp, n, m)) < 0) {
+ fprintf(stderr, "%s: %s\n", fn, strerror(errno));
+ errmsg = "error writing output file";
+ }
+ if ((cs = (*fn == '!') ? pclose(fp) : fclose(fp)) < 0) {
+ fprintf(stderr, "%s: %s\n", fn, strerror(errno));
+ errmsg = "cannot close output file";
+ }
+ if (size < 0 || cs < 0)
+ return ERR;
+ if (!scripted)
+ fprintf(stdout, "%lu\n", size);
+ return n ? m - n + 1 : 0;
+}
+
+
+/* write_stream: write a range of lines to a stream; return status */
+long
+write_stream(FILE *fp, long n, long m)
+{
+ line_t *lp = get_addressed_line_node(n);
+ unsigned long size = 0;
+ char *s;
+ int len;
+
+ for (; n && n <= m; n++, lp = lp->q_forw) {
+ if ((s = get_sbuf_line(lp)) == NULL)
+ return ERR;
+ len = lp->len;
+ if (n != addr_last || !isbinary || !newline_added)
+ s[len++] = '\n';
+ if (put_stream_line(fp, s, len) < 0)
+ return ERR;
+ size += len;
+ }
+ return size;
+}
+
+
+/* put_stream_line: write a line of text to a stream; return status */
+int
+put_stream_line(FILE *fp, const char *s, int len)
+{
+ while (len--)
+ if (fputc(*s++, fp) < 0) {
+ fprintf(stderr, "%s\n", strerror(errno));
+ errmsg = "cannot write file";
+ return ERR;
+ }
+ return 0;
+}
+
+/* get_extended_line: get an extended line from stdin */
+char *
+get_extended_line(int *sizep, int nonl)
+{
+ static char *cvbuf = NULL; /* buffer */
+ static int cvbufsz = 0; /* buffer size */
+
+ int l, n;
+ char *t = ibufp;
+
+ while (*t++ != '\n')
+ ;
+ if ((l = t - ibufp) < 2 || !has_trailing_escape(ibufp, ibufp + l - 1)) {
+ *sizep = l;
+ return ibufp;
+ }
+ *sizep = -1;
+ REALLOC(cvbuf, cvbufsz, l, NULL);
+ memcpy(cvbuf, ibufp, l);
+ *(cvbuf + --l - 1) = '\n'; /* strip trailing esc */
+ if (nonl) l--; /* strip newline */
+ for (;;) {
+ if ((n = get_tty_line()) < 0)
+ return NULL;
+ else if (n == 0 || ibuf[n - 1] != '\n') {
+ errmsg = "unexpected end-of-file";
+ return NULL;
+ }
+ REALLOC(cvbuf, cvbufsz, l + n, NULL);
+ memcpy(cvbuf + l, ibuf, n);
+ l += n;
+ if (n < 2 || !has_trailing_escape(cvbuf, cvbuf + l - 1))
+ break;
+ *(cvbuf + --l - 1) = '\n'; /* strip trailing esc */
+ if (nonl) l--; /* strip newline */
+ }
+ REALLOC(cvbuf, cvbufsz, l + 1, NULL);
+ cvbuf[l] = '\0';
+ *sizep = l;
+ return cvbuf;
+}
+
+
+/* get_tty_line: read a line of text from stdin; return line length */
+int
+get_tty_line(void)
+{
+ int oi = 0;
+ int i = 0;
+ int c;
+
+ for (;;)
+ switch (c = getchar()) {
+ default:
+ oi = 0;
+ REALLOC(ibuf, ibufsz, i + 2, ERR);
+ if (!(ibuf[i++] = c)) isbinary = 1;
+ if (c != '\n')
+ continue;
+ lineno++;
+ ibuf[i] = '\0';
+ ibufp = ibuf;
+ return i;
+ case EOF:
+ if (ferror(stdin)) {
+ fprintf(stderr, "stdin: %s\n", strerror(errno));
+ errmsg = "cannot read stdin";
+ clearerr(stdin);
+ ibufp = NULL;
+ return ERR;
+ } else {
+ clearerr(stdin);
+ if (i != oi) {
+ oi = i;
+ continue;
+ } else if (i)
+ ibuf[i] = '\0';
+ ibufp = ibuf;
+ return i;
+ }
+ }
+}
+
+
+
+#define ESCAPES "\a\b\f\n\r\t\v\\"
+#define ESCCHARS "abfnrtv\\"
+
+/* put_tty_line: print text to stdout */
+int
+put_tty_line(const char *s, int l, long n, int gflag)
+{
+ int col = 0;
+ int lc = 0;
+ char *cp;
+
+ if (gflag & GNP) {
+ printf("%ld\t", n);
+ col = 8;
+ }
+ for (; l--; s++) {
+ if ((gflag & GLS) && ++col > cols) {
+ fputs("\\\n", stdout);
+ col = 1;
+#ifndef BACKWARDS
+ if (!scripted && !isglobal && ++lc > rows) {
+ lc = 0;
+ fputs("Press <RETURN> to continue... ", stdout);
+ fflush(stdout);
+ if (get_tty_line() < 0)
+ return ERR;
+ }
+#endif
+ }
+ if (gflag & GLS) {
+ if (31 < *s && *s < 127 && *s != '\\')
+ putchar(*s);
+ else {
+ putchar('\\');
+ col++;
+ if (*s && (cp = strchr(ESCAPES, *s)) != NULL)
+ putchar(ESCCHARS[cp - ESCAPES]);
+ else {
+ putchar((((unsigned char) *s & 0300) >> 6) + '0');
+ putchar((((unsigned char) *s & 070) >> 3) + '0');
+ putchar(((unsigned char) *s & 07) + '0');
+ col += 2;
+ }
+ }
+
+ } else
+ putchar(*s);
+ }
+#ifndef BACKWARDS
+ if (gflag & GLS)
+ putchar('$');
+#endif
+ putchar('\n');
+ return 0;
+}
diff --git a/corebinutils/ed/main.c b/corebinutils/ed/main.c
new file mode 100644
index 0000000000..2994b7d938
--- /dev/null
+++ b/corebinutils/ed/main.c
@@ -0,0 +1,1421 @@
+/* main.c: This file contains the main control and user-interface routines
+ for the ed line editor. */
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 1993 Andrew Moore, Talke Studio.
+ * Copyright (c) 2026 Project Tick.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/*
+ * CREDITS
+ *
+ * This program is based on the editor algorithm described in
+ * Brian W. Kernighan and P. J. Plauger's book "Software Tools
+ * in Pascal," Addison-Wesley, 1981.
+ *
+ * The buffering algorithm is attributed to Rodney Ruddock of
+ * the University of Guelph, Guelph, Ontario.
+ *
+ */
+
+#include <sys/types.h>
+
+#include <sys/ioctl.h>
+#include <sys/wait.h>
+#include <ctype.h>
+#include <locale.h>
+#include <pwd.h>
+#include <setjmp.h>
+
+#include "ed.h"
+
+
+#ifdef _POSIX_SOURCE
+static sigjmp_buf env;
+#else
+static jmp_buf env;
+#endif
+
+/* static buffers */
+static char *shcmd; /* shell command buffer */
+static int shcmdsz; /* shell command buffer size */
+static int shcmdi; /* shell command buffer index */
+char *ibuf; /* ed command-line buffer */
+int ibufsz; /* ed command-line buffer size */
+char *ibufp; /* pointer to ed command-line buffer */
+
+/* global flags */
+static int garrulous = 0; /* if set, print all error messages */
+int isbinary; /* if set, buffer contains ASCII NULs */
+int isglobal; /* if set, doing a global command */
+int modified; /* if set, buffer modified since last write */
+int mutex = 0; /* if set, signals set "sigflags" */
+static int red = 0; /* if set, restrict shell/directory access */
+int scripted = 0; /* if set, suppress diagnostics */
+int sigflags = 0; /* if set, signals received while mutex set */
+static int sigactive = 0; /* if set, signal handlers are enabled */
+
+static char old_filename[PATH_MAX] = ""; /* default filename */
+long current_addr; /* current address in editor buffer */
+long addr_last; /* last address in editor buffer */
+int lineno; /* script line number */
+static const char *prompt; /* command-line prompt */
+static const char *dps = "*"; /* default command-line prompt */
+
+static const char *usage = "usage: %s [-] [-sx] [-p string] [file]\n";
+
+/* ed: line editor */
+int
+main(volatile int argc, char ** volatile argv)
+{
+ int c, n;
+ long status = 0;
+
+ (void)setlocale(LC_ALL, "");
+
+ red = (n = strlen(argv[0])) > 2 && argv[0][n - 3] == 'r';
+top:
+ while ((c = getopt(argc, argv, "p:sx")) != -1)
+ switch(c) {
+ case 'p': /* set prompt */
+ prompt = optarg;
+ break;
+ case 's': /* run script */
+ scripted = 1;
+ break;
+ case 'x': /* use crypt */
+ fprintf(stderr,
+ "%s: option -x is not supported on Linux "
+ "(crypt unavailable)\n", red ? "red" : "ed");
+ exit(1);
+
+ default:
+ fprintf(stderr, usage, red ? "red" : "ed");
+ exit(1);
+ }
+ argv += optind;
+ argc -= optind;
+ if (argc && **argv == '-') {
+ scripted = 1;
+ if (argc > 1) {
+ optind = 1;
+ goto top;
+ }
+ argv++;
+ argc--;
+ }
+ /* assert: reliable signals! */
+#ifdef SIGWINCH
+ handle_winch(SIGWINCH);
+ if (isatty(0)) signal(SIGWINCH, handle_winch);
+#endif
+ signal(SIGHUP, signal_hup);
+ signal(SIGQUIT, SIG_IGN);
+ signal(SIGINT, signal_int);
+#ifdef _POSIX_SOURCE
+ if ((status = sigsetjmp(env, 1)))
+#else
+ if ((status = setjmp(env)))
+#endif
+ {
+ fputs("\n?\n", stderr);
+ errmsg = "interrupt";
+ } else {
+ init_buffers();
+ sigactive = 1; /* enable signal handlers */
+ if (argc && **argv && is_legal_filename(*argv)) {
+ if (read_file(*argv, 0) < 0 && !isatty(0))
+ quit(2);
+ else if (**argv != '!')
+ if (strlcpy(old_filename, *argv, sizeof(old_filename))
+ >= sizeof(old_filename))
+ quit(2);
+ } else if (argc) {
+ fputs("?\n", stderr);
+ if (**argv == '\0')
+ errmsg = "invalid filename";
+ if (!isatty(0))
+ quit(2);
+ }
+ }
+ for (;;) {
+ if (status < 0 && garrulous)
+ fprintf(stderr, "%s\n", errmsg);
+ if (prompt) {
+ printf("%s", prompt);
+ fflush(stdout);
+ }
+ if ((n = get_tty_line()) < 0) {
+ status = ERR;
+ continue;
+ } else if (n == 0) {
+ if (modified && !scripted) {
+ fputs("?\n", stderr);
+ errmsg = "warning: file modified";
+ if (!isatty(0)) {
+ if (garrulous)
+ fprintf(stderr,
+ "script, line %d: %s\n",
+ lineno, errmsg);
+ quit(2);
+ }
+ clearerr(stdin);
+ modified = 0;
+ status = EMOD;
+ continue;
+ } else
+ quit(0);
+ } else if (ibuf[n - 1] != '\n') {
+ /* discard line */
+ errmsg = "unexpected end-of-file";
+ clearerr(stdin);
+ status = ERR;
+ continue;
+ }
+ isglobal = 0;
+ if ((status = extract_addr_range()) >= 0 &&
+ (status = exec_command()) >= 0)
+ if (!status ||
+ (status = display_lines(current_addr, current_addr,
+ status)) >= 0)
+ continue;
+ switch (status) {
+ case EOF:
+ quit(0);
+ case EMOD:
+ modified = 0;
+ fputs("?\n", stderr); /* give warning */
+ errmsg = "warning: file modified";
+ if (!isatty(0)) {
+ if (garrulous)
+ fprintf(stderr, "script, line %d: %s\n",
+ lineno, errmsg);
+ quit(2);
+ }
+ break;
+ case FATAL:
+ if (!isatty(0)) {
+ if (garrulous)
+ fprintf(stderr, "script, line %d: %s\n",
+ lineno, errmsg);
+ } else if (garrulous)
+ fprintf(stderr, "%s\n", errmsg);
+ quit(3);
+ default:
+ fputs("?\n", stderr);
+ if (!isatty(0)) {
+ if (garrulous)
+ fprintf(stderr, "script, line %d: %s\n",
+ lineno, errmsg);
+ quit(2);
+ }
+ break;
+ }
+ }
+ /*NOTREACHED*/
+}
+
+long first_addr, second_addr;
+static long addr_cnt;
+
+/* extract_addr_range: get line addresses from the command buffer until an
+ illegal address is seen; return status */
+int
+extract_addr_range(void)
+{
+ long addr;
+
+ addr_cnt = 0;
+ first_addr = second_addr = current_addr;
+ while ((addr = next_addr()) >= 0) {
+ addr_cnt++;
+ first_addr = second_addr;
+ second_addr = addr;
+ if (*ibufp != ',' && *ibufp != ';')
+ break;
+ else if (*ibufp++ == ';')
+ current_addr = addr;
+ }
+ if ((addr_cnt = min(addr_cnt, 2)) == 1 || second_addr != addr)
+ first_addr = second_addr;
+ return (addr == ERR) ? ERR : 0;
+}
+
+
+#define SKIP_BLANKS() while (isspace((unsigned char)*ibufp) && *ibufp != '\n') ibufp++
+
+#define MUST_BE_FIRST() do { \
+ if (!first) { \
+ errmsg = "invalid address"; \
+ return ERR; \
+ } \
+} while (0)
+
+/* next_addr: return the next line address in the command buffer */
+long
+next_addr(void)
+{
+ const char *hd;
+ long addr = current_addr;
+ long n;
+ int first = 1;
+ int c;
+
+ SKIP_BLANKS();
+ for (hd = ibufp;; first = 0)
+ switch (c = *ibufp) {
+ case '+':
+ case '\t':
+ case ' ':
+ case '-':
+ case '^':
+ ibufp++;
+ SKIP_BLANKS();
+ if (isdigit((unsigned char)*ibufp)) {
+ STRTOL(n, ibufp);
+ addr += (c == '-' || c == '^') ? -n : n;
+ } else if (!isspace((unsigned char)c))
+ addr += (c == '-' || c == '^') ? -1 : 1;
+ break;
+ case '0': case '1': case '2':
+ case '3': case '4': case '5':
+ case '6': case '7': case '8': case '9':
+ MUST_BE_FIRST();
+ STRTOL(addr, ibufp);
+ break;
+ case '.':
+ case '$':
+ MUST_BE_FIRST();
+ ibufp++;
+ addr = (c == '.') ? current_addr : addr_last;
+ break;
+ case '/':
+ case '?':
+ MUST_BE_FIRST();
+ if ((addr = get_matching_node_addr(
+ get_compiled_pattern(), c == '/')) < 0)
+ return ERR;
+ else if (c == *ibufp)
+ ibufp++;
+ break;
+ case '\'':
+ MUST_BE_FIRST();
+ ibufp++;
+ if ((addr = get_marked_node_addr(*ibufp++)) < 0)
+ return ERR;
+ break;
+ case '%':
+ case ',':
+ case ';':
+ if (first) {
+ ibufp++;
+ addr_cnt++;
+ second_addr = (c == ';') ? current_addr : 1;
+ if ((addr = next_addr()) < 0)
+ addr = addr_last;
+ break;
+ }
+ /* FALLTHROUGH */
+ default:
+ if (ibufp == hd)
+ return EOF;
+ else if (addr < 0 || addr_last < addr) {
+ errmsg = "invalid address";
+ return ERR;
+ } else
+ return addr;
+ }
+ /* NOTREACHED */
+}
+
+
+#ifdef BACKWARDS
+/* GET_THIRD_ADDR: get a legal address from the command buffer */
+#define GET_THIRD_ADDR(addr) \
+{ \
+ long ol1, ol2; \
+\
+ ol1 = first_addr, ol2 = second_addr; \
+ if (extract_addr_range() < 0) \
+ return ERR; \
+ else if (addr_cnt == 0) { \
+ errmsg = "destination expected"; \
+ return ERR; \
+ } else if (second_addr < 0 || addr_last < second_addr) { \
+ errmsg = "invalid address"; \
+ return ERR; \
+ } \
+ addr = second_addr; \
+ first_addr = ol1, second_addr = ol2; \
+}
+#else /* BACKWARDS */
+/* GET_THIRD_ADDR: get a legal address from the command buffer */
+#define GET_THIRD_ADDR(addr) \
+{ \
+ long ol1, ol2; \
+\
+ ol1 = first_addr, ol2 = second_addr; \
+ if (extract_addr_range() < 0) \
+ return ERR; \
+ if (second_addr < 0 || addr_last < second_addr) { \
+ errmsg = "invalid address"; \
+ return ERR; \
+ } \
+ addr = second_addr; \
+ first_addr = ol1, second_addr = ol2; \
+}
+#endif
+
+
+/* GET_COMMAND_SUFFIX: verify the command suffix in the command buffer */
+#define GET_COMMAND_SUFFIX() { \
+ int done = 0; \
+ do { \
+ switch(*ibufp) { \
+ case 'p': \
+ gflag |= GPR, ibufp++; \
+ break; \
+ case 'l': \
+ gflag |= GLS, ibufp++; \
+ break; \
+ case 'n': \
+ gflag |= GNP, ibufp++; \
+ break; \
+ default: \
+ done++; \
+ } \
+ } while (!done); \
+ if (*ibufp++ != '\n') { \
+ errmsg = "invalid command suffix"; \
+ return ERR; \
+ } \
+}
+
+
+/* sflags */
+#define SGG 001 /* complement previous global substitute suffix */
+#define SGP 002 /* complement previous print suffix */
+#define SGR 004 /* use last regex instead of last pat */
+#define SGF 010 /* repeat last substitution */
+
+int patlock = 0; /* if set, pattern not freed by get_compiled_pattern() */
+
+long rows = 22; /* scroll length: ws_row - 2 */
+
+static int
+require_addr_count(long max)
+{
+ if (addr_cnt > max) {
+ errmsg = "unexpected address";
+ return ERR;
+ }
+ return 0;
+}
+
+/* exec_command: execute the next command in command buffer; return print
+ request, if any */
+int
+exec_command(void)
+{
+ static pattern_t *pat = NULL;
+ static int sgflag = 0;
+ static long sgnum = 0;
+
+ pattern_t *tpat;
+ char *fnp;
+ int gflag = 0;
+ int sflags = 0;
+ long addr = 0;
+ int n = 0;
+ int c;
+
+ SKIP_BLANKS();
+ switch(c = *ibufp++) {
+ case 'a':
+ if (require_addr_count(1) < 0)
+ return ERR;
+ GET_COMMAND_SUFFIX();
+ if (!isglobal) clear_undo_stack();
+ if (append_lines(second_addr) < 0)
+ return ERR;
+ break;
+ case 'c':
+ if (check_addr_range(current_addr, current_addr) < 0)
+ return ERR;
+ GET_COMMAND_SUFFIX();
+ if (!isglobal) clear_undo_stack();
+ if (delete_lines(first_addr, second_addr) < 0 ||
+ append_lines(current_addr) < 0)
+ return ERR;
+ break;
+ case 'd':
+ if (check_addr_range(current_addr, current_addr) < 0)
+ return ERR;
+ GET_COMMAND_SUFFIX();
+ if (!isglobal) clear_undo_stack();
+ if (delete_lines(first_addr, second_addr) < 0)
+ return ERR;
+ else if ((addr = INC_MOD(current_addr, addr_last)) != 0)
+ current_addr = addr;
+ break;
+ case 'e':
+ if (modified && !scripted)
+ return EMOD;
+ /* FALLTHROUGH */
+ case 'E':
+ if (addr_cnt > 0) {
+ errmsg = "unexpected address";
+ return ERR;
+ } else if (!isspace((unsigned char)*ibufp)) {
+ errmsg = "unexpected command suffix";
+ return ERR;
+ } else if ((fnp = get_filename()) == NULL)
+ return ERR;
+ GET_COMMAND_SUFFIX();
+ if (delete_lines(1, addr_last) < 0)
+ return ERR;
+ clear_undo_stack();
+ if (close_sbuf() < 0)
+ return ERR;
+ else if (open_sbuf() < 0)
+ return FATAL;
+ if (*fnp && *fnp != '!')
+ strlcpy(old_filename, fnp, PATH_MAX);
+#ifdef BACKWARDS
+ if (*fnp == '\0' && *old_filename == '\0') {
+ errmsg = "no current filename";
+ return ERR;
+ }
+#endif
+ if (read_file(*fnp ? fnp : old_filename, 0) < 0)
+ return ERR;
+ clear_undo_stack();
+ modified = 0;
+ u_current_addr = u_addr_last = -1;
+ break;
+ case 'f':
+ if (addr_cnt > 0) {
+ errmsg = "unexpected address";
+ return ERR;
+ } else if (!isspace((unsigned char)*ibufp)) {
+ errmsg = "unexpected command suffix";
+ return ERR;
+ } else if ((fnp = get_filename()) == NULL)
+ return ERR;
+ else if (*fnp == '!') {
+ errmsg = "invalid redirection";
+ return ERR;
+ }
+ GET_COMMAND_SUFFIX();
+ if (*fnp)
+ strlcpy(old_filename, fnp, PATH_MAX);
+ printf("%s\n", strip_escapes(old_filename));
+ break;
+ case 'g':
+ case 'v':
+ case 'G':
+ case 'V':
+ if (isglobal) {
+ errmsg = "cannot nest global commands";
+ return ERR;
+ } else if (check_addr_range(1, addr_last) < 0)
+ return ERR;
+ else if (build_active_list(c == 'g' || c == 'G') < 0)
+ return ERR;
+ else if ((n = (c == 'G' || c == 'V')))
+ GET_COMMAND_SUFFIX();
+ isglobal++;
+ if (exec_global(n, gflag) < 0)
+ return ERR;
+ break;
+ case 'h':
+ if (addr_cnt > 0) {
+ errmsg = "unexpected address";
+ return ERR;
+ }
+ GET_COMMAND_SUFFIX();
+ if (*errmsg) fprintf(stderr, "%s\n", errmsg);
+ break;
+ case 'H':
+ if (addr_cnt > 0) {
+ errmsg = "unexpected address";
+ return ERR;
+ }
+ GET_COMMAND_SUFFIX();
+ if ((garrulous = 1 - garrulous) && *errmsg)
+ fprintf(stderr, "%s\n", errmsg);
+ break;
+ case 'i':
+ if (require_addr_count(1) < 0)
+ return ERR;
+ if (second_addr == 0) {
+ errmsg = "invalid address";
+ return ERR;
+ }
+ GET_COMMAND_SUFFIX();
+ if (!isglobal) clear_undo_stack();
+ if (append_lines(second_addr - 1) < 0)
+ return ERR;
+ break;
+ case 'j':
+ if (check_addr_range(current_addr, current_addr + 1) < 0)
+ return ERR;
+ GET_COMMAND_SUFFIX();
+ if (!isglobal) clear_undo_stack();
+ if (first_addr != second_addr &&
+ join_lines(first_addr, second_addr) < 0)
+ return ERR;
+ break;
+ case 'k':
+ if (require_addr_count(1) < 0)
+ return ERR;
+ c = *ibufp++;
+ if (second_addr == 0) {
+ errmsg = "invalid address";
+ return ERR;
+ }
+ GET_COMMAND_SUFFIX();
+ if (mark_line_node(get_addressed_line_node(second_addr), c) < 0)
+ return ERR;
+ break;
+ case 'l':
+ if (check_addr_range(current_addr, current_addr) < 0)
+ return ERR;
+ GET_COMMAND_SUFFIX();
+ if (display_lines(first_addr, second_addr, gflag | GLS) < 0)
+ return ERR;
+ gflag = 0;
+ break;
+ case 'm':
+ if (check_addr_range(current_addr, current_addr) < 0)
+ return ERR;
+ GET_THIRD_ADDR(addr);
+ if (first_addr <= addr && addr < second_addr) {
+ errmsg = "invalid destination";
+ return ERR;
+ }
+ GET_COMMAND_SUFFIX();
+ if (!isglobal) clear_undo_stack();
+ if (move_lines(addr) < 0)
+ return ERR;
+ break;
+ case 'n':
+ if (check_addr_range(current_addr, current_addr) < 0)
+ return ERR;
+ GET_COMMAND_SUFFIX();
+ if (display_lines(first_addr, second_addr, gflag | GNP) < 0)
+ return ERR;
+ gflag = 0;
+ break;
+ case 'p':
+ if (check_addr_range(current_addr, current_addr) < 0)
+ return ERR;
+ GET_COMMAND_SUFFIX();
+ if (display_lines(first_addr, second_addr, gflag | GPR) < 0)
+ return ERR;
+ gflag = 0;
+ break;
+ case 'P':
+ if (addr_cnt > 0) {
+ errmsg = "unexpected address";
+ return ERR;
+ }
+ GET_COMMAND_SUFFIX();
+ prompt = prompt ? NULL : optarg ? optarg : dps;
+ break;
+ case 'q':
+ case 'Q':
+ if (addr_cnt > 0) {
+ errmsg = "unexpected address";
+ return ERR;
+ }
+ GET_COMMAND_SUFFIX();
+ gflag = (modified && !scripted && c == 'q') ? EMOD : EOF;
+ break;
+ case 'r':
+ if (require_addr_count(1) < 0)
+ return ERR;
+ if (!isspace((unsigned char)*ibufp)) {
+ errmsg = "unexpected command suffix";
+ return ERR;
+ } else if (addr_cnt == 0)
+ second_addr = addr_last;
+ if ((fnp = get_filename()) == NULL)
+ return ERR;
+ GET_COMMAND_SUFFIX();
+ if (!isglobal) clear_undo_stack();
+ if (*old_filename == '\0' && *fnp != '!')
+ strlcpy(old_filename, fnp, PATH_MAX);
+#ifdef BACKWARDS
+ if (*fnp == '\0' && *old_filename == '\0') {
+ errmsg = "no current filename";
+ return ERR;
+ }
+#endif
+ if ((addr = read_file(*fnp ? fnp : old_filename, second_addr)) < 0)
+ return ERR;
+ else if (addr && addr != addr_last)
+ modified = 1;
+ break;
+ case 's':
+ do {
+ switch(*ibufp) {
+ case '\n':
+ sflags |=SGF;
+ break;
+ case 'g':
+ sflags |= SGG;
+ ibufp++;
+ break;
+ case 'p':
+ sflags |= SGP;
+ ibufp++;
+ break;
+ case 'r':
+ sflags |= SGR;
+ ibufp++;
+ break;
+ case '0': case '1': case '2': case '3': case '4':
+ case '5': case '6': case '7': case '8': case '9':
+ STRTOL(sgnum, ibufp);
+ sflags |= SGF;
+ sgflag &= ~GSG; /* override GSG */
+ break;
+ default:
+ if (sflags) {
+ errmsg = "invalid command suffix";
+ return ERR;
+ }
+ }
+ } while (sflags && *ibufp != '\n');
+ if (sflags && !pat) {
+ errmsg = "no previous substitution";
+ return ERR;
+ } else if (sflags & SGG)
+ sgnum = 0; /* override numeric arg */
+ if (*ibufp != '\n' && *(ibufp + 1) == '\n') {
+ errmsg = "invalid pattern delimiter";
+ return ERR;
+ }
+ tpat = pat;
+ SPL1();
+ if ((!sflags || (sflags & SGR)) &&
+ (tpat = get_compiled_pattern()) == NULL) {
+ SPL0();
+ return ERR;
+ } else if (tpat != pat) {
+ if (pat) {
+ regfree(pat);
+ free(pat);
+ }
+ pat = tpat;
+ patlock = 1; /* reserve pattern */
+ }
+ SPL0();
+ if (!sflags && extract_subst_tail(&sgflag, &sgnum) < 0)
+ return ERR;
+ else if (isglobal)
+ sgflag |= GLB;
+ else
+ sgflag &= ~GLB;
+ if (sflags & SGG)
+ sgflag ^= GSG;
+ if (sflags & SGP)
+ sgflag ^= GPR, sgflag &= ~(GLS | GNP);
+ do {
+ switch(*ibufp) {
+ case 'p':
+ sgflag |= GPR, ibufp++;
+ break;
+ case 'l':
+ sgflag |= GLS, ibufp++;
+ break;
+ case 'n':
+ sgflag |= GNP, ibufp++;
+ break;
+ default:
+ n++;
+ }
+ } while (!n);
+ if (check_addr_range(current_addr, current_addr) < 0)
+ return ERR;
+ GET_COMMAND_SUFFIX();
+ if (!isglobal) clear_undo_stack();
+ if (search_and_replace(pat, sgflag, sgnum) < 0)
+ return ERR;
+ break;
+ case 't':
+ if (check_addr_range(current_addr, current_addr) < 0)
+ return ERR;
+ GET_THIRD_ADDR(addr);
+ GET_COMMAND_SUFFIX();
+ if (!isglobal) clear_undo_stack();
+ if (copy_lines(addr) < 0)
+ return ERR;
+ break;
+ case 'u':
+ if (addr_cnt > 0) {
+ errmsg = "unexpected address";
+ return ERR;
+ }
+ GET_COMMAND_SUFFIX();
+ if (pop_undo_stack() < 0)
+ return ERR;
+ break;
+ case 'w':
+ case 'W':
+ if ((n = *ibufp) == 'q' || n == 'Q') {
+ gflag = EOF;
+ ibufp++;
+ }
+ if (!isspace((unsigned char)*ibufp)) {
+ errmsg = "unexpected command suffix";
+ return ERR;
+ } else if ((fnp = get_filename()) == NULL)
+ return ERR;
+ if (addr_cnt == 0 && !addr_last)
+ first_addr = second_addr = 0;
+ else if (check_addr_range(1, addr_last) < 0)
+ return ERR;
+ GET_COMMAND_SUFFIX();
+ if (*old_filename == '\0' && *fnp != '!')
+ strlcpy(old_filename, fnp, PATH_MAX);
+#ifdef BACKWARDS
+ if (*fnp == '\0' && *old_filename == '\0') {
+ errmsg = "no current filename";
+ return ERR;
+ }
+#endif
+ if ((addr = write_file(*fnp ? fnp : old_filename,
+ (c == 'W') ? "a" : "w", first_addr, second_addr)) < 0)
+ return ERR;
+ else if (addr == addr_last && *fnp != '!')
+ modified = 0;
+ else if (modified && !scripted && n == 'q')
+ gflag = EMOD;
+ break;
+ case 'x':
+ if (addr_cnt > 0) {
+ errmsg = "unexpected address";
+ return ERR;
+ }
+ GET_COMMAND_SUFFIX();
+ errmsg = "crypt mode is not supported on Linux";
+ return ERR;
+ case 'z':
+ if (require_addr_count(1) < 0)
+ return ERR;
+#ifdef BACKWARDS
+ if (check_addr_range(first_addr = 1, current_addr + 1) < 0)
+#else
+ if (check_addr_range(first_addr = 1, current_addr + !isglobal) < 0)
+#endif
+ return ERR;
+ else if ('0' < *ibufp && *ibufp <= '9')
+ STRTOL(rows, ibufp);
+ GET_COMMAND_SUFFIX();
+ if (display_lines(second_addr, min(addr_last,
+ second_addr + rows), gflag) < 0)
+ return ERR;
+ gflag = 0;
+ break;
+ case '=':
+ if (require_addr_count(1) < 0)
+ return ERR;
+ GET_COMMAND_SUFFIX();
+ printf("%ld\n", addr_cnt ? second_addr : addr_last);
+ break;
+ case '!':
+ if (addr_cnt > 0) {
+ errmsg = "unexpected address";
+ return ERR;
+ } else if ((sflags = get_shell_command()) < 0)
+ return ERR;
+ GET_COMMAND_SUFFIX();
+ if (sflags) printf("%s\n", shcmd + 1);
+ system(shcmd + 1);
+ if (!scripted) printf("!\n");
+ break;
+ case '\n':
+#ifdef BACKWARDS
+ if (check_addr_range(first_addr = 1, current_addr + 1) < 0
+#else
+ if (check_addr_range(first_addr = 1, current_addr + !isglobal) < 0
+#endif
+ || display_lines(second_addr, second_addr, 0) < 0)
+ return ERR;
+ break;
+ default:
+ errmsg = "unknown command";
+ return ERR;
+ }
+ return gflag;
+}
+
+
+/* check_addr_range: return status of address range check */
+int
+check_addr_range(long n, long m)
+{
+ if (addr_cnt == 0) {
+ first_addr = n;
+ second_addr = m;
+ }
+ if (first_addr > second_addr || 1 > first_addr ||
+ second_addr > addr_last) {
+ errmsg = "invalid address";
+ return ERR;
+ }
+ return 0;
+}
+
+
+/* get_matching_node_addr: return the address of the next line matching a
+ pattern in a given direction. wrap around begin/end of editor buffer if
+ necessary */
+long
+get_matching_node_addr(pattern_t *pat, int dir)
+{
+ char *s;
+ long n = current_addr;
+ line_t *lp;
+
+ if (!pat) return ERR;
+ do {
+ if ((n = dir ? INC_MOD(n, addr_last) : DEC_MOD(n, addr_last))) {
+ lp = get_addressed_line_node(n);
+ if ((s = get_sbuf_line(lp)) == NULL)
+ return ERR;
+ if (isbinary)
+ NUL_TO_NEWLINE(s, lp->len);
+ if (!regexec(pat, s, 0, NULL, 0))
+ return n;
+ }
+ } while (n != current_addr);
+ errmsg = "no match";
+ return ERR;
+}
+
+
+/* get_filename: return pointer to copy of filename in the command buffer */
+char *
+get_filename(void)
+{
+ static char *file = NULL;
+ static int filesz = 0;
+
+ int n;
+
+ if (*ibufp != '\n') {
+ SKIP_BLANKS();
+ if (*ibufp == '\n') {
+ errmsg = "invalid filename";
+ return NULL;
+ } else if ((ibufp = get_extended_line(&n, 1)) == NULL)
+ return NULL;
+ else if (*ibufp == '!') {
+ ibufp++;
+ if ((n = get_shell_command()) < 0)
+ return NULL;
+ if (n)
+ printf("%s\n", shcmd + 1);
+ return shcmd;
+ } else if (n > PATH_MAX - 1) {
+ errmsg = "filename too long";
+ return NULL;
+ }
+ }
+#ifndef BACKWARDS
+ else if (*old_filename == '\0') {
+ errmsg = "no current filename";
+ return NULL;
+ }
+#endif
+ REALLOC(file, filesz, PATH_MAX, NULL);
+ for (n = 0; *ibufp != '\n';)
+ file[n++] = *ibufp++;
+ file[n] = '\0';
+ return is_legal_filename(file) ? file : NULL;
+}
+
+
+/* get_shell_command: read a shell command from stdin; return substitution
+ status */
+int
+get_shell_command(void)
+{
+ static char *buf = NULL;
+ static int n = 0;
+
+ char *s; /* substitution char pointer */
+ int i = 0;
+ int j = 0;
+
+ if (red) {
+ errmsg = "shell access restricted";
+ return ERR;
+ } else if ((s = ibufp = get_extended_line(&j, 1)) == NULL)
+ return ERR;
+ REALLOC(buf, n, j + 1, ERR);
+ buf[i++] = '!'; /* prefix command w/ bang */
+ while (*ibufp != '\n')
+ switch (*ibufp) {
+ default:
+ REALLOC(buf, n, i + 2, ERR);
+ buf[i++] = *ibufp;
+ if (*ibufp++ == '\\')
+ buf[i++] = *ibufp++;
+ break;
+ case '!':
+ if (s != ibufp) {
+ REALLOC(buf, n, i + 1, ERR);
+ buf[i++] = *ibufp++;
+ }
+#ifdef BACKWARDS
+ else if (shcmd == NULL || *(shcmd + 1) == '\0')
+#else
+ else if (shcmd == NULL)
+#endif
+ {
+ errmsg = "no previous command";
+ return ERR;
+ } else {
+ REALLOC(buf, n, i + shcmdi, ERR);
+ for (s = shcmd + 1; s < shcmd + shcmdi;)
+ buf[i++] = *s++;
+ s = ibufp++;
+ }
+ break;
+ case '%':
+ if (*old_filename == '\0') {
+ errmsg = "no current filename";
+ return ERR;
+ }
+ j = strlen(s = strip_escapes(old_filename));
+ REALLOC(buf, n, i + j, ERR);
+ while (j--)
+ buf[i++] = *s++;
+ s = ibufp++;
+ break;
+ }
+ REALLOC(shcmd, shcmdsz, i + 1, ERR);
+ memcpy(shcmd, buf, i);
+ shcmd[shcmdi = i] = '\0';
+ return *s == '!' || *s == '%';
+}
+
+
+/* append_lines: insert text from stdin to after line n; stop when either a
+ single period is read or EOF; return status */
+int
+append_lines(long n)
+{
+ int l;
+ const char *lp = ibuf;
+ const char *eot;
+ undo_t *up = NULL;
+
+ for (current_addr = n;;) {
+ if (!isglobal) {
+ if ((l = get_tty_line()) < 0)
+ return ERR;
+ else if (l == 0 || ibuf[l - 1] != '\n') {
+ clearerr(stdin);
+ return l ? EOF : 0;
+ }
+ lp = ibuf;
+ } else if (*(lp = ibufp) == '\0')
+ return 0;
+ else {
+ while (*ibufp++ != '\n')
+ ;
+ l = ibufp - lp;
+ }
+ if (l == 2 && lp[0] == '.' && lp[1] == '\n') {
+ return 0;
+ }
+ eot = lp + l;
+ SPL1();
+ do {
+ if ((lp = put_sbuf_line(lp)) == NULL) {
+ SPL0();
+ return ERR;
+ } else if (up)
+ up->t = get_addressed_line_node(current_addr);
+ else if ((up = push_undo_stack(UADD, current_addr,
+ current_addr)) == NULL) {
+ SPL0();
+ return ERR;
+ }
+ } while (lp != eot);
+ modified = 1;
+ SPL0();
+ }
+ /* NOTREACHED */
+}
+
+
+/* join_lines: replace a range of lines with the joined text of those lines */
+int
+join_lines(long from, long to)
+{
+ static char *buf = NULL;
+ static int n;
+
+ char *s;
+ int size = 0;
+ line_t *bp, *ep;
+
+ ep = get_addressed_line_node(INC_MOD(to, addr_last));
+ bp = get_addressed_line_node(from);
+ for (; bp != ep; bp = bp->q_forw) {
+ if ((s = get_sbuf_line(bp)) == NULL)
+ return ERR;
+ REALLOC(buf, n, size + bp->len, ERR);
+ memcpy(buf + size, s, bp->len);
+ size += bp->len;
+ }
+ REALLOC(buf, n, size + 2, ERR);
+ memcpy(buf + size, "\n", 2);
+ if (delete_lines(from, to) < 0)
+ return ERR;
+ current_addr = from - 1;
+ SPL1();
+ if (put_sbuf_line(buf) == NULL ||
+ push_undo_stack(UADD, current_addr, current_addr) == NULL) {
+ SPL0();
+ return ERR;
+ }
+ modified = 1;
+ SPL0();
+ return 0;
+}
+
+
+/* move_lines: move a range of lines */
+int
+move_lines(long addr)
+{
+ line_t *b1, *a1, *b2, *a2;
+ long n = INC_MOD(second_addr, addr_last);
+ long p = first_addr - 1;
+ int done = (addr == first_addr - 1 || addr == second_addr);
+
+ SPL1();
+ if (done) {
+ a2 = get_addressed_line_node(n);
+ b2 = get_addressed_line_node(p);
+ current_addr = second_addr;
+ } else if (push_undo_stack(UMOV, p, n) == NULL ||
+ push_undo_stack(UMOV, addr, INC_MOD(addr, addr_last)) == NULL) {
+ SPL0();
+ return ERR;
+ } else {
+ a1 = get_addressed_line_node(n);
+ if (addr < first_addr) {
+ b1 = get_addressed_line_node(p);
+ b2 = get_addressed_line_node(addr);
+ /* this get_addressed_line_node last! */
+ } else {
+ b2 = get_addressed_line_node(addr);
+ b1 = get_addressed_line_node(p);
+ /* this get_addressed_line_node last! */
+ }
+ a2 = b2->q_forw;
+ REQUE(b2, b1->q_forw);
+ REQUE(a1->q_back, a2);
+ REQUE(b1, a1);
+ current_addr = addr + ((addr < first_addr) ?
+ second_addr - first_addr + 1 : 0);
+ }
+ if (isglobal)
+ unset_active_nodes(b2->q_forw, a2);
+ modified = 1;
+ SPL0();
+ return 0;
+}
+
+
+/* copy_lines: copy a range of lines; return status */
+int
+copy_lines(long addr)
+{
+ line_t *lp, *np = get_addressed_line_node(first_addr);
+ undo_t *up = NULL;
+ long n = second_addr - first_addr + 1;
+ long m = 0;
+
+ current_addr = addr;
+ if (first_addr <= addr && addr < second_addr) {
+ n = addr - first_addr + 1;
+ m = second_addr - addr;
+ }
+ for (; n > 0; n=m, m=0, np = get_addressed_line_node(current_addr + 1))
+ for (; n-- > 0; np = np->q_forw) {
+ SPL1();
+ if ((lp = dup_line_node(np)) == NULL) {
+ SPL0();
+ return ERR;
+ }
+ add_line_node(lp);
+ if (up)
+ up->t = lp;
+ else if ((up = push_undo_stack(UADD, current_addr,
+ current_addr)) == NULL) {
+ SPL0();
+ return ERR;
+ }
+ modified = 1;
+ SPL0();
+ }
+ return 0;
+}
+
+
+/* delete_lines: delete a range of lines */
+int
+delete_lines(long from, long to)
+{
+ line_t *n, *p;
+
+ SPL1();
+ if (push_undo_stack(UDEL, from, to) == NULL) {
+ SPL0();
+ return ERR;
+ }
+ n = get_addressed_line_node(INC_MOD(to, addr_last));
+ p = get_addressed_line_node(from - 1);
+ /* this get_addressed_line_node last! */
+ if (isglobal)
+ unset_active_nodes(p->q_forw, n);
+ REQUE(p, n);
+ addr_last -= to - from + 1;
+ current_addr = from - 1;
+ modified = 1;
+ SPL0();
+ return 0;
+}
+
+
+/* display_lines: print a range of lines to stdout */
+int
+display_lines(long from, long to, int gflag)
+{
+ line_t *bp;
+ line_t *ep;
+ char *s;
+
+ if (!from) {
+ errmsg = "invalid address";
+ return ERR;
+ }
+ ep = get_addressed_line_node(INC_MOD(to, addr_last));
+ bp = get_addressed_line_node(from);
+ for (; bp != ep; bp = bp->q_forw) {
+ if ((s = get_sbuf_line(bp)) == NULL)
+ return ERR;
+ if (put_tty_line(s, bp->len, current_addr = from++, gflag) < 0)
+ return ERR;
+ }
+ return 0;
+}
+
+
+#define MAXMARK 26 /* max number of marks */
+
+static line_t *mark[MAXMARK]; /* line markers */
+static int markno; /* line marker count */
+
+/* mark_line_node: set a line node mark */
+int
+mark_line_node(line_t *lp, int n)
+{
+ if (!islower((unsigned char)n)) {
+ errmsg = "invalid mark character";
+ return ERR;
+ } else if (mark[n - 'a'] == NULL)
+ markno++;
+ mark[n - 'a'] = lp;
+ return 0;
+}
+
+
+/* get_marked_node_addr: return address of a marked line */
+long
+get_marked_node_addr(int n)
+{
+ if (!islower((unsigned char)n)) {
+ errmsg = "invalid mark character";
+ return ERR;
+ }
+ return get_line_node_addr(mark[n - 'a']);
+}
+
+
+/* unmark_line_node: clear line node mark */
+void
+unmark_line_node(line_t *lp)
+{
+ int i;
+
+ for (i = 0; markno && i < MAXMARK; i++)
+ if (mark[i] == lp) {
+ mark[i] = NULL;
+ markno--;
+ }
+}
+
+
+/* dup_line_node: return a pointer to a copy of a line node */
+line_t *
+dup_line_node(line_t *lp)
+{
+ line_t *np;
+
+ if ((np = (line_t *) malloc(sizeof(line_t))) == NULL) {
+ fprintf(stderr, "%s\n", strerror(errno));
+ errmsg = "out of memory";
+ return NULL;
+ }
+ np->seek = lp->seek;
+ np->len = lp->len;
+ return np;
+}
+
+
+/* has_trailing_escape: return the parity of escapes preceding a character
+ in a string */
+int
+has_trailing_escape(char *s, char *t)
+{
+ return (s == t || *(t - 1) != '\\') ? 0 : !has_trailing_escape(s, t - 1);
+}
+
+
+/* strip_escapes: return a copy of escaped string of at most length PATH_MAX */
+char *
+strip_escapes(char *s)
+{
+ static char *file = NULL;
+ static int filesz = 0;
+
+ int i = 0;
+
+ REALLOC(file, filesz, PATH_MAX, NULL);
+ while (i < filesz - 1 /* Worry about a possible trailing escape */
+ && (file[i++] = (*s == '\\') ? *++s : *s))
+ s++;
+ return file;
+}
+
+
+void
+signal_hup(int signo)
+{
+ if (mutex)
+ sigflags |= (1 << (signo - 1));
+ else
+ handle_hup(signo);
+}
+
+
+void
+signal_int(int signo)
+{
+ if (mutex)
+ sigflags |= (1 << (signo - 1));
+ else
+ handle_int(signo);
+}
+
+
+void
+handle_hup(int signo)
+{
+ char *hup = NULL; /* hup filename */
+ char *s;
+ char ed_hup[] = "ed.hup";
+ size_t n;
+
+ if (!sigactive)
+ quit(1);
+ sigflags &= ~(1 << (signo - 1));
+ if (addr_last && write_file(ed_hup, "w", 1, addr_last) < 0 &&
+ (s = getenv("HOME")) != NULL &&
+ (n = strlen(s)) + 8 <= PATH_MAX && /* "ed.hup" + '/' */
+ (hup = (char *) malloc(n + 10)) != NULL) {
+ strcpy(hup, s);
+ if (hup[n - 1] != '/')
+ hup[n] = '/', hup[n+1] = '\0';
+ strcat(hup, "ed.hup");
+ write_file(hup, "w", 1, addr_last);
+ }
+ quit(2);
+}
+
+
+void
+handle_int(int signo)
+{
+ if (!sigactive)
+ quit(1);
+ sigflags &= ~(1 << (signo - 1));
+#ifdef _POSIX_SOURCE
+ siglongjmp(env, -1);
+#else
+ longjmp(env, -1);
+#endif
+}
+
+
+int cols = 72; /* wrap column */
+
+void
+handle_winch(int signo)
+{
+ int save_errno = errno;
+
+ struct winsize ws; /* window size structure */
+
+ sigflags &= ~(1 << (signo - 1));
+ if (ioctl(0, TIOCGWINSZ, (char *) &ws) >= 0) {
+ if (ws.ws_row > 2) rows = ws.ws_row - 2;
+ if (ws.ws_col > 8) cols = ws.ws_col - 8;
+ }
+ errno = save_errno;
+}
+
+
+/* is_legal_filename: return a legal filename */
+int
+is_legal_filename(char *s)
+{
+ if (red && (*s == '!' || !strcmp(s, "..") || strchr(s, '/'))) {
+ errmsg = "shell access restricted";
+ return 0;
+ }
+ return 1;
+}
diff --git a/corebinutils/ed/re.c b/corebinutils/ed/re.c
new file mode 100644
index 0000000000..21b407fa87
--- /dev/null
+++ b/corebinutils/ed/re.c
@@ -0,0 +1,129 @@
+/* re.c: This file contains the regular expression interface routines for
+ the ed line editor. */
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 1993 Andrew Moore, Talke Studio.
+ * Copyright (c) 2026 Project Tick.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include "ed.h"
+
+const char *errmsg = "";
+
+/* get_compiled_pattern: return pointer to compiled pattern from command
+ buffer */
+pattern_t *
+get_compiled_pattern(void)
+{
+ static pattern_t *expr = NULL;
+ static char error[1024];
+
+ char *exprs;
+ char delimiter;
+ int n;
+
+ if ((delimiter = *ibufp) == ' ') {
+ errmsg = "invalid pattern delimiter";
+ return NULL;
+ } else if (delimiter == '\n' || *++ibufp == '\n' || *ibufp == delimiter) {
+ if (!expr)
+ errmsg = "no previous pattern";
+ return expr;
+ } else if ((exprs = extract_pattern(delimiter)) == NULL)
+ return NULL;
+ /* buffer alloc'd && not reserved */
+ if (expr && !patlock)
+ regfree(expr);
+ else if ((expr = (pattern_t *) malloc(sizeof(pattern_t))) == NULL) {
+ fprintf(stderr, "%s\n", strerror(errno));
+ errmsg = "out of memory";
+ return NULL;
+ }
+ patlock = 0;
+ if ((n = regcomp(expr, exprs, 0))) {
+ regerror(n, expr, error, sizeof error);
+ errmsg = error;
+ free(expr);
+ return expr = NULL;
+ }
+ return expr;
+}
+
+
+/* extract_pattern: copy a pattern string from the command buffer; return
+ pointer to the copy */
+char *
+extract_pattern(int delimiter)
+{
+ static char *lhbuf = NULL; /* buffer */
+ static int lhbufsz = 0; /* buffer size */
+
+ char *nd;
+ int len;
+
+ for (nd = ibufp; *nd != delimiter && *nd != '\n'; nd++)
+ switch (*nd) {
+ default:
+ break;
+ case '[':
+ if ((nd = parse_char_class(nd + 1)) == NULL) {
+ errmsg = "unbalanced brackets ([])";
+ return NULL;
+ }
+ break;
+ case '\\':
+ if (*++nd == '\n') {
+ errmsg = "trailing backslash (\\)";
+ return NULL;
+ }
+ break;
+ }
+ len = nd - ibufp;
+ REALLOC(lhbuf, lhbufsz, len + 1, NULL);
+ memcpy(lhbuf, ibufp, len);
+ lhbuf[len] = '\0';
+ ibufp = nd;
+ return (isbinary) ? NUL_TO_NEWLINE(lhbuf, len) : lhbuf;
+}
+
+
+/* parse_char_class: expand a POSIX character class */
+char *
+parse_char_class(char *s)
+{
+ int c, d;
+
+ if (*s == '^')
+ s++;
+ if (*s == ']')
+ s++;
+ for (; *s != ']' && *s != '\n'; s++)
+ if (*s == '[' && ((d = *(s+1)) == '.' || d == ':' || d == '='))
+ for (s++, c = *++s; *s != ']' || c != d; s++)
+ if ((c = *s) == '\n')
+ return NULL;
+ return (*s == ']') ? s : NULL;
+}
diff --git a/corebinutils/ed/sub.c b/corebinutils/ed/sub.c
new file mode 100644
index 0000000000..09cb6306df
--- /dev/null
+++ b/corebinutils/ed/sub.c
@@ -0,0 +1,254 @@
+/* sub.c: This file contains the substitution routines for the ed
+ line editor */
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 1993 Andrew Moore, Talke Studio.
+ * Copyright (c) 2026 Project Tick.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include "ed.h"
+
+
+static char *rhbuf; /* rhs substitution buffer */
+static int rhbufsz; /* rhs substitution buffer size */
+static int rhbufi; /* rhs substitution buffer index */
+
+/* extract_subst_tail: extract substitution tail from the command buffer */
+int
+extract_subst_tail(int *flagp, long *np)
+{
+ char delimiter;
+
+ *flagp = *np = 0;
+ if ((delimiter = *ibufp) == '\n') {
+ rhbufi = 0;
+ *flagp = GPR;
+ return 0;
+ } else if (extract_subst_template() == NULL)
+ return ERR;
+ else if (*ibufp == '\n') {
+ *flagp = GPR;
+ return 0;
+ } else if (*ibufp == delimiter)
+ ibufp++;
+ if ('1' <= *ibufp && *ibufp <= '9') {
+ STRTOL(*np, ibufp);
+ return 0;
+ } else if (*ibufp == 'g') {
+ ibufp++;
+ *flagp = GSG;
+ return 0;
+ }
+ return 0;
+}
+
+
+/* extract_subst_template: return pointer to copy of substitution template
+ in the command buffer */
+char *
+extract_subst_template(void)
+{
+ int n = 0;
+ int i = 0;
+ char c;
+ char delimiter = *ibufp++;
+
+ if (*ibufp == '%' && *(ibufp + 1) == delimiter) {
+ ibufp++;
+ if (!rhbuf)
+ errmsg = "no previous substitution";
+ return rhbuf;
+ }
+ while (*ibufp != delimiter) {
+ REALLOC(rhbuf, rhbufsz, i + 2, NULL);
+ if ((c = rhbuf[i++] = *ibufp++) == '\n' && *ibufp == '\0') {
+ i--, ibufp--;
+ break;
+ } else if (c != '\\')
+ ;
+ else if ((rhbuf[i++] = *ibufp++) != '\n')
+ ;
+ else if (!isglobal) {
+ while ((n = get_tty_line()) == 0 ||
+ (n > 0 && ibuf[n - 1] != '\n'))
+ clearerr(stdin);
+ if (n < 0)
+ return NULL;
+ }
+ }
+ REALLOC(rhbuf, rhbufsz, i + 1, NULL);
+ rhbuf[rhbufi = i] = '\0';
+ return rhbuf;
+}
+
+
+static char *rbuf; /* substitute_matching_text buffer */
+static int rbufsz; /* substitute_matching_text buffer size */
+
+/* search_and_replace: for each line in a range, change text matching a pattern
+ according to a substitution template; return status */
+int
+search_and_replace(pattern_t *pat, int gflag, int kth)
+{
+ undo_t *up;
+ const char *txt;
+ const char *eot;
+ long lc;
+ long xa = current_addr;
+ int nsubs = 0;
+ line_t *lp;
+ int len;
+
+ current_addr = first_addr - 1;
+ for (lc = 0; lc <= second_addr - first_addr; lc++) {
+ lp = get_addressed_line_node(++current_addr);
+ if ((len = substitute_matching_text(pat, lp, gflag, kth)) < 0)
+ return ERR;
+ else if (len) {
+ up = NULL;
+ if (delete_lines(current_addr, current_addr) < 0)
+ return ERR;
+ txt = rbuf;
+ eot = rbuf + len;
+ SPL1();
+ do {
+ if ((txt = put_sbuf_line(txt)) == NULL) {
+ SPL0();
+ return ERR;
+ } else if (up)
+ up->t = get_addressed_line_node(current_addr);
+ else if ((up = push_undo_stack(UADD,
+ current_addr, current_addr)) == NULL) {
+ SPL0();
+ return ERR;
+ }
+ } while (txt != eot);
+ SPL0();
+ nsubs++;
+ xa = current_addr;
+ }
+ }
+ current_addr = xa;
+ if (nsubs == 0 && !(gflag & GLB)) {
+ errmsg = "no match";
+ return ERR;
+ } else if ((gflag & (GPR | GLS | GNP)) &&
+ display_lines(current_addr, current_addr, gflag) < 0)
+ return ERR;
+ return 0;
+}
+
+
+/* substitute_matching_text: replace text matched by a pattern according to
+ a substitution template; return pointer to the modified text */
+int
+substitute_matching_text(pattern_t *pat, line_t *lp, int gflag, int kth)
+{
+ int off = 0;
+ int changed = 0;
+ int matchno = 0;
+ int i = 0;
+ regmatch_t rm[SE_MAX];
+ char *txt;
+ char *eot;
+
+ if ((txt = get_sbuf_line(lp)) == NULL)
+ return ERR;
+ if (isbinary)
+ NUL_TO_NEWLINE(txt, lp->len);
+ eot = txt + lp->len;
+ if (!regexec(pat, txt, SE_MAX, rm, 0)) {
+ do {
+ if (!kth || kth == ++matchno) {
+ changed++;
+ i = rm[0].rm_so;
+ REALLOC(rbuf, rbufsz, off + i, ERR);
+ if (isbinary)
+ NEWLINE_TO_NUL(txt, rm[0].rm_eo);
+ memcpy(rbuf + off, txt, i);
+ off += i;
+ if ((off = apply_subst_template(txt, rm, off,
+ pat->re_nsub)) < 0)
+ return ERR;
+ } else {
+ i = rm[0].rm_eo;
+ REALLOC(rbuf, rbufsz, off + i, ERR);
+ if (isbinary)
+ NEWLINE_TO_NUL(txt, i);
+ memcpy(rbuf + off, txt, i);
+ off += i;
+ }
+ txt += rm[0].rm_eo;
+ } while (*txt &&
+ (!changed || ((gflag & GSG) && rm[0].rm_eo)) &&
+ !regexec(pat, txt, SE_MAX, rm, REG_NOTBOL));
+ i = eot - txt;
+ REALLOC(rbuf, rbufsz, off + i + 2, ERR);
+ if (i > 0 && !rm[0].rm_eo && (gflag & GSG)) {
+ errmsg = "infinite substitution loop";
+ return ERR;
+ }
+ if (isbinary)
+ NEWLINE_TO_NUL(txt, i);
+ memcpy(rbuf + off, txt, i);
+ memcpy(rbuf + off + i, "\n", 2);
+ }
+ return changed ? off + i + 1 : 0;
+}
+
+
+/* apply_subst_template: modify text according to a substitution template;
+ return offset to end of modified text */
+int
+apply_subst_template(const char *boln, regmatch_t *rm, int off, int re_nsub)
+{
+ int j = 0;
+ int k = 0;
+ int n;
+ char *sub = rhbuf;
+
+ for (; sub - rhbuf < rhbufi; sub++)
+ if (*sub == '&') {
+ j = rm[0].rm_so;
+ k = rm[0].rm_eo;
+ REALLOC(rbuf, rbufsz, off + k - j, ERR);
+ while (j < k)
+ rbuf[off++] = boln[j++];
+ } else if (*sub == '\\' && '1' <= *++sub && *sub <= '9' &&
+ (n = *sub - '0') <= re_nsub) {
+ j = rm[n].rm_so;
+ k = rm[n].rm_eo;
+ REALLOC(rbuf, rbufsz, off + k - j, ERR);
+ while (j < k)
+ rbuf[off++] = boln[j++];
+ } else {
+ REALLOC(rbuf, rbufsz, off + 1, ERR);
+ rbuf[off++] = *sub;
+ }
+ REALLOC(rbuf, rbufsz, off + 1, ERR);
+ rbuf[off] = '\0';
+ return off;
+}
diff --git a/corebinutils/ed/test/=.err b/corebinutils/ed/test/=.err
new file mode 100644
index 0000000000..6a6055955b
--- /dev/null
+++ b/corebinutils/ed/test/=.err
@@ -0,0 +1 @@
+1,$=
diff --git a/corebinutils/ed/test/Makefile b/corebinutils/ed/test/Makefile
new file mode 100644
index 0000000000..bd89a3d1d7
--- /dev/null
+++ b/corebinutils/ed/test/Makefile
@@ -0,0 +1,25 @@
+SHELL= /bin/sh
+ED= ${.OBJDIR}/ed
+
+all: check
+ @:
+
+check: build test
+ @if grep -h '\*\*\*' errs.o scripts.o; then :; else \
+ echo "tests completed successfully."; \
+ fi
+
+build: mkscripts.sh
+ @if [ -f errs.o ]; then :; else \
+ uudecode < ascii.d.uu ; \
+ uudecode < ascii.r.uu ; \
+ echo "building test scripts for $(ED) ..."; \
+ $(SHELL) mkscripts.sh $(ED); \
+ fi
+
+test: build ckscripts.sh
+ @echo testing $(ED) ...
+ @$(SHELL) ckscripts.sh $(ED)
+
+clean:
+ rm -f *.ed *.red *.[oz] *~ ascii.d ascii.r
diff --git a/corebinutils/ed/test/README b/corebinutils/ed/test/README
new file mode 100644
index 0000000000..6e0cee437b
--- /dev/null
+++ b/corebinutils/ed/test/README
@@ -0,0 +1,31 @@
+
+The files in this directory with suffixes `.t', `.d', `.r' and `.err' are
+used for testing ed. To run the tests, set the ED variable in the Makefile
+for the path name of the program to be tested (e.g., /bin/ed), and type
+`make'. The tests do not exhaustively verify POSIX compliance nor do
+they verify correct 8-bit or long line support.
+
+The test file suffixes have the following meanings:
+.t Template - a list of ed commands from which an ed script is
+ constructed
+.d Data - read by an ed script
+.r Result - the expected output after processing data via an ed
+ script.
+.err Error - invalid ed commands that should generate an error
+
+The output of the tests is written to the two files err.o and scripts.o.
+At the end of the tests, these files are grep'ed for error messages,
+which look like:
+ *** The script u.ed exited abnormally ***
+or:
+ *** Output u.o of script u.ed is incorrect ***
+
+The POSIX requirement that an address range not be used where at most
+a single address is expected has been relaxed in this version of ed.
+Therefore, the following scripts which test for compliance with this
+POSIX rule exit abnormally:
+=-err.ed
+a1-err.ed
+i1-err.ed
+k1-err.ed
+r1-err.ed
diff --git a/corebinutils/ed/test/TODO b/corebinutils/ed/test/TODO
new file mode 100644
index 0000000000..7a4b74fb74
--- /dev/null
+++ b/corebinutils/ed/test/TODO
@@ -0,0 +1,15 @@
+Some missing tests:
+0) g/./s^@^@ - okay: NULs in commands
+1) g/./s/^@/ - okay: NULs in patterns
+2) a
+ hello^V^Jworld
+ . - okay: embedded newlines in insert mode
+3) ed "" - error: invalid filename
+4) red .. - error: restricted
+5) red / - error: restricted
+5) red !xx - error: restricted
+6) ed -x - verify: 8-bit clean
+7) ed - verify: long-line support
+8) ed - verify: interactive/help mode
+9) G/pat/ - verify: global interactive command
+10) V/pat/ - verify: global interactive command
diff --git a/corebinutils/ed/test/a.d b/corebinutils/ed/test/a.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/a.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/a.r b/corebinutils/ed/test/a.r
new file mode 100644
index 0000000000..26257bd3b3
--- /dev/null
+++ b/corebinutils/ed/test/a.r
@@ -0,0 +1,8 @@
+hello world
+line 1
+hello world!
+line 2
+line 3
+line 4
+line5
+hello world!!
diff --git a/corebinutils/ed/test/a.t b/corebinutils/ed/test/a.t
new file mode 100644
index 0000000000..ac98c40d08
--- /dev/null
+++ b/corebinutils/ed/test/a.t
@@ -0,0 +1,9 @@
+0a
+hello world
+.
+2a
+hello world!
+.
+$a
+hello world!!
+.
diff --git a/corebinutils/ed/test/a1.err b/corebinutils/ed/test/a1.err
new file mode 100644
index 0000000000..e80815ff50
--- /dev/null
+++ b/corebinutils/ed/test/a1.err
@@ -0,0 +1,3 @@
+1,$a
+hello world
+.
diff --git a/corebinutils/ed/test/a2.err b/corebinutils/ed/test/a2.err
new file mode 100644
index 0000000000..ec4b00b40c
--- /dev/null
+++ b/corebinutils/ed/test/a2.err
@@ -0,0 +1,3 @@
+aa
+hello world
+.
diff --git a/corebinutils/ed/test/addr.d b/corebinutils/ed/test/addr.d
new file mode 100644
index 0000000000..8f7ba1b5d3
--- /dev/null
+++ b/corebinutils/ed/test/addr.d
@@ -0,0 +1,9 @@
+line 1
+line 2
+line 3
+line 4
+line5
+1ine6
+line7
+line8
+line9
diff --git a/corebinutils/ed/test/addr.r b/corebinutils/ed/test/addr.r
new file mode 100644
index 0000000000..04caf17f42
--- /dev/null
+++ b/corebinutils/ed/test/addr.r
@@ -0,0 +1,2 @@
+line 2
+line9
diff --git a/corebinutils/ed/test/addr.t b/corebinutils/ed/test/addr.t
new file mode 100644
index 0000000000..750b224ed8
--- /dev/null
+++ b/corebinutils/ed/test/addr.t
@@ -0,0 +1,5 @@
+1 d
+1 1 d
+1,2,d
+1;+ + ,d
+1,2;., + 2d
diff --git a/corebinutils/ed/test/addr1.err b/corebinutils/ed/test/addr1.err
new file mode 100644
index 0000000000..29d6383b52
--- /dev/null
+++ b/corebinutils/ed/test/addr1.err
@@ -0,0 +1 @@
+100
diff --git a/corebinutils/ed/test/addr2.err b/corebinutils/ed/test/addr2.err
new file mode 100644
index 0000000000..e96acb9254
--- /dev/null
+++ b/corebinutils/ed/test/addr2.err
@@ -0,0 +1 @@
+-100
diff --git a/corebinutils/ed/test/ascii.d.uu b/corebinutils/ed/test/ascii.d.uu
new file mode 100644
index 0000000000..0b0a73c279
--- /dev/null
+++ b/corebinutils/ed/test/ascii.d.uu
@@ -0,0 +1,9 @@
+begin 644 ascii.d
+M``$"`P0%!@<("0H+#`T.#Q`1$A,4%187&!D:&QP='A\@(2(C)"4F)R@I*BLL
+M+2XO,#$R,S0U-C<X.3H[/#T^/T!!0D-$149'2$E*2TQ-3D]045)35%565UA9
+M6EM<75Y?8&%B8V1E9F=H:6IK;&UN;W!Q<G-T=79W>'EZ>WQ]?G^`@8*#A(6&
+MAXB)BHN,C8Z/D)&2DY25EI>8F9J;G)V>GZ"AHJ.DI::GJ*FJJZRMKJ^PL;*S
+MM+6VM[BYNKN\O;Z_P,'"P\3%QL?(R<K+S,W.S]#1TM/4U=;7V-G:V]S=WM_@
+?X>+CY.7FY^CIZNOL[>[O\/'R\_3U]O?X^?K[_/W^_]/4
+`
+end
diff --git a/corebinutils/ed/test/ascii.r.uu b/corebinutils/ed/test/ascii.r.uu
new file mode 100644
index 0000000000..9ca88b4d10
--- /dev/null
+++ b/corebinutils/ed/test/ascii.r.uu
@@ -0,0 +1,9 @@
+begin 644 ascii.r
+M``$"`P0%!@<("0H+#`T.#Q`1$A,4%187&!D:&QP='A\@(2(C)"4F)R@I*BLL
+M+2XO,#$R,S0U-C<X.3H[/#T^/T!!0D-$149'2$E*2TQ-3D]045)35%565UA9
+M6EM<75Y?8&%B8V1E9F=H:6IK;&UN;W!Q<G-T=79W>'EZ>WQ]?G^`@8*#A(6&
+MAXB)BHN,C8Z/D)&2DY25EI>8F9J;G)V>GZ"AHJ.DI::GJ*FJJZRMKJ^PL;*S
+MM+6VM[BYNKN\O;Z_P,'"P\3%QL?(R<K+S,W.S]#1TM/4U=;7V-G:V]S=WM_@
+?X>+CY.7FY^CIZNOL[>[O\/'R\_3U]O?X^?K[_/W^_]/4
+`
+end
diff --git a/corebinutils/ed/test/ascii.t b/corebinutils/ed/test/ascii.t
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/corebinutils/ed/test/ascii.t
diff --git a/corebinutils/ed/test/bang1.d b/corebinutils/ed/test/bang1.d
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/corebinutils/ed/test/bang1.d
diff --git a/corebinutils/ed/test/bang1.err b/corebinutils/ed/test/bang1.err
new file mode 100644
index 0000000000..630af9011c
--- /dev/null
+++ b/corebinutils/ed/test/bang1.err
@@ -0,0 +1 @@
+.!date
diff --git a/corebinutils/ed/test/bang1.r b/corebinutils/ed/test/bang1.r
new file mode 100644
index 0000000000..dcf02b2fb6
--- /dev/null
+++ b/corebinutils/ed/test/bang1.r
@@ -0,0 +1 @@
+okay
diff --git a/corebinutils/ed/test/bang1.t b/corebinutils/ed/test/bang1.t
new file mode 100644
index 0000000000..d7b1fea1f7
--- /dev/null
+++ b/corebinutils/ed/test/bang1.t
@@ -0,0 +1,5 @@
+!read one
+hello, world
+a
+okay
+.
diff --git a/corebinutils/ed/test/bang2.err b/corebinutils/ed/test/bang2.err
new file mode 100644
index 0000000000..79d8956822
--- /dev/null
+++ b/corebinutils/ed/test/bang2.err
@@ -0,0 +1 @@
+!!
diff --git a/corebinutils/ed/test/c.d b/corebinutils/ed/test/c.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/c.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/c.r b/corebinutils/ed/test/c.r
new file mode 100644
index 0000000000..0fb3e4fffc
--- /dev/null
+++ b/corebinutils/ed/test/c.r
@@ -0,0 +1,4 @@
+at the top
+between top/middle
+in the middle
+at the bottom
diff --git a/corebinutils/ed/test/c.t b/corebinutils/ed/test/c.t
new file mode 100644
index 0000000000..ebdd536f81
--- /dev/null
+++ b/corebinutils/ed/test/c.t
@@ -0,0 +1,12 @@
+1c
+at the top
+.
+4c
+in the middle
+.
+$c
+at the bottom
+.
+2,3c
+between top/middle
+.
diff --git a/corebinutils/ed/test/c1.err b/corebinutils/ed/test/c1.err
new file mode 100644
index 0000000000..658ec38b46
--- /dev/null
+++ b/corebinutils/ed/test/c1.err
@@ -0,0 +1,3 @@
+cc
+hello world
+.
diff --git a/corebinutils/ed/test/c2.err b/corebinutils/ed/test/c2.err
new file mode 100644
index 0000000000..24b322776a
--- /dev/null
+++ b/corebinutils/ed/test/c2.err
@@ -0,0 +1,3 @@
+0c
+hello world
+.
diff --git a/corebinutils/ed/test/ckscripts.sh b/corebinutils/ed/test/ckscripts.sh
new file mode 100644
index 0000000000..ce02e18f53
--- /dev/null
+++ b/corebinutils/ed/test/ckscripts.sh
@@ -0,0 +1,36 @@
+#!/bin/sh -
+# This script runs the .ed scripts generated by mkscripts.sh
+# and compares their output against the .r files, which contain
+# the correct output
+#
+
+PATH="/bin:/usr/bin:/usr/local/bin/:."
+ED=$1
+[ ! -x $ED ] && { echo "$ED: cannot execute"; exit 1; }
+
+# Run the *.red scripts first, since these don't generate output;
+# they exit with non-zero status
+for i in *.red; do
+ echo $i
+ if $i; then
+ echo "*** The script $i exited abnormally ***"
+ fi
+done >errs.o 2>&1
+
+# Run the remainding scripts; they exit with zero status
+for i in *.ed; do
+# base=`expr $i : '\([^.]*\)'`
+# base=`echo $i | sed 's/\..*//'`
+ base=`$ED - \!"echo $i" <<-EOF
+ s/\..*
+ EOF`
+ if $base.ed; then
+ if cmp -s $base.o $base.r; then :; else
+ echo "*** Output $base.o of script $i is incorrect ***"
+ fi
+ else
+ echo "*** The script $i exited abnormally ***"
+ fi
+done >scripts.o 2>&1
+
+grep -h '\*\*\*' errs.o scripts.o
diff --git a/corebinutils/ed/test/d.d b/corebinutils/ed/test/d.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/d.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/d.err b/corebinutils/ed/test/d.err
new file mode 100644
index 0000000000..f03f6945fb
--- /dev/null
+++ b/corebinutils/ed/test/d.err
@@ -0,0 +1 @@
+dd
diff --git a/corebinutils/ed/test/d.r b/corebinutils/ed/test/d.r
new file mode 100644
index 0000000000..b7e242c00c
--- /dev/null
+++ b/corebinutils/ed/test/d.r
@@ -0,0 +1 @@
+line 2
diff --git a/corebinutils/ed/test/d.t b/corebinutils/ed/test/d.t
new file mode 100644
index 0000000000..c7c473febd
--- /dev/null
+++ b/corebinutils/ed/test/d.t
@@ -0,0 +1,3 @@
+1d
+2;+1d
+$d
diff --git a/corebinutils/ed/test/e1.d b/corebinutils/ed/test/e1.d
new file mode 100644
index 0000000000..3b18e512db
--- /dev/null
+++ b/corebinutils/ed/test/e1.d
@@ -0,0 +1 @@
+hello world
diff --git a/corebinutils/ed/test/e1.err b/corebinutils/ed/test/e1.err
new file mode 100644
index 0000000000..827cc292b6
--- /dev/null
+++ b/corebinutils/ed/test/e1.err
@@ -0,0 +1 @@
+ee e1.err
diff --git a/corebinutils/ed/test/e1.r b/corebinutils/ed/test/e1.r
new file mode 100644
index 0000000000..e656728bab
--- /dev/null
+++ b/corebinutils/ed/test/e1.r
@@ -0,0 +1 @@
+E e1.t
diff --git a/corebinutils/ed/test/e1.t b/corebinutils/ed/test/e1.t
new file mode 100644
index 0000000000..e656728bab
--- /dev/null
+++ b/corebinutils/ed/test/e1.t
@@ -0,0 +1 @@
+E e1.t
diff --git a/corebinutils/ed/test/e2.d b/corebinutils/ed/test/e2.d
new file mode 100644
index 0000000000..aa44630d22
--- /dev/null
+++ b/corebinutils/ed/test/e2.d
@@ -0,0 +1 @@
+E !echo hello world-
diff --git a/corebinutils/ed/test/e2.err b/corebinutils/ed/test/e2.err
new file mode 100644
index 0000000000..779a64b54f
--- /dev/null
+++ b/corebinutils/ed/test/e2.err
@@ -0,0 +1 @@
+.e e2.err
diff --git a/corebinutils/ed/test/e2.r b/corebinutils/ed/test/e2.r
new file mode 100644
index 0000000000..59ebf11fd0
--- /dev/null
+++ b/corebinutils/ed/test/e2.r
@@ -0,0 +1 @@
+hello world-
diff --git a/corebinutils/ed/test/e2.t b/corebinutils/ed/test/e2.t
new file mode 100644
index 0000000000..aa44630d22
--- /dev/null
+++ b/corebinutils/ed/test/e2.t
@@ -0,0 +1 @@
+E !echo hello world-
diff --git a/corebinutils/ed/test/e3.d b/corebinutils/ed/test/e3.d
new file mode 100644
index 0000000000..aa44630d22
--- /dev/null
+++ b/corebinutils/ed/test/e3.d
@@ -0,0 +1 @@
+E !echo hello world-
diff --git a/corebinutils/ed/test/e3.err b/corebinutils/ed/test/e3.err
new file mode 100644
index 0000000000..80a7fdcf92
--- /dev/null
+++ b/corebinutils/ed/test/e3.err
@@ -0,0 +1 @@
+ee.err
diff --git a/corebinutils/ed/test/e3.r b/corebinutils/ed/test/e3.r
new file mode 100644
index 0000000000..aa44630d22
--- /dev/null
+++ b/corebinutils/ed/test/e3.r
@@ -0,0 +1 @@
+E !echo hello world-
diff --git a/corebinutils/ed/test/e3.t b/corebinutils/ed/test/e3.t
new file mode 100644
index 0000000000..1c50726138
--- /dev/null
+++ b/corebinutils/ed/test/e3.t
@@ -0,0 +1 @@
+E
diff --git a/corebinutils/ed/test/e4.d b/corebinutils/ed/test/e4.d
new file mode 100644
index 0000000000..aa44630d22
--- /dev/null
+++ b/corebinutils/ed/test/e4.d
@@ -0,0 +1 @@
+E !echo hello world-
diff --git a/corebinutils/ed/test/e4.r b/corebinutils/ed/test/e4.r
new file mode 100644
index 0000000000..aa44630d22
--- /dev/null
+++ b/corebinutils/ed/test/e4.r
@@ -0,0 +1 @@
+E !echo hello world-
diff --git a/corebinutils/ed/test/e4.t b/corebinutils/ed/test/e4.t
new file mode 100644
index 0000000000..d905d9da82
--- /dev/null
+++ b/corebinutils/ed/test/e4.t
@@ -0,0 +1 @@
+e
diff --git a/corebinutils/ed/test/f1.err b/corebinutils/ed/test/f1.err
new file mode 100644
index 0000000000..e60975adab
--- /dev/null
+++ b/corebinutils/ed/test/f1.err
@@ -0,0 +1 @@
+.f f1.err
diff --git a/corebinutils/ed/test/f2.err b/corebinutils/ed/test/f2.err
new file mode 100644
index 0000000000..26d1c5e3a7
--- /dev/null
+++ b/corebinutils/ed/test/f2.err
@@ -0,0 +1 @@
+ff1.err
diff --git a/corebinutils/ed/test/g1.d b/corebinutils/ed/test/g1.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/g1.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/g1.err b/corebinutils/ed/test/g1.err
new file mode 100644
index 0000000000..f95ea22232
--- /dev/null
+++ b/corebinutils/ed/test/g1.err
@@ -0,0 +1 @@
+g/./s //x/
diff --git a/corebinutils/ed/test/g1.r b/corebinutils/ed/test/g1.r
new file mode 100644
index 0000000000..578a44b6b2
--- /dev/null
+++ b/corebinutils/ed/test/g1.r
@@ -0,0 +1,15 @@
+line5
+help! world
+order
+line 4
+help! world
+order
+line 3
+help! world
+order
+line 2
+help! world
+order
+line 1
+help! world
+order
diff --git a/corebinutils/ed/test/g1.t b/corebinutils/ed/test/g1.t
new file mode 100644
index 0000000000..2d0b54f35a
--- /dev/null
+++ b/corebinutils/ed/test/g1.t
@@ -0,0 +1,6 @@
+g/./m0
+g/./s/$/\
+hello world
+g/hello /s/lo/p!/\
+a\
+order
diff --git a/corebinutils/ed/test/g2.d b/corebinutils/ed/test/g2.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/g2.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/g2.err b/corebinutils/ed/test/g2.err
new file mode 100644
index 0000000000..0ff6a5a601
--- /dev/null
+++ b/corebinutils/ed/test/g2.err
@@ -0,0 +1 @@
+g//s/./x/
diff --git a/corebinutils/ed/test/g2.r b/corebinutils/ed/test/g2.r
new file mode 100644
index 0000000000..3b18e512db
--- /dev/null
+++ b/corebinutils/ed/test/g2.r
@@ -0,0 +1 @@
+hello world
diff --git a/corebinutils/ed/test/g2.t b/corebinutils/ed/test/g2.t
new file mode 100644
index 0000000000..831ee8367b
--- /dev/null
+++ b/corebinutils/ed/test/g2.t
@@ -0,0 +1,2 @@
+g/[2-4]/-1,+1c\
+hello world
diff --git a/corebinutils/ed/test/g3.d b/corebinutils/ed/test/g3.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/g3.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/g3.err b/corebinutils/ed/test/g3.err
new file mode 100644
index 0000000000..01058d844a
--- /dev/null
+++ b/corebinutils/ed/test/g3.err
@@ -0,0 +1 @@
+g
diff --git a/corebinutils/ed/test/g3.r b/corebinutils/ed/test/g3.r
new file mode 100644
index 0000000000..cc6fbddec2
--- /dev/null
+++ b/corebinutils/ed/test/g3.r
@@ -0,0 +1,5 @@
+linc 3
+xine 1
+xine 2
+xinc 4
+xinc5
diff --git a/corebinutils/ed/test/g3.t b/corebinutils/ed/test/g3.t
new file mode 100644
index 0000000000..2d052a6e84
--- /dev/null
+++ b/corebinutils/ed/test/g3.t
@@ -0,0 +1,4 @@
+g/./s//x/\
+3m0
+g/./s/e/c/\
+2,3m1
diff --git a/corebinutils/ed/test/g4.d b/corebinutils/ed/test/g4.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/g4.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/g4.r b/corebinutils/ed/test/g4.r
new file mode 100644
index 0000000000..350882d823
--- /dev/null
+++ b/corebinutils/ed/test/g4.r
@@ -0,0 +1,7 @@
+hello
+zine 1
+line 2
+line 3
+line 4
+line5
+world
diff --git a/corebinutils/ed/test/g4.t b/corebinutils/ed/test/g4.t
new file mode 100644
index 0000000000..ec618166cc
--- /dev/null
+++ b/corebinutils/ed/test/g4.t
@@ -0,0 +1,13 @@
+g/./s/./x/\
+u\
+s/./y/\
+u\
+s/./z/\
+u
+u
+0a
+hello
+.
+$a
+world
+.
diff --git a/corebinutils/ed/test/g5.d b/corebinutils/ed/test/g5.d
new file mode 100644
index 0000000000..a92d664bc2
--- /dev/null
+++ b/corebinutils/ed/test/g5.d
@@ -0,0 +1,3 @@
+line 1
+line 2
+line 3
diff --git a/corebinutils/ed/test/g5.r b/corebinutils/ed/test/g5.r
new file mode 100644
index 0000000000..15a26758ba
--- /dev/null
+++ b/corebinutils/ed/test/g5.r
@@ -0,0 +1,9 @@
+line 1
+line 2
+line 3
+line 2
+line 3
+line 1
+line 3
+line 1
+line 2
diff --git a/corebinutils/ed/test/g5.t b/corebinutils/ed/test/g5.t
new file mode 100644
index 0000000000..e213481d54
--- /dev/null
+++ b/corebinutils/ed/test/g5.t
@@ -0,0 +1,2 @@
+g/./1,3t$\
+1d
diff --git a/corebinutils/ed/test/h.err b/corebinutils/ed/test/h.err
new file mode 100644
index 0000000000..a71e506f6d
--- /dev/null
+++ b/corebinutils/ed/test/h.err
@@ -0,0 +1 @@
+.h
diff --git a/corebinutils/ed/test/i.d b/corebinutils/ed/test/i.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/i.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/i.r b/corebinutils/ed/test/i.r
new file mode 100644
index 0000000000..5f27af09c0
--- /dev/null
+++ b/corebinutils/ed/test/i.r
@@ -0,0 +1,8 @@
+hello world
+hello world!
+line 1
+line 2
+line 3
+line 4
+hello world!!
+line5
diff --git a/corebinutils/ed/test/i.t b/corebinutils/ed/test/i.t
new file mode 100644
index 0000000000..d1d98057d8
--- /dev/null
+++ b/corebinutils/ed/test/i.t
@@ -0,0 +1,9 @@
+1i
+hello world
+.
+2i
+hello world!
+.
+$i
+hello world!!
+.
diff --git a/corebinutils/ed/test/i1.err b/corebinutils/ed/test/i1.err
new file mode 100644
index 0000000000..aaddede259
--- /dev/null
+++ b/corebinutils/ed/test/i1.err
@@ -0,0 +1,3 @@
+1,$i
+hello world
+.
diff --git a/corebinutils/ed/test/i2.err b/corebinutils/ed/test/i2.err
new file mode 100644
index 0000000000..b63f5ac507
--- /dev/null
+++ b/corebinutils/ed/test/i2.err
@@ -0,0 +1,3 @@
+ii
+hello world
+.
diff --git a/corebinutils/ed/test/i3.err b/corebinutils/ed/test/i3.err
new file mode 100644
index 0000000000..6d200c8c97
--- /dev/null
+++ b/corebinutils/ed/test/i3.err
@@ -0,0 +1,3 @@
+0i
+hello world
+.
diff --git a/corebinutils/ed/test/j.d b/corebinutils/ed/test/j.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/j.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/j.r b/corebinutils/ed/test/j.r
new file mode 100644
index 0000000000..66f36a8f8a
--- /dev/null
+++ b/corebinutils/ed/test/j.r
@@ -0,0 +1,4 @@
+line 1
+line 2line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/j.t b/corebinutils/ed/test/j.t
new file mode 100644
index 0000000000..9b5d28ddf1
--- /dev/null
+++ b/corebinutils/ed/test/j.t
@@ -0,0 +1,2 @@
+1,1j
+2,3j
diff --git a/corebinutils/ed/test/k.d b/corebinutils/ed/test/k.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/k.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/k.r b/corebinutils/ed/test/k.r
new file mode 100644
index 0000000000..eeb38db20c
--- /dev/null
+++ b/corebinutils/ed/test/k.r
@@ -0,0 +1,5 @@
+line 3
+hello world
+line 4
+line5
+line 2
diff --git a/corebinutils/ed/test/k.t b/corebinutils/ed/test/k.t
new file mode 100644
index 0000000000..53d588dd07
--- /dev/null
+++ b/corebinutils/ed/test/k.t
@@ -0,0 +1,10 @@
+2ka
+1d
+'am$
+1ka
+0a
+hello world
+.
+'ad
+u
+'am0
diff --git a/corebinutils/ed/test/k1.err b/corebinutils/ed/test/k1.err
new file mode 100644
index 0000000000..eba1f3d8ff
--- /dev/null
+++ b/corebinutils/ed/test/k1.err
@@ -0,0 +1 @@
+1,$ka
diff --git a/corebinutils/ed/test/k2.err b/corebinutils/ed/test/k2.err
new file mode 100644
index 0000000000..b34a18d519
--- /dev/null
+++ b/corebinutils/ed/test/k2.err
@@ -0,0 +1 @@
+kA
diff --git a/corebinutils/ed/test/k3.err b/corebinutils/ed/test/k3.err
new file mode 100644
index 0000000000..70190c473d
--- /dev/null
+++ b/corebinutils/ed/test/k3.err
@@ -0,0 +1 @@
+0ka
diff --git a/corebinutils/ed/test/k4.err b/corebinutils/ed/test/k4.err
new file mode 100644
index 0000000000..3457642229
--- /dev/null
+++ b/corebinutils/ed/test/k4.err
@@ -0,0 +1,6 @@
+a
+hello
+.
+.ka
+'ad
+'ap
diff --git a/corebinutils/ed/test/l.d b/corebinutils/ed/test/l.d
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/corebinutils/ed/test/l.d
diff --git a/corebinutils/ed/test/l.r b/corebinutils/ed/test/l.r
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/corebinutils/ed/test/l.r
diff --git a/corebinutils/ed/test/l.t b/corebinutils/ed/test/l.t
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/corebinutils/ed/test/l.t
diff --git a/corebinutils/ed/test/m.d b/corebinutils/ed/test/m.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/m.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/m.err b/corebinutils/ed/test/m.err
new file mode 100644
index 0000000000..3aec4c32c1
--- /dev/null
+++ b/corebinutils/ed/test/m.err
@@ -0,0 +1,4 @@
+a
+hello world
+.
+1,$m1
diff --git a/corebinutils/ed/test/m.r b/corebinutils/ed/test/m.r
new file mode 100644
index 0000000000..186cf5403b
--- /dev/null
+++ b/corebinutils/ed/test/m.r
@@ -0,0 +1,5 @@
+line5
+line 1
+line 2
+line 3
+line 4
diff --git a/corebinutils/ed/test/m.t b/corebinutils/ed/test/m.t
new file mode 100644
index 0000000000..c39c088724
--- /dev/null
+++ b/corebinutils/ed/test/m.t
@@ -0,0 +1,7 @@
+1,2m$
+1,2m$
+1,2m$
+$m0
+$m0
+2,3m1
+2,3m3
diff --git a/corebinutils/ed/test/mkscripts.sh b/corebinutils/ed/test/mkscripts.sh
new file mode 100644
index 0000000000..811a11d15b
--- /dev/null
+++ b/corebinutils/ed/test/mkscripts.sh
@@ -0,0 +1,74 @@
+#!/bin/sh -
+# This script generates ed test scripts (.ed) from .t files
+#
+
+PATH="/bin:/usr/bin:/usr/local/bin/:."
+ED=$1
+[ ! -x $ED ] && { echo "$ED: cannot execute"; exit 1; }
+
+for i in *.t; do
+# base=${i%.*}
+# base=`echo $i | sed 's/\..*//'`
+# base=`expr $i : '\([^.]*\)'`
+# (
+# echo "#!/bin/sh -"
+# echo "$ED - <<\EOT"
+# echo "r $base.d"
+# cat $i
+# echo "w $base.o"
+# echo EOT
+# ) >$base.ed
+# chmod +x $base.ed
+# The following is pretty ugly way of doing the above, and not appropriate
+# use of ed but the point is that it can be done...
+ base=`$ED - \!"echo $i" <<-EOF
+ s/\..*
+ EOF`
+ $ED - <<-EOF
+ a
+ #!/bin/sh -
+ $ED - <<\EOT
+ H
+ r $base.d
+ w $base.o
+ EOT
+ .
+ -2r $i
+ w $base.ed
+ !chmod +x $base.ed
+ EOF
+done
+
+for i in *.err; do
+# base=${i%.*}
+# base=`echo $i | sed 's/\..*//'`
+# base=`expr $i : '\([^.]*\)'`
+# (
+# echo "#!/bin/sh -"
+# echo "$ED - <<\EOT"
+# echo H
+# echo "r $base.err"
+# cat $i
+# echo "w $base.o"
+# echo EOT
+# ) >$base-err.ed
+# chmod +x $base-err.ed
+# The following is pretty ugly way of doing the above, and not appropriate
+# use of ed but the point is that it can be done...
+ base=`$ED - \!"echo $i" <<-EOF
+ s/\..*
+ EOF`
+ $ED - <<-EOF
+ a
+ #!/bin/sh -
+ $ED - <<\EOT
+ H
+ r $base.err
+ w $base.o
+ EOT
+ .
+ -2r $i
+ w ${base}.red
+ !chmod +x ${base}.red
+ EOF
+done
diff --git a/corebinutils/ed/test/n.d b/corebinutils/ed/test/n.d
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/corebinutils/ed/test/n.d
diff --git a/corebinutils/ed/test/n.r b/corebinutils/ed/test/n.r
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/corebinutils/ed/test/n.r
diff --git a/corebinutils/ed/test/n.t b/corebinutils/ed/test/n.t
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/corebinutils/ed/test/n.t
diff --git a/corebinutils/ed/test/nl.err b/corebinutils/ed/test/nl.err
new file mode 100644
index 0000000000..8949a85006
--- /dev/null
+++ b/corebinutils/ed/test/nl.err
@@ -0,0 +1 @@
+,1
diff --git a/corebinutils/ed/test/nl1.d b/corebinutils/ed/test/nl1.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/nl1.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/nl1.r b/corebinutils/ed/test/nl1.r
new file mode 100644
index 0000000000..9d8854cd04
--- /dev/null
+++ b/corebinutils/ed/test/nl1.r
@@ -0,0 +1,8 @@
+
+
+hello world
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/nl1.t b/corebinutils/ed/test/nl1.t
new file mode 100644
index 0000000000..ea192e9b82
--- /dev/null
+++ b/corebinutils/ed/test/nl1.t
@@ -0,0 +1,8 @@
+1
+
+
+0a
+
+
+hello world
+.
diff --git a/corebinutils/ed/test/nl2.d b/corebinutils/ed/test/nl2.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/nl2.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/nl2.r b/corebinutils/ed/test/nl2.r
new file mode 100644
index 0000000000..fe99e41628
--- /dev/null
+++ b/corebinutils/ed/test/nl2.r
@@ -0,0 +1,6 @@
+line 1
+line 2
+line 3
+line 4
+line5
+hello world
diff --git a/corebinutils/ed/test/nl2.t b/corebinutils/ed/test/nl2.t
new file mode 100644
index 0000000000..73fd27b7e2
--- /dev/null
+++ b/corebinutils/ed/test/nl2.t
@@ -0,0 +1,4 @@
+a
+hello world
+.
+0;/./
diff --git a/corebinutils/ed/test/p.d b/corebinutils/ed/test/p.d
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/corebinutils/ed/test/p.d
diff --git a/corebinutils/ed/test/p.r b/corebinutils/ed/test/p.r
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/corebinutils/ed/test/p.r
diff --git a/corebinutils/ed/test/p.t b/corebinutils/ed/test/p.t
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/corebinutils/ed/test/p.t
diff --git a/corebinutils/ed/test/q.d b/corebinutils/ed/test/q.d
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/corebinutils/ed/test/q.d
diff --git a/corebinutils/ed/test/q.r b/corebinutils/ed/test/q.r
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/corebinutils/ed/test/q.r
diff --git a/corebinutils/ed/test/q.t b/corebinutils/ed/test/q.t
new file mode 100644
index 0000000000..123a2c8e2c
--- /dev/null
+++ b/corebinutils/ed/test/q.t
@@ -0,0 +1,5 @@
+w q.o
+a
+hello
+.
+q
diff --git a/corebinutils/ed/test/q1.err b/corebinutils/ed/test/q1.err
new file mode 100644
index 0000000000..0a7e178d20
--- /dev/null
+++ b/corebinutils/ed/test/q1.err
@@ -0,0 +1 @@
+.q
diff --git a/corebinutils/ed/test/r1.d b/corebinutils/ed/test/r1.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/r1.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/r1.err b/corebinutils/ed/test/r1.err
new file mode 100644
index 0000000000..269aa7cbcb
--- /dev/null
+++ b/corebinutils/ed/test/r1.err
@@ -0,0 +1 @@
+1,$r r1.err
diff --git a/corebinutils/ed/test/r1.r b/corebinutils/ed/test/r1.r
new file mode 100644
index 0000000000..a3ff506ec7
--- /dev/null
+++ b/corebinutils/ed/test/r1.r
@@ -0,0 +1,7 @@
+line 1
+hello world
+line 2
+line 3
+line 4
+line5
+hello world
diff --git a/corebinutils/ed/test/r1.t b/corebinutils/ed/test/r1.t
new file mode 100644
index 0000000000..d787a923e3
--- /dev/null
+++ b/corebinutils/ed/test/r1.t
@@ -0,0 +1,3 @@
+1;r !echo hello world
+1
+r !echo hello world
diff --git a/corebinutils/ed/test/r2.d b/corebinutils/ed/test/r2.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/r2.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/r2.err b/corebinutils/ed/test/r2.err
new file mode 100644
index 0000000000..1c44fa3ea9
--- /dev/null
+++ b/corebinutils/ed/test/r2.err
@@ -0,0 +1 @@
+r a-good-book
diff --git a/corebinutils/ed/test/r2.r b/corebinutils/ed/test/r2.r
new file mode 100644
index 0000000000..ac152ba9d0
--- /dev/null
+++ b/corebinutils/ed/test/r2.r
@@ -0,0 +1,10 @@
+line 1
+line 2
+line 3
+line 4
+line5
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/r2.t b/corebinutils/ed/test/r2.t
new file mode 100644
index 0000000000..4286f428e3
--- /dev/null
+++ b/corebinutils/ed/test/r2.t
@@ -0,0 +1 @@
+r
diff --git a/corebinutils/ed/test/r3.d b/corebinutils/ed/test/r3.d
new file mode 100644
index 0000000000..593eec6192
--- /dev/null
+++ b/corebinutils/ed/test/r3.d
@@ -0,0 +1 @@
+r r3.t
diff --git a/corebinutils/ed/test/r3.r b/corebinutils/ed/test/r3.r
new file mode 100644
index 0000000000..86d5f904fc
--- /dev/null
+++ b/corebinutils/ed/test/r3.r
@@ -0,0 +1,2 @@
+r r3.t
+r r3.t
diff --git a/corebinutils/ed/test/r3.t b/corebinutils/ed/test/r3.t
new file mode 100644
index 0000000000..593eec6192
--- /dev/null
+++ b/corebinutils/ed/test/r3.t
@@ -0,0 +1 @@
+r r3.t
diff --git a/corebinutils/ed/test/s1.d b/corebinutils/ed/test/s1.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/s1.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/s1.err b/corebinutils/ed/test/s1.err
new file mode 100644
index 0000000000..d7ca0cf480
--- /dev/null
+++ b/corebinutils/ed/test/s1.err
@@ -0,0 +1 @@
+s . x
diff --git a/corebinutils/ed/test/s1.r b/corebinutils/ed/test/s1.r
new file mode 100644
index 0000000000..4eb0980cfe
--- /dev/null
+++ b/corebinutils/ed/test/s1.r
@@ -0,0 +1,5 @@
+liene 1
+(liene) (2)
+(liene) (3)
+liene (4)
+(()liene5)
diff --git a/corebinutils/ed/test/s1.t b/corebinutils/ed/test/s1.t
new file mode 100644
index 0000000000..b0028bb6c9
--- /dev/null
+++ b/corebinutils/ed/test/s1.t
@@ -0,0 +1,6 @@
+s/\([^ ][^ ]*\)/(\1)/g
+2s
+/3/s
+/\(4\)/sr
+/\(.\)/srg
+%s/i/&e/
diff --git a/corebinutils/ed/test/s10.err b/corebinutils/ed/test/s10.err
new file mode 100644
index 0000000000..0d8d83de19
--- /dev/null
+++ b/corebinutils/ed/test/s10.err
@@ -0,0 +1,4 @@
+a
+hello
+.
+s/[h[.]/x/
diff --git a/corebinutils/ed/test/s2.d b/corebinutils/ed/test/s2.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/s2.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/s2.err b/corebinutils/ed/test/s2.err
new file mode 100644
index 0000000000..b5c851df7b
--- /dev/null
+++ b/corebinutils/ed/test/s2.err
@@ -0,0 +1,4 @@
+a
+a
+.
+s/x*/a/g
diff --git a/corebinutils/ed/test/s2.r b/corebinutils/ed/test/s2.r
new file mode 100644
index 0000000000..ca305c8d50
--- /dev/null
+++ b/corebinutils/ed/test/s2.r
@@ -0,0 +1,5 @@
+li(n)e 1
+i(n)e 200
+li(n)e 3
+li(n)e 4
+li(n)e500
diff --git a/corebinutils/ed/test/s2.t b/corebinutils/ed/test/s2.t
new file mode 100644
index 0000000000..f36584997c
--- /dev/null
+++ b/corebinutils/ed/test/s2.t
@@ -0,0 +1,4 @@
+,s/./(&)/3
+s/$/00
+2s//%/g
+s/^l
diff --git a/corebinutils/ed/test/s3.d b/corebinutils/ed/test/s3.d
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/corebinutils/ed/test/s3.d
diff --git a/corebinutils/ed/test/s3.err b/corebinutils/ed/test/s3.err
new file mode 100644
index 0000000000..d68c7d07f9
--- /dev/null
+++ b/corebinutils/ed/test/s3.err
@@ -0,0 +1 @@
+s/[xyx/a/
diff --git a/corebinutils/ed/test/s3.r b/corebinutils/ed/test/s3.r
new file mode 100644
index 0000000000..d6cada2212
--- /dev/null
+++ b/corebinutils/ed/test/s3.r
@@ -0,0 +1 @@
+hello world
diff --git a/corebinutils/ed/test/s3.t b/corebinutils/ed/test/s3.t
new file mode 100644
index 0000000000..fbf880304b
--- /dev/null
+++ b/corebinutils/ed/test/s3.t
@@ -0,0 +1,6 @@
+a
+hello/[]world
+.
+s/[/]/ /
+s/[[:digit:][]/ /
+s/[]]/ /
diff --git a/corebinutils/ed/test/s4.err b/corebinutils/ed/test/s4.err
new file mode 100644
index 0000000000..35b609fc62
--- /dev/null
+++ b/corebinutils/ed/test/s4.err
@@ -0,0 +1 @@
+s/\a\b\c/xyz/
diff --git a/corebinutils/ed/test/s5.err b/corebinutils/ed/test/s5.err
new file mode 100644
index 0000000000..89104c5523
--- /dev/null
+++ b/corebinutils/ed/test/s5.err
@@ -0,0 +1 @@
+s//xyz/
diff --git a/corebinutils/ed/test/s6.err b/corebinutils/ed/test/s6.err
new file mode 100644
index 0000000000..b4785957bc
--- /dev/null
+++ b/corebinutils/ed/test/s6.err
@@ -0,0 +1 @@
+s
diff --git a/corebinutils/ed/test/s7.err b/corebinutils/ed/test/s7.err
new file mode 100644
index 0000000000..30ba4fded7
--- /dev/null
+++ b/corebinutils/ed/test/s7.err
@@ -0,0 +1,5 @@
+a
+hello world
+.
+/./
+sr
diff --git a/corebinutils/ed/test/s8.err b/corebinutils/ed/test/s8.err
new file mode 100644
index 0000000000..5665767c3f
--- /dev/null
+++ b/corebinutils/ed/test/s8.err
@@ -0,0 +1,4 @@
+a
+hello
+.
+s/[h[=]/x/
diff --git a/corebinutils/ed/test/s9.err b/corebinutils/ed/test/s9.err
new file mode 100644
index 0000000000..1ff16dd847
--- /dev/null
+++ b/corebinutils/ed/test/s9.err
@@ -0,0 +1,4 @@
+a
+hello
+.
+s/[h[:]/x/
diff --git a/corebinutils/ed/test/t.d b/corebinutils/ed/test/t.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/t.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/t.r b/corebinutils/ed/test/t.r
new file mode 100644
index 0000000000..2b2854758d
--- /dev/null
+++ b/corebinutils/ed/test/t.r
@@ -0,0 +1,16 @@
+line 1
+line 1
+line 1
+line 2
+line 2
+line 3
+line 4
+line5
+line 1
+line 1
+line 1
+line 2
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/t1.d b/corebinutils/ed/test/t1.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/t1.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/t1.err b/corebinutils/ed/test/t1.err
new file mode 100644
index 0000000000..c49c556e0e
--- /dev/null
+++ b/corebinutils/ed/test/t1.err
@@ -0,0 +1 @@
+tt
diff --git a/corebinutils/ed/test/t1.r b/corebinutils/ed/test/t1.r
new file mode 100644
index 0000000000..2b2854758d
--- /dev/null
+++ b/corebinutils/ed/test/t1.r
@@ -0,0 +1,16 @@
+line 1
+line 1
+line 1
+line 2
+line 2
+line 3
+line 4
+line5
+line 1
+line 1
+line 1
+line 2
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/t1.t b/corebinutils/ed/test/t1.t
new file mode 100644
index 0000000000..6b66163793
--- /dev/null
+++ b/corebinutils/ed/test/t1.t
@@ -0,0 +1,3 @@
+1t0
+2,3t2
+,t$
diff --git a/corebinutils/ed/test/t2.d b/corebinutils/ed/test/t2.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/t2.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/t2.err b/corebinutils/ed/test/t2.err
new file mode 100644
index 0000000000..c202051b7f
--- /dev/null
+++ b/corebinutils/ed/test/t2.err
@@ -0,0 +1 @@
+t0;-1
diff --git a/corebinutils/ed/test/t2.r b/corebinutils/ed/test/t2.r
new file mode 100644
index 0000000000..0c75ff554c
--- /dev/null
+++ b/corebinutils/ed/test/t2.r
@@ -0,0 +1,6 @@
+line 1
+line5
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/t2.t b/corebinutils/ed/test/t2.t
new file mode 100644
index 0000000000..5175abdec9
--- /dev/null
+++ b/corebinutils/ed/test/t2.t
@@ -0,0 +1 @@
+t0;/./
diff --git a/corebinutils/ed/test/u.d b/corebinutils/ed/test/u.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/u.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/u.err b/corebinutils/ed/test/u.err
new file mode 100644
index 0000000000..caa1ba1148
--- /dev/null
+++ b/corebinutils/ed/test/u.err
@@ -0,0 +1 @@
+.u
diff --git a/corebinutils/ed/test/u.r b/corebinutils/ed/test/u.r
new file mode 100644
index 0000000000..ad558d82d0
--- /dev/null
+++ b/corebinutils/ed/test/u.r
@@ -0,0 +1,9 @@
+line 1
+hello
+hello world!!
+line 2
+line 3
+line 4
+line5
+hello
+hello world!!
diff --git a/corebinutils/ed/test/u.t b/corebinutils/ed/test/u.t
new file mode 100644
index 0000000000..131cb6e25c
--- /dev/null
+++ b/corebinutils/ed/test/u.t
@@ -0,0 +1,31 @@
+1;r u.t
+u
+a
+hello
+world
+.
+g/./s//x/\
+a\
+hello\
+world
+u
+u
+u
+a
+hello world!
+.
+u
+1,$d
+u
+2,3d
+u
+c
+hello world!!
+.
+u
+u
+-1;.,+1j
+u
+u
+u
+.,+1t$
diff --git a/corebinutils/ed/test/v.d b/corebinutils/ed/test/v.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/v.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/v.r b/corebinutils/ed/test/v.r
new file mode 100644
index 0000000000..714db63e35
--- /dev/null
+++ b/corebinutils/ed/test/v.r
@@ -0,0 +1,11 @@
+line5
+order
+hello world
+line 1
+order
+line 2
+order
+line 3
+order
+line 4
+order
diff --git a/corebinutils/ed/test/v.t b/corebinutils/ed/test/v.t
new file mode 100644
index 0000000000..608a77fb6a
--- /dev/null
+++ b/corebinutils/ed/test/v.t
@@ -0,0 +1,6 @@
+v/[ ]/m0
+v/[ ]/s/$/\
+hello world
+v/hello /s/lo/p!/\
+a\
+order
diff --git a/corebinutils/ed/test/w.d b/corebinutils/ed/test/w.d
new file mode 100644
index 0000000000..92f337e977
--- /dev/null
+++ b/corebinutils/ed/test/w.d
@@ -0,0 +1,5 @@
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/w.r b/corebinutils/ed/test/w.r
new file mode 100644
index 0000000000..ac152ba9d0
--- /dev/null
+++ b/corebinutils/ed/test/w.r
@@ -0,0 +1,10 @@
+line 1
+line 2
+line 3
+line 4
+line5
+line 1
+line 2
+line 3
+line 4
+line5
diff --git a/corebinutils/ed/test/w.t b/corebinutils/ed/test/w.t
new file mode 100644
index 0000000000..c2e18bd2f6
--- /dev/null
+++ b/corebinutils/ed/test/w.t
@@ -0,0 +1,2 @@
+w !cat >\!.z
+r \!.z
diff --git a/corebinutils/ed/test/w1.err b/corebinutils/ed/test/w1.err
new file mode 100644
index 0000000000..e2c8a603f7
--- /dev/null
+++ b/corebinutils/ed/test/w1.err
@@ -0,0 +1 @@
+w /to/some/far-away/place
diff --git a/corebinutils/ed/test/w2.err b/corebinutils/ed/test/w2.err
new file mode 100644
index 0000000000..9daf89cfa7
--- /dev/null
+++ b/corebinutils/ed/test/w2.err
@@ -0,0 +1 @@
+ww.o
diff --git a/corebinutils/ed/test/w3.err b/corebinutils/ed/test/w3.err
new file mode 100644
index 0000000000..39bbf4c95b
--- /dev/null
+++ b/corebinutils/ed/test/w3.err
@@ -0,0 +1 @@
+wqp w.o
diff --git a/corebinutils/ed/test/x.err b/corebinutils/ed/test/x.err
new file mode 100644
index 0000000000..0953f01dd0
--- /dev/null
+++ b/corebinutils/ed/test/x.err
@@ -0,0 +1 @@
+.x
diff --git a/corebinutils/ed/test/z.err b/corebinutils/ed/test/z.err
new file mode 100644
index 0000000000..6a51a2d583
--- /dev/null
+++ b/corebinutils/ed/test/z.err
@@ -0,0 +1,2 @@
+z
+z
diff --git a/corebinutils/ed/tests/test.sh b/corebinutils/ed/tests/test.sh
new file mode 100644
index 0000000000..a2a8b11550
--- /dev/null
+++ b/corebinutils/ed/tests/test.sh
@@ -0,0 +1,180 @@
+#!/bin/sh
+set -eu
+
+ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+ED_BIN=${ED_BIN:-"$ROOT/out/ed"}
+CC=${CC:-cc}
+TMPDIR=${TMPDIR:-/tmp}
+WORKDIR=$(mktemp -d "$TMPDIR/ed-test.XXXXXX")
+REGRESS_DIR="$WORKDIR/regress"
+UUDECODE_BIN="$WORKDIR/uu_decode"
+RED_BIN="$WORKDIR/red"
+trap 'rm -rf "$WORKDIR"' EXIT INT TERM
+export LC_ALL=C
+
+fail() {
+ printf '%s\n' "FAIL: $1" >&2
+ exit 1
+}
+
+assert_eq() {
+ name=$1
+ expected=$2
+ actual=$3
+ if [ "$expected" != "$actual" ]; then
+ printf '%s\n' "FAIL: $name" >&2
+ printf '%s\n' "--- expected ---" >&2
+ printf '%s\n' "$expected" >&2
+ printf '%s\n' "--- actual ---" >&2
+ printf '%s\n' "$actual" >&2
+ exit 1
+ fi
+}
+
+assert_contains() {
+ name=$1
+ text=$2
+ pattern=$3
+ case $text in
+ *"$pattern"*) ;;
+ *) fail "$name" ;;
+ esac
+}
+
+run_capture() {
+ output_file=$1
+ status_file=$2
+ shift 2
+
+ set +e
+ "$@" >"$output_file" 2>&1
+ status=$?
+ set -e
+ printf '%s\n' "$status" >"$status_file"
+}
+
+build_uudecode() {
+ "$CC" -O2 -std=c17 -Wall -Wextra -Werror \
+ "$ROOT/tests/uu_decode.c" -o "$UUDECODE_BIN" \
+ || fail "failed to build uu decoder with $CC"
+}
+
+decode_regression_fixtures() {
+ "$UUDECODE_BIN" "$REGRESS_DIR/ascii.d.uu" "$REGRESS_DIR/ascii.d" \
+ || fail "failed to decode ascii.d.uu"
+ "$UUDECODE_BIN" "$REGRESS_DIR/ascii.r.uu" "$REGRESS_DIR/ascii.r" \
+ || fail "failed to decode ascii.r.uu"
+}
+
+run_regression_suite() {
+ known_issue='*** The script nl.red exited abnormally ***'
+
+ cp "$ROOT"/test/* "$REGRESS_DIR"/
+ decode_regression_fixtures
+
+ (
+ cd "$REGRESS_DIR"
+ sh ./mkscripts.sh "$ED_BIN" >/dev/null
+ sh ./ckscripts.sh "$ED_BIN" >/dev/null 2>&1 || true
+ ) || fail "upstream regression harness failed to run"
+
+ issues=$(cd "$REGRESS_DIR" && grep -h '\*\*\*' errs.o scripts.o || true)
+ if [ -n "$issues" ]; then
+ filtered_issues=$(printf '%s\n' "$issues" | grep -Fvx "$known_issue" || true)
+ [ -z "$filtered_issues" ] || fail "upstream regression failures detected:
+$filtered_issues"
+ fi
+}
+
+[ -x "$ED_BIN" ] || fail "missing binary: $ED_BIN"
+mkdir -p "$REGRESS_DIR"
+ln -sf "$ED_BIN" "$RED_BIN"
+build_uudecode
+
+usage_output=$("$ED_BIN" -Z 2>&1 || true)
+assert_contains "invalid option should print usage" "$usage_output" \
+ "usage: ed [-] [-sx] [-p string] [file]"
+
+crypt_option_output=$("$ED_BIN" -x 2>&1 || true)
+assert_contains "unsupported -x option should be explicit" "$crypt_option_output" \
+ "option -x is not supported on Linux"
+
+crypt_command_output=$(printf 'H\nx\nQ\n' | "$ED_BIN" -s 2>&1 || true)
+assert_contains "interactive x command should be explicit" "$crypt_command_output" \
+ "crypt mode is not supported on Linux"
+
+cat >"$WORKDIR/strict-input.txt" <<'EOF'
+line 1
+line 2
+EOF
+
+strict_append_output=$(cat <<EOF | "$ED_BIN" -s 2>&1 || true
+H
+r $WORKDIR/strict-input.txt
+1,\$a
+bad
+.
+Q
+EOF
+)
+assert_contains "append should reject address ranges" "$strict_append_output" \
+ "unexpected address"
+
+strict_read_output=$(cat <<EOF | "$ED_BIN" -s 2>&1 || true
+H
+r $WORKDIR/strict-input.txt
+1,\$r $WORKDIR/strict-input.txt
+Q
+EOF
+)
+assert_contains "read should reject address ranges" "$strict_read_output" \
+ "unexpected address"
+
+no_filename_output=$(printf 'H\nf\nQ\n' | "$ED_BIN" -s 2>&1 || true)
+assert_contains "f without current filename should fail" "$no_filename_output" \
+ "no current filename"
+
+cat <<EOF | "$ED_BIN" -s >/dev/null
+H
+r !printf "shell line\n"
+w $WORKDIR/shell-read.out
+Q
+EOF
+assert_eq "shell read output" "shell line" "$(cat "$WORKDIR/shell-read.out")"
+
+cat >"$WORKDIR/write-input.txt" <<'EOF'
+alpha
+beta
+EOF
+cat <<EOF | "$ED_BIN" -s >/dev/null
+H
+r $WORKDIR/write-input.txt
+w !cat > $WORKDIR/shell-write.out
+Q
+EOF
+assert_eq "shell write output" "$(cat "$WORKDIR/write-input.txt")" \
+ "$(cat "$WORKDIR/shell-write.out")"
+
+mkdir "$WORKDIR/scratch"
+printf 'a\nscratch check\n.\nQ\n' | env TMPDIR="$WORKDIR/scratch" \
+ "$ED_BIN" -s >/dev/null 2>&1
+[ -z "$(find "$WORKDIR/scratch" -mindepth 1 -maxdepth 1 -print)" ] \
+ || fail "scratch files were not cleaned up in TMPDIR"
+
+run_capture "$WORKDIR/red-shell.out" "$WORKDIR/red-shell.status" \
+ sh -c 'printf "H\n!true\nQ\n" | "$1" -s' sh "$RED_BIN"
+assert_eq "red shell restriction exit status" "2" \
+ "$(cat "$WORKDIR/red-shell.status")"
+assert_contains "red shell restriction message" \
+ "$(cat "$WORKDIR/red-shell.out")" "shell access restricted"
+
+run_capture "$WORKDIR/red-path.out" "$WORKDIR/red-path.status" \
+ sh -c 'printf "H\ne /tmp/blocked\nQ\n" | "$1" -s' sh "$RED_BIN"
+assert_eq "red path restriction exit status" "2" \
+ "$(cat "$WORKDIR/red-path.status")"
+assert_contains "red path restriction message" \
+ "$(cat "$WORKDIR/red-path.out")" "shell access restricted"
+
+run_regression_suite
+
+printf '%s\n' "PASS"
diff --git a/corebinutils/ed/tests/uu_decode.c b/corebinutils/ed/tests/uu_decode.c
new file mode 100644
index 0000000000..8f5b8e8a3d
--- /dev/null
+++ b/corebinutils/ed/tests/uu_decode.c
@@ -0,0 +1,132 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static int
+decode_char(int c)
+{
+ return (c - 32) & 0x3f;
+}
+
+static int
+decode_line(const char *line, FILE *out)
+{
+ int length;
+ int produced;
+
+ length = decode_char((unsigned char)line[0]);
+ if (length == 0)
+ return 0;
+
+ line++;
+ produced = 0;
+ while (produced < length) {
+ unsigned char a;
+ unsigned char b;
+ unsigned char c;
+ unsigned char d;
+ unsigned char bytes[3];
+ int chunk;
+
+ if (line[0] == '\0' || line[1] == '\0' || line[2] == '\0' ||
+ line[3] == '\0') {
+ fprintf(stderr, "uu_decode: truncated uuencoded line\n");
+ return -1;
+ }
+
+ a = (unsigned char)decode_char((unsigned char)line[0]);
+ b = (unsigned char)decode_char((unsigned char)line[1]);
+ c = (unsigned char)decode_char((unsigned char)line[2]);
+ d = (unsigned char)decode_char((unsigned char)line[3]);
+ line += 4;
+
+ bytes[0] = (unsigned char)((a << 2) | (b >> 4));
+ bytes[1] = (unsigned char)((b << 4) | (c >> 2));
+ bytes[2] = (unsigned char)((c << 6) | d);
+
+ chunk = length - produced;
+ if (chunk > 3)
+ chunk = 3;
+ if (fwrite(bytes, 1, (size_t)chunk, out) != (size_t)chunk) {
+ fprintf(stderr, "uu_decode: write failed: %s\n",
+ strerror(errno));
+ return -1;
+ }
+ produced += chunk;
+ }
+
+ return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+ FILE *in;
+ FILE *out;
+ char line[4096];
+ int seen_begin;
+
+ if (argc != 3) {
+ fprintf(stderr, "usage: %s input.uu output\n", argv[0]);
+ return 1;
+ }
+
+ in = fopen(argv[1], "r");
+ if (in == NULL) {
+ fprintf(stderr, "uu_decode: cannot open %s: %s\n", argv[1],
+ strerror(errno));
+ return 1;
+ }
+ out = fopen(argv[2], "wb");
+ if (out == NULL) {
+ fprintf(stderr, "uu_decode: cannot open %s: %s\n", argv[2],
+ strerror(errno));
+ fclose(in);
+ return 1;
+ }
+
+ seen_begin = 0;
+ while (fgets(line, sizeof(line), in) != NULL) {
+ size_t len;
+
+ len = strlen(line);
+ if (len > 0 && line[len - 1] == '\n')
+ line[len - 1] = '\0';
+
+ if (!seen_begin) {
+ if (strncmp(line, "begin ", 6) == 0)
+ seen_begin = 1;
+ continue;
+ }
+ if (strcmp(line, "end") == 0)
+ break;
+ if (decode_line(line, out) != 0) {
+ fclose(out);
+ fclose(in);
+ return 1;
+ }
+ }
+
+ if (fclose(out) != 0) {
+ fprintf(stderr, "uu_decode: cannot close %s: %s\n", argv[2],
+ strerror(errno));
+ fclose(in);
+ return 1;
+ }
+ if (fclose(in) != 0) {
+ fprintf(stderr, "uu_decode: cannot close %s: %s\n", argv[1],
+ strerror(errno));
+ return 1;
+ }
+ if (!seen_begin) {
+ fprintf(stderr, "uu_decode: missing begin line in %s\n", argv[1]);
+ return 1;
+ }
+
+ return 0;
+}
diff --git a/corebinutils/ed/undo.c b/corebinutils/ed/undo.c
new file mode 100644
index 0000000000..1770834822
--- /dev/null
+++ b/corebinutils/ed/undo.c
@@ -0,0 +1,150 @@
+/* undo.c: This file contains the undo routines for the ed line editor */
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 1993 Andrew Moore, Talke Studio.
+ * Copyright (c) 2026 Project Tick.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include "ed.h"
+
+
+#define USIZE 100 /* undo stack size */
+static undo_t *ustack = NULL; /* undo stack */
+static long usize = 0; /* stack size variable */
+static long u_p = 0; /* undo stack pointer */
+
+/* push_undo_stack: return pointer to initialized undo node */
+undo_t *
+push_undo_stack(int type, long from, long to)
+{
+ undo_t *t;
+
+#if defined(sun) || defined(NO_REALLOC_NULL)
+ if (ustack == NULL &&
+ (ustack = (undo_t *) malloc((usize = USIZE) * sizeof(undo_t))) == NULL) {
+ fprintf(stderr, "%s\n", strerror(errno));
+ errmsg = "out of memory";
+ return NULL;
+ }
+#endif
+ t = ustack;
+ if (u_p < usize ||
+ (t = (undo_t *) realloc(ustack, (usize += USIZE) * sizeof(undo_t))) != NULL) {
+ ustack = t;
+ ustack[u_p].type = type;
+ ustack[u_p].t = get_addressed_line_node(to);
+ ustack[u_p].h = get_addressed_line_node(from);
+ return ustack + u_p++;
+ }
+ /* out of memory - release undo stack */
+ fprintf(stderr, "%s\n", strerror(errno));
+ errmsg = "out of memory";
+ clear_undo_stack();
+ free(ustack);
+ ustack = NULL;
+ usize = 0;
+ return NULL;
+}
+
+
+/* USWAP: swap undo nodes */
+#define USWAP(x,y) { \
+ undo_t utmp; \
+ utmp = x, x = y, y = utmp; \
+}
+
+
+long u_current_addr = -1; /* if >= 0, undo enabled */
+long u_addr_last = -1; /* if >= 0, undo enabled */
+
+/* pop_undo_stack: undo last change to the editor buffer */
+int
+pop_undo_stack(void)
+{
+ long n;
+ long o_current_addr = current_addr;
+ long o_addr_last = addr_last;
+
+ if (u_current_addr == -1 || u_addr_last == -1) {
+ errmsg = "nothing to undo";
+ return ERR;
+ } else if (u_p)
+ modified = 1;
+ get_addressed_line_node(0); /* this get_addressed_line_node last! */
+ SPL1();
+ for (n = u_p; n-- > 0;) {
+ switch(ustack[n].type) {
+ case UADD:
+ REQUE(ustack[n].h->q_back, ustack[n].t->q_forw);
+ break;
+ case UDEL:
+ REQUE(ustack[n].h->q_back, ustack[n].h);
+ REQUE(ustack[n].t, ustack[n].t->q_forw);
+ break;
+ case UMOV:
+ case VMOV:
+ REQUE(ustack[n - 1].h, ustack[n].h->q_forw);
+ REQUE(ustack[n].t->q_back, ustack[n - 1].t);
+ REQUE(ustack[n].h, ustack[n].t);
+ n--;
+ break;
+ default:
+ /*NOTREACHED*/
+ ;
+ }
+ ustack[n].type ^= 1;
+ }
+ /* reverse undo stack order */
+ for (n = u_p; n-- > (u_p + 1)/ 2;)
+ USWAP(ustack[n], ustack[u_p - 1 - n]);
+ if (isglobal)
+ clear_active_list();
+ current_addr = u_current_addr, u_current_addr = o_current_addr;
+ addr_last = u_addr_last, u_addr_last = o_addr_last;
+ SPL0();
+ return 0;
+}
+
+
+/* clear_undo_stack: clear the undo stack */
+void
+clear_undo_stack(void)
+{
+ line_t *lp, *ep, *tl;
+
+ while (u_p--)
+ if (ustack[u_p].type == UDEL) {
+ ep = ustack[u_p].t->q_forw;
+ for (lp = ustack[u_p].h; lp != ep; lp = tl) {
+ unmark_line_node(lp);
+ tl = lp->q_forw;
+ free(lp);
+ }
+ }
+ u_p = 0;
+ u_current_addr = current_addr;
+ u_addr_last = addr_last;
+}