summaryrefslogtreecommitdiff
path: root/util/sbase
diff options
context:
space:
mode:
Diffstat (limited to 'util/sbase')
-rw-r--r--util/sbase/.gitignore103
-rw-r--r--util/sbase/LICENSE63
-rw-r--r--util/sbase/Makefile256
-rw-r--r--util/sbase/README147
-rw-r--r--util/sbase/TODO92
-rw-r--r--util/sbase/arg.h65
-rw-r--r--util/sbase/basename.122
-rw-r--r--util/sbase/basename.c37
-rw-r--r--util/sbase/cal.168
-rw-r--r--util/sbase/cal.c226
-rw-r--r--util/sbase/cat.127
-rw-r--r--util/sbase/cat.c52
-rw-r--r--util/sbase/chgrp.147
-rw-r--r--util/sbase/chgrp.c75
-rw-r--r--util/sbase/chmod.174
-rw-r--r--util/sbase/chmod.c77
-rw-r--r--util/sbase/chown.157
-rw-r--r--util/sbase/chown.c104
-rw-r--r--util/sbase/chroot.125
-rw-r--r--util/sbase/chroot.c49
-rw-r--r--util/sbase/cksum.124
-rw-r--r--util/sbase/cksum.c132
-rw-r--r--util/sbase/cmp.149
-rw-r--r--util/sbase/cmp.c82
-rw-r--r--util/sbase/cols.156
-rw-r--r--util/sbase/cols.c98
-rw-r--r--util/sbase/comm.140
-rw-r--r--util/sbase/comm.c97
-rw-r--r--util/sbase/compat.h6
-rw-r--r--util/sbase/config.mk15
-rw-r--r--util/sbase/cp.171
-rw-r--r--util/sbase/cp.c63
-rw-r--r--util/sbase/cron.123
-rw-r--r--util/sbase/cron.c566
-rw-r--r--util/sbase/crypt.h12
-rw-r--r--util/sbase/cut.169
-rw-r--r--util/sbase/cut.c215
-rw-r--r--util/sbase/date.181
-rw-r--r--util/sbase/date.c103
-rw-r--r--util/sbase/dd.191
-rw-r--r--util/sbase/dd.c237
-rw-r--r--util/sbase/dirname.119
-rw-r--r--util/sbase/dirname.c27
-rw-r--r--util/sbase/du.157
-rw-r--r--util/sbase/du.c167
-rw-r--r--util/sbase/echo.127
-rw-r--r--util/sbase/echo.c24
-rw-r--r--util/sbase/ed.1238
-rw-r--r--util/sbase/ed.c1650
-rw-r--r--util/sbase/env.147
-rw-r--r--util/sbase/env.c49
-rw-r--r--util/sbase/expand.147
-rw-r--r--util/sbase/expand.c131
-rw-r--r--util/sbase/expr.1101
-rw-r--r--util/sbase/expr.c244
-rw-r--r--util/sbase/false.113
-rw-r--r--util/sbase/false.c6
-rw-r--r--util/sbase/find.1151
-rw-r--r--util/sbase/find.c1103
-rw-r--r--util/sbase/flock.135
-rw-r--r--util/sbase/flock.c82
-rw-r--r--util/sbase/fold.139
-rw-r--r--util/sbase/fold.c130
-rw-r--r--util/sbase/fs.h47
-rw-r--r--util/sbase/getconf.157
-rw-r--r--util/sbase/getconf.c108
-rw-r--r--util/sbase/grep.194
-rw-r--r--util/sbase/grep.c290
-rw-r--r--util/sbase/head.140
-rw-r--r--util/sbase/head.c77
-rw-r--r--util/sbase/hostname.118
-rw-r--r--util/sbase/hostname.c36
-rw-r--r--util/sbase/join.1105
-rw-r--r--util/sbase/join.c529
-rw-r--r--util/sbase/kill.139
-rw-r--r--util/sbase/kill.c131
-rw-r--r--util/sbase/libutf/Makefile6
-rw-r--r--util/sbase/libutf/fgetrune.c36
-rw-r--r--util/sbase/libutf/fputrune.c27
-rw-r--r--util/sbase/libutf/isalnumrune.c9
-rw-r--r--util/sbase/libutf/isalpharune.c830
-rw-r--r--util/sbase/libutf/isblankrune.c9
-rw-r--r--util/sbase/libutf/iscntrlrune.c18
-rw-r--r--util/sbase/libutf/isdigitrune.c80
-rw-r--r--util/sbase/libutf/isgraphrune.c9
-rw-r--r--util/sbase/libutf/isprintrune.c10
-rw-r--r--util/sbase/libutf/ispunctrune.c9
-rw-r--r--util/sbase/libutf/isspacerune.c31
-rw-r--r--util/sbase/libutf/istitlerune.c31
-rw-r--r--util/sbase/libutf/isxdigitrune.c9
-rw-r--r--util/sbase/libutf/lowerrune.c356
-rw-r--r--util/sbase/libutf/mkrunetype.awk240
-rw-r--r--util/sbase/libutf/rune.c148
-rw-r--r--util/sbase/libutf/runetype.c41
-rw-r--r--util/sbase/libutf/runetype.h26
-rw-r--r--util/sbase/libutf/upperrune.c265
-rw-r--r--util/sbase/libutf/utf.c142
-rw-r--r--util/sbase/libutf/utftorunestr.c27
-rw-r--r--util/sbase/libutil/concat.c23
-rw-r--r--util/sbase/libutil/confirm.c22
-rw-r--r--util/sbase/libutil/cp.c176
-rw-r--r--util/sbase/libutil/crypt.c184
-rw-r--r--util/sbase/libutil/ealloc.c88
-rw-r--r--util/sbase/libutil/enmasse.c38
-rw-r--r--util/sbase/libutil/eprintf.c57
-rw-r--r--util/sbase/libutil/eregcomp.c27
-rw-r--r--util/sbase/libutil/estrtod.c18
-rw-r--r--util/sbase/libutil/fnck.c22
-rw-r--r--util/sbase/libutil/fshut.c43
-rw-r--r--util/sbase/libutil/getlines.c32
-rw-r--r--util/sbase/libutil/human.c25
-rw-r--r--util/sbase/libutil/linecmp.c17
-rw-r--r--util/sbase/libutil/md5.c148
-rw-r--r--util/sbase/libutil/memmem.c66
-rw-r--r--util/sbase/libutil/mkdirp.c39
-rw-r--r--util/sbase/libutil/mode.c152
-rw-r--r--util/sbase/libutil/parseoffset.c61
-rw-r--r--util/sbase/libutil/putword.c16
-rw-r--r--util/sbase/libutil/reallocarray.c56
-rw-r--r--util/sbase/libutil/recurse.c108
-rw-r--r--util/sbase/libutil/rm.c49
-rw-r--r--util/sbase/libutil/sha1.c144
-rw-r--r--util/sbase/libutil/sha224.c26
-rw-r--r--util/sbase/libutil/sha256.c154
-rw-r--r--util/sbase/libutil/sha384.c26
-rw-r--r--util/sbase/libutil/sha512-224.c26
-rw-r--r--util/sbase/libutil/sha512-256.c26
-rw-r--r--util/sbase/libutil/sha512.c175
-rw-r--r--util/sbase/libutil/strcasestr.c38
-rw-r--r--util/sbase/libutil/strlcat.c63
-rw-r--r--util/sbase/libutil/strlcpy.c59
-rw-r--r--util/sbase/libutil/strnsubst.c61
-rw-r--r--util/sbase/libutil/strsep.c37
-rw-r--r--util/sbase/libutil/strtonum.c85
-rw-r--r--util/sbase/libutil/unescape.c58
-rw-r--r--util/sbase/libutil/writeall.c21
-rw-r--r--util/sbase/link.116
-rw-r--r--util/sbase/link.c27
-rw-r--r--util/sbase/ln.161
-rw-r--r--util/sbase/ln.c103
-rw-r--r--util/sbase/logger.152
-rw-r--r--util/sbase/logger.c91
-rw-r--r--util/sbase/logname.113
-rw-r--r--util/sbase/logname.c29
-rw-r--r--util/sbase/ls.196
-rw-r--r--util/sbase/ls.c489
-rw-r--r--util/sbase/md5.h18
-rw-r--r--util/sbase/md5sum.132
-rw-r--r--util/sbase/md5sum.c46
-rw-r--r--util/sbase/mkdir.134
-rw-r--r--util/sbase/mkdir.c49
-rw-r--r--util/sbase/mkfifo.128
-rw-r--r--util/sbase/mkfifo.c39
-rw-r--r--util/sbase/mknod.144
-rw-r--r--util/sbase/mknod.c72
-rw-r--r--util/sbase/mktemp.150
-rw-r--r--util/sbase/mktemp.c92
-rw-r--r--util/sbase/mv.136
-rw-r--r--util/sbase/mv.c68
-rw-r--r--util/sbase/nice.136
-rw-r--r--util/sbase/nice.c56
-rw-r--r--util/sbase/nl.1116
-rw-r--r--util/sbase/nl.c212
-rw-r--r--util/sbase/nohup.140
-rw-r--r--util/sbase/nohup.c48
-rw-r--r--util/sbase/od.180
-rw-r--r--util/sbase/od.c332
-rw-r--r--util/sbase/paste.147
-rw-r--r--util/sbase/paste.c144
-rw-r--r--util/sbase/pathchk.131
-rw-r--r--util/sbase/pathchk.c104
-rw-r--r--util/sbase/printenv.130
-rw-r--r--util/sbase/printenv.c39
-rw-r--r--util/sbase/printf.133
-rw-r--r--util/sbase/printf.c188
-rw-r--r--util/sbase/pwd.129
-rw-r--r--util/sbase/pwd.c50
-rw-r--r--util/sbase/queue.h648
-rw-r--r--util/sbase/readlink.132
-rw-r--r--util/sbase/readlink.c54
-rw-r--r--util/sbase/renice.138
-rw-r--r--util/sbase/renice.c93
-rw-r--r--util/sbase/rev.122
-rw-r--r--util/sbase/rev.c74
-rw-r--r--util/sbase/rm.137
-rw-r--r--util/sbase/rm.c87
-rw-r--r--util/sbase/rmdir.129
-rw-r--r--util/sbase/rmdir.c49
-rwxr-xr-xutil/sbase/scripts/getconf.sh218
-rwxr-xr-xutil/sbase/scripts/install21
-rwxr-xr-xutil/sbase/scripts/mkbox103
-rwxr-xr-xutil/sbase/scripts/mkproto24
-rwxr-xr-xutil/sbase/scripts/uninstall32
-rw-r--r--util/sbase/sed.1173
-rw-r--r--util/sbase/sed.c1738
-rw-r--r--util/sbase/seq.140
-rw-r--r--util/sbase/seq.c147
-rw-r--r--util/sbase/setsid.118
-rw-r--r--util/sbase/setsid.c48
-rw-r--r--util/sbase/sha1.h18
-rw-r--r--util/sbase/sha1sum.132
-rw-r--r--util/sbase/sha1sum.c45
-rw-r--r--util/sbase/sha224.h16
-rw-r--r--util/sbase/sha224sum.132
-rw-r--r--util/sbase/sha224sum.c45
-rw-r--r--util/sbase/sha256.h18
-rw-r--r--util/sbase/sha256sum.132
-rw-r--r--util/sbase/sha256sum.c45
-rw-r--r--util/sbase/sha384.h16
-rw-r--r--util/sbase/sha384sum.132
-rw-r--r--util/sbase/sha384sum.c45
-rw-r--r--util/sbase/sha512-224.h16
-rw-r--r--util/sbase/sha512-224sum.132
-rw-r--r--util/sbase/sha512-224sum.c45
-rw-r--r--util/sbase/sha512-256.h16
-rw-r--r--util/sbase/sha512-256sum.132
-rw-r--r--util/sbase/sha512-256sum.c45
-rw-r--r--util/sbase/sha512.h18
-rw-r--r--util/sbase/sha512sum.132
-rw-r--r--util/sbase/sha512sum.c45
-rw-r--r--util/sbase/sleep.118
-rw-r--r--util/sbase/sleep.c30
-rw-r--r--util/sbase/sort.198
-rw-r--r--util/sbase/sort.c437
-rw-r--r--util/sbase/split.146
-rw-r--r--util/sbase/split.c111
-rw-r--r--util/sbase/sponge.119
-rw-r--r--util/sbase/sponge.c42
-rw-r--r--util/sbase/strings.150
-rw-r--r--util/sbase/strings.c98
-rw-r--r--util/sbase/sync.117
-rw-r--r--util/sbase/sync.c25
-rw-r--r--util/sbase/tail.151
-rw-r--r--util/sbase/tail.c229
-rw-r--r--util/sbase/tar.176
-rw-r--r--util/sbase/tar.c662
-rw-r--r--util/sbase/tee.126
-rw-r--r--util/sbase/tee.c60
-rw-r--r--util/sbase/test.1131
-rw-r--r--util/sbase/test.c247
-rw-r--r--util/sbase/text.h16
-rw-r--r--util/sbase/tftp.132
-rw-r--r--util/sbase/tftp.c309
-rw-r--r--util/sbase/time.145
-rw-r--r--util/sbase/time.c73
-rw-r--r--util/sbase/touch.163
-rw-r--r--util/sbase/touch.c159
-rw-r--r--util/sbase/tr.184
-rw-r--r--util/sbase/tr.c300
-rw-r--r--util/sbase/true.113
-rw-r--r--util/sbase/true.c6
-rw-r--r--util/sbase/tsort.170
-rw-r--r--util/sbase/tsort.c209
-rw-r--r--util/sbase/tty.124
-rw-r--r--util/sbase/tty.c31
-rw-r--r--util/sbase/uname.135
-rw-r--r--util/sbase/uname.c62
-rw-r--r--util/sbase/unexpand.141
-rw-r--r--util/sbase/unexpand.c174
-rw-r--r--util/sbase/uniq.145
-rw-r--r--util/sbase/uniq.c144
-rw-r--r--util/sbase/unlink.119
-rw-r--r--util/sbase/unlink.c27
-rw-r--r--util/sbase/utf.h69
-rw-r--r--util/sbase/util.h97
-rw-r--r--util/sbase/uudecode.146
-rw-r--r--util/sbase/uudecode.c282
-rw-r--r--util/sbase/uuencode.134
-rw-r--r--util/sbase/uuencode.c129
-rw-r--r--util/sbase/wc.128
-rw-r--r--util/sbase/wc.c122
-rw-r--r--util/sbase/which.144
-rw-r--r--util/sbase/which.c101
-rw-r--r--util/sbase/whoami.19
-rw-r--r--util/sbase/whoami.c37
-rw-r--r--util/sbase/xargs.1121
-rw-r--r--util/sbase/xargs.c362
-rw-r--r--util/sbase/xinstall.186
-rw-r--r--util/sbase/xinstall.c194
-rw-r--r--util/sbase/yes.114
-rw-r--r--util/sbase/yes.c25
281 files changed, 28415 insertions, 0 deletions
diff --git a/util/sbase/.gitignore b/util/sbase/.gitignore
new file mode 100644
index 00000000..247ecd36
--- /dev/null
+++ b/util/sbase/.gitignore
@@ -0,0 +1,103 @@
+*.o
+/build
+/getconf.h
+/libutf.a
+/libutil.a
+/basename
+/cal
+/cat
+/chgrp
+/chmod
+/chown
+/chroot
+/cksum
+/cmp
+/cols
+/comm
+/cp
+/cron
+/cut
+/date
+/dd
+/dirname
+/du
+/echo
+/ed
+/env
+/expand
+/expr
+/false
+/find
+/flock
+/fold
+/getconf
+/grep
+/head
+/hostname
+/join
+/kill
+/link
+/ln
+/logger
+/logname
+/ls
+/md5sum
+/mkdir
+/mkfifo
+/mknod
+/mktemp
+/mv
+/nice
+/nl
+/nohup
+/od
+/paste
+/pathchk
+/printenv
+/printf
+/pwd
+/readlink
+/renice
+/rev
+/rm
+/rmdir
+/sbase-box
+/sed
+/seq
+/setsid
+/sha1sum
+/sha224sum
+/sha256sum
+/sha384sum
+/sha512sum
+/sha512-224sum
+/sha512-256sum
+/sleep
+/sort
+/split
+/sponge
+/strings
+/sync
+/tail
+/tar
+/tee
+/test
+/tftp
+/time
+/touch
+/tr
+/true
+/tsort
+/tty
+/uname
+/unexpand
+/uniq
+/unlink
+/uudecode
+/uuencode
+/wc
+/which
+/whoami
+/xargs
+/xinstall
+/yes
diff --git a/util/sbase/LICENSE b/util/sbase/LICENSE
new file mode 100644
index 00000000..bbbfa8c5
--- /dev/null
+++ b/util/sbase/LICENSE
@@ -0,0 +1,63 @@
+MIT License
+
+© 2011 Connor Lane Smith <cls@lubutu.com>
+© 2011-2016 Dimitris Papastamos <sin@2f30.org>
+© 2014-2016 Laslo Hunhold <dev@frign.de>
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+
+Authors/contributors include:
+
+© 2011 Kamil Cholewiński <harry666t@gmail.com>
+© 2011 Rob Pilling <robpilling@gmail.com>
+© 2011 Hiltjo Posthuma <hiltjo@codemadness.org>
+© 2011 pancake <pancake@youterm.com>
+© 2011 Random832 <random832@fastmail.us>
+© 2012 William Haddon <william@haddonthethird.net>
+© 2012 Kurt H. Maier <khm@sciops.net>
+© 2012 Christoph Lohmann <20h@r-36.net>
+© 2012 David Galos <galosd83@students.rowan.edu>
+© 2012 Robert Ransom <rransom.8774@gmail.com>
+© 2013 Jakob Kramer <jakob.kramer@gmx.de>
+© 2013 Anselm R Garbe <anselm@garbe.us>
+© 2013 Truls Becken <truls.becken@gmail.com>
+© 2013 dsp <dsp@2f30.org>
+© 2013 Markus Teich <markus.teich@stusta.mhn.de>
+© 2013 Jesse Ogle <jesse.p.ogle@gmail.com>
+© 2013 Lorenzo Cogotti <miciamail@hotmail.it>
+© 2013 Federico G. Benavento <benavento@gmail.com>
+© 2013 Roberto E. Vargas Caballero <k0ga@shike2.com>
+© 2013 Christian Hesse <mail@eworm.de>
+© 2013 Markus Wichmann <nullplan@gmx.net>
+© 2014 Silvan Jegen <s.jegen@gmail.com>
+© 2014 Daniel Bainton <dpb@driftaway.org>
+© 2014 Tuukka Kataja <stuge@xor.fi>
+© 2014 Jeffrey Picard <jeff@jeffreypicard.com>
+© 2014 Evan Gates <evan.gates@gmail.com>
+© 2014 Michael Forney <mforney@mforney.org>
+© 2014 Ari Malinen <ari.malinen@gmail.com>
+© 2014 Brandon Mulcahy <brandon@jangler.info>
+© 2014 Adria Garriga <rhaps0dy@installgentoo.com>
+© 2014-2015 Greg Reagle <greg.reagle@umbc.edu>
+© 2015 Tai Chi Minh Ralph Eastwood <tcmreastwood@gmail.com>
+© 2015 Quentin Rameau <quinq@quinq.eu.org>
+© 2015 Dionysis Grigoropoulos <info@erethon.com>
+© 2015 Wolfgang Corcoran-Mathe <wcm@sigwinch.xyz>
+© 2016 Mattias Andrée <maandree@kth.se>
+© 2016 Eivind Uggedal <eivind@uggedal.com>
diff --git a/util/sbase/Makefile b/util/sbase/Makefile
new file mode 100644
index 00000000..2d409ff3
--- /dev/null
+++ b/util/sbase/Makefile
@@ -0,0 +1,256 @@
+.POSIX:
+include config.mk
+
+.SUFFIXES:
+.SUFFIXES: .o .c
+
+CPPFLAGS =\
+ -D_DEFAULT_SOURCE \
+ -D_NETBSD_SOURCE \
+ -D_BSD_SOURCE \
+ -D_XOPEN_SOURCE=700 \
+ -D_FILE_OFFSET_BITS=64
+
+HDR =\
+ arg.h\
+ compat.h\
+ crypt.h\
+ fs.h\
+ md5.h\
+ queue.h\
+ sha1.h\
+ sha224.h\
+ sha256.h\
+ sha384.h\
+ sha512.h\
+ sha512-224.h\
+ sha512-256.h\
+ text.h\
+ utf.h\
+ util.h
+
+LIBUTFOBJ =\
+ libutf/fgetrune.o\
+ libutf/fputrune.o\
+ libutf/isalnumrune.o\
+ libutf/isalpharune.o\
+ libutf/isblankrune.o\
+ libutf/iscntrlrune.o\
+ libutf/isdigitrune.o\
+ libutf/isgraphrune.o\
+ libutf/isprintrune.o\
+ libutf/ispunctrune.o\
+ libutf/isspacerune.o\
+ libutf/istitlerune.o\
+ libutf/isxdigitrune.o\
+ libutf/lowerrune.o\
+ libutf/rune.o\
+ libutf/runetype.o\
+ libutf/upperrune.o\
+ libutf/utf.o\
+ libutf/utftorunestr.o
+
+LIBUTILOBJ =\
+ libutil/concat.o\
+ libutil/cp.o\
+ libutil/crypt.o\
+ libutil/confirm.o\
+ libutil/ealloc.o\
+ libutil/enmasse.o\
+ libutil/eprintf.o\
+ libutil/eregcomp.o\
+ libutil/estrtod.o\
+ libutil/fnck.o\
+ libutil/fshut.o\
+ libutil/getlines.o\
+ libutil/human.o\
+ libutil/linecmp.o\
+ libutil/md5.o\
+ libutil/memmem.o\
+ libutil/mkdirp.o\
+ libutil/mode.o\
+ libutil/parseoffset.o\
+ libutil/putword.o\
+ libutil/reallocarray.o\
+ libutil/recurse.o\
+ libutil/rm.o\
+ libutil/sha1.o\
+ libutil/sha224.o\
+ libutil/sha256.o\
+ libutil/sha384.o\
+ libutil/sha512.o\
+ libutil/sha512-224.o\
+ libutil/sha512-256.o\
+ libutil/strcasestr.o\
+ libutil/strlcat.o\
+ libutil/strlcpy.o\
+ libutil/strsep.o\
+ libutil/strnsubst.o\
+ libutil/strtonum.o\
+ libutil/unescape.o\
+ libutil/writeall.o
+
+LIB = libutf.a libutil.a
+
+BIN =\
+ basename\
+ cal\
+ cat\
+ chgrp\
+ chmod\
+ chown\
+ chroot\
+ cksum\
+ cmp\
+ cols\
+ comm\
+ cp\
+ cron\
+ cut\
+ date\
+ dd\
+ dirname\
+ du\
+ echo\
+ ed\
+ env\
+ expand\
+ expr\
+ false\
+ find\
+ flock\
+ fold\
+ getconf\
+ grep\
+ head\
+ hostname\
+ join\
+ kill\
+ link\
+ ln\
+ logger\
+ logname\
+ ls\
+ md5sum\
+ mkdir\
+ mkfifo\
+ mknod\
+ mktemp\
+ mv\
+ nice\
+ nl\
+ nohup\
+ od\
+ paste\
+ pathchk\
+ printenv\
+ printf\
+ pwd\
+ readlink\
+ renice\
+ rev\
+ rm\
+ rmdir\
+ sed\
+ seq\
+ setsid\
+ sha1sum\
+ sha224sum\
+ sha256sum\
+ sha384sum\
+ sha512sum\
+ sha512-224sum\
+ sha512-256sum\
+ sleep\
+ sort\
+ split\
+ sponge\
+ strings\
+ sync\
+ tail\
+ tar\
+ tee\
+ test\
+ tftp\
+ time\
+ touch\
+ tr\
+ true\
+ tsort\
+ tty\
+ uname\
+ unexpand\
+ uniq\
+ unlink\
+ uudecode\
+ uuencode\
+ wc\
+ which\
+ whoami\
+ xargs\
+ xinstall\
+ yes
+
+OBJ = $(LIBUTFOBJ) $(LIBUTILOBJ)
+
+all: $(BIN)
+
+$(BIN): $(LIB)
+
+$(OBJ) $(BIN): $(HDR)
+
+.o:
+ $(CC) $(LDFLAGS) -o $@ $< $(LIB)
+
+.c.o:
+ $(CC) $(CPPFLAGS) $(CFLAGS) -o $@ -c $<
+
+.c:
+ $(CC) $(CPPFLAGS) $(CFLAGS) -o $@ $< $(LIB)
+
+libutf.a: $(LIBUTFOBJ)
+ $(AR) $(ARFLAGS) $@ $?
+ $(RANLIB) $@
+
+libutil.a: $(LIBUTILOBJ)
+ $(AR) $(ARFLAGS) $@ $?
+ $(RANLIB) $@
+
+getconf: getconf.h
+
+getconf.h:
+ scripts/getconf.sh > $@
+
+proto: $(BIN)
+ scripts/mkproto $(DESTDIR)$(PREFIX) $(DESTDIR)$(MANPREFIX) proto
+
+install uninstall: proto
+ scripts/$@ proto
+
+sbase-box-install: sbase-box proto
+ scripts/install proto
+ $(DESTDIR)$(PREFIX)/bin/sbase-box -i $(DESTDIR)$(PREFIX)/bin/
+
+sbase-box-uninstall: sbase-box proto
+ $(DESTDIR)$(PREFIX)/bin/sbase-box -d $(DESTDIR)$(PREFIX)/bin/
+ scripts/uninstall proto
+
+dist: clean
+ mkdir -p sbase
+ cp -R LICENSE Makefile README TODO config.mk *.c *.1 *.h libutf libutil sbase
+ mv sbase sbase-$(VERSION)
+ tar -cf sbase-$(VERSION).tar sbase-$(VERSION)
+ gzip sbase-$(VERSION).tar
+ rm -rf sbase-$(VERSION)
+
+sbase-box: $(BIN)
+ scripts/mkbox
+ $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) -o $@ build/*.c $(LIB)
+
+clean:
+ rm -f $(BIN) $(OBJ) $(LIB) sbase-box sbase-$(VERSION).tar.gz
+ rm -f getconf.h
+ rm -f proto
+ rm -rf build
+
+.PHONY: all install uninstall dist sbase-box-install sbase-box-uninstall clean
diff --git a/util/sbase/README b/util/sbase/README
new file mode 100644
index 00000000..a3390a95
--- /dev/null
+++ b/util/sbase/README
@@ -0,0 +1,147 @@
+sbase - suckless unix tools
+===========================
+
+sbase is a collection of unix tools that are inherently portable across
+UNIX and UNIX-like systems.
+
+The complement of sbase is ubase[1] which is Linux-specific and provides
+all the non-portable tools. Together they are intended to form a base
+system similar to busybox but much smaller and suckless.
+
+Building
+--------
+
+To build sbase, simply type make. You may have to fiddle with config.mk
+depending on your system.
+
+You can also build sbase-box, which generates a single binary containing
+all the required tools. You can then symlink the individual tools to
+sbase-box or run: make sbase-box-install
+
+To run the tools for sbase-box directly use: sbase-box cmd [args]
+
+Ideally you will want to statically link sbase. If you are on Linux we
+recommend using musl-libc[2].
+
+Portability
+-----------
+
+sbase has been compiled on a variety of different operating systems,
+including Linux, *BSD, OSX, Haiku, Solaris, SCO OpenServer and others.
+
+Various combinations of operating systems and architectures have also
+been built.
+
+You can build sbase with gcc, clang, tcc, nwcc and pcc.
+
+Status
+------
+
+The following tools are implemented:
+
+'#' -> UTF-8 support, '=' -> Implicit UTF-8 support, '*' -> Finished,
+'|' -> Audited, 'o' -> POSIX 2013 compliant, 'x' -> Non-POSIX,
+'0' -> NUL handling, '()' -> Petty flag
+
+ UTILITY MISSING
+ ------- -------
+0=*|o basename .
+0=*|o cal .
+0=*|o cat .
+0=*|o chgrp .
+0=*|o chmod .
+0=*|o chown .
+0=*|x chroot .
+0=*|o cksum .
+0=*|o cmp .
+0#*|x cols .
+0=*|o comm .
+0=*|o cp .
+0=*|x cron .
+0#*|o cut .
+0=*|o date .
+0=*|o dd .
+0=*|o dirname .
+0=*|o du .
+0=*|o echo .
+ o ed .
+0=*|o env .
+0#*|o expand .
+0#*|o expr .
+0=*|o false .
+0= find .
+0=* x flock .
+0#*|o fold .
+0=*|o getconf (-v)
+ =*|o grep .
+0=*|o head .
+0=*|x hostname .
+0=*|x install .
+0=* o join .
+0=*|o kill .
+0=*|o link .
+0=*|o ln .
+0=*|o logger .
+0=*|o logname .
+0#* o ls (-C, -k, -m, -p, -s, -x)
+0=*|x md5sum .
+0=*|o mkdir .
+0=*|o mkfifo .
+0=*|x mknod .
+0=*|x mktemp .
+0=*|o mv (-i)
+0=*|o nice .
+0#*|o nl .
+0=*|o nohup .
+0=*|o od .
+ #*|o paste .
+0#* o pathchk .
+0=*|x printenv .
+0#*|o printf .
+0=*|o pwd .
+0=*|x readlink .
+0=*|o renice .
+0#* x rev .
+0=*|o rm .
+0=*|o rmdir .
+ # sed .
+0=*|x seq .
+0=*|x setsid .
+0=*|x sha1sum .
+0=* x sha224sum .
+0=*|x sha256sum .
+0=* x sha384sum .
+0=*|x sha512sum .
+0=* x sha512-224sum .
+0=* x sha512-256sum .
+0=*|o sleep .
+0#*|o sort .
+0=*|o split .
+0=*|x sponge .
+0#*|o strings .
+0=*|x sync .
+0=*|o tail .
+0=*|x tar .
+0=*|o tee .
+0=*|o test .
+0=*|x tftp .
+0=*|o time .
+0=*|o touch .
+0#*|o tr .
+0=*|o true .
+0=* o tsort .
+0=*|o tty .
+0=*|o uname .
+0#*|o unexpand .
+0=*|o uniq .
+0=*|o unlink .
+0=*|o uudecode .
+0=*|o uuencode .
+0#*|o wc .
+0=*|x which .
+0=*|x whoami .
+0=*|o xargs .
+0=*|x yes .
+
+[1] http://git.suckless.org/ubase/
+[2] http://www.musl-libc.org/
diff --git a/util/sbase/TODO b/util/sbase/TODO
new file mode 100644
index 00000000..38c9f868
--- /dev/null
+++ b/util/sbase/TODO
@@ -0,0 +1,92 @@
+The following list of commands is taken from the toybox roadmap[0] and
+has been stripped down accordingly. Commands that belong to ubase[1]
+are not listed here as well as commands that fall outside the scope of
+sbase such as vi and sh are also not listed here.
+
+at
+awk
+bc
+diff
+patch
+stty
+
+If you are looking for some work to do on sbase, another option is to
+pick a utility from the list in the README which has missing flags or
+features noted.
+
+What also needs to be implemented is the capability of the tools to
+handle data with NUL-bytes in it.
+
+The return values of mdcheckline() in crypt.c need to be fixed (0 -> success,
+1 -> error).
+
+[0] http://landley.net/toybox/roadmap.html
+[1] http://git.suckless.org/ubase/
+
+Bugs
+====
+
+ed
+--
+* cat <<EOF | ed
+ i
+ LLL
+ .
+ s/$/\\
+
+* cat <<EOF | ed
+ 0a
+ int radix = 16;
+ int Pflag;
+ int Aflag;
+ int vflag;
+ int gflag;
+ int uflag;
+ int arflag;
+
+ .
+ ?radix?;/^$/-s/^/static /
+* cat <<EOF | ed
+ 0a
+ Line
+ .
+ s# *##
+* cat <<EOF | ed
+ 0a
+ line
+ .
+ 1g/^$/p
+
+* cat <<EOF | ed
+ 0a
+ line1
+ line2
+ line3
+ .
+ g/^$/d
+ ,p
+
+* Editing huge files doesn't work well.
+
+
+printf
+------
+* Flags for string conversion-specifier (%s) are not supported.
+* Escape sequences that expand to '%' are treated as beginning of
+ conversion specification.
+* An trailing '%' at the end of a format string causes a read past
+ the end of the string.
+
+tr
+--
+* When a character class is present, all other characters in the
+ string are ignored.
+
+sbase-box
+---------
+* List of commands does not contain `install` (only `xinstall`).
+
+
+xargs
+-----
+* Add -L.
diff --git a/util/sbase/arg.h b/util/sbase/arg.h
new file mode 100644
index 00000000..0b23c53a
--- /dev/null
+++ b/util/sbase/arg.h
@@ -0,0 +1,65 @@
+/*
+ * Copy me if you can.
+ * by 20h
+ */
+
+#ifndef ARG_H__
+#define ARG_H__
+
+extern char *argv0;
+
+/* use main(int argc, char *argv[]) */
+#define ARGBEGIN for (argv0 = *argv, argv++, argc--;\
+ argv[0] && argv[0][0] == '-'\
+ && argv[0][1];\
+ argc--, argv++) {\
+ char argc_;\
+ char **argv_;\
+ int brk_;\
+ if (argv[0][1] == '-' && argv[0][2] == '\0') {\
+ argv++;\
+ argc--;\
+ break;\
+ }\
+ for (brk_ = 0, argv[0]++, argv_ = argv;\
+ argv[0][0] && !brk_;\
+ argv[0]++) {\
+ if (argv_ != argv)\
+ break;\
+ argc_ = argv[0][0];\
+ switch (argc_)
+
+/* Handles obsolete -NUM syntax */
+#define ARGNUM case '0':\
+ case '1':\
+ case '2':\
+ case '3':\
+ case '4':\
+ case '5':\
+ case '6':\
+ case '7':\
+ case '8':\
+ case '9'
+
+#define ARGEND }\
+ }
+
+#define ARGC() argc_
+
+#define ARGNUMF() (brk_ = 1, estrtonum(argv[0], 0, INT_MAX))
+
+#define EARGF(x) ((argv[0][1] == '\0' && argv[1] == NULL)?\
+ ((x), abort(), (char *)0) :\
+ (brk_ = 1, (argv[0][1] != '\0')?\
+ (&argv[0][1]) :\
+ (argc--, argv++, argv[0])))
+
+#define ARGF() ((argv[0][1] == '\0' && argv[1] == NULL)?\
+ (char *)0 :\
+ (brk_ = 1, (argv[0][1] != '\0')?\
+ (&argv[0][1]) :\
+ (argc--, argv++, argv[0])))
+
+#define LNGARG() &argv[0][0]
+
+#endif
diff --git a/util/sbase/basename.1 b/util/sbase/basename.1
new file mode 100644
index 00000000..2717c580
--- /dev/null
+++ b/util/sbase/basename.1
@@ -0,0 +1,22 @@
+.Dd October 8, 2015
+.Dt BASENAME 1
+.Os sbase
+.Sh NAME
+.Nm basename
+.Nd strip leading directory components of a path
+.Sh SYNOPSIS
+.Nm
+.Ar path
+.Op Ar suffix
+.Sh DESCRIPTION
+.Nm
+writes
+.Ar path
+without leading directory components and
+.Ar suffix
+to stdout.
+.Sh SEE ALSO
+.Xr dirname 1 ,
+.Xr basename 3
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/basename.c b/util/sbase/basename.c
new file mode 100644
index 00000000..94a2848f
--- /dev/null
+++ b/util/sbase/basename.c
@@ -0,0 +1,37 @@
+/* See LICENSE file for copyright and license details. */
+#include <libgen.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s path [suffix]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ ssize_t off;
+ char *p;
+
+ ARGBEGIN {
+ default:
+ usage();
+ } ARGEND
+
+ if (argc != 1 && argc != 2)
+ usage();
+
+ p = basename(argv[0]);
+ if (argc == 2) {
+ off = strlen(p) - strlen(argv[1]);
+ if (off > 0 && !strcmp(p + off, argv[1]))
+ p[off] = '\0';
+ }
+ puts(p);
+
+ return fshut(stdout, "<stdout>");
+}
diff --git a/util/sbase/cal.1 b/util/sbase/cal.1
new file mode 100644
index 00000000..2918ea98
--- /dev/null
+++ b/util/sbase/cal.1
@@ -0,0 +1,68 @@
+.Dd October 8, 2015
+.Dt CAL 1
+.Os sbase
+.Sh NAME
+.Nm cal
+.Nd show calendar
+.Sh SYNOPSIS
+.Nm
+.Op Fl 1 | Fl 3 | Fl y | Fl n Ar num
+.Op Fl s | Fl m | Fl f Ar num
+.Op Fl c Ar num
+.Oo Oo Ar month Oc Ar year Oc
+.Sh DESCRIPTION
+.Nm
+writes a calendar of
+.Ar month
+and
+.Ar year
+or the current month to stdout.
+If
+.Ar year
+is given without
+.Ar month ,
+.Nm
+writes a 3-column calendar of the whole
+year to stdout.
+The date formatting is according to
+.Xr localtime 3 .
+.Pp
+The Julian calendar is used until Sep 2, 1752.
+The Gregorian calendar is used starting the next day on Sep 14, 1752.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl 1
+Print current month.
+This is the default.
+.It Fl 3
+Print previous, current and next month.
+.It Fl c Ar num
+Print
+.Ar num
+calendars in a row.
+The default is 3.
+.It Fl f Ar num
+Set
+.Ar num
+(0 is Sunday, 6 is Saturday) as first day of week.
+.It Fl m
+Set Monday as first day of week.
+.It Fl n Ar num
+Output
+.Ar num
+months starting from and including the current month.
+.It Fl s
+Set Sunday as first day of week.
+.It Fl y
+Print the entire
+.Ar year
+or current year.
+.El
+.Sh SEE ALSO
+.Xr localtime 3
+.Sh STANDARDS
+POSIX.1-2013.
+.Pp
+The flags
+.Op Fl 13cfmnsy
+are an extension to that specification.
diff --git a/util/sbase/cal.c b/util/sbase/cal.c
new file mode 100644
index 00000000..a8c91f19
--- /dev/null
+++ b/util/sbase/cal.c
@@ -0,0 +1,226 @@
+/* See LICENSE file for copyright and license details. */
+#include <limits.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "util.h"
+
+enum { JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC };
+enum caltype { JULIAN, GREGORIAN };
+enum { TRANS_YEAR = 1752, TRANS_MONTH = SEP, TRANS_DAY = 2 };
+
+static struct tm *ltime;
+
+static int
+isleap(size_t year, enum caltype cal)
+{
+ if (cal == GREGORIAN) {
+ if (year % 400 == 0)
+ return 1;
+ if (year % 100 == 0)
+ return 0;
+ return (year % 4 == 0);
+ }
+ else { /* cal == Julian */
+ return (year % 4 == 0);
+ }
+}
+
+static int
+monthlength(size_t year, int month, enum caltype cal)
+{
+ int mdays[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
+
+ return (month == FEB && isleap(year, cal)) ? 29 : mdays[month];
+}
+
+/* From http://www.tondering.dk/claus/cal/chrweek.php#calcdow */
+static int
+dayofweek(size_t year, int month, int dom, enum caltype cal)
+{
+ size_t y;
+ int m, a;
+
+ a = (13 - month) / 12;
+ y = year - a;
+ m = month + 12 * a - 1;
+
+ if (cal == GREGORIAN)
+ return (dom + y + y / 4 - y / 100 + y / 400 + (31 * m) / 12) % 7;
+ else /* cal == Julian */
+ return (5 + dom + y + y / 4 + (31 * m) / 12) % 7;
+}
+
+static void
+printgrid(size_t year, int month, int fday, int line)
+{
+ enum caltype cal;
+ int offset, dom, d = 0, trans; /* are we in the transition from Julian to Gregorian? */
+ int today = 0;
+
+ cal = (year < TRANS_YEAR || (year == TRANS_YEAR && month <= TRANS_MONTH)) ? JULIAN : GREGORIAN;
+ trans = (year == TRANS_YEAR && month == TRANS_MONTH);
+ offset = dayofweek(year, month, 1, cal) - fday;
+
+ if (offset < 0)
+ offset += 7;
+ if (line == 1) {
+ for (; d < offset; ++d)
+ printf(" ");
+ dom = 1;
+ } else {
+ dom = 8 - offset + (line - 2) * 7;
+ if (trans && !(line == 2 && fday == 3))
+ dom += 11;
+ }
+ if (ltime && year == ltime->tm_year + 1900 && month == ltime->tm_mon)
+ today = ltime->tm_mday;
+ for (; d < 7 && dom <= monthlength(year, month, cal); ++d, ++dom) {
+ if (dom == today)
+ printf("\x1b[7m%2d\x1b[0m ", dom); /* highlight today's date */
+ else
+ printf("%2d ", dom);
+ if (trans && dom == TRANS_DAY)
+ dom += 11;
+ }
+ for (; d < 7; ++d)
+ printf(" ");
+}
+
+static void
+drawcal(size_t year, int month, size_t ncols, size_t nmons, int fday)
+{
+ char *smon[] = { "January", "February", "March", "April",
+ "May", "June", "July", "August",
+ "September", "October", "November", "December" };
+ char *days[] = { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa", };
+ size_t m, n, col, cur_year, cur_month, dow;
+ int line, pad;
+ char month_year[sizeof("Su Mo Tu We Th Fr Sa")];
+
+ for (m = 0; m < nmons; ) {
+ n = m;
+ for (col = 0; m < nmons && col < ncols; ++col, ++m) {
+ cur_year = year + m / 12;
+ cur_month = month + m % 12;
+ if (cur_month > 11) {
+ cur_month -= 12;
+ cur_year += 1;
+ }
+ snprintf(month_year, sizeof(month_year), "%s %zu", smon[cur_month], cur_year);
+ pad = sizeof(month_year) - 1 - strlen(month_year);
+ printf("%*s%s%*s ", pad / 2 + pad % 2, "", month_year, pad / 2, "");
+ }
+ putchar('\n');
+ for (col = 0, m = n; m < nmons && col < ncols; ++col, ++m) {
+ for (dow = fday; dow < (fday + 7); ++dow)
+ printf("%s ", days[dow % 7]);
+ printf(" ");
+ }
+ putchar('\n');
+ for (line = 1; line <= 6; ++line) {
+ for (col = 0, m = n; m < nmons && col < ncols; ++col, ++m) {
+ cur_year = year + m / 12;
+ cur_month = month + m % 12;
+ if (cur_month > 11) {
+ cur_month -= 12;
+ cur_year += 1;
+ }
+ printgrid(cur_year, cur_month, fday, line);
+ printf(" ");
+ }
+ putchar('\n');
+ }
+ }
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-1 | -3 | -y | -n num] "
+ "[-s | -m | -f num] [-c num] [[month] year]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ time_t now;
+ size_t year, ncols, nmons;
+ int fday, month;
+
+ now = time(NULL);
+ ltime = localtime(&now);
+ year = ltime->tm_year + 1900;
+ month = ltime->tm_mon + 1;
+ fday = 0;
+
+ if (!isatty(STDOUT_FILENO))
+ ltime = NULL; /* don't highlight today's date */
+
+ ncols = 3;
+ nmons = 0;
+
+ ARGBEGIN {
+ case '1':
+ nmons = 1;
+ break;
+ case '3':
+ nmons = 3;
+ if (--month == 0) {
+ month = 12;
+ year--;
+ }
+ break;
+ case 'c':
+ ncols = estrtonum(EARGF(usage()), 0, MIN(SIZE_MAX, LLONG_MAX));
+ break;
+ case 'f':
+ fday = estrtonum(EARGF(usage()), 0, 6);
+ break;
+ case 'm': /* Monday */
+ fday = 1;
+ break;
+ case 'n':
+ nmons = estrtonum(EARGF(usage()), 1, MIN(SIZE_MAX, LLONG_MAX));
+ break;
+ case 's': /* Sunday */
+ fday = 0;
+ break;
+ case 'y':
+ month = 1;
+ nmons = 12;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (nmons == 0) {
+ if (argc == 1) {
+ month = 1;
+ nmons = 12;
+ } else {
+ nmons = 1;
+ }
+ }
+
+ switch (argc) {
+ case 2:
+ month = estrtonum(argv[0], 1, 12);
+ argv++;
+ case 1: /* fallthrough */
+ year = estrtonum(argv[0], 0, INT_MAX);
+ break;
+ case 0:
+ break;
+ default:
+ usage();
+ }
+
+ drawcal(year, month - 1, ncols, nmons, fday);
+
+ return fshut(stdout, "<stdout>");
+}
diff --git a/util/sbase/cat.1 b/util/sbase/cat.1
new file mode 100644
index 00000000..e6172298
--- /dev/null
+++ b/util/sbase/cat.1
@@ -0,0 +1,27 @@
+.Dd October 8, 2015
+.Dt CAT 1
+.Os sbase
+.Sh NAME
+.Nm cat
+.Nd concatenate files
+.Sh SYNOPSIS
+.Nm
+.Op Fl u
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+reads each
+.Ar file
+in sequence and writes it to stdout.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl u
+Unbuffered output.
+.El
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/cat.c b/util/sbase/cat.c
new file mode 100644
index 00000000..211e8d11
--- /dev/null
+++ b/util/sbase/cat.c
@@ -0,0 +1,52 @@
+/* See LICENSE file for copyright and license details. */
+#include <fcntl.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-u] [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int fd, ret = 0;
+
+ ARGBEGIN {
+ case 'u':
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc) {
+ if (concat(0, "<stdin>", 1, "<stdout>") < 0)
+ ret = 1;
+ } else {
+ for (; *argv; argc--, argv++) {
+ if (!strcmp(*argv, "-")) {
+ *argv = "<stdin>";
+ fd = 0;
+ } else if ((fd = open(*argv, O_RDONLY)) < 0) {
+ weprintf("open %s:", *argv);
+ ret = 1;
+ continue;
+ }
+ switch (concat(fd, *argv, 1, "<stdout>")) {
+ case -1:
+ ret = 1;
+ break;
+ case -2:
+ return 1; /* exit on write error */
+ }
+ if (fd != 0)
+ close(fd);
+ }
+ }
+
+ return ret;
+}
diff --git a/util/sbase/chgrp.1 b/util/sbase/chgrp.1
new file mode 100644
index 00000000..ee44a54c
--- /dev/null
+++ b/util/sbase/chgrp.1
@@ -0,0 +1,47 @@
+.Dd October 8, 2015
+.Dt CHGRP 1
+.Os sbase
+.Sh NAME
+.Nm chgrp
+.Nd change file group ownership
+.Sh SYNOPSIS
+.Nm
+.Op Fl h
+.Oo
+.Fl R
+.Op Fl H | L | P
+.Oc
+.Ar group
+.Ar file ...
+.Sh DESCRIPTION
+.Nm
+sets the group id of each
+.Ar file
+to the gid of
+.Ar group .
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl h
+Preserve
+.Ar file
+if it is a symbolic link.
+.It Fl R
+Change file group ownerships recursively.
+.It Fl H
+Dereference
+.Ar file
+if it is a symbolic link.
+.It Fl L
+Dereference all symbolic links.
+.It Fl P
+Preserve symbolic links.
+This is the default.
+.El
+.Sh SEE ALSO
+.Xr chmod 1 ,
+.Xr chown 1 ,
+.Xr chmod 2 ,
+.Xr chown 2 ,
+.Xr getgrnam 3
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/chgrp.c b/util/sbase/chgrp.c
new file mode 100644
index 00000000..4042a0dd
--- /dev/null
+++ b/util/sbase/chgrp.c
@@ -0,0 +1,75 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <grp.h>
+#include <unistd.h>
+
+#include "fs.h"
+#include "util.h"
+
+static int hflag = 0;
+static gid_t gid = -1;
+static int ret = 0;
+
+static void
+chgrp(int dirfd, const char *name, struct stat *st, void *data, struct recursor *r)
+{
+ int flags = 0;
+
+ if ((r->maxdepth == 0 && r->follow == 'P') || (r->follow == 'H' && r->depth) || (hflag && !(r->depth)))
+ flags |= AT_SYMLINK_NOFOLLOW;
+ if (fchownat(dirfd, name, -1, gid, flags) < 0) {
+ weprintf("chown %s:", r->path);
+ ret = 1;
+ } else if (S_ISDIR(st->st_mode)) {
+ recurse(dirfd, name, NULL, r);
+ }
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-h] [-R [-H | -L | -P]] group file ...\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct group *gr;
+ struct recursor r = { .fn = chgrp, .maxdepth = 1, .follow = 'P' };
+
+ ARGBEGIN {
+ case 'h':
+ hflag = 1;
+ break;
+ case 'R':
+ r.maxdepth = 0;
+ break;
+ case 'H':
+ case 'L':
+ case 'P':
+ r.follow = ARGC();
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (argc < 2)
+ usage();
+
+ errno = 0;
+ if ((gr = getgrnam(argv[0]))) {
+ gid = gr->gr_gid;
+ } else {
+ if (errno)
+ eprintf("getgrnam %s:", argv[0]);
+ gid = estrtonum(argv[0], 0, UINT_MAX);
+ }
+
+ for (argc--, argv++; *argv; argc--, argv++)
+ recurse(AT_FDCWD, *argv, NULL, &r);
+
+ return ret || recurse_status;
+}
diff --git a/util/sbase/chmod.1 b/util/sbase/chmod.1
new file mode 100644
index 00000000..f579b3fa
--- /dev/null
+++ b/util/sbase/chmod.1
@@ -0,0 +1,74 @@
+.Dd December 21, 2019
+.Dt CHMOD 1
+.Os sbase
+.Sh NAME
+.Nm chmod
+.Nd change file modes
+.Sh SYNOPSIS
+.Nm
+.Op Fl R
+.Ar mode
+.Ar file ...
+.Sh DESCRIPTION
+.Nm
+changes the file mode of each
+.Ar file
+to
+.Ar mode .
+.Pp
+If
+.Ar mode
+is
+.Em octal
+"[sog]e"
+.Bl -tag -width Ds
+.It s
+.Xr sticky 1 => s += 1
+.Pp
+.Xr setgid 2 => s += 2
+.Pp
+.Xr setuid 4 => s += 4
+.It o|g|e
+owner | group | everyone
+.Pp
+.Xr execute 1 => o|g|e += 1
+.Pp
+.Xr write 2 => o|g|e += 2
+.Pp
+.Xr read 4 => o|g|e += 4
+.El
+.Pp
+Leading zeroes may be omitted.
+.Pp
+If
+.Ar mode
+is
+.Em symbolic
+"[ugoa]*[+-=][rwxXst]*"
+.Bl -tag -width Ds
+.It u|g|o|a
+owner | group | other (non-group) | everyone
+.It +|-|=
+add | remove | set
+.It r|w|x|s|t
+read | write | execute | setuid and setgid | sticky
+.It X
+execute, if directory or at least one execute bit is already set
+.El
+.Pp
+Symbolic links are followed if they are passed as operands, and ignored
+if they are encountered during directory traversal.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl R
+Change modes recursively.
+.El
+.Sh SEE ALSO
+.Xr chgrp 1 ,
+.Xr umask 1
+.Sh STANDARDS
+POSIX.1-2013.
+.Pp
+The
+.Op Fl HLP
+flags are an extension to that specification.
diff --git a/util/sbase/chmod.c b/util/sbase/chmod.c
new file mode 100644
index 00000000..c79488bb
--- /dev/null
+++ b/util/sbase/chmod.c
@@ -0,0 +1,77 @@
+/* See LICENSE file for copyright and license details. */
+#include <fcntl.h>
+#include <sys/stat.h>
+
+#include "fs.h"
+#include "util.h"
+
+static char *modestr = "";
+static mode_t mask = 0;
+static int ret = 0;
+
+static void
+chmodr(int dirfd, const char *name, struct stat *st, void *data, struct recursor *r)
+{
+ mode_t m;
+
+ m = parsemode(modestr, st->st_mode, mask);
+ if (!S_ISLNK(st->st_mode) && fchmodat(dirfd, name, m, 0) < 0) {
+ weprintf("chmod %s:", r->path);
+ ret = 1;
+ } else if (S_ISDIR(st->st_mode)) {
+ recurse(dirfd, name, NULL, r);
+ }
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-R] mode file ...\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct recursor r = { .fn = chmodr, .maxdepth = 1, .follow = 'H', .flags = DIRFIRST };
+ size_t i;
+
+ argv0 = *argv, argv0 ? (argc--, argv++) : (void *)0;
+
+ for (; *argv && (*argv)[0] == '-'; argc--, argv++) {
+ if (!(*argv)[1])
+ usage();
+ for (i = 1; (*argv)[i]; i++) {
+ switch ((*argv)[i]) {
+ case 'R':
+ r.maxdepth = 0;
+ break;
+ case 'r': case 'w': case 'x': case 'X': case 's': case 't':
+ /* -[rwxXst] are valid modes, so we're done */
+ if (i == 1)
+ goto done;
+ /* fallthrough */
+ case '-':
+ /* -- terminator */
+ if (i == 1 && !(*argv)[i + 1]) {
+ argv++;
+ argc--;
+ goto done;
+ }
+ /* fallthrough */
+ default:
+ usage();
+ }
+ }
+ }
+done:
+ mask = getumask();
+ modestr = *argv;
+
+ if (argc < 2)
+ usage();
+
+ for (--argc, ++argv; *argv; argc--, argv++)
+ recurse(AT_FDCWD, *argv, NULL, &r);
+
+ return ret || recurse_status;
+}
diff --git a/util/sbase/chown.1 b/util/sbase/chown.1
new file mode 100644
index 00000000..8afdfcab
--- /dev/null
+++ b/util/sbase/chown.1
@@ -0,0 +1,57 @@
+.Dd October 8, 2015
+.Dt CHOWN 1
+.Os sbase
+.Sh NAME
+.Nm chown
+.Nd change file ownership
+.Sh SYNOPSIS
+.Nm
+.Op Fl h
+.Oo
+.Fl R
+.Op Fl H | L | P
+.Oc
+.Ar owner Ns Op Pf : Op Ar group
+.Op Ar file ...
+.Nm
+.Op Fl h
+.Oo
+.Fl R
+.Op Fl H | L | P
+.Oc
+.Pf : Ar group
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+sets the user and/or group id of each
+.Ar file
+to the uid of
+.Ar owner
+and/or the gid of
+.Ar group
+respectively.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl h
+Preserve
+.Ar file
+if it is a symbolic link.
+.It Fl R
+Change file ownerships recursively.
+.It Fl H
+Dereference
+.Ar file
+if it is a symbolic link.
+.It Fl L
+Dereference all symbolic links.
+.It Fl P
+Preserve symbolic links.
+This is the default.
+.El
+.Sh SEE ALSO
+.Xr chmod 1 ,
+.Xr chown 2 ,
+.Xr getgrnam 3 ,
+.Xr getpwnam 3
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/chown.c b/util/sbase/chown.c
new file mode 100644
index 00000000..71628eb6
--- /dev/null
+++ b/util/sbase/chown.c
@@ -0,0 +1,104 @@
+/* See LICENSE file for copyright and license details. */
+#include <errno.h>
+#include <fcntl.h>
+#include <grp.h>
+#include <limits.h>
+#include <pwd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "fs.h"
+#include "util.h"
+
+static int hflag = 0;
+static uid_t uid = -1;
+static gid_t gid = -1;
+static int ret = 0;
+
+static void
+chownpwgr(int dirfd, const char *name, struct stat *st, void *data, struct recursor *r)
+{
+ int flags = 0;
+
+ if ((r->maxdepth == 0 && r->follow == 'P') || (r->follow == 'H' && r->depth) || (hflag && !(r->depth)))
+ flags |= AT_SYMLINK_NOFOLLOW;
+
+ if (fchownat(dirfd, name, uid, gid, flags) < 0) {
+ weprintf("chown %s:", r->path);
+ ret = 1;
+ } else if (S_ISDIR(st->st_mode)) {
+ recurse(dirfd, name, NULL, r);
+ }
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-h] [-R [-H | -L | -P]] owner[:[group]] file ...\n"
+ " %s [-h] [-R [-H | -L | -P]] :group file ...\n",
+ argv0, argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct group *gr;
+ struct passwd *pw;
+ struct recursor r = { .fn = chownpwgr, .maxdepth = 1, .follow = 'P' };
+ char *owner, *group;
+
+ ARGBEGIN {
+ case 'h':
+ hflag = 1;
+ break;
+ case 'r':
+ case 'R':
+ r.maxdepth = 0;
+ break;
+ case 'H':
+ case 'L':
+ case 'P':
+ r.follow = ARGC();
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (argc < 2)
+ usage();
+
+ owner = argv[0];
+ if ((group = strchr(owner, ':')))
+ *group++ = '\0';
+
+ if (owner && *owner) {
+ errno = 0;
+ pw = getpwnam(owner);
+ if (pw) {
+ uid = pw->pw_uid;
+ } else {
+ if (errno)
+ eprintf("getpwnam %s:", owner);
+ uid = estrtonum(owner, 0, UINT_MAX);
+ }
+ }
+ if (group && *group) {
+ errno = 0;
+ gr = getgrnam(group);
+ if (gr) {
+ gid = gr->gr_gid;
+ } else {
+ if (errno)
+ eprintf("getgrnam %s:", group);
+ gid = estrtonum(group, 0, UINT_MAX);
+ }
+ }
+ if (uid == (uid_t)-1 && gid == (gid_t)-1)
+ usage();
+
+ for (argc--, argv++; *argv; argc--, argv++)
+ recurse(AT_FDCWD, *argv, NULL, &r);
+
+ return ret || recurse_status;
+}
diff --git a/util/sbase/chroot.1 b/util/sbase/chroot.1
new file mode 100644
index 00000000..ff49fe91
--- /dev/null
+++ b/util/sbase/chroot.1
@@ -0,0 +1,25 @@
+.Dd October 8, 2015
+.Dt CHROOT 1
+.Os sbase
+.Sh NAME
+.Nm chroot
+.Nd run a command or shell with a different root directory
+.Sh SYNOPSIS
+.Nm
+.Ar dir
+.Op Ar cmd Op Ar arg ...
+.Sh DESCRIPTION
+.Nm
+runs
+.Ar cmd
+after changing the root directory to
+.Ar dir
+with the
+.Xr chroot 2
+system call and after changing the working directory to the new root.
+If
+.Ar cmd
+is not specified, an interactive shell is started in the new root.
+.Sh SEE ALSO
+.Xr chdir 2 ,
+.Xr chroot 2
diff --git a/util/sbase/chroot.c b/util/sbase/chroot.c
new file mode 100644
index 00000000..45f2dc7a
--- /dev/null
+++ b/util/sbase/chroot.c
@@ -0,0 +1,49 @@
+/* See LICENSE file for copyright and license details. */
+#include <errno.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s dir [cmd [arg ...]]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ char *shell[] = { "/bin/sh", "-i", NULL }, *aux, *cmd;
+ int savederrno;
+
+ ARGBEGIN {
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc)
+ usage();
+
+ if ((aux = getenv("SHELL")))
+ shell[0] = aux;
+
+ if (chroot(argv[0]) < 0)
+ eprintf("chroot %s:", argv[0]);
+
+ if (chdir("/") < 0)
+ eprintf("chdir:");
+
+ if (argc == 1) {
+ cmd = *shell;
+ execvp(cmd, shell);
+ } else {
+ cmd = argv[1];
+ execvp(cmd, argv + 1);
+ }
+
+ savederrno = errno;
+ weprintf("execvp %s:", cmd);
+
+ _exit(126 + (savederrno == ENOENT));
+}
diff --git a/util/sbase/cksum.1 b/util/sbase/cksum.1
new file mode 100644
index 00000000..6e2657a4
--- /dev/null
+++ b/util/sbase/cksum.1
@@ -0,0 +1,24 @@
+.Dd October 8, 2015
+.Dt CKSUM 1
+.Os sbase
+.Sh NAME
+.Nm cksum
+.Nd compute file checksum
+.Sh SYNOPSIS
+.Nm
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+calculates a cyclic redundancy check (CRC) of
+.Ar file
+according to
+.St -iso8802-3
+and writes it, the file size in bytes and path to stdout.
+.Pp
+If no
+.Ar file
+is given,
+.Nm
+reads from stdin.
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/cksum.c b/util/sbase/cksum.c
new file mode 100644
index 00000000..50107b2b
--- /dev/null
+++ b/util/sbase/cksum.c
@@ -0,0 +1,132 @@
+/* See LICENSE file for copyright and license details. */
+#include <fcntl.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static int ret = 0;
+static const unsigned long crctab[] = { 0x00000000,
+0x04c11db7, 0x09823b6e, 0x0d4326d9, 0x130476dc, 0x17c56b6b,
+0x1a864db2, 0x1e475005, 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6,
+0x2b4bcb61, 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd,
+0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9, 0x5f15adac,
+0x5bd4b01b, 0x569796c2, 0x52568b75, 0x6a1936c8, 0x6ed82b7f,
+0x639b0da6, 0x675a1011, 0x791d4014, 0x7ddc5da3, 0x709f7b7a,
+0x745e66cd, 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039,
+0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5, 0xbe2b5b58,
+0xbaea46ef, 0xb7a96036, 0xb3687d81, 0xad2f2d84, 0xa9ee3033,
+0xa4ad16ea, 0xa06c0b5d, 0xd4326d90, 0xd0f37027, 0xddb056fe,
+0xd9714b49, 0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95,
+0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1, 0xe13ef6f4,
+0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d, 0x34867077, 0x30476dc0,
+0x3d044b19, 0x39c556ae, 0x278206ab, 0x23431b1c, 0x2e003dc5,
+0x2ac12072, 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16,
+0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca, 0x7897ab07,
+0x7c56b6b0, 0x71159069, 0x75d48dde, 0x6b93dddb, 0x6f52c06c,
+0x6211e6b5, 0x66d0fb02, 0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1,
+0x53dc6066, 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba,
+0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, 0xbfa1b04b,
+0xbb60adfc, 0xb6238b25, 0xb2e29692, 0x8aad2b2f, 0x8e6c3698,
+0x832f1041, 0x87ee0df6, 0x99a95df3, 0x9d684044, 0x902b669d,
+0x94ea7b2a, 0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e,
+0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2, 0xc6bcf05f,
+0xc27dede8, 0xcf3ecb31, 0xcbffd686, 0xd5b88683, 0xd1799b34,
+0xdc3abded, 0xd8fba05a, 0x690ce0ee, 0x6dcdfd59, 0x608edb80,
+0x644fc637, 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb,
+0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f, 0x5c007b8a,
+0x58c1663d, 0x558240e4, 0x51435d53, 0x251d3b9e, 0x21dc2629,
+0x2c9f00f0, 0x285e1d47, 0x36194d42, 0x32d850f5, 0x3f9b762c,
+0x3b5a6b9b, 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff,
+0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623, 0xf12f560e,
+0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7, 0xe22b20d2, 0xe6ea3d65,
+0xeba91bbc, 0xef68060b, 0xd727bbb6, 0xd3e6a601, 0xdea580d8,
+0xda649d6f, 0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3,
+0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7, 0xae3afba2,
+0xaafbe615, 0xa7b8c0cc, 0xa379dd7b, 0x9b3660c6, 0x9ff77d71,
+0x92b45ba8, 0x9675461f, 0x8832161a, 0x8cf30bad, 0x81b02d74,
+0x857130c3, 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640,
+0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c, 0x7b827d21,
+0x7f436096, 0x7200464f, 0x76c15bf8, 0x68860bfd, 0x6c47164a,
+0x61043093, 0x65c52d24, 0x119b4be9, 0x155a565e, 0x18197087,
+0x1cd86d30, 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec,
+0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, 0x2497d08d,
+0x2056cd3a, 0x2d15ebe3, 0x29d4f654, 0xc5a92679, 0xc1683bce,
+0xcc2b1d17, 0xc8ea00a0, 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb,
+0xdbee767c, 0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18,
+0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4, 0x89b8fd09,
+0x8d79e0be, 0x803ac667, 0x84fbdbd0, 0x9abc8bd5, 0x9e7d9662,
+0x933eb0bb, 0x97ffad0c, 0xafb010b1, 0xab710d06, 0xa6322bdf,
+0xa2f33668, 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4
+};
+
+static void
+cksum(int fd, const char *s)
+{
+ ssize_t n;
+ size_t len = 0, i;
+ uint32_t ck = 0;
+ unsigned char buf[BUFSIZ];
+
+ while ((n = read(fd, buf, sizeof(buf))) > 0) {
+ for (i = 0; i < n; i++)
+ ck = (ck << 8) ^ crctab[(ck >> 24) ^ buf[i]];
+ len += n;
+ }
+ if (n < 0) {
+ weprintf("read %s:", s ? s : "<stdin>");
+ ret = 1;
+ return;
+ }
+
+ for (i = len; i; i >>= 8)
+ ck = (ck << 8) ^ crctab[(ck >> 24) ^ (i & 0xFF)];
+
+ printf("%"PRIu32" %zu", ~ck, len);
+ if (s) {
+ putchar(' ');
+ fputs(s, stdout);
+ }
+ putchar('\n');
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int fd;
+
+ ARGBEGIN {
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc) {
+ cksum(0, NULL);
+ } else {
+ for (; *argv; argc--, argv++) {
+ if (!strcmp(*argv, "-")) {
+ *argv = "<stdin>";
+ fd = 0;
+ } else if ((fd = open(*argv, O_RDONLY)) < 0) {
+ weprintf("open %s:", *argv);
+ ret = 1;
+ continue;
+ }
+ cksum(fd, *argv);
+ if (fd != 0)
+ close(fd);
+ }
+ }
+
+ ret |= fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/cmp.1 b/util/sbase/cmp.1
new file mode 100644
index 00000000..750d11a8
--- /dev/null
+++ b/util/sbase/cmp.1
@@ -0,0 +1,49 @@
+.Dd October 8, 2015
+.Dt CMP 1
+.Os sbase
+.Sh NAME
+.Nm cmp
+.Nd compare two files
+.Sh SYNOPSIS
+.Nm
+.Op Fl l | Fl s
+.Ar file1 file2
+.Sh DESCRIPTION
+.Nm
+compares
+.Ar file1
+and
+.Ar file2
+byte by byte.
+If they differ,
+.Nm
+writes the first differing byte- and line-number to stdout.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl l
+Print byte-number and bytes (in octal) for each difference.
+.It Fl s
+Print nothing and only return status.
+.El
+.Sh EXIT STATUS
+.Bl -tag -width Ds
+.It 0
+.Ar file1
+and
+.Ar file2
+are identical.
+.It 1
+.Ar file1
+and
+.Ar file2
+are different.
+.It > 1
+An error occurred.
+.El
+.Sh SEE ALSO
+.Xr comm 1 ,
+.Xr diff 1
+.Sh STANDARDS
+POSIX.1-2013.
+.Pp
+The "char" in the default result format has been replaced with "byte".
diff --git a/util/sbase/cmp.c b/util/sbase/cmp.c
new file mode 100644
index 00000000..83ab149e
--- /dev/null
+++ b/util/sbase/cmp.c
@@ -0,0 +1,82 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ enprintf(2, "usage: %s [-l | -s] file1 file2\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ FILE *fp[2];
+ size_t line = 1, n;
+ int ret = 0, lflag = 0, sflag = 0, same = 1, b[2];
+
+ ARGBEGIN {
+ case 'l':
+ lflag = 1;
+ break;
+ case 's':
+ sflag = 1;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (argc != 2 || (lflag && sflag))
+ usage();
+
+ for (n = 0; n < 2; n++) {
+ if (!strcmp(argv[n], "-")) {
+ argv[n] = "<stdin>";
+ fp[n] = stdin;
+ } else {
+ if (!(fp[n] = fopen(argv[n], "r"))) {
+ if (!sflag)
+ weprintf("fopen %s:", argv[n]);
+ return 2;
+ }
+ }
+ }
+
+ for (n = 1; ; n++) {
+ b[0] = getc(fp[0]);
+ b[1] = getc(fp[1]);
+
+ if (b[0] == b[1]) {
+ if (b[0] == EOF)
+ break;
+ else if (b[0] == '\n')
+ line++;
+ continue;
+ } else if (b[0] == EOF || b[1] == EOF) {
+ if (!sflag)
+ weprintf("EOF on %s\n", argv[(b[0] != EOF)]);
+ same = 0;
+ break;
+ } else if (!lflag) {
+ if (!sflag)
+ printf("%s %s differ: byte %zu, line %zu\n",
+ argv[0], argv[1], n, line);
+ same = 0;
+ break;
+ } else {
+ printf("%zu %o %o\n", n, b[0], b[1]);
+ same = 0;
+ }
+ }
+
+ if (!ret)
+ ret = !same;
+ if (fshut(fp[0], argv[0]) | (fp[0] != fp[1] && fshut(fp[1], argv[1])) |
+ fshut(stdout, "<stdout>"))
+ ret = 2;
+
+ return ret;
+}
diff --git a/util/sbase/cols.1 b/util/sbase/cols.1
new file mode 100644
index 00000000..67c2e8ea
--- /dev/null
+++ b/util/sbase/cols.1
@@ -0,0 +1,56 @@
+.Dd October 8, 2015
+.Dt COLS 1
+.Os sbase
+.Sh NAME
+.Nm cols
+.Nd columnize output
+.Sh SYNOPSIS
+.Nm
+.Op Fl c Ar num
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+reads each
+.Ar file
+in sequence and writes them to stdout, in as many vertical
+columns as will fit in
+.Ar num
+character columns.
+If no
+.Ar file
+is given,
+.Nm
+reads from stdin.
+.Pp
+By default
+.Nm cols
+tries to figure out the width of the output device.
+If that fails, it defaults to 65 chars.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl c Ar num
+Set maximum number of character columns to
+.Ar num ,
+unless input lines exceed this limit.
+.El
+.Sh ENVIRONMENT
+.Bl -tag -width Ds
+.It COLUMNS
+The width of the output device.
+.El
+.Sh HISTORY
+.Nm
+is similar to
+.Xr mc 1
+in Plan 9. It was renamed to
+.Nm
+to avoid the name collision with the popular file manager
+Midnight Commander.
+.Sh CAVEATS
+This implementation of
+.Nm
+assumes that each UTF-8 code point occupies one character cell,
+and thus mishandles TAB characters (among others).
+.Pp
+.Nm
+currently mangles files which contain embedded NULs.
diff --git a/util/sbase/cols.c b/util/sbase/cols.c
new file mode 100644
index 00000000..428cd79d
--- /dev/null
+++ b/util/sbase/cols.c
@@ -0,0 +1,98 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/ioctl.h>
+
+#include <limits.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "text.h"
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-c num] [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ FILE *fp;
+ struct winsize w;
+ struct linebuf b = EMPTY_LINEBUF;
+ size_t chars = 65, maxlen = 0, i, j, k, len, cols, rows;
+ int cflag = 0, ret = 0;
+ char *p;
+
+ ARGBEGIN {
+ case 'c':
+ cflag = 1;
+ chars = estrtonum(EARGF(usage()), 1, MIN(LLONG_MAX, SIZE_MAX));
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!cflag) {
+ if ((p = getenv("COLUMNS")))
+ chars = estrtonum(p, 1, MIN(LLONG_MAX, SIZE_MAX));
+ else if (!ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) && w.ws_col > 0)
+ chars = w.ws_col;
+ }
+
+ if (!argc) {
+ getlines(stdin, &b);
+ } else {
+ for (; *argv; argc--, argv++) {
+ if (!strcmp(*argv, "-")) {
+ *argv = "<stdin>";
+ fp = stdin;
+ } else if (!(fp = fopen(*argv, "r"))) {
+ weprintf("fopen %s:", *argv);
+ ret = 1;
+ continue;
+ }
+ getlines(fp, &b);
+ if (fp != stdin && fshut(fp, *argv))
+ ret = 1;
+ }
+ }
+
+ for (i = 0; i < b.nlines; i++) {
+ for (j = 0, len = 0; j < b.lines[i].len; j++) {
+ if (UTF8_POINT(b.lines[i].data[j]))
+ len++;
+ }
+ if (len && b.lines[i].data[b.lines[i].len - 1] == '\n') {
+ b.lines[i].data[--(b.lines[i].len)] = '\0';
+ len--;
+ }
+ if (len > maxlen)
+ maxlen = len;
+ }
+
+ for (cols = 1; (cols + 1) * maxlen + cols <= chars; cols++);
+ rows = b.nlines / cols + (b.nlines % cols > 0);
+
+ for (i = 0; i < rows; i++) {
+ for (j = 0; j < cols && i + j * rows < b.nlines; j++) {
+ for (k = 0, len = 0; k < b.lines[i + j * rows].len; k++) {
+ if (UTF8_POINT(b.lines[i + j * rows].data[k]))
+ len++;
+ }
+ fwrite(b.lines[i + j * rows].data, 1,
+ b.lines[i + j * rows].len, stdout);
+ if (j < cols - 1)
+ for (k = len; k < maxlen + 1; k++)
+ putchar(' ');
+ }
+ putchar('\n');
+ }
+
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/comm.1 b/util/sbase/comm.1
new file mode 100644
index 00000000..5df70c2c
--- /dev/null
+++ b/util/sbase/comm.1
@@ -0,0 +1,40 @@
+.Dd October 8, 2015
+.Dt COMM 1
+.Os sbase
+.Sh NAME
+.Nm comm
+.Nd select or reject lines common to two files
+.Sh SYNOPSIS
+.Nm
+.Op Fl 123
+.Ar file1
+.Ar file2
+.Sh DESCRIPTION
+.Nm
+reads
+.Ar file1
+and
+.Ar file2 ,
+which should both be sorted lexically, and writes three text columns
+to stdout:
+.Bl -tag -width Ds
+.It 1
+Lines only in
+.Ar file1 .
+.It 2
+Lines only in
+.Ar file2 .
+.It 3
+Common lines.
+.El
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl 1 | Fl 2 | Fl 3
+Suppress column 1 | 2 | 3
+.El
+.Sh SEE ALSO
+.Xr cmp 1 ,
+.Xr sort 1 ,
+.Xr uniq 1
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/comm.c b/util/sbase/comm.c
new file mode 100644
index 00000000..fbd50d9b
--- /dev/null
+++ b/util/sbase/comm.c
@@ -0,0 +1,97 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "text.h"
+#include "util.h"
+
+static int show = 0x07;
+
+static void
+printline(int pos, struct line *line)
+{
+ int i;
+
+ if (!(show & (0x1 << pos)))
+ return;
+
+ for (i = 0; i < pos; i++) {
+ if (show & (0x1 << i))
+ putchar('\t');
+ }
+ fwrite(line->data, 1, line->len, stdout);
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-123] file1 file2\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ FILE *fp[2];
+ static struct line line[2];
+ size_t linecap[2] = { 0, 0 };
+ ssize_t len;
+ int ret = 0, i, diff = 0, seenline = 0;
+
+ ARGBEGIN {
+ case '1':
+ case '2':
+ case '3':
+ show &= 0x07 ^ (1 << (ARGC() - '1'));
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (argc != 2)
+ usage();
+
+ for (i = 0; i < 2; i++) {
+ if (!strcmp(argv[i], "-")) {
+ argv[i] = "<stdin>";
+ fp[i] = stdin;
+ } else if (!(fp[i] = fopen(argv[i], "r"))) {
+ eprintf("fopen %s:", argv[i]);
+ }
+ }
+
+ for (;;) {
+ for (i = 0; i < 2; i++) {
+ if (diff && i == (diff < 0))
+ continue;
+ if ((len = getline(&(line[i].data), &linecap[i],
+ fp[i])) > 0) {
+ line[i].len = len;
+ seenline = 1;
+ continue;
+ }
+ if (ferror(fp[i]))
+ eprintf("getline %s:", argv[i]);
+ if ((diff || seenline) && line[!i].data[0])
+ printline(!i, &line[!i]);
+ while ((len = getline(&(line[!i].data), &linecap[!i],
+ fp[!i])) > 0) {
+ line[!i].len = len;
+ printline(!i, &line[!i]);
+ }
+ if (ferror(fp[!i]))
+ eprintf("getline %s:", argv[!i]);
+ goto end;
+ }
+ diff = linecmp(&line[0], &line[1]);
+ LIMIT(diff, -1, 1);
+ seenline = 0;
+ printline((2 - diff) % 3, &line[MAX(0, diff)]);
+ }
+end:
+ ret |= fshut(fp[0], argv[0]);
+ ret |= (fp[0] != fp[1]) && fshut(fp[1], argv[1]);
+ ret |= fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/compat.h b/util/sbase/compat.h
new file mode 100644
index 00000000..e2154a62
--- /dev/null
+++ b/util/sbase/compat.h
@@ -0,0 +1,6 @@
+/* See LICENSE file for copyright and license details. */
+#include <limits.h>
+
+#ifndef HOST_NAME_MAX
+#define HOST_NAME_MAX _POSIX_HOST_NAME_MAX
+#endif
diff --git a/util/sbase/config.mk b/util/sbase/config.mk
new file mode 100644
index 00000000..69dda343
--- /dev/null
+++ b/util/sbase/config.mk
@@ -0,0 +1,15 @@
+# sbase version
+VERSION = 0.1
+
+# paths
+PREFIX = /usr/local
+MANPREFIX = $(PREFIX)/share/man
+
+# tools
+#CC =
+#AR =
+RANLIB = ranlib
+
+# -lrt might be needed on some systems
+# CFLAGS =
+# LDFLAGS =
diff --git a/util/sbase/cp.1 b/util/sbase/cp.1
new file mode 100644
index 00000000..74027eaa
--- /dev/null
+++ b/util/sbase/cp.1
@@ -0,0 +1,71 @@
+.Dd April 22, 2025
+.Dt CP 1
+.Os sbase
+.Sh NAME
+.Nm cp
+.Nd copy files and directories
+.Sh SYNOPSIS
+.Nm
+.Op Fl afipv
+.Oo
+.Fl R
+.Op Fl H | L | P
+.Oc
+.Ar source ...
+.Ar dest
+.Sh DESCRIPTION
+.Nm
+copies
+.Ar source
+to
+.Ar dest .
+If more than one
+.Ar source
+is given
+.Ar dest
+has to be a directory.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl a
+Preserve block devices, character devices, sockets and FIFOs.
+Implies
+.Fl p ,
+.Fl P
+and
+.Fl R .
+.It Fl f
+If an existing
+.Ar dest
+cannot be opened, remove it and try again.
+.It Fl i
+Interactive prompt before overwrite.
+.It Fl p
+Preserve mode, timestamp and permissions.
+.It Fl v
+Write "'source' -> 'dest'" for each
+.Ar source
+to stdout.
+.It Fl H
+Dereference
+.Ar source
+if it is a symbolic link.
+.It Fl L
+Dereference all symbolic links.
+This is the default without
+.Fl R .
+.It Fl P
+Preserve symbolic links.
+This is the default with
+.Fl R .
+.It Fl R
+Traverse directories recursively.
+If this flag is not specified, directories are not copied.
+.El
+.Sh SEE ALSO
+.Xr mv 1
+.Sh STANDARDS
+POSIX.1-2013.
+.Pp
+The
+.Op Fl av
+flags are an extension to that specification.
diff --git a/util/sbase/cp.c b/util/sbase/cp.c
new file mode 100644
index 00000000..af0fa610
--- /dev/null
+++ b/util/sbase/cp.c
@@ -0,0 +1,63 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+
+#include "fs.h"
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-afipv] [-R [-H | -L | -P]] source ... dest\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct stat st;
+
+ ARGBEGIN {
+ case 'i':
+ cp_iflag = 1;
+ break;
+ case 'a':
+ cp_follow = 'P';
+ cp_aflag = cp_pflag = cp_rflag = 1;
+ break;
+ case 'f':
+ cp_fflag = 1;
+ break;
+ case 'p':
+ cp_pflag = 1;
+ break;
+ case 'r':
+ case 'R':
+ cp_rflag = 1;
+ break;
+ case 'v':
+ cp_vflag = 1;
+ break;
+ case 'H':
+ case 'L':
+ case 'P':
+ cp_follow = ARGC();
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (argc < 2)
+ usage();
+
+ if (!cp_follow)
+ cp_follow = cp_rflag ? 'P' : 'L';
+
+ if (argc > 2) {
+ if (stat(argv[argc - 1], &st) < 0)
+ eprintf("stat %s:", argv[argc - 1]);
+ if (!S_ISDIR(st.st_mode))
+ eprintf("%s: not a directory\n", argv[argc - 1]);
+ }
+ enmasse(argc, argv, cp);
+
+ return fshut(stdout, "<stdout>") || cp_status;
+}
diff --git a/util/sbase/cron.1 b/util/sbase/cron.1
new file mode 100644
index 00000000..1cb90a44
--- /dev/null
+++ b/util/sbase/cron.1
@@ -0,0 +1,23 @@
+.Dd October 8, 2015
+.Dt CRON 1
+.Os sbase
+.Sh NAME
+.Nm cron
+.Nd clock daemon
+.Sh SYNOPSIS
+.Nm
+.Op Fl f Ar file
+.Op Fl n
+.Sh DESCRIPTION
+.Nm
+schedules commands to be run at specified dates and times.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl f Ar file
+Use the specified
+.Ar file
+instead of the default
+.Pa /etc/crontab .
+.It Fl n
+Do not daemonize.
+.El
diff --git a/util/sbase/cron.c b/util/sbase/cron.c
new file mode 100644
index 00000000..77304ccf
--- /dev/null
+++ b/util/sbase/cron.c
@@ -0,0 +1,566 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <errno.h>
+#include <limits.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <ctype.h>
+#include <string.h>
+#include <syslog.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "queue.h"
+#include "util.h"
+
+struct field {
+ enum {
+ ERROR,
+ WILDCARD,
+ NUMBER,
+ RANGE,
+ REPEAT,
+ LIST
+ } type;
+ long *val;
+ int len;
+};
+
+struct ctabentry {
+ struct field min;
+ struct field hour;
+ struct field mday;
+ struct field mon;
+ struct field wday;
+ char *cmd;
+ TAILQ_ENTRY(ctabentry) entry;
+};
+
+struct jobentry {
+ char *cmd;
+ pid_t pid;
+ TAILQ_ENTRY(jobentry) entry;
+};
+
+static sig_atomic_t chldreap;
+static sig_atomic_t reload;
+static sig_atomic_t quit;
+static TAILQ_HEAD(, ctabentry) ctabhead = TAILQ_HEAD_INITIALIZER(ctabhead);
+static TAILQ_HEAD(, jobentry) jobhead = TAILQ_HEAD_INITIALIZER(jobhead);
+static char *config = "/etc/crontab";
+static char *pidfile = "/var/run/crond.pid";
+static int nflag;
+
+static void
+loginfo(const char *fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ if (nflag == 0)
+ vsyslog(LOG_INFO, fmt, ap);
+ else
+ vfprintf(stdout, fmt, ap);
+ fflush(stdout);
+ va_end(ap);
+}
+
+static void
+logwarn(const char *fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ if (nflag == 0)
+ vsyslog(LOG_WARNING, fmt, ap);
+ else
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+}
+
+static void
+logerr(const char *fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ if (nflag == 0)
+ vsyslog(LOG_ERR, fmt, ap);
+ else
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+}
+
+static void
+runjob(char *cmd)
+{
+ struct jobentry *je;
+ time_t t;
+ pid_t pid;
+
+ t = time(NULL);
+
+ /* If command is already running, skip it */
+ TAILQ_FOREACH(je, &jobhead, entry) {
+ if (strcmp(je->cmd, cmd) == 0) {
+ loginfo("already running %s pid: %d at %s",
+ je->cmd, je->pid, ctime(&t));
+ return;
+ }
+ }
+
+ switch ((pid = fork())) {
+ case -1:
+ logerr("error: failed to fork job: %s time: %s",
+ cmd, ctime(&t));
+ return;
+ case 0:
+ setsid();
+ loginfo("run: %s pid: %d at %s",
+ cmd, getpid(), ctime(&t));
+ execl("/bin/sh", "/bin/sh", "-c", cmd, (char *)NULL);
+ logerr("error: failed to execute job: %s time: %s",
+ cmd, ctime(&t));
+ _exit(1);
+ default:
+ je = emalloc(sizeof(*je));
+ je->cmd = estrdup(cmd);
+ je->pid = pid;
+ TAILQ_INSERT_TAIL(&jobhead, je, entry);
+ }
+}
+
+static void
+waitjob(void)
+{
+ struct jobentry *je, *tmp;
+ int status;
+ time_t t;
+ pid_t pid;
+
+ t = time(NULL);
+
+ while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
+ je = NULL;
+ TAILQ_FOREACH(tmp, &jobhead, entry) {
+ if (tmp->pid == pid) {
+ je = tmp;
+ break;
+ }
+ }
+ if (je) {
+ TAILQ_REMOVE(&jobhead, je, entry);
+ free(je->cmd);
+ free(je);
+ }
+ if (WIFEXITED(status) == 1)
+ loginfo("complete: pid: %d returned: %d time: %s",
+ pid, WEXITSTATUS(status), ctime(&t));
+ else if (WIFSIGNALED(status) == 1)
+ loginfo("complete: pid: %d terminated by signal: %s time: %s",
+ pid, strsignal(WTERMSIG(status)), ctime(&t));
+ else if (WIFSTOPPED(status) == 1)
+ loginfo("complete: pid: %d stopped by signal: %s time: %s",
+ pid, strsignal(WSTOPSIG(status)), ctime(&t));
+ }
+}
+
+static int
+isleap(int year)
+{
+ if (year % 400 == 0)
+ return 1;
+ if (year % 100 == 0)
+ return 0;
+ return (year % 4 == 0);
+}
+
+static int
+daysinmon(int mon, int year)
+{
+ int days[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
+ if (year < 1900)
+ year += 1900;
+ if (isleap(year))
+ days[1] = 29;
+ return days[mon];
+}
+
+static int
+matchentry(struct ctabentry *cte, struct tm *tm)
+{
+ struct {
+ struct field *f;
+ int tm;
+ int len;
+ } matchtbl[] = {
+ { .f = &cte->min, .tm = tm->tm_min, .len = 60 },
+ { .f = &cte->hour, .tm = tm->tm_hour, .len = 24 },
+ { .f = &cte->mday, .tm = tm->tm_mday, .len = daysinmon(tm->tm_mon, tm->tm_year) },
+ { .f = &cte->mon, .tm = tm->tm_mon, .len = 12 },
+ { .f = &cte->wday, .tm = tm->tm_wday, .len = 7 },
+ };
+ size_t i;
+ int j;
+
+ for (i = 0; i < LEN(matchtbl); i++) {
+ switch (matchtbl[i].f->type) {
+ case WILDCARD:
+ continue;
+ case NUMBER:
+ if (matchtbl[i].f->val[0] == matchtbl[i].tm)
+ continue;
+ break;
+ case RANGE:
+ if (matchtbl[i].f->val[0] <= matchtbl[i].tm)
+ if (matchtbl[i].f->val[1] >= matchtbl[i].tm)
+ continue;
+ break;
+ case REPEAT:
+ if (matchtbl[i].tm > 0) {
+ if (matchtbl[i].tm % matchtbl[i].f->val[0] == 0)
+ continue;
+ } else {
+ if (matchtbl[i].len % matchtbl[i].f->val[0] == 0)
+ continue;
+ }
+ break;
+ case LIST:
+ for (j = 0; j < matchtbl[i].f->len; j++)
+ if (matchtbl[i].f->val[j] == matchtbl[i].tm)
+ break;
+ if (j < matchtbl[i].f->len)
+ continue;
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+ if (i != LEN(matchtbl))
+ return 0;
+ return 1;
+}
+
+static int
+parsefield(const char *field, long low, long high, struct field *f)
+{
+ int i;
+ char *e1, *e2;
+ const char *p;
+
+ p = field;
+ while (isdigit(*p))
+ p++;
+
+ f->type = ERROR;
+
+ switch (*p) {
+ case '*':
+ if (strcmp(field, "*") == 0) {
+ f->val = NULL;
+ f->len = 0;
+ f->type = WILDCARD;
+ } else if (strncmp(field, "*/", 2) == 0) {
+ f->val = emalloc(sizeof(*f->val));
+ f->len = 1;
+
+ errno = 0;
+ f->val[0] = strtol(field + 2, &e1, 10);
+ if (e1[0] != '\0' || errno != 0 || f->val[0] == 0)
+ break;
+
+ f->type = REPEAT;
+ }
+ break;
+ case '\0':
+ f->val = emalloc(sizeof(*f->val));
+ f->len = 1;
+
+ errno = 0;
+ f->val[0] = strtol(field, &e1, 10);
+ if (e1[0] != '\0' || errno != 0)
+ break;
+
+ f->type = NUMBER;
+ break;
+ case '-':
+ f->val = emalloc(2 * sizeof(*f->val));
+ f->len = 2;
+
+ errno = 0;
+ f->val[0] = strtol(field, &e1, 10);
+ if (e1[0] != '-' || errno != 0)
+ break;
+
+ errno = 0;
+ f->val[1] = strtol(e1 + 1, &e2, 10);
+ if (e2[0] != '\0' || errno != 0)
+ break;
+
+ f->type = RANGE;
+ break;
+ case ',':
+ for (i = 1; isdigit(*p) || *p == ','; p++)
+ if (*p == ',')
+ i++;
+ f->val = emalloc(i * sizeof(*f->val));
+ f->len = i;
+
+ errno = 0;
+ f->val[0] = strtol(field, &e1, 10);
+ if (f->val[0] < low || f->val[0] > high)
+ break;
+
+ for (i = 1; *e1 == ',' && errno == 0; i++) {
+ errno = 0;
+ f->val[i] = strtol(e1 + 1, &e2, 10);
+ e1 = e2;
+ }
+ if (e1[0] != '\0' || errno != 0)
+ break;
+
+ f->type = LIST;
+ break;
+ default:
+ return -1;
+ }
+
+ for (i = 0; i < f->len; i++)
+ if (f->val[i] < low || f->val[i] > high)
+ f->type = ERROR;
+
+ if (f->type == ERROR) {
+ free(f->val);
+ return -1;
+ }
+
+ return 0;
+}
+
+static void
+freecte(struct ctabentry *cte, int nfields)
+{
+ switch (nfields) {
+ case 6:
+ free(cte->cmd);
+ case 5:
+ free(cte->wday.val);
+ case 4:
+ free(cte->mon.val);
+ case 3:
+ free(cte->mday.val);
+ case 2:
+ free(cte->hour.val);
+ case 1:
+ free(cte->min.val);
+ }
+ free(cte);
+}
+
+static void
+unloadentries(void)
+{
+ struct ctabentry *cte, *tmp;
+
+ for (cte = TAILQ_FIRST(&ctabhead); cte; cte = tmp) {
+ tmp = TAILQ_NEXT(cte, entry);
+ TAILQ_REMOVE(&ctabhead, cte, entry);
+ freecte(cte, 6);
+ }
+}
+
+static int
+loadentries(void)
+{
+ struct ctabentry *cte;
+ FILE *fp;
+ char *line = NULL, *p, *col;
+ int r = 0, y;
+ size_t size = 0;
+ ssize_t len;
+ struct fieldlimits {
+ char *name;
+ long min;
+ long max;
+ struct field *f;
+ } flim[] = {
+ { "min", 0, 59, NULL },
+ { "hour", 0, 23, NULL },
+ { "mday", 1, 31, NULL },
+ { "mon", 1, 12, NULL },
+ { "wday", 0, 6, NULL }
+ };
+ size_t x;
+
+ if ((fp = fopen(config, "r")) == NULL) {
+ logerr("error: can't open %s: %s\n", config, strerror(errno));
+ return -1;
+ }
+
+ for (y = 0; (len = getline(&line, &size, fp)) != -1; y++) {
+ p = line;
+ if (line[0] == '#' || line[0] == '\n' || line[0] == '\0')
+ continue;
+
+ cte = emalloc(sizeof(*cte));
+ flim[0].f = &cte->min;
+ flim[1].f = &cte->hour;
+ flim[2].f = &cte->mday;
+ flim[3].f = &cte->mon;
+ flim[4].f = &cte->wday;
+
+ for (x = 0; x < LEN(flim); x++) {
+ do
+ col = strsep(&p, "\t\n ");
+ while (col && col[0] == '\0');
+
+ if (!col || parsefield(col, flim[x].min, flim[x].max, flim[x].f) < 0) {
+ logerr("error: failed to parse `%s' field on line %d\n",
+ flim[x].name, y + 1);
+ freecte(cte, x);
+ r = -1;
+ break;
+ }
+ }
+
+ if (r == -1)
+ break;
+
+ col = strsep(&p, "\n");
+ if (col)
+ while (col[0] == '\t' || col[0] == ' ')
+ col++;
+ if (!col || col[0] == '\0') {
+ logerr("error: missing `cmd' field on line %d\n",
+ y + 1);
+ freecte(cte, 5);
+ r = -1;
+ break;
+ }
+ cte->cmd = estrdup(col);
+
+ TAILQ_INSERT_TAIL(&ctabhead, cte, entry);
+ }
+
+ if (r < 0)
+ unloadentries();
+
+ free(line);
+ fclose(fp);
+
+ return r;
+}
+
+static void
+reloadentries(void)
+{
+ unloadentries();
+ if (loadentries() < 0)
+ logwarn("warning: discarding old crontab entries\n");
+}
+
+static void
+sighandler(int sig)
+{
+ switch (sig) {
+ case SIGCHLD:
+ chldreap = 1;
+ break;
+ case SIGHUP:
+ reload = 1;
+ break;
+ case SIGTERM:
+ quit = 1;
+ break;
+ }
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-f file] [-n]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ FILE *fp;
+ struct ctabentry *cte;
+ time_t t;
+ struct tm *tm;
+ struct sigaction sa;
+
+ ARGBEGIN {
+ case 'n':
+ nflag = 1;
+ break;
+ case 'f':
+ config = EARGF(usage());
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (argc > 0)
+ usage();
+
+ if (nflag == 0) {
+ openlog(argv[0], LOG_CONS | LOG_PID, LOG_CRON);
+ if (daemon(1, 0) < 0) {
+ logerr("error: failed to daemonize %s\n", strerror(errno));
+ return 1;
+ }
+ if ((fp = fopen(pidfile, "w"))) {
+ fprintf(fp, "%d\n", getpid());
+ fclose(fp);
+ }
+ }
+
+ sa.sa_handler = sighandler;
+ sigfillset(&sa.sa_mask);
+ sa.sa_flags = SA_RESTART;
+ sigaction(SIGCHLD, &sa, NULL);
+ sigaction(SIGHUP, &sa, NULL);
+ sigaction(SIGTERM, &sa, NULL);
+
+ loadentries();
+
+ while (1) {
+ t = time(NULL);
+ sleep(60 - t % 60);
+
+ if (quit == 1) {
+ if (nflag == 0)
+ unlink(pidfile);
+ unloadentries();
+ /* Don't wait or kill forked processes, just exit */
+ break;
+ }
+
+ if (reload == 1 || chldreap == 1) {
+ if (reload == 1) {
+ reloadentries();
+ reload = 0;
+ }
+ if (chldreap == 1) {
+ waitjob();
+ chldreap = 0;
+ }
+ continue;
+ }
+
+ TAILQ_FOREACH(cte, &ctabhead, entry) {
+ t = time(NULL);
+ tm = localtime(&t);
+ if (matchentry(cte, tm) == 1)
+ runjob(cte->cmd);
+ }
+ }
+
+ if (nflag == 0)
+ closelog();
+
+ return 0;
+}
diff --git a/util/sbase/crypt.h b/util/sbase/crypt.h
new file mode 100644
index 00000000..2fd2932e
--- /dev/null
+++ b/util/sbase/crypt.h
@@ -0,0 +1,12 @@
+/* See LICENSE file for copyright and license details. */
+struct crypt_ops {
+ void (*init)(void *);
+ void (*update)(void *, const void *, unsigned long);
+ void (*sum)(void *, uint8_t *);
+ void *s;
+};
+
+int cryptcheck(int, char **, struct crypt_ops *, uint8_t *, size_t);
+int cryptmain(int, char **, struct crypt_ops *, uint8_t *, size_t);
+int cryptsum(struct crypt_ops *, int, const char *, uint8_t *);
+void mdprint(const uint8_t *, const char *, size_t);
diff --git a/util/sbase/cut.1 b/util/sbase/cut.1
new file mode 100644
index 00000000..7a5174e6
--- /dev/null
+++ b/util/sbase/cut.1
@@ -0,0 +1,69 @@
+.Dd October 8, 2015
+.Dt CUT 1
+.Os sbase
+.Sh NAME
+.Nm cut
+.Nd extract columns of data
+.Sh SYNOPSIS
+.Nm
+.Fl b Ar list
+.Op Fl n
+.Op Ar file ...
+.Nm
+.Fl c Ar list
+.Op Ar file ...
+.Nm
+.Fl f Ar list
+.Op Fl d Ar delim
+.Op Fl s
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+out bytes, characters or delimited fields from each line of
+.Ar file
+and write to stdout.
+.Pp
+If no
+.Ar file
+is given or
+.Ar file
+is '-',
+.Nm
+reads from stdin.
+.Pp
+.Ar list
+is a comma or space separated list of numbers and ranges starting
+from 1.
+Ranges have the form 'N-M'. If N or M is missing, beginning or end
+of line is assumed.
+Numbers and ranges may be repeated, overlapping and in any order.
+.Pp
+Selected input is written in the same order it is read
+and is written exactly once.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl b Ar list | Fl c Ar list
+.Ar list
+specifies byte | character positions.
+.It Fl d Ar delim
+Use
+.Ar delim
+as field delimiter, which can be an arbitrary string.
+Default is '\et'.
+.It Fl f Ar list
+.Ar list
+specifies field numbers.
+Lines not containing field delimiters are passed through, unless
+.Fl s
+is specified.
+.It Fl n
+Do not split multibyte characters.
+A character is written when its last byte is selected.
+.It Fl s
+Suppress lines not containing field delimiters.
+.El
+.Sh STANDARDS
+POSIX.1-2013.
+.Pp
+The possibility of separating numbers and ranges with a space and specifying
+multibyte delimiters of arbitrary length is an extension to that specification.
diff --git a/util/sbase/cut.c b/util/sbase/cut.c
new file mode 100644
index 00000000..a50bdcb5
--- /dev/null
+++ b/util/sbase/cut.c
@@ -0,0 +1,215 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "text.h"
+#include "utf.h"
+#include "util.h"
+
+typedef struct Range {
+ size_t min, max;
+ struct Range *next;
+} Range;
+
+static Range *list = NULL;
+static char mode = 0;
+static char *delim = "\t";
+static size_t delimlen = 1;
+static int nflag = 0;
+static int sflag = 0;
+
+static void
+insert(Range *r)
+{
+ Range *l, *p, *t;
+
+ for (p = NULL, l = list; l; p = l, l = l->next) {
+ if (r->max && r->max + 1 < l->min) {
+ r->next = l;
+ break;
+ } else if (!l->max || r->min < l->max + 2) {
+ l->min = MIN(r->min, l->min);
+ for (p = l, t = l->next; t; p = t, t = t->next)
+ if (r->max && r->max + 1 < t->min)
+ break;
+ l->max = (p->max && r->max) ? MAX(p->max, r->max) : 0;
+ l->next = t;
+ return;
+ }
+ }
+ if (p)
+ p->next = r;
+ else
+ list = r;
+}
+
+static void
+parselist(char *str)
+{
+ char *s;
+ size_t n = 1;
+ Range *r;
+
+ if (!*str)
+ eprintf("empty list\n");
+ for (s = str; *s; s++) {
+ if (*s == ' ')
+ *s = ',';
+ if (*s == ',')
+ n++;
+ }
+ r = ereallocarray(NULL, n, sizeof(*r));
+ for (s = str; n; n--, s++) {
+ r->min = (*s == '-') ? 1 : strtoul(s, &s, 10);
+ r->max = (*s == '-') ? strtoul(s + 1, &s, 10) : r->min;
+ r->next = NULL;
+ if (!r->min || (r->max && r->max < r->min) || (*s && *s != ','))
+ eprintf("bad list value\n");
+ insert(r++);
+ }
+}
+
+static size_t
+seek(struct line *s, size_t pos, size_t *prev, size_t count)
+{
+ size_t n = pos - *prev, i, j;
+
+ if (mode == 'b') {
+ if (n >= s->len)
+ return s->len;
+ if (nflag)
+ while (n && !UTF8_POINT(s->data[n]))
+ n--;
+ *prev += n;
+ return n;
+ } else if (mode == 'c') {
+ for (n++, i = 0; i < s->len; i++)
+ if (UTF8_POINT(s->data[i]) && !--n)
+ break;
+ } else {
+ for (i = (count < delimlen + 1) ? 0 : delimlen; n && i < s->len; ) {
+ if ((s->len - i) >= delimlen &&
+ !memcmp(s->data + i, delim, delimlen)) {
+ if (!--n && count)
+ break;
+ i += delimlen;
+ continue;
+ }
+ for (j = 1; j + i <= s->len && !fullrune(s->data + i, j); j++);
+ i += j;
+ }
+ }
+ *prev = pos;
+
+ return i;
+}
+
+static void
+cut(FILE *fp, const char *fname)
+{
+ Range *r;
+ struct line s;
+ static struct line line;
+ static size_t size;
+ size_t i, n, p;
+ ssize_t len;
+
+ while ((len = getline(&line.data, &size, fp)) > 0) {
+ line.len = len;
+ if (line.data[line.len - 1] == '\n')
+ line.data[--line.len] = '\0';
+ if (mode == 'f' && !memmem(line.data, line.len, delim, delimlen)) {
+ if (!sflag) {
+ fwrite(line.data, 1, line.len, stdout);
+ fputc('\n', stdout);
+ }
+ continue;
+ }
+ for (i = 0, p = 1, s = line, r = list; r; r = r->next) {
+ n = seek(&s, r->min, &p, i);
+ s.data += n;
+ s.len -= n;
+ i += (mode == 'f') ? delimlen : 1;
+ if (!s.len)
+ break;
+ if (!r->max) {
+ fwrite(s.data, 1, s.len, stdout);
+ break;
+ }
+ n = seek(&s, r->max + 1, &p, i);
+ i += (mode == 'f') ? delimlen : 1;
+ if (fwrite(s.data, 1, n, stdout) != n)
+ eprintf("fwrite <stdout>:");
+ s.data += n;
+ s.len -= n;
+ }
+ putchar('\n');
+ }
+ if (ferror(fp))
+ eprintf("getline %s:", fname);
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s -b list [-n] [file ...]\n"
+ " %s -c list [file ...]\n"
+ " %s -f list [-d delim] [-s] [file ...]\n",
+ argv0, argv0, argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ FILE *fp;
+ int ret = 0;
+
+ ARGBEGIN {
+ case 'b':
+ case 'c':
+ case 'f':
+ mode = ARGC();
+ parselist(EARGF(usage()));
+ break;
+ case 'd':
+ delim = EARGF(usage());
+ if (!*delim)
+ eprintf("empty delimiter\n");
+ delimlen = unescape(delim);
+ break;
+ case 'n':
+ nflag = 1;
+ break;
+ case 's':
+ sflag = 1;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!mode)
+ usage();
+
+ if (!argc)
+ cut(stdin, "<stdin>");
+ else {
+ for (; *argv; argc--, argv++) {
+ if (!strcmp(*argv, "-")) {
+ *argv = "<stdin>";
+ fp = stdin;
+ } else if (!(fp = fopen(*argv, "r"))) {
+ weprintf("fopen %s:", *argv);
+ ret = 1;
+ continue;
+ }
+ cut(fp, *argv);
+ if (fp != stdin && fshut(fp, *argv))
+ ret = 1;
+ }
+ }
+
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/date.1 b/util/sbase/date.1
new file mode 100644
index 00000000..fb9fb31b
--- /dev/null
+++ b/util/sbase/date.1
@@ -0,0 +1,81 @@
+.Dd October 8, 2015
+.Dt DATE 1
+.Os sbase
+.Sh NAME
+.Nm date
+.Nd print or set date and time
+.Sh SYNOPSIS
+.Nm
+.Op Fl d Ar time
+.Op Fl u
+.Oo
+.Cm + Ns Ar format |
+.Sm off
+.Ar mmddHHMM Oo Oo Ar CC Oc Ar yy Oc
+.Sm on
+.Oc
+.Sh DESCRIPTION
+.Nm
+prints the date and time according to
+.Xr locale 7
+or
+.Ar format
+using
+.Xr strftime 3
+or sets the date.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl d Ar time
+Print
+.Ar time
+given as the number of seconds since the
+Unix epoch 1970-01-01T00:00:00Z.
+.It Fl u
+Print or set UTC time instead of local time.
+.El
+.Pp
+An operand with a leading plus
+.Pq Cm +
+sign signals a user-defined format string using
+.Xr strftime 3
+conversion specifications.
+.Pp
+An operand without a leading plus sign is interpreted as a value
+for setting the system's current date and time.
+The canonical representation for setting the date and time is:
+.Pp
+.Bl -tag -width Ds -compact -offset indent
+.It Ar mm
+The month of the year, from 01 to 12.
+.It Ar dd
+The day of the month, from 01 to 31.
+.It Ar HH
+The hour of the day, from 00 to 23.
+.It Ar MM
+The minute of the hour, from 00 to 59.
+.It Ar CC
+The first two digits of the year (the century).
+.It Ar yy
+The second two digits of the year.
+If
+.Ar yy
+is specified, but
+.Ar CC
+is not, a value for
+.Ar yy
+between 69 and 99 results in a
+.Ar CC
+value of 19.
+Otherwise, a
+.Ar CC
+value of 20 is used.
+.El
+.Pp
+The century and year are optional.
+The default is the current year.
+.Sh STANDARDS
+POSIX.1-2013.
+.Pp
+The
+.Op Fl d
+flag is an extension to that specification.
diff --git a/util/sbase/date.c b/util/sbase/date.c
new file mode 100644
index 00000000..109f3710
--- /dev/null
+++ b/util/sbase/date.c
@@ -0,0 +1,103 @@
+/* See LICENSE file for copyright and license details. */
+#include <ctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-u] [-d time] [+format | mmddHHMM[[CC]yy]]\n", argv0);
+}
+
+static int
+datefield(const char *s, size_t i)
+{
+ if (!isdigit(s[i]) || !isdigit(s[i+1]))
+ eprintf("invalid date format: %s\n", s);
+
+ return (s[i] - '0') * 10 + (s[i+1] - '0');
+}
+
+static void
+setdate(const char *s, struct tm *now)
+{
+ struct tm date;
+ struct timespec ts;
+
+ switch (strlen(s)) {
+ case 8:
+ date.tm_year = now->tm_year;
+ break;
+ case 10:
+ date.tm_year = datefield(s, 8);
+ if (date.tm_year < 69)
+ date.tm_year += 100;
+ break;
+ case 12:
+ date.tm_year = ((datefield(s, 8) - 19) * 100) + datefield(s, 10);
+ break;
+ default:
+ eprintf("invalid date format: %s\n", s);
+ break;
+ }
+
+ date.tm_mon = datefield(s, 0) - 1;
+ date.tm_mday = datefield(s, 2);
+ date.tm_hour = datefield(s, 4);
+ date.tm_min = datefield(s, 6);
+ date.tm_sec = 0;
+ date.tm_isdst = -1;
+
+ ts.tv_sec = mktime(&date);
+ if (ts.tv_sec == -1)
+ eprintf("mktime:");
+ ts.tv_nsec = 0;
+
+ if (clock_settime(CLOCK_REALTIME, &ts) == -1)
+ eprintf("clock_settime:");
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct tm *now;
+ time_t t;
+ char buf[BUFSIZ], *fmt = "%a %b %e %H:%M:%S %Z %Y";
+
+ t = time(NULL);
+ if (t == -1)
+ eprintf("time:");
+
+ ARGBEGIN {
+ case 'd':
+ t = estrtonum(EARGF(usage()), 0, LLONG_MAX);
+ break;
+ case 'u':
+ if (setenv("TZ", "UTC0", 1) < 0)
+ eprintf("setenv:");
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!(now = localtime(&t)))
+ eprintf("localtime:");
+ if (argc) {
+ if (argc != 1)
+ usage();
+ if (argv[0][0] != '+') {
+ setdate(argv[0], now);
+ return 0;
+ }
+ fmt = &argv[0][1];
+ }
+
+ strftime(buf, sizeof(buf), fmt, now);
+ puts(buf);
+
+ return fshut(stdout, "<stdout>");
+}
diff --git a/util/sbase/dd.1 b/util/sbase/dd.1
new file mode 100644
index 00000000..39c5c228
--- /dev/null
+++ b/util/sbase/dd.1
@@ -0,0 +1,91 @@
+.Dd April 28, 2020
+.Dt DD 1
+.Os sbase
+.Sh NAME
+.Nm dd
+.Nd convert and copy a file
+.Sh SYNOPSIS
+.Nm
+.Op Ar operand Ns ...
+.Sh DESCRIPTION
+.Nm
+copies its input to its output, possibly after conversion, using
+the specified block sizes,
+.Pp
+The following operands are available:
+.Bl -tag -width ibs=expr
+.It Cm if= Ns Ar file
+Read from the file named by
+.Ar file
+instead of standard input.
+.It Cm of= Ns Ar file
+Write to the file named by
+.Ar file
+instead of standard output.
+.It Cm ibs= Ns Ar expr
+Set the input block size to
+.Ar expr
+(defaults to 512).
+.It Cm obs= Ns Ar expr
+Set the output block size to
+.Ar expr
+(defaults to 512).
+.It Cm bs= Ns Ar expr
+Set the input and output block sizes to
+.Ar expr .
+Additionally, if no conversion other than
+.Cm noerror ,
+.Cm notrunc ,
+or
+.Cm sync
+is specified, input blocks are copied as single output blocks, even
+when the input block is short.
+.It Cm skip= Ns Ar n
+Skip
+.Ar n
+input blocks before starting to copy.
+.It Cm seek= Ns Ar n
+Skip
+.Ar n
+output blocks before starting to copy.
+.It Cm count= Ns Ar n
+Copy at most
+.Ar n
+input blocks.
+.It Cm conv= Ns Ar value Ns Op , Ns Ar value Ns ...
+Apply the conversions specified by
+.Ar value .
+.Bl -tag -width Ds
+.It Cm lcase
+Map uppercase characters to the corresponding lowercase character
+using
+.Fn tolower .
+.It Cm ucase
+Map lowercase characters to the corresponding uppercase character
+using
+.Fn toupper .
+.It Cm swab
+Swap each pair of bytes in the input block.
+If there is an odd number of bytes in a block, the last one is
+unmodified.
+.It Cm noerror
+In case of an error reading from the input, do not fail.
+Instead, print a diagnostic message and a summary of the current
+status.
+.It Cm notrunc
+Do not truncate the output file.
+.It Cm sync
+In case of a partial input block, pad with null bytes to form a
+complete block.
+.El
+.El
+.Sh STANDARDS
+The
+.Nm
+utility is compliant with the
+.St -p1003.1-2008
+specification, except that it does not implement the
+.Cm block
+and
+.Cm unblock
+conversions.
diff --git a/util/sbase/dd.c b/util/sbase/dd.c
new file mode 100644
index 00000000..36eb4094
--- /dev/null
+++ b/util/sbase/dd.c
@@ -0,0 +1,237 @@
+/* See LICENSE file for copyright and license details. */
+#include <ctype.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static off_t ifull, ofull, ipart, opart;
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [operand...]\n", argv0);
+}
+
+static size_t
+parsesize(char *expr)
+{
+ char *s = expr;
+ size_t n = 1;
+
+ for (;;) {
+ n *= strtoumax(s, &s, 10);
+ switch (*s) {
+ case 'k': n <<= 10; s++; break;
+ case 'b': n <<= 9; s++; break;
+ }
+ if (*s != 'x' || !s[1])
+ break;
+ s++;
+ }
+ if (*s || n == 0)
+ eprintf("invalid block size expression '%s'\n", expr);
+
+ return n;
+}
+
+static void
+bswap(unsigned char *buf, size_t len)
+{
+ int c;
+
+ for (len &= ~1; len > 0; buf += 2, len -= 2) {
+ c = buf[0];
+ buf[0] = buf[1];
+ buf[1] = c;
+ }
+}
+
+static void
+lcase(unsigned char *buf, size_t len)
+{
+ for (; len > 0; buf++, len--)
+ buf[0] = tolower(buf[0]);
+}
+
+static void
+ucase(unsigned char *buf, size_t len)
+{
+ for (; len > 0; buf++, len--)
+ buf[0] = toupper(buf[0]);
+}
+
+static void
+summary(void)
+{
+ fprintf(stderr, "%"PRIdMAX"+%"PRIdMAX" records in\n", (intmax_t)ifull, (intmax_t)ipart);
+ fprintf(stderr, "%"PRIdMAX"+%"PRIdMAX" records out\n", (intmax_t)ofull, (intmax_t)opart);
+}
+
+int
+main(int argc, char *argv[])
+{
+ enum {
+ LCASE = 1 << 0,
+ UCASE = 1 << 1,
+ SWAB = 1 << 2,
+ NOERROR = 1 << 3,
+ NOTRUNC = 1 << 4,
+ SYNC = 1 << 5,
+ } conv = 0;
+ char *arg, *val, *end;
+ const char *iname = "-", *oname = "-";
+ int ifd = 0, ofd = 1, eof = 0;
+ size_t len, bs = 0, ibs = 512, obs = 512, ipos = 0, opos = 0;
+ off_t skip = 0, seek = 0, count = -1;
+ ssize_t ret;
+ unsigned char *buf;
+
+ argv0 = argc ? (argc--, *argv++) : "dd";
+ for (; argc > 0; argc--, argv++) {
+ arg = *argv;
+ val = strchr(arg, '=');
+ if (!val)
+ usage();
+ *val++ = '\0';
+ if (strcmp(arg, "if") == 0) {
+ iname = val;
+ } else if (strcmp(arg, "of") == 0) {
+ oname = val;
+ } else if (strcmp(arg, "ibs") == 0) {
+ ibs = parsesize(val);
+ } else if (strcmp(arg, "obs") == 0) {
+ obs = parsesize(val);
+ } else if (strcmp(arg, "bs") == 0) {
+ bs = parsesize(val);
+ } else if (strcmp(arg, "skip") == 0) {
+ skip = estrtonum(val, 0, LLONG_MAX);
+ } else if (strcmp(arg, "seek") == 0) {
+ seek = estrtonum(val, 0, LLONG_MAX);
+ } else if (strcmp(arg, "count") == 0) {
+ count = estrtonum(val, 0, LLONG_MAX);
+ } else if (strcmp(arg, "conv") == 0) {
+ do {
+ end = strchr(val, ',');
+ if (end)
+ *end++ = '\0';
+ if (strcmp(val, "lcase") == 0)
+ conv |= LCASE;
+ else if (strcmp(val, "ucase") == 0)
+ conv |= UCASE;
+ else if (strcmp(val, "swab") == 0)
+ conv |= SWAB;
+ else if (strcmp(val, "noerror") == 0)
+ conv |= NOERROR;
+ else if (strcmp(val, "notrunc") == 0)
+ conv |= NOTRUNC;
+ else if (strcmp(val, "sync") == 0)
+ conv |= SYNC;
+ else
+ eprintf("unknown conv flag '%s'\n", val);
+ val = end;
+ } while (val);
+ } else {
+ weprintf("unknown operand '%s'\n", arg);
+ usage();
+ }
+ }
+
+ if (bs)
+ ibs = obs = bs;
+ if (strcmp(iname, "-") != 0) {
+ ifd = open(iname, O_RDONLY);
+ if (ifd < 0)
+ eprintf("open %s:", iname);
+ }
+ if (strcmp(oname, "-") != 0) {
+ ofd = open(oname, O_WRONLY | O_CREAT | (conv & NOTRUNC || seek ? 0 : O_TRUNC), 0666);
+ if (ofd < 0)
+ eprintf("open %s:", oname);
+ }
+
+ len = MAX(ibs, obs) + ibs;
+ buf = emalloc(len);
+ if (skip && lseek(ifd, skip * ibs, SEEK_SET) < 0) {
+ while (skip--) {
+ ret = read(ifd, buf, ibs);
+ if (ret < 0)
+ eprintf("read:");
+ if (ret == 0) {
+ eof = 1;
+ break;
+ }
+ }
+ }
+ if (seek) {
+ if (!(conv & NOTRUNC) && ftruncate(ofd, seek * ibs) != 0)
+ eprintf("ftruncate:");
+ if (lseek(ofd, seek * ibs, SEEK_SET) < 0)
+ eprintf("lseek:");
+ /* XXX: handle non-seekable files */
+ }
+ while (!eof) {
+ while (ipos - opos < obs) {
+ if (ifull + ipart == count) {
+ eof = 1;
+ break;
+ }
+ ret = read(ifd, buf + ipos, ibs);
+ if (ret == 0) {
+ eof = 1;
+ break;
+ }
+ if (ret < 0) {
+ weprintf("read:");
+ if (!(conv & NOERROR))
+ return 1;
+ summary();
+ if (!(conv & SYNC))
+ continue;
+ ret = 0;
+ }
+ if (ret < ibs) {
+ ipart++;
+ if (conv & SYNC) {
+ memset(buf + ipos + ret, 0, ibs - ret);
+ ret = ibs;
+ }
+ } else {
+ ifull++;
+ }
+ if (conv & SWAB)
+ bswap(buf + ipos, ret);
+ if (conv & LCASE)
+ lcase(buf + ipos, ret);
+ if (conv & UCASE)
+ ucase(buf + ipos, ret);
+ ipos += ret;
+ if (bs && !(conv & (SWAB | LCASE | UCASE)))
+ break;
+ }
+ if (ipos == opos)
+ break;
+ do {
+ ret = write(ofd, buf + opos, MIN(obs, ipos - opos));
+ if (ret < 0)
+ eprintf("write:");
+ if (ret == 0)
+ eprintf("write returned 0\n");
+ if (ret < obs)
+ opart++;
+ else
+ ofull++;
+ opos += ret;
+ } while (ipos - opos >= (eof ? 1 : obs));
+ if (opos < ipos)
+ memmove(buf, buf + opos, ipos - opos);
+ ipos -= opos;
+ opos = 0;
+ }
+ summary();
+
+ return 0;
+}
diff --git a/util/sbase/dirname.1 b/util/sbase/dirname.1
new file mode 100644
index 00000000..742c9912
--- /dev/null
+++ b/util/sbase/dirname.1
@@ -0,0 +1,19 @@
+.Dd October 8, 2015
+.Dt DIRNAME 1
+.Os sbase
+.Sh NAME
+.Nm dirname
+.Nd strip final path component
+.Sh SYNOPSIS
+.Nm
+.Ar path
+.Sh DESCRIPTION
+.Nm
+writes
+.Ar path
+with its final path component removed to stdout.
+.Sh SEE ALSO
+.Xr basename 1 ,
+.Xr dirname 3
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/dirname.c b/util/sbase/dirname.c
new file mode 100644
index 00000000..45e1a7e2
--- /dev/null
+++ b/util/sbase/dirname.c
@@ -0,0 +1,27 @@
+/* See LICENSE file for copyright and license details. */
+#include <libgen.h>
+#include <stdio.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s path\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ ARGBEGIN {
+ default:
+ usage();
+ } ARGEND
+
+ if (argc != 1)
+ usage();
+
+ puts(dirname(argv[0]));
+
+ return fshut(stdout, "<stdout>");
+}
diff --git a/util/sbase/du.1 b/util/sbase/du.1
new file mode 100644
index 00000000..ef49052e
--- /dev/null
+++ b/util/sbase/du.1
@@ -0,0 +1,57 @@
+.Dd October 8, 2015
+.Dt DU 1
+.Os sbase
+.Sh NAME
+.Nm du
+.Nd display disk usage statistics
+.Sh SYNOPSIS
+.Nm
+.Op Fl a | s
+.Op Fl d Ar depth
+.Op Fl h
+.Op Fl k
+.Op Fl H | L | P
+.Op Fl x
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+displays the file system block usage for each
+.Ar file
+argument and for each directory in the file hierarchy rooted in directory
+argument.
+If no
+.Ar file
+is specified, the block usage of the hierarchy rooted in the current directory
+is displayed.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl a
+Display an entry for each file in the file hierarchy.
+.It Fl s
+Display only the grand total for the specified files.
+.It Fl d Ar depth
+Maximum directory depth to print files and directories.
+.It Fl h
+Enable human-readable output.
+.It Fl k
+By default all sizes are reported in 512-byte block counts.
+The
+.Fl k
+option causes the numbers to be reported in kilobyte counts.
+.It Fl H
+Only dereference symbolic links that are passed as command line arguments when
+recursively traversing directories.
+.It Fl L
+Always dereference symbolic links while recursively traversing directories.
+.It Fl P
+Don't dereference symbolic links.
+This is the default.
+.It Fl x
+Do not traverse file systems mount points.
+.El
+.Sh STANDARDS
+POSIX.1-2013.
+.Pp
+The
+.Op Fl dhP
+flags are an extension to that specification.
diff --git a/util/sbase/du.c b/util/sbase/du.c
new file mode 100644
index 00000000..782b09a2
--- /dev/null
+++ b/util/sbase/du.c
@@ -0,0 +1,167 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <search.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include "fs.h"
+#include "util.h"
+
+static size_t maxdepth = SIZE_MAX;
+static size_t blksize = 512;
+
+static int aflag = 0;
+static int sflag = 0;
+static int hflag = 0;
+
+struct file {
+ dev_t devno;
+ ino_t inode;
+};
+
+static void
+printpath(off_t n, const char *path)
+{
+ if (hflag)
+ printf("%s\t%s\n", humansize(n * blksize), path);
+ else
+ printf("%jd\t%s\n", (intmax_t)n, path);
+}
+
+static off_t
+nblks(blkcnt_t blocks)
+{
+ return (512 * blocks + blksize - 1) / blksize;
+}
+
+static int
+cmp(const void *p1, const void *p2)
+{
+ const struct file *f1 = p1, *f2 = p2;
+
+ if (f1->devno > f2->devno)
+ return -1;
+ if (f1->devno < f2->devno)
+ return 1;
+
+ /* f1->devno == f2->devno */
+ if (f1->inode < f2->inode)
+ return -1;
+ if (f1->inode > f2->inode)
+ return 1;
+
+ return 0;
+}
+
+static int
+duplicated(dev_t dev, ino_t ino)
+{
+ static void *tree;
+ struct file **fpp, *fp, file = {dev, ino};
+
+ if ((fpp = tsearch(&file, &tree, cmp)) == NULL)
+ eprintf("%s:", argv0);
+
+ if (*fpp != &file)
+ return 1;
+
+ /* new file added */
+ fp = emalloc(sizeof(*fp));
+ *fp = file;
+ *fpp = fp;
+
+ return 0;
+}
+
+static void
+du(int dirfd, const char *path, struct stat *st, void *data, struct recursor *r)
+{
+ off_t *total = data, subtotal;
+
+ subtotal = nblks(st->st_blocks);
+ if (S_ISDIR(st->st_mode)) {
+ recurse(dirfd, path, &subtotal, r);
+ } else if (r->follow != 'P' || st->st_nlink > 1) {
+ if (duplicated(st->st_dev, st->st_ino))
+ goto print;
+ }
+
+ *total += subtotal;
+
+print:
+ if (!r->depth)
+ printpath(*total, r->path);
+ else if (!sflag && r->depth <= maxdepth && (S_ISDIR(st->st_mode) || aflag))
+ printpath(subtotal, r->path);
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-a | -s] [-d depth] [-h] [-k] [-H | -L | -P] [-x] [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct recursor r = { .fn = du, .follow = 'P' };
+ off_t n = 0;
+ int kflag = 0, dflag = 0;
+ char *bsize;
+
+ ARGBEGIN {
+ case 'a':
+ aflag = 1;
+ break;
+ case 'd':
+ dflag = 1;
+ maxdepth = estrtonum(EARGF(usage()), 0, MIN(LLONG_MAX, SIZE_MAX));
+ break;
+ case 'h':
+ hflag = 1;
+ break;
+ case 'k':
+ kflag = 1;
+ break;
+ case 's':
+ sflag = 1;
+ break;
+ case 'x':
+ r.flags |= SAMEDEV;
+ break;
+ case 'H':
+ case 'L':
+ case 'P':
+ r.follow = ARGC();
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if ((aflag && sflag) || (dflag && sflag))
+ usage();
+
+ bsize = getenv("BLOCKSIZE");
+ if (bsize)
+ blksize = estrtonum(bsize, 1, MIN(LLONG_MAX, SIZE_MAX));
+ if (kflag)
+ blksize = 1024;
+
+ if (!argc) {
+ recurse(AT_FDCWD, ".", &n, &r);
+ } else {
+ for (; *argv; argc--, argv++) {
+ n = 0;
+ recurse(AT_FDCWD, *argv, &n, &r);
+ }
+ }
+
+ return fshut(stdout, "<stdout>") || recurse_status;
+}
diff --git a/util/sbase/echo.1 b/util/sbase/echo.1
new file mode 100644
index 00000000..04ba8b9b
--- /dev/null
+++ b/util/sbase/echo.1
@@ -0,0 +1,27 @@
+.Dd October 8, 2015
+.Dt ECHO 1
+.Os sbase
+.Sh NAME
+.Nm echo
+.Nd print arguments
+.Sh SYNOPSIS
+.Nm
+.Op Fl n
+.Op Ar string ...
+.Sh DESCRIPTION
+.Nm
+writes each
+.Ar string
+to stdout, separated by spaces and terminated by
+a newline.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl n
+Do not print the terminating newline.
+.El
+.Sh STANDARDS
+POSIX.1-2013.
+.Pp
+The
+.Op Fl n
+flag is an extension to that specification.
diff --git a/util/sbase/echo.c b/util/sbase/echo.c
new file mode 100644
index 00000000..a5526311
--- /dev/null
+++ b/util/sbase/echo.c
@@ -0,0 +1,24 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdio.h>
+#include <string.h>
+#include "util.h"
+
+int
+main(int argc, char *argv[])
+{
+ int nflag = 0;
+
+ argv0 = *argv, argv0 ? (argc--, argv++) : (void *)0;
+
+ if (*argv && !strcmp(*argv, "-n")) {
+ nflag = 1;
+ argc--, argv++;
+ }
+
+ for (; *argv; argc--, argv++)
+ putword(stdout, *argv);
+ if (!nflag)
+ putchar('\n');
+
+ return fshut(stdout, "<stdout>");
+}
diff --git a/util/sbase/ed.1 b/util/sbase/ed.1
new file mode 100644
index 00000000..14e88346
--- /dev/null
+++ b/util/sbase/ed.1
@@ -0,0 +1,238 @@
+.Dd December 27, 2016
+.Dt ED 1
+.Os sbase
+.Sh NAME
+.Nm ed
+.Nd text editor
+.Sh SYNOPSIS
+.Nm
+.Op Fl s
+.Op Fl p Ar string
+.Op Ar file
+.Sh DESCRIPTION
+.Nm
+is the standard text editor.
+It performs line-oriented operations on a buffer; The buffer's contents are
+manipulated in command mode and text is written to the buffer in input mode.
+Command mode is the default.
+To exit input mode enter a dot ('.') on a line of its own.
+.Pp
+If
+.Nm
+is invoked with a file as an argument, it will simulate an edit command and read
+the file's contents into a buffer.
+Changes to this buffer are local to
+.Nm
+until a write command is given.
+.Pp
+.Nm
+uses the basic regular expression syntax and allows any character but space and
+newline to be used as a delimiter in regular expressions.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl s
+Suppress diagnostic messages
+.It Fl p Ar string
+Use
+.Ar string
+as a prompt when in command mode
+.El
+.Sh EXTENDED DESCRIPTION
+.Ss Addresses
+Commands operate on addresses.
+Addresses are used to refer to lines within the buffer.
+Address ranges may have spaces before and after the separator.
+Unless otherwise specified, 0 is an invalid address.
+The following symbols are valid addresses:
+.Bl -tag -width Ds
+.It n
+The nth line.
+.It .
+The current line, or "dot".
+.It $
+The last line.
+.It +
+The next line.
+.It +n
+The nth next line.
+.It ^ or -
+The previous line.
+.It ^n or -n
+The nth previous line.
+.It x,y
+The range of lines from x to y.
+The default value of x is 1, and the default value of y is $.
+.It x;y
+As above, except that the current line is set to x.
+Omitting x in this case uses the current line as the default value.
+.It /re/
+The next line matching re.
+.It ?re?
+The last line matching re.
+.It 'c
+The line marked by c. See k below.
+.El
+.Ss Commands
+.Nm
+expects to see one command per line, with the following exception: commands may
+be suffixed with either a list, number, or print command.
+These suffixed commands are run after the command they're suffixed to has
+executed.
+.Pp
+The following is the list of commands that
+.Nm
+knows about.
+The parentheses contain the default addresses that a command uses.
+.Bl -tag -width Ds
+.It (.)a
+Append text after the addressed line.
+The dot is set to the last line entered.
+If no text was entered, the dot is set to the addressed line.
+An address of 0 appends to the start of the buffer.
+.It (.,.)c
+Delete the addressed lines and then accept input to replace them.
+The dot is set to the last line entered.
+If no text was entered, the dot is set to the line before the deleted lines.
+.It (.,.)d
+Delete the addressed lines.
+If there is a line after the deleted range, the dot is set to it.
+Otherwise, the dot is set to the line before the deleted range.
+.It e Ar file
+Delete the contents of the buffer and load in
+.Ar file
+for editing, printing the bytes read to standard output.
+If no filename is given,
+.Nm
+uses the currently remembered filename.
+The remembered filename is set to
+.Ar file
+for later use.
+.It E Ar file
+As above, but without warning if the current buffer has unsaved changes.
+.It f Ar file
+Set the currently remembered filename to
+.Ar file
+, or print the currently remembered filename if
+.Ar file
+is omitted.
+.It (1,$)g/re/command
+Apply command to lines matching re.
+The dot is set to the matching line before command is executed.
+When each matching line has been operated on, the dot is set to the last line
+operated on.
+If no lines match then the dot remains unchanged.
+The command used may not be g, G, v, or V.
+.It (1,$)G/re/
+Interactively edit the range of line addresses that match re.
+The dot is set to the matching line and printed before a command is input.
+When each matching line has been operated on, the dot is set to the last line
+operated on.
+If no lines match then the dot remains unchanged.
+The command used may not be a, c, i, g, G, v, or V.
+.It h
+Print the reason for the most recent error.
+.It H
+Toggle error explanations.
+If on, the above behaviour is produced on all subsequent errors.
+.It (.)i
+Insert text into the buffer before the addressed line.
+The dot is set to the last line entered.
+If no text was entered, the dot is set to the addressed line
+.It (.,.+1)j
+Join two lines together.
+If only one address is given, nothing happens.
+The dot is set to the newly joined line.
+.It (.)kc
+Mark the line with the lower case character c. The dot is unchanged.
+.It (.,.)l
+Unambiguously print the addressed lines.
+The dot is set to the last line written.
+.It (.,.)m(.)
+Move lines in the buffer to the line address on the right hand side.
+An address of 0 on the right hand side moves to the start of the buffer.
+The dot is set to the last line moved.
+.It (.,.)n
+Print the addressed lines and their numbers.
+The dot is set to the last line printed.
+.It (.,.)p
+Print the addressed lines.
+The dot is set to the last line printed.
+.It P
+Toggle the prompt.
+Defaults to off, but is switched on if the -p flag is used.
+.It q
+Quit
+.Nm
+, warning if there are unsaved changes.
+.It Q
+As above, but without warning if the current buffer has unsaved changes.
+.It ($)r Ar file
+Read in
+.Ar file
+and append it to the current buffer, printing the bytes read to standard output.
+The currently remembered filename isn't changed unless it's empty.
+An address of 0 reads the file into the start of the buffer.
+.It (.,.)s/re/replacement/flags
+Substitute re for replacement in lines matching re.
+An & within replacement is replaced with the whole string matched by re.
+Backrefs can be used with the form \\n, where n is a positive non-zero integer.
+When % is the only character in replacement, it is substituted for the
+replacement string from the last substitute command.
+If a newline is part of replacement then the matched string is split into two
+lines; this cannot be done as part of a g or v command.
+If flags contains an integer n, then the nth match is replaced.
+If flags contains g, all matches are replaced.
+The dot is set to the last line matched.
+.It (.,.)t(.)
+As m, but copying instead of moving.
+The dot is set to the last line added.
+.It u
+Undo the last change.
+The dot is set to whatever it was before the undone command was performed.
+.It (1.$)v/re/command
+As with g, but operating on lines that don't match re.
+.It (1.$)V/re/
+As with G, but operating on lines that don't match re.
+.It (1,$)w Ar file
+Write the addressed lines to
+.Ar file
+, overwriting its previous contents if the file exists, and print the number of
+bytes written.
+If no filename is given the currently remembered filename will be used instead.
+The dot is unchanged.
+.It (1,$)W Ar file
+As above, but instead of overwriting the contents of
+.Ar file
+the addressed lines are appended to
+.Ar file
+instead.
+.It (.+1)
+Print the addressed line.
+Sets the dot to that line.
+.It ($)=
+Print the line number of the addressed line.
+The dot is unchanged.
+.It &
+Repeat the last command.
+.It ! Ar command
+Execute
+.Ar command
+using sh.
+If the first character of
+.Ar command
+is '!' then it is replaced with the text of the previous command.
+An unescaped % is replaced with the currently remembered filename.
+! does not process escape characters.
+When
+.Ar command
+returns a '!' is printed.
+The dot is unchanged.
+.El
+.Sh SEE ALSO
+.Xr sed 1 ,
+.Xr regexp 3
+.Sh STANDARDS
+POSIX.1-2013.
+Except where noted here:
+g and v operate on single commands rather than lists delimited with '\e'.
+e, E, r, w, and W commands cannot accept shell escapes.
diff --git a/util/sbase/ed.c b/util/sbase/ed.c
new file mode 100644
index 00000000..bec9b2da
--- /dev/null
+++ b/util/sbase/ed.c
@@ -0,0 +1,1650 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <regex.h>
+#include <unistd.h>
+
+#include <ctype.h>
+#include <limits.h>
+#include <setjmp.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "util.h"
+
+#define REGEXSIZE 100
+#define LINESIZE 80
+#define NUMLINES 32
+#define CACHESIZ 4096
+#define AFTER 0
+#define BEFORE 1
+
+typedef struct {
+ char *str;
+ size_t cap;
+ size_t siz;
+} String;
+
+struct hline {
+ off_t seek;
+ char global;
+ int next, prev;
+};
+
+struct undo {
+ int curln, lastln;
+ size_t nr, cap;
+ struct link {
+ int to1, from1;
+ int to2, from2;
+ } *vec;
+};
+
+static char *prompt = "*";
+static regex_t *pattern;
+static regmatch_t matchs[10];
+static String lastre;
+
+static int optverbose, optprompt, exstatus, optdiag = 1;
+static int marks['z' - 'a'];
+static int nlines, line1, line2;
+static int curln, lastln, ocurln, olastln;
+static jmp_buf savesp;
+static char *lasterr;
+static size_t idxsize, lastidx;
+static struct hline *zero;
+static String text;
+static char savfname[FILENAME_MAX];
+static char tmpname[FILENAME_MAX];
+static int scratch;
+static int pflag, modflag, uflag, gflag;
+static size_t csize;
+static String cmdline;
+static char *ocmdline;
+static int inputidx;
+static char *rhs;
+static char *lastmatch;
+static struct undo udata;
+static int newcmd;
+static int eol, bol;
+
+static sig_atomic_t intr, hup;
+
+static void undo(void);
+
+static void
+error(char *msg)
+{
+ exstatus = 1;
+ lasterr = msg;
+ puts("?");
+
+ if (optverbose)
+ puts(msg);
+ if (!newcmd)
+ undo();
+
+ curln = ocurln;
+ longjmp(savesp, 1);
+}
+
+static int
+nextln(int line)
+{
+ ++line;
+ return (line > lastln) ? 0 : line;
+}
+
+static int
+prevln(int line)
+{
+ --line;
+ return (line < 0) ? lastln : line;
+}
+
+static String *
+copystring(String *s, char *from)
+{
+ size_t len;
+ char *t;
+
+ if ((t = strdup(from)) == NULL)
+ error("out of memory");
+ len = strlen(t);
+
+ free(s->str);
+ s->str = t;
+ s->siz = len;
+ s->cap = len;
+
+ return s;
+}
+
+static String *
+string(String *s)
+{
+ free(s->str);
+ s->str = NULL;
+ s->siz = 0;
+ s->cap = 0;
+
+ return s;
+}
+
+static char *
+addchar(char c, String *s)
+{
+ size_t cap = s->cap, siz = s->siz;
+ char *t = s->str;
+
+ if (siz >= cap &&
+ (cap > SIZE_MAX - LINESIZE ||
+ (t = realloc(t, cap += LINESIZE)) == NULL))
+ error("out of memory");
+ t[siz++] = c;
+ s->siz = siz;
+ s->cap = cap;
+ s->str = t;
+ return t;
+}
+
+static void chksignals(void);
+
+static int
+input(void)
+{
+ int ch;
+
+ chksignals();
+
+ ch = cmdline.str[inputidx];
+ if (ch != '\0')
+ inputidx++;
+ return ch;
+}
+
+static int
+back(int c)
+{
+ if (c == '\0')
+ return c;
+ return cmdline.str[--inputidx] = c;
+}
+
+static int
+makeline(char *s, int *off)
+{
+ struct hline *lp;
+ size_t len;
+ char *begin = s;
+ int c;
+
+ if (lastidx >= idxsize) {
+ lp = NULL;
+ if (idxsize <= SIZE_MAX - NUMLINES)
+ lp = reallocarray(zero, idxsize + NUMLINES, sizeof(*lp));
+ if (!lp)
+ error("out of memory");
+ idxsize += NUMLINES;
+ zero = lp;
+ }
+ lp = zero + lastidx;
+ lp->global = 0;
+
+ if (!s) {
+ lp->seek = -1;
+ len = 0;
+ } else {
+ while ((c = *s++) && c != '\n')
+ ;
+ len = s - begin;
+ if ((lp->seek = lseek(scratch, 0, SEEK_END)) < 0 ||
+ write(scratch, begin, len) < 0) {
+ error("input/output error");
+ }
+ }
+ if (off)
+ *off = len;
+ ++lastidx;
+ return lp - zero;
+}
+
+static int
+getindex(int line)
+{
+ struct hline *lp;
+ int n;
+
+ if (line == -1)
+ line = 0;
+ for (n = 0, lp = zero; n != line; n++)
+ lp = zero + lp->next;
+
+ return lp - zero;
+}
+
+static char *
+gettxt(int line)
+{
+ static char buf[CACHESIZ];
+ static off_t lasto;
+ struct hline *lp;
+ off_t off, block;
+ ssize_t n;
+ char *p;
+
+ lp = zero + getindex(line);
+ text.siz = 0;
+ off = lp->seek;
+
+ if (off == (off_t) -1)
+ return addchar('\0', &text);
+
+repeat:
+ if (!csize || off < lasto || off - lasto >= csize) {
+ block = off & ~(CACHESIZ-1);
+ if (lseek(scratch, block, SEEK_SET) < 0 ||
+ (n = read(scratch, buf, CACHESIZ)) < 0) {
+ error("input/output error");
+ }
+ csize = n;
+ lasto = block;
+ }
+ for (p = buf + off - lasto; p < buf + csize && *p != '\n'; ++p) {
+ ++off;
+ addchar(*p, &text);
+ }
+ if (csize && p == buf + csize)
+ goto repeat;
+
+ addchar('\n', &text);
+ addchar('\0', &text);
+ return text.str;
+}
+
+static void
+setglobal(int i, int v)
+{
+ zero[getindex(i)].global = v;
+}
+
+static void
+clearundo(void)
+{
+ free(udata.vec);
+ udata.vec = NULL;
+ newcmd = udata.nr = udata.cap = 0;
+ modflag = 0;
+}
+
+static void
+newundo(int from1, int from2)
+{
+ struct link *p;
+
+ if (newcmd) {
+ clearundo();
+ udata.curln = ocurln;
+ udata.lastln = olastln;
+ }
+ if (udata.nr >= udata.cap) {
+ size_t siz = (udata.cap + 10) * sizeof(struct link);
+ if ((p = realloc(udata.vec, siz)) == NULL)
+ error("out of memory");
+ udata.vec = p;
+ udata.cap = udata.cap + 10;
+ }
+ p = &udata.vec[udata.nr++];
+ p->from1 = from1;
+ p->to1 = zero[from1].next;
+ p->from2 = from2;
+ p->to2 = zero[from2].prev;
+}
+
+/*
+ * relink: to1 <- from1
+ * from2 -> to2
+ */
+static void
+relink(int to1, int from1, int from2, int to2)
+{
+ newundo(from1, from2);
+ zero[from1].next = to1;
+ zero[from2].prev = to2;
+ modflag = 1;
+}
+
+static void
+undo(void)
+{
+ struct link *p;
+
+ if (udata.nr == 0)
+ return;
+ for (p = &udata.vec[udata.nr-1]; udata.nr > 0; --p) {
+ --udata.nr;
+ zero[p->from1].next = p->to1;
+ zero[p->from2].prev = p->to2;
+ }
+ free(udata.vec);
+ udata.vec = NULL;
+ udata.cap = 0;
+ curln = udata.curln;
+ lastln = udata.lastln;
+}
+
+static void
+inject(char *s, int where)
+{
+ int off, k, begin, end;
+
+ if (where == BEFORE) {
+ begin = getindex(curln-1);
+ end = getindex(nextln(curln-1));
+ } else {
+ begin = getindex(curln);
+ end = getindex(nextln(curln));
+ }
+ while (*s) {
+ k = makeline(s, &off);
+ s += off;
+ relink(k, begin, k, begin);
+ relink(end, k, end, k);
+ ++lastln;
+ ++curln;
+ begin = k;
+ }
+}
+
+static void
+clearbuf(void)
+{
+ if (scratch)
+ close(scratch);
+ remove(tmpname);
+ free(zero);
+ zero = NULL;
+ scratch = csize = idxsize = lastidx = curln = lastln = 0;
+ modflag = lastln = curln = 0;
+}
+
+static void
+setscratch(void)
+{
+ int r, k;
+ char *dir;
+
+ clearbuf();
+ clearundo();
+ if ((dir = getenv("TMPDIR")) == NULL)
+ dir = "/tmp";
+ r = snprintf(tmpname, sizeof(tmpname), "%s/%s",
+ dir, "ed.XXXXXX");
+ if (r < 0 || (size_t)r >= sizeof(tmpname))
+ error("scratch filename too long");
+ if ((scratch = mkstemp(tmpname)) < 0)
+ error("failed to create scratch file");
+ if ((k = makeline(NULL, NULL)))
+ error("input/output error in scratch file");
+ relink(k, k, k, k);
+ clearundo();
+}
+
+static void
+compile(int delim)
+{
+ int n, ret, c,bracket;
+ static char buf[BUFSIZ];
+
+ if (!isgraph(delim))
+ error("invalid pattern delimiter");
+
+ eol = bol = bracket = lastre.siz = 0;
+ for (n = 0;; ++n) {
+ c = input();
+ if (c == delim && !bracket || c == '\0') {
+ break;
+ } else if (c == '^') {
+ bol = 1;
+ } else if (c == '$') {
+ eol = 1;
+ } else if (c == '\\') {
+ addchar(c, &lastre);
+ c = input();
+ } else if (c == '[') {
+ bracket = 1;
+ } else if (c == ']') {
+ bracket = 0;
+ }
+ addchar(c, &lastre);
+ }
+ if (n == 0) {
+ if (!pattern)
+ error("no previous pattern");
+ return;
+ }
+ addchar('\0', &lastre);
+
+ if (pattern)
+ regfree(pattern);
+ if (!pattern && (!(pattern = malloc(sizeof(*pattern)))))
+ error("out of memory");
+ if ((ret = regcomp(pattern, lastre.str, REG_NEWLINE))) {
+ regerror(ret, pattern, buf, sizeof(buf));
+ error(buf);
+ }
+}
+
+static int
+match(int num)
+{
+ lastmatch = gettxt(num);
+ return !regexec(pattern, lastmatch, 10, matchs, 0);
+}
+
+static int
+rematch(int num)
+{
+ regoff_t off = matchs[0].rm_eo;
+
+ if (!regexec(pattern, lastmatch + off, 10, matchs, 0)) {
+ lastmatch += off;
+ return 1;
+ }
+
+ return 0;
+}
+
+static int
+search(int way)
+{
+ int i;
+
+ i = curln;
+ do {
+ chksignals();
+
+ i = (way == '?') ? prevln(i) : nextln(i);
+ if (i > 0 && match(i))
+ return i;
+ } while (i != curln);
+
+ error("invalid address");
+ return -1; /* not reached */
+}
+
+static void
+skipblank(void)
+{
+ char c;
+
+ while ((c = input()) == ' ' || c == '\t')
+ ;
+ back(c);
+}
+
+static void
+ensureblank(void)
+{
+ char c;
+
+ switch ((c = input())) {
+ case ' ':
+ case '\t':
+ skipblank();
+ case '\0':
+ back(c);
+ break;
+ default:
+ error("unknown command");
+ }
+}
+
+static int
+getnum(void)
+{
+ int ln, n, c;
+
+ for (ln = 0; isdigit(c = input()); ln += n) {
+ if (ln > INT_MAX/10)
+ goto invalid;
+ n = c - '0';
+ ln *= 10;
+ if (INT_MAX - ln < n)
+ goto invalid;
+ }
+ back(c);
+ return ln;
+
+invalid:
+ error("invalid address");
+ return -1; /* not reached */
+}
+
+static int
+linenum(int *line)
+{
+ int ln, c;
+
+ skipblank();
+
+ switch (c = input()) {
+ case '.':
+ ln = curln;
+ break;
+ case '\'':
+ skipblank();
+ if (!islower(c = input()))
+ error("invalid mark character");
+ if (!(ln = marks[c - 'a']))
+ error("invalid address");
+ break;
+ case '$':
+ ln = lastln;
+ break;
+ case '?':
+ case '/':
+ compile(c);
+ ln = search(c);
+ break;
+ case '^':
+ case '-':
+ case '+':
+ ln = curln;
+ back(c);
+ break;
+ default:
+ back(c);
+ if (isdigit(c))
+ ln = getnum();
+ else
+ return 0;
+ break;
+ }
+ *line = ln;
+ return 1;
+}
+
+static int
+address(int *line)
+{
+ int ln, sign, c, num;
+
+ if (!linenum(&ln))
+ return 0;
+
+ for (;;) {
+ skipblank();
+ if ((c = input()) != '+' && c != '-' && c != '^')
+ break;
+ sign = c == '+' ? 1 : -1;
+ num = isdigit(back(input())) ? getnum() : 1;
+ num *= sign;
+ if (INT_MAX - ln < num)
+ goto invalid;
+ ln += num;
+ }
+ back(c);
+
+ if (ln < 0 || ln > lastln)
+ error("invalid address");
+ *line = ln;
+ return 1;
+
+invalid:
+ error("invalid address");
+ return -1; /* not reached */
+}
+
+static void
+getlst(void)
+{
+ int ln, c;
+
+ if ((c = input()) == ',') {
+ line1 = 1;
+ line2 = lastln;
+ nlines = lastln;
+ return;
+ } else if (c == ';') {
+ line1 = curln;
+ line2 = lastln;
+ nlines = lastln - curln + 1;
+ return;
+ }
+ back(c);
+ line2 = curln;
+ for (nlines = 0; address(&ln); ) {
+ line1 = line2;
+ line2 = ln;
+ ++nlines;
+
+ skipblank();
+ if ((c = input()) != ',' && c != ';') {
+ back(c);
+ break;
+ }
+ if (c == ';')
+ curln = line2;
+ }
+ if (nlines > 2)
+ nlines = 2;
+ else if (nlines <= 1)
+ line1 = line2;
+}
+
+static void
+deflines(int def1, int def2)
+{
+ if (!nlines) {
+ line1 = def1;
+ line2 = def2;
+ }
+ if (line1 > line2 || line1 < 0 || line2 > lastln)
+ error("invalid address");
+}
+
+static void
+quit(void)
+{
+ clearbuf();
+ exit(exstatus);
+}
+
+static void
+setinput(char *s)
+{
+ copystring(&cmdline, s);
+ inputidx = 0;
+}
+
+static void
+getinput(void)
+{
+ int ch;
+
+ string(&cmdline);
+
+ while ((ch = getchar()) != '\n' && ch != EOF) {
+ if (ch == '\\') {
+ if ((ch = getchar()) == EOF)
+ break;
+ if (ch != '\n') {
+ ungetc(ch, stdin);
+ ch = '\\';
+ }
+ }
+ addchar(ch, &cmdline);
+ }
+
+ addchar('\0', &cmdline);
+ inputidx = 0;
+
+ if (ch == EOF) {
+ chksignals();
+ if (ferror(stdin)) {
+ exstatus = 1;
+ fputs("ed: error reading input\n", stderr);
+ }
+ quit();
+ }
+}
+
+static int
+moreinput(void)
+{
+ if (!uflag)
+ return cmdline.str[inputidx] != '\0';
+
+ getinput();
+ return 1;
+}
+
+static void dowrite(const char *, int);
+
+static void
+dump(void)
+{
+ char *home;
+
+ if (modflag)
+ return;
+
+ line1 = nextln(0);
+ line2 = lastln;
+
+ if (!setjmp(savesp)) {
+ dowrite("ed.hup", 1);
+ return;
+ }
+
+ home = getenv("HOME");
+ if (!home || chdir(home) < 0)
+ return;
+
+ if (!setjmp(savesp))
+ dowrite("ed.hup", 1);
+}
+
+static void
+chksignals(void)
+{
+ if (hup) {
+ exstatus = 1;
+ dump();
+ quit();
+ }
+
+ if (intr) {
+ intr = 0;
+ newcmd = 1;
+ clearerr(stdin);
+ error("Interrupt");
+ }
+}
+
+static void
+dowrite(const char *fname, int trunc)
+{
+ size_t bytecount = 0;
+ int i, r, line;
+ FILE *aux;
+ static int sh;
+ static FILE *fp;
+
+ if (fp) {
+ sh ? pclose(fp) : fclose(fp);
+ fp = NULL;
+ }
+
+ if(fname[0] == '!') {
+ sh = 1;
+ fname++;
+ if((fp = popen(fname, "w")) == NULL)
+ error("bad exec");
+ } else {
+ sh = 0;
+ if ((fp = fopen(fname, "w")) == NULL)
+ error("cannot open input file");
+ }
+
+ line = curln;
+ for (i = line1; i <= line2; ++i) {
+ chksignals();
+
+ gettxt(i);
+ bytecount += text.siz - 1;
+ fwrite(text.str, 1, text.siz - 1, fp);
+ }
+
+ curln = line2;
+
+ aux = fp;
+ fp = NULL;
+ r = sh ? pclose(aux) : fclose(aux);
+ if (r)
+ error("input/output error");
+ strcpy(savfname, fname);
+ modflag = 0;
+ curln = line;
+ if (optdiag)
+ printf("%zu\n", bytecount);
+}
+
+static void
+doread(const char *fname)
+{
+ size_t cnt;
+ ssize_t n;
+ char *p;
+ FILE *aux;
+ static size_t len;
+ static char *s;
+ static FILE *fp;
+
+ if (fp)
+ fclose(fp);
+ if ((fp = fopen(fname, "r")) == NULL)
+ error("cannot open input file");
+
+ curln = line2;
+ for (cnt = 0; (n = getline(&s, &len, fp)) > 0; cnt += (size_t)n) {
+ chksignals();
+ if (s[n-1] != '\n') {
+ if (len == SIZE_MAX || !(p = realloc(s, ++len)))
+ error("out of memory");
+ s = p;
+ s[n-1] = '\n';
+ s[n] = '\0';
+ }
+ inject(s, AFTER);
+ }
+ if (optdiag)
+ printf("%zu\n", cnt);
+
+ aux = fp;
+ fp = NULL;
+ if (fclose(aux))
+ error("input/output error");
+}
+
+static void
+doprint(void)
+{
+ int i, c;
+ char *s, *str;
+
+ if (line1 <= 0 || line2 > lastln)
+ error("incorrect address");
+ for (i = line1; i <= line2; ++i) {
+ chksignals();
+ if (pflag == 'n')
+ printf("%d\t", i);
+ for (s = gettxt(i); (c = *s) != '\n'; ++s) {
+ if (pflag != 'l')
+ goto print_char;
+ switch (c) {
+ case '$':
+ str = "\\$";
+ goto print_str;
+ case '\t':
+ str = "\\t";
+ goto print_str;
+ case '\b':
+ str = "\\b";
+ goto print_str;
+ case '\\':
+ str = "\\\\";
+ goto print_str;
+ default:
+ if (!isprint(c)) {
+ printf("\\x%x", 0xFF & c);
+ break;
+ }
+ print_char:
+ putchar(c);
+ break;
+ print_str:
+ fputs(str, stdout);
+ break;
+ }
+ }
+ if (pflag == 'l')
+ fputs("$", stdout);
+ putc('\n', stdout);
+ }
+ curln = i - 1;
+}
+
+static void
+dohelp(void)
+{
+ if (lasterr)
+ puts(lasterr);
+}
+
+static void
+chkprint(int flag)
+{
+ int c;
+
+ if (flag) {
+ if ((c = input()) == 'p' || c == 'l' || c == 'n')
+ pflag = c;
+ else
+ back(c);
+ }
+ if ((c = input()) != '\0' && c != '\n')
+ error("invalid command suffix");
+}
+
+static char *
+getfname(int comm)
+{
+ int c;
+ char *bp;
+ static char fname[FILENAME_MAX];
+
+ skipblank();
+ for (bp = fname; bp < &fname[FILENAME_MAX]; *bp++ = c) {
+ if ((c = input()) == '\0')
+ break;
+ }
+ if (bp == fname) {
+ if (savfname[0] == '\0')
+ error("no current filename");
+ return savfname;
+ } else if (bp == &fname[FILENAME_MAX]) {
+ error("file name too long");
+ } else {
+ *bp = '\0';
+ if (savfname[0] == '\0' || comm == 'e' || comm == 'f')
+ strcpy(savfname, fname);
+ return fname;
+ }
+
+ return NULL; /* not reached */
+}
+
+static void
+append(int num)
+{
+ int ch;
+ static String line;
+
+ curln = num;
+ while (moreinput()) {
+ string(&line);
+ while ((ch = input()) != '\n' && ch != '\0')
+ addchar(ch, &line);
+ addchar('\n', &line);
+ addchar('\0', &line);
+
+ if (!strcmp(line.str, ".\n") || !strcmp(line.str, "."))
+ break;
+ inject(line.str, AFTER);
+ }
+}
+
+static void
+delete(int from, int to)
+{
+ int lto, lfrom;
+
+ if (!from)
+ error("incorrect address");
+
+ lfrom = getindex(prevln(from));
+ lto = getindex(nextln(to));
+ lastln -= to - from + 1;
+ curln = (from > lastln) ? lastln : from;;
+ relink(lto, lfrom, lto, lfrom);
+}
+
+static void
+move(int where)
+{
+ int before, after, lto, lfrom;
+
+ if (!line1 || (where >= line1 && where <= line2))
+ error("incorrect address");
+
+ before = getindex(prevln(line1));
+ after = getindex(nextln(line2));
+ lfrom = getindex(line1);
+ lto = getindex(line2);
+ relink(after, before, after, before);
+
+ if (where < line1) {
+ curln = where + line1 - line2 + 1;
+ } else {
+ curln = where;
+ where -= line1 - line2 + 1;
+ }
+ before = getindex(where);
+ after = getindex(nextln(where));
+ relink(lfrom, before, lfrom, before);
+ relink(after, lto, after, lto);
+}
+
+static void
+join(void)
+{
+ int i;
+ char *t, c;
+ static String s;
+
+ string(&s);
+ for (i = line1;; i = nextln(i)) {
+ chksignals();
+ for (t = gettxt(i); (c = *t) != '\n'; ++t)
+ addchar(*t, &s);
+ if (i == line2)
+ break;
+ }
+
+ addchar('\n', &s);
+ addchar('\0', &s);
+ delete(line1, line2);
+ inject(s.str, BEFORE);
+ free(s.str);
+}
+
+static void
+scroll(int num)
+{
+ int max, ln, cnt;
+
+ if (!line1 || line1 == lastln)
+ error("incorrect address");
+
+ ln = line1;
+ max = line1 + num;
+ if (max > lastln)
+ max = lastln;
+ for (cnt = line1; cnt < max; cnt++) {
+ chksignals();
+ fputs(gettxt(ln), stdout);
+ ln = nextln(ln);
+ }
+ curln = ln;
+}
+
+static void
+copy(int where)
+{
+
+ if (!line1)
+ error("incorrect address");
+ curln = where;
+
+ while (line1 <= line2) {
+ chksignals();
+ inject(gettxt(line1), AFTER);
+ if (line2 >= curln)
+ line2 = nextln(line2);
+ line1 = nextln(line1);
+ if (line1 >= curln)
+ line1 = nextln(line1);
+ }
+}
+
+static void
+execsh(void)
+{
+ static String cmd;
+ char *p;
+ int c, repl = 0;
+
+ skipblank();
+ if ((c = input()) != '!') {
+ back(c);
+ string(&cmd);
+ } else if (cmd.siz) {
+ --cmd.siz;
+ repl = 1;
+ } else {
+ error("no previous command");
+ }
+
+ while ((c = input()) != '\0') {
+ switch (c) {
+ case '%':
+ if (savfname[0] == '\0')
+ error("no current filename");
+ repl = 1;
+ for (p = savfname; *p; ++p)
+ addchar(*p, &cmd);
+ break;
+ case '\\':
+ c = input();
+ if (c != '%') {
+ back(c);
+ c = '\\';
+ }
+ default:
+ addchar(c, &cmd);
+ }
+ }
+ addchar('\0', &cmd);
+
+ if (repl)
+ puts(cmd.str);
+ system(cmd.str);
+ if (optdiag)
+ puts("!");
+}
+
+static void
+getrhs(int delim)
+{
+ int c;
+ static String s;
+
+ string(&s);
+ while ((c = input()) != '\0' && c != delim)
+ addchar(c, &s);
+ addchar('\0', &s);
+ if (c == '\0') {
+ pflag = 'p';
+ back(c);
+ }
+
+ if (!strcmp("%", s.str)) {
+ if (!rhs)
+ error("no previous substitution");
+ free(s.str);
+ } else {
+ free(rhs);
+ rhs = s.str;
+ }
+ s.str = NULL;
+}
+
+static int
+getnth(void)
+{
+ int c;
+
+ if ((c = input()) == 'g') {
+ return -1;
+ } else if (isdigit(c)) {
+ if (c == '0')
+ return -1;
+ return c - '0';
+ } else {
+ back(c);
+ return 1;
+ }
+}
+
+static void
+addpre(String *s)
+{
+ char *p;
+
+ for (p = lastmatch; p < lastmatch + matchs[0].rm_so; ++p)
+ addchar(*p, s);
+}
+
+static void
+addpost(String *s)
+{
+ char c, *p;
+
+ for (p = lastmatch + matchs[0].rm_eo; (c = *p); ++p)
+ addchar(c, s);
+ addchar('\0', s);
+}
+
+static int
+addsub(String *s, int nth, int nmatch)
+{
+ char *end, *q, *p, c;
+ int sub;
+
+ if (nth != nmatch && nth != -1) {
+ q = lastmatch + matchs[0].rm_so;
+ end = lastmatch + matchs[0].rm_eo;
+ while (q < end)
+ addchar(*q++, s);
+ return 0;
+ }
+
+ for (p = rhs; (c = *p); ++p) {
+ switch (c) {
+ case '&':
+ sub = 0;
+ goto copy_match;
+ case '\\':
+ if ((c = *++p) == '\0')
+ return 1;
+ if (!isdigit(c))
+ goto copy_char;
+ sub = c - '0';
+ copy_match:
+ q = lastmatch + matchs[sub].rm_so;
+ end = lastmatch + matchs[sub].rm_eo;
+ while (q < end)
+ addchar(*q++, s);
+ break;
+ default:
+ copy_char:
+ addchar(c, s);
+ break;
+ }
+ }
+ return 1;
+}
+
+static void
+subline(int num, int nth)
+{
+ int i, m, changed;
+ static String s;
+
+ string(&s);
+ i = changed = 0;
+ for (m = match(num); m; m = rematch(num)) {
+ chksignals();
+ addpre(&s);
+ changed |= addsub(&s, nth, ++i);
+ if (eol || bol)
+ break;
+ }
+ if (!changed)
+ return;
+ addpost(&s);
+ delete(num, num);
+ curln = prevln(num);
+ inject(s.str, AFTER);
+}
+
+static void
+subst(int nth)
+{
+ int i, line, next;
+
+ line = line1;
+ for (i = 0; i < line2 - line1 + 1; i++) {
+ chksignals();
+
+ next = getindex(nextln(line));
+ subline(line, nth);
+
+ /*
+ * The substitution command can add lines, so
+ * we have to skip lines until we find the
+ * index that we saved before the substitution
+ */
+ do
+ line = nextln(line);
+ while (getindex(line) != next);
+ }
+}
+
+static void
+docmd(void)
+{
+ int cmd, c, line3, num, trunc;
+
+repeat:
+ skipblank();
+ cmd = input();
+ trunc = pflag = 0;
+ switch (cmd) {
+ case '&':
+ skipblank();
+ chkprint(0);
+ if (!ocmdline)
+ error("no previous command");
+ setinput(ocmdline);
+ getlst();
+ goto repeat;
+ case '!':
+ execsh();
+ break;
+ case '\0':
+ num = gflag ? curln : curln+1;
+ deflines(num, num);
+ line1 = line2;
+ pflag = 'p';
+ goto print;
+ case 'l':
+ case 'n':
+ case 'p':
+ back(cmd);
+ chkprint(1);
+ deflines(curln, curln);
+ goto print;
+ case 'g':
+ case 'G':
+ case 'v':
+ case 'V':
+ error("cannot nest global commands");
+ case 'H':
+ if (nlines > 0)
+ goto unexpected;
+ chkprint(0);
+ optverbose ^= 1;
+ break;
+ case 'h':
+ if (nlines > 0)
+ goto unexpected;
+ chkprint(0);
+ dohelp();
+ break;
+ case 'w':
+ trunc = 1;
+ case 'W':
+ ensureblank();
+ deflines(nextln(0), lastln);
+ dowrite(getfname(cmd), trunc);
+ break;
+ case 'r':
+ ensureblank();
+ if (nlines > 1)
+ goto bad_address;
+ deflines(lastln, lastln);
+ doread(getfname(cmd));
+ break;
+ case 'd':
+ chkprint(1);
+ deflines(curln, curln);
+ delete(line1, line2);
+ break;
+ case '=':
+ if (nlines > 1)
+ goto bad_address;
+ chkprint(1);
+ deflines(lastln, lastln);
+ printf("%d\n", line1);
+ break;
+ case 'u':
+ if (nlines > 0)
+ goto bad_address;
+ chkprint(1);
+ if (udata.nr == 0)
+ error("nothing to undo");
+ undo();
+ break;
+ case 's':
+ deflines(curln, curln);
+ c = input();
+ compile(c);
+ getrhs(c);
+ num = getnth();
+ chkprint(1);
+ subst(num);
+ break;
+ case 'i':
+ if (nlines > 1)
+ goto bad_address;
+ chkprint(1);
+ deflines(curln, curln);
+ if (!line1)
+ line1++;
+ append(prevln(line1));
+ break;
+ case 'a':
+ if (nlines > 1)
+ goto bad_address;
+ chkprint(1);
+ deflines(curln, curln);
+ append(line1);
+ break;
+ case 'm':
+ deflines(curln, curln);
+ if (!address(&line3))
+ line3 = curln;
+ chkprint(1);
+ move(line3);
+ break;
+ case 't':
+ deflines(curln, curln);
+ if (!address(&line3))
+ line3 = curln;
+ chkprint(1);
+ copy(line3);
+ break;
+ case 'c':
+ chkprint(1);
+ deflines(curln, curln);
+ delete(line1, line2);
+ append(prevln(line1));
+ break;
+ case 'j':
+ chkprint(1);
+ deflines(curln, curln+1);
+ if (line1 != line2 && curln != 0)
+ join();
+ break;
+ case 'z':
+ if (nlines > 1)
+ goto bad_address;
+ if (isdigit(back(input())))
+ num = getnum();
+ else
+ num = 24;
+ chkprint(1);
+ scroll(num);
+ break;
+ case 'k':
+ if (nlines > 1)
+ goto bad_address;
+ if (!islower(c = input()))
+ error("invalid mark character");
+ chkprint(1);
+ deflines(curln, curln);
+ marks[c - 'a'] = line1;
+ break;
+ case 'P':
+ if (nlines > 0)
+ goto unexpected;
+ chkprint(1);
+ optprompt ^= 1;
+ break;
+ case 'Q':
+ modflag = 0;
+ case 'q':
+ if (nlines > 0)
+ goto unexpected;
+ if (modflag)
+ goto modified;
+ quit();
+ break;
+ case 'f':
+ ensureblank();
+ if (nlines > 0)
+ goto unexpected;
+ if (back(input()) != '\0')
+ getfname(cmd);
+ else
+ puts(savfname);
+ chkprint(0);
+ break;
+ case 'E':
+ modflag = 0;
+ case 'e':
+ ensureblank();
+ if (nlines > 0)
+ goto unexpected;
+ if (modflag)
+ goto modified;
+ getfname(cmd);
+ setscratch();
+ deflines(curln, curln);
+ doread(savfname);
+ clearundo();
+ break;
+ default:
+ error("unknown command");
+ bad_address:
+ error("invalid address");
+ modified:
+ modflag = 0;
+ error("warning: file modified");
+ unexpected:
+ error("unexpected address");
+ }
+
+ if (!pflag)
+ return;
+ line1 = line2 = curln;
+
+print:
+ doprint();
+}
+
+static int
+chkglobal(void)
+{
+ int delim, c, dir, i, v;
+
+ uflag = 1;
+ gflag = 0;
+ skipblank();
+
+ switch (c = input()) {
+ case 'g':
+ uflag = 0;
+ case 'G':
+ dir = 1;
+ break;
+ case 'v':
+ uflag = 0;
+ case 'V':
+ dir = 0;
+ break;
+ default:
+ back(c);
+ return 0;
+ }
+ gflag = 1;
+ deflines(nextln(0), lastln);
+ delim = input();
+ compile(delim);
+
+ for (i = 1; i <= lastln; ++i) {
+ chksignals();
+ if (i >= line1 && i <= line2)
+ v = match(i) == dir;
+ else
+ v = 0;
+ setglobal(i, v);
+ }
+
+ return 1;
+}
+
+static void
+savecmd(void)
+{
+ int ch;
+
+ skipblank();
+ ch = input();
+ if (ch != '&') {
+ ocmdline = strdup(cmdline.str);
+ if (ocmdline == NULL)
+ error("out of memory");
+ }
+ back(ch);
+}
+
+static void
+doglobal(void)
+{
+ int cnt, ln, k, idx;
+
+ skipblank();
+ gflag = 1;
+ if (uflag)
+ chkprint(0);
+
+ ln = line1;
+ for (cnt = 0; cnt < lastln; ) {
+ chksignals();
+ k = getindex(ln);
+ if (zero[k].global) {
+ zero[k].global = 0;
+ curln = ln;
+ nlines = 0;
+
+ if (!uflag) {
+ idx = inputidx;
+ getlst();
+ docmd();
+ inputidx = idx;
+ continue;
+ }
+
+ line1 = line2 = ln;
+ pflag = 0;
+ doprint();
+
+ for (;;) {
+ getinput();
+ if (strcmp(cmdline.str, "") == 0)
+ break;
+ savecmd();
+ getlst();
+ docmd();
+ }
+
+ } else {
+ cnt++;
+ ln = nextln(ln);
+ }
+ }
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-s] [-p] [file]\n", argv0);
+}
+
+static void
+sigintr(int n)
+{
+ intr = 1;
+}
+
+static void
+sighup(int dummy)
+{
+ hup = 1;
+}
+
+static void
+edit(void)
+{
+ for (;;) {
+ newcmd = 1;
+ ocurln = curln;
+ olastln = lastln;
+ if (optprompt) {
+ fputs(prompt, stdout);
+ fflush(stdout);
+ }
+
+ getinput();
+ getlst();
+ chkglobal() ? doglobal() : docmd();
+ }
+}
+
+static void
+init(char *fname)
+{
+ size_t len;
+
+ setscratch();
+ if (!fname)
+ return;
+ if ((len = strlen(fname)) >= FILENAME_MAX || len == 0)
+ error("incorrect filename");
+ memcpy(savfname, fname, len);
+ doread(fname);
+ clearundo();
+}
+
+int
+main(int argc, char *argv[])
+{
+ ARGBEGIN {
+ case 'p':
+ prompt = EARGF(usage());
+ optprompt = 1;
+ break;
+ case 's':
+ optdiag = 0;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (argc > 1)
+ usage();
+
+ if (!setjmp(savesp)) {
+ sigaction(SIGINT,
+ &(struct sigaction) {.sa_handler = sigintr},
+ NULL);
+ sigaction(SIGHUP,
+ &(struct sigaction) {.sa_handler = sighup},
+ NULL);
+ sigaction(SIGQUIT,
+ &(struct sigaction) {.sa_handler = SIG_IGN},
+ NULL);
+ init(*argv);
+ }
+ edit();
+
+ /* not reached */
+ return 0;
+}
diff --git a/util/sbase/env.1 b/util/sbase/env.1
new file mode 100644
index 00000000..f25f54b1
--- /dev/null
+++ b/util/sbase/env.1
@@ -0,0 +1,47 @@
+.Dd October 8, 2015
+.Dt ENV 1
+.Os sbase
+.Sh NAME
+.Nm env
+.Nd modify the environment, then print it or run a command
+.Sh SYNOPSIS
+.Nm
+.Op Fl i
+.Oo Fl u Ar var Oc ...
+.Oo Ar var Ns = Ns Ar value Oc ...
+.Oo Ar cmd Oo arg ... Oc Oc
+.Sh DESCRIPTION
+.Nm
+unsets each
+.Ar var ,
+then adds or sets each
+.Ar ( var , value )
+tuple in the environment.
+.Pp
+If
+.Ar cmd
+is given, it is executed in this new environment;
+otherwise, the modified environment is printed to stdout.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl i
+Completely ignore the existing environment and execute
+.Ar cmd
+only with each
+.Ar ( var , value )
+tuple specified.
+.It Fl u Ar var
+Unset
+.Ar var
+in the environment.
+.El
+.Sh SEE ALSO
+.Xr printenv 1 ,
+.Xr putenv 3 ,
+.Xr environ 7
+.Sh STANDARDS
+POSIX.1-2013.
+.Pp
+The
+.Op Fl u
+flag is an extension to that specification.
diff --git a/util/sbase/env.c b/util/sbase/env.c
new file mode 100644
index 00000000..5d7e8a55
--- /dev/null
+++ b/util/sbase/env.c
@@ -0,0 +1,49 @@
+/* See LICENSE file for copyright and license details. */
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "util.h"
+
+extern char **environ;
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-i] [-u var] ... [var=value] ... [cmd [arg ...]]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int savederrno;
+
+ ARGBEGIN {
+ case 'i':
+ *environ = NULL;
+ break;
+ case 'u':
+ if (unsetenv(EARGF(usage())) < 0)
+ eprintf("unsetenv:");
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ for (; *argv && strchr(*argv, '='); argc--, argv++)
+ putenv(*argv);
+
+ if (*argv) {
+ execvp(*argv, argv);
+ savederrno = errno;
+ weprintf("execvp %s:", *argv);
+ _exit(126 + (savederrno == ENOENT));
+ }
+
+ for (; *environ; environ++)
+ puts(*environ);
+
+ return fshut(stdout, "<stdout>");
+}
diff --git a/util/sbase/expand.1 b/util/sbase/expand.1
new file mode 100644
index 00000000..9cf20fb4
--- /dev/null
+++ b/util/sbase/expand.1
@@ -0,0 +1,47 @@
+.Dd October 8, 2015
+.Dt EXPAND 1
+.Os sbase
+.Sh NAME
+.Nm expand
+.Nd expand tabs to spaces
+.Sh SYNOPSIS
+.Nm
+.Op Fl i
+.Op Fl t Ar tablist
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+converts tabs to spaces in each
+.Ar file
+as specified in
+.Ar tablist .
+If no file is given,
+.Nm
+reads from stdin.
+.Pp
+Backspace characters are preserved and decrement the column count
+for tab calculations.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl i
+Only expand tabs at the beginning of lines, i.e. expand each
+line until a character different from '\et' and ' ' is reached.
+.It Fl t Ar tablist
+Specify tab size or tabstops.
+.Ar tablist
+is a list of one (in the former case) or multiple (in the latter case)
+strictly positive integers separated by ' ' or ','.
+.Pp
+The default
+.Ar tablist
+is "8".
+.El
+.Sh SEE ALSO
+.Xr fold 1 ,
+.Xr unexpand 1
+.Sh STANDARDS
+POSIX.1-2013.
+.Pp
+The
+.Op Fl i
+flag is an extension to that specification.
diff --git a/util/sbase/expand.c b/util/sbase/expand.c
new file mode 100644
index 00000000..f534134f
--- /dev/null
+++ b/util/sbase/expand.c
@@ -0,0 +1,131 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "utf.h"
+#include "util.h"
+
+static int iflag = 0;
+static size_t *tablist = NULL;
+static size_t tablistlen = 0;
+
+static size_t
+parselist(const char *s)
+{
+ size_t i;
+ char *p, *tmp;
+
+ tmp = estrdup(s);
+ for (i = 0; (p = strsep(&tmp, " ,")); i++) {
+ if (*p == '\0')
+ eprintf("empty field in tablist\n");
+ tablist = ereallocarray(tablist, i + 1, sizeof(*tablist));
+ tablist[i] = estrtonum(p, 1, MIN(LLONG_MAX, SIZE_MAX));
+ if (i > 0 && tablist[i - 1] >= tablist[i])
+ eprintf("tablist must be ascending\n");
+ }
+ tablist = ereallocarray(tablist, i + 1, sizeof(*tablist));
+ /* tab length = 1 for the overflowing case later in the matcher */
+ tablist[i] = 1;
+
+ return i;
+}
+
+static int
+expand(const char *file, FILE *fp)
+{
+ size_t bol = 1, col = 0, i;
+ Rune r;
+
+ while (efgetrune(&r, fp, file)) {
+ switch (r) {
+ case '\t':
+ if (tablistlen == 1)
+ i = 0;
+ else for (i = 0; i < tablistlen; i++)
+ if (col < tablist[i])
+ break;
+ if (bol || !iflag) {
+ do {
+ col++;
+ putchar(' ');
+ } while (col % tablist[i]);
+ } else {
+ putchar('\t');
+ col = tablist[i];
+ }
+ break;
+ case '\b':
+ bol = 0;
+ if (col)
+ col--;
+ putchar('\b');
+ break;
+ case '\n':
+ bol = 1;
+ col = 0;
+ putchar('\n');
+ break;
+ default:
+ col++;
+ if (r != ' ')
+ bol = 0;
+ efputrune(&r, stdout, "<stdout>");
+ break;
+ }
+ }
+
+ return 0;
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-i] [-t tablist] [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ FILE *fp;
+ int ret = 0;
+ char *tl = "8";
+
+ ARGBEGIN {
+ case 'i':
+ iflag = 1;
+ break;
+ case 't':
+ tl = EARGF(usage());
+ if (!*tl)
+ eprintf("tablist cannot be empty\n");
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ tablistlen = parselist(tl);
+
+ if (!argc) {
+ expand("<stdin>", stdin);
+ } else {
+ for (; *argv; argc--, argv++) {
+ if (!strcmp(*argv, "-")) {
+ *argv = "<stdin>";
+ fp = stdin;
+ } else if (!(fp = fopen(*argv, "r"))) {
+ weprintf("fopen %s:", *argv);
+ ret = 1;
+ continue;
+ }
+ expand(*argv, fp);
+ if (fp != stdin && fshut(fp, *argv))
+ ret = 1;
+ }
+ }
+
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/expr.1 b/util/sbase/expr.1
new file mode 100644
index 00000000..4710fc40
--- /dev/null
+++ b/util/sbase/expr.1
@@ -0,0 +1,101 @@
+.Dd October 8, 2015
+.Dt EXPR 1
+.Os sbase
+.Sh NAME
+.Nm expr
+.Nd evaluate expression
+.Sh SYNOPSIS
+.Nm
+.Ar expression
+.Sh DESCRIPTION
+.Nm
+evaluates
+.Ar expression
+and writes the result to stdout.
+.Pp
+There are two elemental expressions,
+.Sy integer
+and
+.Sy string .
+Let
+.Sy expr
+be a non-elemental expression and
+.Sy expr1 ,
+.Sy expr2
+arbitrary expressions.
+Then
+.Sy expr
+has the recursive form
+.Sy expr = [(] expr1 operand expr2 [)] .
+.Pp
+With
+.Sy operand
+being in order of increasing precedence:
+.Bl -tag -width Ds
+.It |
+Evaluate to
+.Sy expr1
+if it is neither an empty string nor 0; otherwise evaluate to
+.Sy expr2 .
+.It &
+Evaluate to
+.Sy expr1
+if
+.Sy expr1
+and
+.Sy expr2
+are neither empty strings nor 0; otherwise evaluate to 0.
+.It = > >= < <= !=
+If
+.Sy expr1
+and
+.Sy expr2
+are integers, evaluate to 1 if the relation is true and 0 if it is false.
+If
+.Sy expr1
+and
+.Sy expr2
+are strings, apply the relation to the return value of
+.Xr strcmp 3 .
+.It + -
+If
+.Sy expr1
+and
+.Sy expr2
+are integers, evaluate to their sum or subtraction.
+.It * / %
+If
+.Sy expr1
+and
+.Sy expr2
+are integers, evaluate to their multiplication, division or remainder.
+.It :
+Evaluate to the number of characters matched in
+.Sy expr1
+against
+.Sy expr2 . expr2
+is anchored with an implicit '^'.
+.Pp
+You can't directly match the empty string, since zero matched characters
+resolve equally to a failed match.
+To work around this limitation, use "expr X'' : 'X$' instead of "expr ''
+: '$'"
+.El
+.Sh EXIT STATUS
+.Bl -tag -width Ds
+.It 0
+.Ar expression
+is neither an empty string nor 0.
+.It 1
+.Ar expression
+is an empty string or 0.
+.It 2
+.Ar expression
+is invalid.
+.It > 2
+An error occurred.
+.El
+.Sh SEE ALSO
+.Xr test 1
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/expr.c b/util/sbase/expr.c
new file mode 100644
index 00000000..044c6c1a
--- /dev/null
+++ b/util/sbase/expr.c
@@ -0,0 +1,244 @@
+/* See LICENSE file for copyright and license details. */
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "utf.h"
+#include "util.h"
+
+/* tokens, one-character operators represent themselves */
+enum {
+ VAL = CHAR_MAX + 1, GE, LE, NE
+};
+
+struct val {
+ char *str;
+ long long num;
+};
+
+static void
+tonum(struct val *v)
+{
+ const char *errstr;
+ long long d;
+
+ /* check if val is the result of an earlier calculation */
+ if (!v->str)
+ return;
+
+ d = strtonum(v->str, LLONG_MIN, LLONG_MAX, &errstr);
+ if (errstr)
+ enprintf(2, "error: expected integer, got %s\n", v->str);
+ v->num = d;
+}
+
+static void
+ezero(struct val *v)
+{
+ if (v->num != 0)
+ return;
+ enprintf(2, "division by zero\n");
+}
+
+static int
+valcmp(struct val *a, struct val *b)
+{
+ int ret;
+ const char *err1, *err2;
+ long long d1, d2;
+
+ d1 = strtonum(a->str, LLONG_MIN, LLONG_MAX, &err1);
+ d2 = strtonum(b->str, LLONG_MIN, LLONG_MAX, &err2);
+
+ if (!err1 && !err2) {
+ ret = (d1 > d2) - (d1 < d2);
+ } else {
+ ret = strcmp(a->str, b->str);
+ }
+
+ return ret;
+}
+
+static void
+match(struct val *vstr, struct val *vregx, struct val *ret)
+{
+ regex_t re;
+ regmatch_t matches[2];
+ size_t anchlen;
+ char *s, *p, *anchreg;
+ char *str = vstr->str, *regx = vregx->str;
+
+ /* anchored regex */
+ anchlen = strlen(regx) + 1 + 1;
+ anchreg = emalloc(anchlen);
+ estrlcpy(anchreg, "^", anchlen);
+ estrlcat(anchreg, regx, anchlen);
+ enregcomp(3, &re, anchreg, 0);
+ free(anchreg);
+
+ if (regexec(&re, str, 2, matches, 0)) {
+ regfree(&re);
+ ret->str = re.re_nsub ? "" : NULL;
+ return;
+ } else if (re.re_nsub) {
+ regfree(&re);
+
+ s = str + matches[1].rm_so;
+ p = str + matches[1].rm_eo;
+ *p = '\0';
+ ret->str = enstrdup(3, s);
+ return;
+ } else {
+ regfree(&re);
+ str += matches[0].rm_so;
+ ret->num = utfnlen(str, matches[0].rm_eo - matches[0].rm_so);
+ return;
+ }
+}
+
+static void
+doop(int *ophead, int *opp, struct val *valhead, struct val *valp)
+{
+ struct val ret = { .str = NULL, .num = 0 }, *a, *b;
+ int op;
+
+ /* an operation "a op b" needs an operator and two values */
+ if (opp[-1] == '(')
+ enprintf(2, "syntax error: extra (\n");
+ if (valp - valhead < 2)
+ enprintf(2, "syntax error: missing expression or extra operator\n");
+
+ a = valp - 2;
+ b = valp - 1;
+ op = opp[-1];
+
+ switch (op) {
+ case '|':
+ if ( a->str && *a->str) ret.str = a->str;
+ else if (!a->str && a->num) ret.num = a->num;
+ else if ( b->str && *b->str) ret.str = b->str;
+ else ret.num = b->num;
+ break;
+ case '&':
+ if (((a->str && *a->str) || a->num) &&
+ ((b->str && *b->str) || b->num)) {
+ ret.str = a->str;
+ ret.num = a->num;
+ }
+ break;
+
+ case '=': ret.num = (valcmp(a, b) == 0); break;
+ case '>': ret.num = (valcmp(a, b) > 0); break;
+ case GE : ret.num = (valcmp(a, b) >= 0); break;
+ case '<': ret.num = (valcmp(a, b) < 0); break;
+ case LE : ret.num = (valcmp(a, b) <= 0); break;
+ case NE : ret.num = (valcmp(a, b) != 0); break;
+
+ case '+': tonum(a); tonum(b); ret.num = a->num + b->num; break;
+ case '-': tonum(a); tonum(b); ret.num = a->num - b->num; break;
+ case '*': tonum(a); tonum(b); ret.num = a->num * b->num; break;
+ case '/': tonum(a); tonum(b); ezero(b); ret.num = a->num / b->num; break;
+ case '%': tonum(a); tonum(b); ezero(b); ret.num = a->num % b->num; break;
+
+ case ':': match(a, b, &ret); break;
+ }
+
+ valp[-2] = ret;
+}
+
+static int
+lex(char *s, struct val *v)
+{
+ int type = VAL;
+ char *ops = "|&=><+-*/%():";
+
+ if (s[0] && strchr(ops, s[0]) && !s[1]) {
+ /* one-char operand */
+ type = s[0];
+ } else if (s[0] && strchr("><!", s[0]) && s[1] == '=' && !s[2]) {
+ /* two-char operand */
+ type = (s[0] == '>') ? GE : (s[0] == '<') ? LE : NE;
+ }
+
+ return type;
+}
+
+static int
+parse(char *expr[], int numexpr)
+{
+ struct val *valhead, *valp, v = { .str = NULL, .num = 0 };
+ int *ophead, *opp, type, lasttype = 0;
+ char prec[] = {
+ [ 0 ] = 0, [VAL] = 0, ['('] = 0, [')'] = 0,
+ ['|'] = 1,
+ ['&'] = 2,
+ ['='] = 3, ['>'] = 3, [GE] = 3, ['<'] = 3, [LE] = 3, [NE] = 3,
+ ['+'] = 4, ['-'] = 4,
+ ['*'] = 5, ['/'] = 5, ['%'] = 5,
+ [':'] = 6,
+ };
+
+ valp = valhead = enreallocarray(3, NULL, numexpr, sizeof(*valp));
+ opp = ophead = enreallocarray(3, NULL, numexpr, sizeof(*opp));
+ for (; *expr; expr++) {
+ switch ((type = lex(*expr, &v))) {
+ case VAL:
+ /* treatment of *expr is not known until
+ * doop(); treat as a string for now */
+ valp->str = *expr;
+ valp++;
+ break;
+ case '(':
+ *opp++ = type;
+ break;
+ case ')':
+ if (lasttype == '(')
+ enprintf(2, "syntax error: empty ( )\n");
+ while (opp > ophead && opp[-1] != '(')
+ doop(ophead, opp--, valhead, valp--);
+ if (opp == ophead)
+ enprintf(2, "syntax error: extra )\n");
+ opp--;
+ break;
+ default: /* operator */
+ if (prec[lasttype])
+ enprintf(2, "syntax error: extra operator\n");
+ while (opp > ophead && prec[opp[-1]] >= prec[type])
+ doop(ophead, opp--, valhead, valp--);
+ *opp++ = type;
+ break;
+ }
+ lasttype = type;
+ v.str = NULL;
+ v.num = 0;
+ }
+ while (opp > ophead)
+ doop(ophead, opp--, valhead, valp--);
+ if (valp == valhead)
+ enprintf(2, "syntax error: missing expression\n");
+ if (--valp > valhead)
+ enprintf(2, "syntax error: extra expression\n");
+
+ if (valp->str)
+ puts(valp->str);
+ else
+ printf("%lld\n", valp->num);
+
+ return (valp->str && *valp->str) || valp->num;
+}
+
+int
+main(int argc, char *argv[])
+{
+ int ret;
+
+ argv0 = *argv, argv0 ? (argc--, argv++) : (void *)0;
+
+ ret = !parse(argv, argc);
+
+ if (fshut(stdout, "<stdout>"))
+ ret = 3;
+
+ return ret;
+}
diff --git a/util/sbase/false.1 b/util/sbase/false.1
new file mode 100644
index 00000000..e6adf570
--- /dev/null
+++ b/util/sbase/false.1
@@ -0,0 +1,13 @@
+.Dd October 8, 2015
+.Dt FALSE 1
+.Os sbase
+.Sh NAME
+.Nm false
+.Nd return failure
+.Sh SYNOPSIS
+.Nm
+.Sh DESCRIPTION
+.Nm
+returns a status code indicating failure.
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/false.c b/util/sbase/false.c
new file mode 100644
index 00000000..fce3fd98
--- /dev/null
+++ b/util/sbase/false.c
@@ -0,0 +1,6 @@
+/* See LICENSE file for copyright and license details. */
+int
+main(void)
+{
+ return 1;
+}
diff --git a/util/sbase/find.1 b/util/sbase/find.1
new file mode 100644
index 00000000..00f26306
--- /dev/null
+++ b/util/sbase/find.1
@@ -0,0 +1,151 @@
+.Dd July 30, 2025
+.Dt FIND 1
+.Os sbase
+.Sh NAME
+.Nm find
+.Nd find files
+.Sh SYNOPSIS
+.Nm
+.Op Fl H | L
+.Ar path Op ...
+.Op Ar expression
+.Sh DESCRIPTION
+.Nm
+walks a file hierarchy starting at each
+.Ar path
+and applies the
+.Ar expression
+to each file encountered.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl H
+Dereference symbolic links provided as
+.Ar path .
+.It Fl L
+Dereference all symbolic links encountered.
+.El
+.Sh EXTENDED DESCRIPTION
+.Ar expression
+is a combination of the following primaries and boolean operators.
+In the following descriptions the number n can be replaced by +n, n, or
+-n, to mean more than, exactly, or less than n respectively.
+.Ss Primaries
+.Bl -tag -width Ds
+.It Fl name Ar pattern
+True if the name of the file matches the given pattern.
+.It Fl path Ar pattern
+True if the path to the file matches the given pattern.
+.It Fl nouser
+True if the file belongs to a user for which
+.Xr getpwuid 3
+returns NULL.
+.It Fl nogroup
+True if the file belongs to a group for which
+.Xr getgrgid 3
+returns NULL.
+.It Fl xdev
+True.
+Do not enter directory on a different device.
+.It Fl prune
+True.
+Do not enter directory.
+.It Fl perm Ar mode
+True if permissions on the file match mode.
+Mode is a symbolic mode as used in chmod.
+A leading '-' in mode checks that at least all bits in mode are set in
+permissions for file.
+Without the leading '-' the permissions for file must exactly match
+mode.
+.It Fl type Ar t
+True if file is of type specified by
+.Ar t .
+.Bl -tag -width Ds
+.It Ar b
+block special
+.It Ar c
+character special
+.It Ar d
+directory
+.It Ar l
+symbolic link
+.It Ar p
+FIFO
+.It Ar f
+regular file
+.It Ar s
+socket
+.El
+.It Fl links Ar n
+True if file has
+.Ar n
+links.
+.It Fl user Ar name
+True if file belongs to user
+.Ar name .
+.It Fl group Ar name
+True if file belongs to group
+.Ar name .
+.It Fl size Ar n[c]
+True if file size in 512 byte sectors (rounded up), or bytes (if
+.Ar c
+is given), is
+.Ar n .
+.It Fl atime n
+True if file access time is
+.Ar n
+days.
+.It Fl ctime
+True if file status change time is
+.Ar n
+days.
+.It Fl mtime
+True if file modified time is
+.Ar n
+days.
+.It Fl exec Ar cmd [arg ...] \&;
+Execute cmd with given arguments, replacing each {} in argument list
+with the current file.
+True if cmd exits with status 0.
+.It Fl exec Ar cmd [arg ...] {} +
+True.
+Add as many files as possible to argument list and execute when the list
+is full or all files have been found.
+.It Fl ok Ar cmd [arg ...] \&;
+Prompt the user on each file encountered whether or not to execute cmd
+as with -exec.
+True if the user responds yes and cmd exits with status 0, false
+otherwise.
+.It Fl print
+True.
+Print the current pathname followed by a newline ('\en') character.
+.It Fl print0
+True.
+Print the current pathname followed by a NUL ('\e0') character.
+.It Fl newer Ar file
+True if the modification time of the current file is newer than that of
+the provided file.
+.It Fl depth
+True.
+Causes find to evaluate files within in a directory before the directory
+itself.
+.El
+.Ss Operators
+In order of decreasing precedence
+.Bl -tag -width Ds
+.It Ar \&( expression \&)
+True if expression is true.
+.It Ar \&! expression
+True if expression if false.
+.It Ar expression [ Fl a ] Ar expression
+True if both expressions are true.
+Second expression is not evaluated if first expression is false.
+.Fl a
+is implied if there is no operator between primaries.
+.It Ar expression Fl o Ar expression
+True if either expression is true.
+Second expression is not evaluated if first expression is true.
+.El
+.Pp
+If no expression is supplied, -print is used.
+If an expression is supplied but none of -print, -exec, or -ok is
+supplied, then -a -print is appended to the expressions.
diff --git a/util/sbase/find.c b/util/sbase/find.c
new file mode 100644
index 00000000..5bc1a1f1
--- /dev/null
+++ b/util/sbase/find.c
@@ -0,0 +1,1103 @@
+/* See LICENSE file for copyright and license details. */
+#include <dirent.h>
+#include <errno.h>
+#include <fnmatch.h>
+#include <grp.h>
+#include <libgen.h>
+#include <pwd.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include "util.h"
+
+/* because putting integers in pointers is undefined by the standard */
+union extra {
+ void *p;
+ intmax_t i;
+};
+
+/* Argument passed into a primary's function */
+struct arg {
+ char *path;
+ struct stat *st;
+ union extra extra;
+};
+
+/* Information about each primary, for lookup table */
+struct pri_info {
+ char *name;
+ int (*func)(struct arg *arg);
+ char **(*getarg)(char **argv, union extra *extra);
+ void (*freearg)(union extra extra);
+ char narg; /* -xdev, -depth, -print don't take args but have getarg() */
+};
+
+/* Information about operators, for lookup table */
+struct op_info {
+ char *name; /* string representation of op */
+ char type; /* from tok.type */
+ char prec; /* precedence */
+ char nargs; /* number of arguments (unary or binary) */
+ char lassoc; /* left associative */
+};
+
+/* Token when lexing/parsing
+ * (although also used for the expression tree) */
+struct tok {
+ struct tok *left, *right; /* if (type == NOT) left = NULL */
+ union extra extra;
+ union {
+ struct pri_info *pinfo; /* if (type == PRIM) */
+ struct op_info *oinfo;
+ } u;
+ enum {
+ PRIM = 0, LPAR, RPAR, NOT, AND, OR, END
+ } type;
+};
+
+/* structures used for arg.extra.p and tok.extra.p */
+struct permarg {
+ mode_t mode;
+ char exact;
+};
+
+struct okarg {
+ char ***braces;
+ char **argv;
+};
+
+/* for all arguments that take a number
+ * +n, n, -n mean > n, == n, < n respectively */
+struct narg {
+ int (*cmp)(int a, int b);
+ int n;
+};
+
+struct sizearg {
+ struct narg n;
+ char bytes; /* size is in bytes, not 512 byte sectors */
+};
+
+struct execarg {
+ union {
+ struct {
+ char ***braces; /* NULL terminated list of pointers into argv where {} were */
+ } s; /* semicolon */
+ struct {
+ size_t arglen; /* number of bytes in argv before files are added */
+ size_t filelen; /* numer of bytes in file names added to argv */
+ size_t first; /* index one past last arg, where first file goes */
+ size_t next; /* index where next file goes */
+ size_t cap; /* capacity of argv */
+ } p; /* plus */
+ } u;
+ char **argv; /* NULL terminated list of arguments (allocated if isplus) */
+ char isplus; /* -exec + instead of -exec ; */
+};
+
+/* used to find loops while recursing through directory structure */
+struct findhist {
+ struct findhist *next;
+ char *path;
+ dev_t dev;
+ ino_t ino;
+};
+
+/* Utility */
+static int spawn(char *argv[]);
+static int do_stat(char *path, struct stat *sb, struct findhist *hist);
+
+/* Primaries */
+static int pri_name (struct arg *arg);
+static int pri_path (struct arg *arg);
+static int pri_nouser (struct arg *arg);
+static int pri_nogroup(struct arg *arg);
+static int pri_xdev (struct arg *arg);
+static int pri_prune (struct arg *arg);
+static int pri_perm (struct arg *arg);
+static int pri_type (struct arg *arg);
+static int pri_links (struct arg *arg);
+static int pri_user (struct arg *arg);
+static int pri_group (struct arg *arg);
+static int pri_size (struct arg *arg);
+static int pri_atime (struct arg *arg);
+static int pri_ctime (struct arg *arg);
+static int pri_mtime (struct arg *arg);
+static int pri_exec (struct arg *arg);
+static int pri_ok (struct arg *arg);
+static int pri_print (struct arg *arg);
+static int pri_print0 (struct arg *arg);
+static int pri_newer (struct arg *arg);
+static int pri_depth (struct arg *arg);
+
+/* Getargs */
+static char **get_name_arg (char *argv[], union extra *extra);
+static char **get_path_arg (char *argv[], union extra *extra);
+static char **get_xdev_arg (char *argv[], union extra *extra);
+static char **get_perm_arg (char *argv[], union extra *extra);
+static char **get_type_arg (char *argv[], union extra *extra);
+static char **get_n_arg (char *argv[], union extra *extra);
+static char **get_user_arg (char *argv[], union extra *extra);
+static char **get_group_arg(char *argv[], union extra *extra);
+static char **get_size_arg (char *argv[], union extra *extra);
+static char **get_exec_arg (char *argv[], union extra *extra);
+static char **get_ok_arg (char *argv[], union extra *extra);
+static char **get_print_arg(char *argv[], union extra *extra);
+static char **get_newer_arg(char *argv[], union extra *extra);
+static char **get_depth_arg(char *argv[], union extra *extra);
+
+/* Freeargs */
+static void free_extra (union extra extra);
+static void free_exec_arg(union extra extra);
+static void free_ok_arg (union extra extra);
+
+/* Parsing/Building/Running */
+static void fill_narg(char *s, struct narg *n);
+static struct pri_info *find_primary(char *name);
+static struct op_info *find_op(char *name);
+static void parse(int argc, char **argv);
+static int eval(struct tok *tok, struct arg *arg);
+static void find(char *path, struct findhist *hist);
+static void usage(void);
+
+/* for comparisons with narg */
+static int cmp_gt(int a, int b) { return a > b; }
+static int cmp_eq(int a, int b) { return a == b; }
+static int cmp_lt(int a, int b) { return a < b; }
+
+/* order from find(1p), may want to alphabetize */
+static struct pri_info primaries[] = {
+ { "-name" , pri_name , get_name_arg , NULL , 1 },
+ { "-path" , pri_path , get_path_arg , NULL , 1 },
+ { "-nouser" , pri_nouser , NULL , NULL , 1 },
+ { "-nogroup", pri_nogroup, NULL , NULL , 1 },
+ { "-xdev" , pri_xdev , get_xdev_arg , NULL , 0 },
+ { "-prune" , pri_prune , NULL , NULL , 1 },
+ { "-perm" , pri_perm , get_perm_arg , free_extra , 1 },
+ { "-type" , pri_type , get_type_arg , NULL , 1 },
+ { "-links" , pri_links , get_n_arg , free_extra , 1 },
+ { "-user" , pri_user , get_user_arg , NULL , 1 },
+ { "-group" , pri_group , get_group_arg, NULL , 1 },
+ { "-size" , pri_size , get_size_arg , free_extra , 1 },
+ { "-atime" , pri_atime , get_n_arg , free_extra , 1 },
+ { "-ctime" , pri_ctime , get_n_arg , free_extra , 1 },
+ { "-mtime" , pri_mtime , get_n_arg , free_extra , 1 },
+ { "-exec" , pri_exec , get_exec_arg , free_exec_arg, 1 },
+ { "-ok" , pri_ok , get_ok_arg , free_ok_arg , 1 },
+ { "-print" , pri_print , get_print_arg, NULL , 0 },
+ { "-print0" , pri_print0 , get_print_arg, NULL , 0 },
+ { "-newer" , pri_newer , get_newer_arg, NULL , 1 },
+ { "-depth" , pri_depth , get_depth_arg, NULL , 0 },
+
+ { NULL, NULL, NULL, NULL, 0 }
+};
+
+static struct op_info ops[] = {
+ { "(" , LPAR, 0, 0, 0 }, /* parens are handled specially */
+ { ")" , RPAR, 0, 0, 0 },
+ { "!" , NOT , 3, 1, 0 },
+ { "-a", AND , 2, 2, 1 },
+ { "-o", OR , 1, 2, 1 },
+
+ { NULL, 0, 0, 0, 0 }
+};
+
+extern char **environ;
+
+static struct tok *toks; /* holds allocated array of all toks created while parsing */
+static struct tok *root; /* points to root of expression tree, inside toks array */
+
+static struct timespec start; /* time find was started, used for -[acm]time */
+
+static size_t envlen; /* number of bytes in environ, used to calculate against ARG_MAX */
+static size_t argmax; /* value of ARG_MAX retrieved using sysconf(3p) */
+
+static struct {
+ char ret ; /* return value from main */
+ char depth; /* -depth, directory contents before directory itself */
+ char h ; /* -H, follow symlinks on command line */
+ char l ; /* -L, follow all symlinks (command line and search) */
+ char prune; /* hit -prune */
+ char xdev ; /* -xdev, prune directories on different devices */
+ char print; /* whether we will need -print when parsing */
+} gflags;
+
+/*
+ * Utility
+ */
+static int
+spawn(char *argv[])
+{
+ pid_t pid;
+ int status;
+
+ /* flush stdout so that -print output always appears before
+ * any output from the command and does not get cut-off in
+ * the middle of a line. */
+ fflush(stdout);
+
+ switch((pid = fork())) {
+ case -1:
+ eprintf("fork:");
+ case 0:
+ execvp(*argv, argv);
+ weprintf("exec %s failed:", *argv);
+ _exit(1);
+ }
+
+ /* FIXME: proper course of action for waitpid() on EINTR? */
+ waitpid(pid, &status, 0);
+ return status;
+}
+
+static int
+do_stat(char *path, struct stat *sb, struct findhist *hist)
+{
+ if (gflags.l || (gflags.h && !hist)) {
+ if (stat(path, sb) == 0) {
+ return 0;
+ } else if (errno != ENOENT && errno != ENOTDIR) {
+ return -1;
+ }
+ }
+
+ return lstat(path, sb);
+}
+
+/*
+ * Primaries
+ */
+static int
+pri_name(struct arg *arg)
+{
+ int ret;
+ char *path;
+
+ path = estrdup(arg->path);
+ ret = !fnmatch((char *)arg->extra.p, basename(path), 0);
+ free(path);
+
+ return ret;
+}
+
+static int
+pri_path(struct arg *arg)
+{
+ return !fnmatch((char *)arg->extra.p, arg->path, 0);
+}
+
+/* FIXME: what about errors? find(1p) literally just says
+ * "for which the getpwuid() function ... returns NULL" */
+static int
+pri_nouser(struct arg *arg)
+{
+ return !getpwuid(arg->st->st_uid);
+}
+
+static int
+pri_nogroup(struct arg *arg)
+{
+ return !getgrgid(arg->st->st_gid);
+}
+
+static int
+pri_xdev(struct arg *arg)
+{
+ return 1;
+}
+
+static int
+pri_prune(struct arg *arg)
+{
+ return gflags.prune = 1;
+}
+
+static int
+pri_perm(struct arg *arg)
+{
+ struct permarg *p = (struct permarg *)arg->extra.p;
+
+ return (arg->st->st_mode & 07777 & (p->exact ? -1U : p->mode)) == p->mode;
+}
+
+static int
+pri_type(struct arg *arg)
+{
+ switch ((char)arg->extra.i) {
+ default : return 0; /* impossible, but placate warnings */
+ case 'b': return S_ISBLK (arg->st->st_mode);
+ case 'c': return S_ISCHR (arg->st->st_mode);
+ case 'd': return S_ISDIR (arg->st->st_mode);
+ case 'l': return S_ISLNK (arg->st->st_mode);
+ case 'p': return S_ISFIFO(arg->st->st_mode);
+ case 'f': return S_ISREG (arg->st->st_mode);
+ case 's': return S_ISSOCK(arg->st->st_mode);
+ }
+}
+
+static int
+pri_links(struct arg *arg)
+{
+ struct narg *n = arg->extra.p;
+ return n->cmp(arg->st->st_nlink, n->n);
+}
+
+static int
+pri_user(struct arg *arg)
+{
+ return arg->st->st_uid == (uid_t)arg->extra.i;
+}
+
+static int
+pri_group(struct arg *arg)
+{
+ return arg->st->st_gid == (gid_t)arg->extra.i;
+}
+
+static int
+pri_size(struct arg *arg)
+{
+ struct sizearg *s = arg->extra.p;
+ off_t size = arg->st->st_size;
+
+ if (!s->bytes)
+ size = size / 512 + !!(size % 512);
+
+ return s->n.cmp(size, s->n.n);
+}
+
+/* FIXME: ignoring nanoseconds in atime, ctime, mtime */
+static int
+pri_atime(struct arg *arg)
+{
+ struct narg *n = arg->extra.p;
+ return n->cmp((start.tv_sec - arg->st->st_atime) / 86400, n->n);
+}
+
+static int
+pri_ctime(struct arg *arg)
+{
+ struct narg *n = arg->extra.p;
+ return n->cmp((start.tv_sec - arg->st->st_ctime) / 86400, n->n);
+}
+
+static int
+pri_mtime(struct arg *arg)
+{
+ struct narg *n = arg->extra.p;
+ return n->cmp((start.tv_sec - arg->st->st_mtime) / 86400, n->n);
+}
+
+static int
+pri_exec(struct arg *arg)
+{
+ int status;
+ size_t len;
+ char **sp, ***brace;
+ struct execarg *e = arg->extra.p;
+
+ if (e->isplus) {
+ len = strlen(arg->path) + 1;
+
+ /* if we reached ARG_MAX, fork, exec, wait, free file names, reset list */
+ if (len + e->u.p.arglen + e->u.p.filelen + envlen > argmax) {
+ e->argv[e->u.p.next] = NULL;
+
+ status = spawn(e->argv);
+ gflags.ret = gflags.ret || status;
+
+ for (sp = e->argv + e->u.p.first; *sp; sp++)
+ free(*sp);
+
+ e->u.p.next = e->u.p.first;
+ e->u.p.filelen = 0;
+ }
+
+ /* if we have too many files, realloc (with space for NULL termination) */
+ if (e->u.p.next + 1 == e->u.p.cap)
+ e->argv = ereallocarray(e->argv, e->u.p.cap *= 2, sizeof(*e->argv));
+
+ e->argv[e->u.p.next++] = estrdup(arg->path);
+ e->u.p.filelen += len + sizeof(arg->path);
+
+ return 1;
+ } else {
+ /* insert path everywhere user gave us {} */
+ for (brace = e->u.s.braces; *brace; brace++)
+ **brace = arg->path;
+
+ status = spawn(e->argv);
+ return !status;
+ }
+}
+
+static int
+pri_ok(struct arg *arg)
+{
+ int status, reply;
+ char ***brace, c;
+ struct okarg *o = arg->extra.p;
+
+ fprintf(stderr, "%s: %s ? ", *o->argv, arg->path);
+ reply = fgetc(stdin);
+
+ /* throw away rest of line */
+ while ((c = fgetc(stdin)) != '\n' && c != EOF)
+ /* FIXME: what if the first character of the rest of the line is a null
+ * byte? */
+ ;
+
+ if (feof(stdin)) /* FIXME: ferror()? */
+ clearerr(stdin);
+
+ if (reply != 'y' && reply != 'Y')
+ return 0;
+
+ /* insert filename everywhere user gave us {} */
+ for (brace = o->braces; *brace; brace++)
+ **brace = arg->path;
+
+ status = spawn(o->argv);
+ return !!status;
+}
+
+static int
+pri_print(struct arg *arg)
+{
+ if (puts(arg->path) == EOF)
+ eprintf("puts failed:");
+ return 1;
+}
+
+static int
+pri_print0(struct arg *arg)
+{
+ if (fwrite(arg->path, strlen(arg->path) + 1, 1, stdout) != 1)
+ eprintf("fwrite failed:");
+ return 1;
+}
+
+/* FIXME: ignoring nanoseconds */
+static int
+pri_newer(struct arg *arg)
+{
+ return arg->st->st_mtime > (time_t)arg->extra.i;
+}
+
+static int
+pri_depth(struct arg *arg)
+{
+ return 1;
+}
+
+/*
+ * Getargs
+ * consume any arguments for given primary and fill extra
+ * return pointer to last argument, the pointer will be incremented in parse()
+ */
+static char **
+get_name_arg(char *argv[], union extra *extra)
+{
+ extra->p = *argv;
+ return argv;
+}
+
+static char **
+get_path_arg(char *argv[], union extra *extra)
+{
+ extra->p = *argv;
+ return argv;
+}
+
+static char **
+get_xdev_arg(char *argv[], union extra *extra)
+{
+ gflags.xdev = 1;
+ return argv;
+}
+
+static char **
+get_perm_arg(char *argv[], union extra *extra)
+{
+ mode_t mask;
+ struct permarg *p = extra->p = emalloc(sizeof(*p));
+
+ if (**argv == '-')
+ (*argv)++;
+ else
+ p->exact = 1;
+
+ mask = umask(0);
+ umask(mask);
+
+ p->mode = parsemode(*argv, 0, mask);
+
+ return argv;
+}
+
+static char **
+get_type_arg(char *argv[], union extra *extra)
+{
+ if (!strchr("bcdlpfs", **argv))
+ eprintf("invalid type %c for -type primary\n", **argv);
+
+ extra->i = **argv;
+ return argv;
+}
+
+static char **
+get_n_arg(char *argv[], union extra *extra)
+{
+ struct narg *n = extra->p = emalloc(sizeof(*n));
+ fill_narg(*argv, n);
+ return argv;
+}
+
+static char **
+get_user_arg(char *argv[], union extra *extra)
+{
+ char *end;
+ struct passwd *p = getpwnam(*argv);
+
+ if (p) {
+ extra->i = p->pw_uid;
+ } else {
+ extra->i = strtol(*argv, &end, 10);
+ if (end == *argv || *end)
+ eprintf("unknown user '%s'\n", *argv);
+ }
+ return argv;
+}
+
+static char **
+get_group_arg(char *argv[], union extra *extra)
+{
+ char *end;
+ struct group *g = getgrnam(*argv);
+
+ if (g) {
+ extra->i = g->gr_gid;
+ } else {
+ extra->i = strtol(*argv, &end, 10);
+ if (end == *argv || *end)
+ eprintf("unknown group '%s'\n", *argv);
+ }
+ return argv;
+}
+
+static char **
+get_size_arg(char *argv[], union extra *extra)
+{
+ char *p = *argv + strlen(*argv);
+ struct sizearg *s = extra->p = emalloc(sizeof(*s));
+ /* if the number is followed by 'c', the size will by in bytes */
+ if ((s->bytes = (p > *argv && *--p == 'c')))
+ *p = '\0';
+
+ fill_narg(*argv, &s->n);
+ return argv;
+}
+
+static char **
+get_exec_arg(char *argv[], union extra *extra)
+{
+ char **arg, **new, ***braces;
+ int nbraces = 0;
+ struct execarg *e = extra->p = emalloc(sizeof(*e));
+
+ for (arg = argv; *arg; arg++)
+ if (!strcmp(*arg, ";"))
+ break;
+ else if (arg > argv && !strcmp(*(arg - 1), "{}") && !strcmp(*arg, "+"))
+ break;
+ else if (!strcmp(*arg, "{}"))
+ nbraces++;
+
+ if (!*arg)
+ eprintf("no terminating ; or {} + for -exec primary\n");
+
+ e->isplus = **arg == '+';
+ *arg = NULL;
+
+ if (e->isplus) {
+ *(arg - 1) = NULL; /* don't need the {} in there now */
+ e->u.p.arglen = e->u.p.filelen = 0;
+ e->u.p.first = e->u.p.next = arg - argv - 1;
+ e->u.p.cap = (arg - argv) * 2;
+ e->argv = ereallocarray(NULL, e->u.p.cap, sizeof(*e->argv));
+
+ for (arg = argv, new = e->argv; *arg; arg++, new++) {
+ *new = *arg;
+ e->u.p.arglen += strlen(*arg) + 1 + sizeof(*arg);
+ }
+ arg++; /* due to our extra NULL */
+ } else {
+ e->argv = argv;
+ e->u.s.braces = ereallocarray(NULL, ++nbraces, sizeof(*e->u.s.braces)); /* ++ for NULL */
+
+ for (arg = argv, braces = e->u.s.braces; *arg; arg++)
+ if (!strcmp(*arg, "{}"))
+ *braces++ = arg;
+ *braces = NULL;
+ }
+ gflags.print = 0;
+ return arg;
+}
+
+static char **
+get_ok_arg(char *argv[], union extra *extra)
+{
+ char **arg, ***braces;
+ int nbraces = 0;
+ struct okarg *o = extra->p = emalloc(sizeof(*o));
+
+ for (arg = argv; *arg; arg++)
+ if (!strcmp(*arg, ";"))
+ break;
+ else if (!strcmp(*arg, "{}"))
+ nbraces++;
+
+ if (!*arg)
+ eprintf("no terminating ; for -ok primary\n");
+ *arg = NULL;
+
+ o->argv = argv;
+ o->braces = ereallocarray(NULL, ++nbraces, sizeof(*o->braces));
+
+ for (arg = argv, braces = o->braces; *arg; arg++)
+ if (!strcmp(*arg, "{}"))
+ *braces++ = arg;
+ *braces = NULL;
+
+ gflags.print = 0;
+ return arg;
+}
+
+static char **
+get_print_arg(char *argv[], union extra *extra)
+{
+ gflags.print = 0;
+ return argv;
+}
+
+/* FIXME: ignoring nanoseconds */
+static char **
+get_newer_arg(char *argv[], union extra *extra)
+{
+ struct stat st;
+
+ if (do_stat(*argv, &st, NULL))
+ eprintf("failed to stat '%s':", *argv);
+
+ extra->i = st.st_mtime;
+ return argv;
+}
+
+static char **
+get_depth_arg(char *argv[], union extra *extra)
+{
+ gflags.depth = 1;
+ return argv;
+}
+
+/*
+ * Freeargs
+ */
+static void
+free_extra(union extra extra)
+{
+ free(extra.p);
+}
+
+static void
+free_exec_arg(union extra extra)
+{
+ int status;
+ char **arg;
+ struct execarg *e = extra.p;
+
+ if (!e->isplus) {
+ free(e->u.s.braces);
+ } else {
+ e->argv[e->u.p.next] = NULL;
+
+ /* if we have files, do the last exec */
+ if (e->u.p.first != e->u.p.next) {
+ status = spawn(e->argv);
+ gflags.ret = gflags.ret || status;
+ }
+ for (arg = e->argv + e->u.p.first; *arg; arg++)
+ free(*arg);
+ free(e->argv);
+ }
+ free(e);
+}
+
+static void
+free_ok_arg(union extra extra)
+{
+ struct okarg *o = extra.p;
+
+ free(o->braces);
+ free(o);
+}
+
+/*
+ * Parsing/Building/Running
+ */
+static void
+fill_narg(char *s, struct narg *n)
+{
+ char *end;
+
+ switch (*s) {
+ case '+': n->cmp = cmp_gt; s++; break;
+ case '-': n->cmp = cmp_lt; s++; break;
+ default : n->cmp = cmp_eq; break;
+ }
+ n->n = strtol(s, &end, 10);
+ if (end == s || *end)
+ eprintf("bad number '%s'\n", s);
+}
+
+static struct pri_info *
+find_primary(char *name)
+{
+ struct pri_info *p;
+
+ for (p = primaries; p->name; p++)
+ if (!strcmp(name, p->name))
+ return p;
+ return NULL;
+}
+
+static struct op_info *
+find_op(char *name)
+{
+ struct op_info *o;
+
+ for (o = ops; o->name; o++)
+ if (!strcmp(name, o->name))
+ return o;
+ return NULL;
+}
+
+/* given the expression from the command line
+ * 1) convert arguments from strings to tok and place in an array duplicating
+ * the infix expression given, inserting "-a" where it was omitted
+ * 2) allocate an array to hold the correct number of tok, and convert from
+ * infix to rpn (using shunting-yard), add -a and -print if necessary
+ * 3) evaluate the rpn filling in left and right pointers to create an
+ * expression tree (tok are still all contained in the rpn array, just
+ * pointing at eachother)
+ */
+static void
+parse(int argc, char **argv)
+{
+ struct tok *tok, *rpn, *out, **top, *infix, **stack;
+ struct op_info *op;
+ struct pri_info *pri;
+ char **arg;
+ int lasttype = -1;
+ size_t ntok = 0;
+ struct tok and = { .u.oinfo = find_op("-a"), .type = AND };
+
+ gflags.print = 1;
+
+ /* convert argv to infix expression of tok, inserting in *tok */
+ infix = ereallocarray(NULL, 2 * argc + 1, sizeof(*infix));
+ for (arg = argv, tok = infix; *arg; arg++, tok++) {
+ pri = find_primary(*arg);
+
+ if (pri) { /* token is a primary, fill out tok and get arguments */
+ if (lasttype == PRIM || lasttype == RPAR) {
+ *tok++ = and;
+ ntok++;
+ }
+ if (pri->getarg) {
+ if (pri->narg && !*++arg)
+ eprintf("no argument for primary %s\n", pri->name);
+ arg = pri->getarg(arg, &tok->extra);
+ }
+ tok->u.pinfo = pri;
+ tok->type = PRIM;
+ } else if ((op = find_op(*arg))) { /* token is an operator */
+ if (lasttype == LPAR && op->type == RPAR)
+ eprintf("empty parens\n");
+ if ((lasttype == PRIM || lasttype == RPAR) &&
+ (op->type == NOT || op->type == LPAR)) { /* need another implicit -a */
+ *tok++ = and;
+ ntok++;
+ }
+ tok->type = op->type;
+ tok->u.oinfo = op;
+ } else {
+ /* token is neither primary nor operator, must be */
+ if ((*arg)[0] == '-') /* an unsupported option */
+ eprintf("unknown operand: %s\n", *arg);
+ else /* or a path in the wrong place */
+ eprintf("paths must precede expression: %s\n", *arg);
+ }
+ if (tok->type != LPAR && tok->type != RPAR)
+ ntok++; /* won't have parens in rpn */
+ lasttype = tok->type;
+ }
+ tok->type = END;
+ ntok++;
+
+ if (gflags.print && (arg != argv)) /* need to add -a -print (not just -print) */
+ gflags.print++;
+
+ /* use shunting-yard to convert from infix to rpn
+ * https://en.wikipedia.org/wiki/Shunting-yard_algorithm
+ * read from infix, resulting rpn ends up in rpn, next position in rpn is out
+ * push operators onto stack, next position in stack is top */
+ rpn = ereallocarray(NULL, ntok + gflags.print, sizeof(*rpn));
+ stack = ereallocarray(NULL, argc + gflags.print, sizeof(*stack));
+ for (tok = infix, out = rpn, top = stack; tok->type != END; tok++) {
+ switch (tok->type) {
+ case PRIM: *out++ = *tok; break;
+ case LPAR: *top++ = tok; break;
+ case RPAR:
+ while (top-- > stack && (*top)->type != LPAR)
+ *out++ = **top;
+ if (top < stack)
+ eprintf("extra )\n");
+ break;
+ default:
+ /* this expression can be simplified, but I decided copy the
+ * verbage from the wikipedia page in order to more clearly explain
+ * what's going on */
+ while (top-- > stack &&
+ (( tok->u.oinfo->lassoc && tok->u.oinfo->prec <= (*top)->u.oinfo->prec) ||
+ (!tok->u.oinfo->lassoc && tok->u.oinfo->prec < (*top)->u.oinfo->prec)))
+ *out++ = **top;
+
+ /* top now points to either an operator we didn't pop, or stack[-1]
+ * either way we need to increment it before using it, then
+ * increment again so the stack works */
+ top++;
+ *top++ = tok;
+ break;
+ }
+ }
+ while (top-- > stack) {
+ if ((*top)->type == LPAR)
+ eprintf("extra (\n");
+ *out++ = **top;
+ }
+
+ /* if there was no expression, use -print
+ * if there was an expression but no -print, -exec, or -ok, add -a -print
+ * in rpn, not infix */
+ if (gflags.print)
+ *out++ = (struct tok){ .u.pinfo = find_primary("-print"), .type = PRIM };
+ if (gflags.print == 2)
+ *out++ = and;
+
+ out->type = END;
+
+ /* rpn now holds all operators and arguments in reverse polish notation
+ * values are pushed onto stack, operators pop values off stack into left
+ * and right pointers, pushing operator node back onto stack
+ * could probably just do this during shunting-yard, but this is simpler
+ * code IMO */
+ for (tok = rpn, top = stack; tok->type != END; tok++) {
+ if (tok->type == PRIM) {
+ *top++ = tok;
+ } else {
+ if (top - stack < tok->u.oinfo->nargs)
+ eprintf("insufficient arguments for operator %s\n", tok->u.oinfo->name);
+ tok->right = *--top;
+ tok->left = tok->u.oinfo->nargs == 2 ? *--top : NULL;
+ *top++ = tok;
+ }
+ }
+ if (--top != stack)
+ eprintf("extra arguments\n");
+
+ toks = rpn;
+ root = *top;
+
+ free(infix);
+ free(stack);
+}
+
+/* for a primary, run and return result
+ * for an operator evaluate the left side of the tree, decide whether or not to
+ * evaluate the right based on the short-circuit boolean logic, return result
+ * NOTE: operator NOT has NULL left side, expression on right side
+ */
+static int
+eval(struct tok *tok, struct arg *arg)
+{
+ int ret;
+
+ if (!tok)
+ return 0;
+
+ if (tok->type == PRIM) {
+ arg->extra = tok->extra;
+ return tok->u.pinfo->func(arg);
+ }
+
+ ret = eval(tok->left, arg);
+
+ if ((tok->type == AND && ret) || (tok->type == OR && !ret) || tok->type == NOT)
+ ret = eval(tok->right, arg);
+
+ return ret ^ (tok->type == NOT);
+}
+
+/* evaluate path, if it's a directory iterate through directory entries and
+ * recurse
+ */
+static void
+find(char *path, struct findhist *hist)
+{
+ struct stat st;
+ DIR *dir;
+ struct dirent *de;
+ struct findhist *f, cur;
+ size_t namelen, pathcap = 0, len;
+ struct arg arg = { path, &st, { NULL } };
+ char *p, *pathbuf = NULL;
+
+ len = strlen(path) + 2; /* \0 and '/' */
+
+ if (do_stat(path, &st, hist) < 0) {
+ weprintf("failed to stat %s:", path);
+ gflags.ret = 1;
+ return;
+ }
+
+ gflags.prune = 0;
+
+ /* don't eval now iff we will hit the eval at the bottom which means
+ * 1. we are a directory 2. we have -depth 3. we don't have -xdev or we are
+ * on same device (so most of the time we eval here) */
+ if (!S_ISDIR(st.st_mode) ||
+ !gflags.depth ||
+ (gflags.xdev && hist && st.st_dev != hist->dev))
+ eval(root, &arg);
+
+ if (!S_ISDIR(st.st_mode) ||
+ gflags.prune ||
+ (gflags.xdev && hist && st.st_dev != hist->dev))
+ return;
+
+ for (f = hist; f; f = f->next) {
+ if (f->dev == st.st_dev && f->ino == st.st_ino) {
+ weprintf("loop detected '%s' is '%s'\n", path, f->path);
+ gflags.ret = 1;
+ return;
+ }
+ }
+ cur.next = hist;
+ cur.path = path;
+ cur.dev = st.st_dev;
+ cur.ino = st.st_ino;
+
+ if (!(dir = opendir(path))) {
+ weprintf("failed to opendir %s:", path);
+ gflags.ret = 1;
+ /* should we just ignore this since we hit an error? */
+ if (gflags.depth)
+ eval(root, &arg);
+ return;
+ }
+
+ while (errno = 0, (de = readdir(dir))) {
+ if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, ".."))
+ continue;
+ namelen = strlen(de->d_name);
+ if (len + namelen > pathcap) {
+ pathcap = len + namelen;
+ pathbuf = erealloc(pathbuf, pathcap);
+ }
+ p = pathbuf + estrlcpy(pathbuf, path, pathcap);
+ if (*--p != '/')
+ estrlcat(pathbuf, "/", pathcap);
+ estrlcat(pathbuf, de->d_name, pathcap);
+ find(pathbuf, &cur);
+ }
+ free(pathbuf);
+ if (errno) {
+ weprintf("readdir %s:", path);
+ gflags.ret = 1;
+ closedir(dir);
+ return;
+ }
+ closedir(dir);
+
+ if (gflags.depth)
+ eval(root, &arg);
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-H | -L] path ... [expression ...]\n", argv0);
+}
+
+int
+main(int argc, char **argv)
+{
+ char **paths;
+ int npaths;
+ struct tok *t;
+
+ ARGBEGIN {
+ case 'H':
+ gflags.h = 1;
+ gflags.l = 0;
+ break;
+ case 'L':
+ gflags.l = 1;
+ gflags.h = 0;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ paths = argv;
+
+ for (; *argv && **argv != '-' && strcmp(*argv, "!") && strcmp(*argv, "("); argv++)
+ ;
+
+ if (!(npaths = argv - paths))
+ eprintf("must specify a path\n");
+
+ parse(argc - npaths, argv);
+
+ /* calculate number of bytes in environ for -exec {} + ARG_MAX avoidance
+ * libc implementation defined whether null bytes, pointers, and alignment
+ * are counted, so count them */
+ for (argv = environ; *argv; argv++)
+ envlen += strlen(*argv) + 1 + sizeof(*argv);
+
+ if ((argmax = sysconf(_SC_ARG_MAX)) == (size_t)-1)
+ argmax = _POSIX_ARG_MAX;
+
+ if (clock_gettime(CLOCK_REALTIME, &start) < 0)
+ weprintf("clock_gettime() failed:");
+
+ while (npaths--)
+ find(*paths++, NULL);
+
+ for (t = toks; t->type != END; t++)
+ if (t->type == PRIM && t->u.pinfo->freearg)
+ t->u.pinfo->freearg(t->extra);
+ free(toks);
+
+ gflags.ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return gflags.ret;
+}
diff --git a/util/sbase/flock.1 b/util/sbase/flock.1
new file mode 100644
index 00000000..9dfaf394
--- /dev/null
+++ b/util/sbase/flock.1
@@ -0,0 +1,35 @@
+.Dd October 8, 2015
+.Dt FLOCK 1
+.Os sbase
+.Sh NAME
+.Nm flock
+.Nd tool to manage locks on files
+.Sh SYNOPSIS
+.Nm
+.Op Fl nosux
+.Ar file
+.Ar cmd Op arg ...
+.Sh DESCRIPTION
+.Nm
+is used to manage advisory locks on open files.
+It is commonly used to prevent long running cron jobs from running in
+parallel.
+If
+.Ar file
+does not exist, it will be created.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl n
+Set non-blocking mode on the lock.
+Fail immediately if the lock cannot be acquired.
+.It Fl o
+Close the file descriptor before exec to avoid having the exec'ed
+program holding on to the lock.
+.It Fl s
+Acquire a shared lock.
+.It Fl u
+Release the lock.
+.It Fl x
+Acquire an exclusive lock.
+This is the default.
+.El
diff --git a/util/sbase/flock.c b/util/sbase/flock.c
new file mode 100644
index 00000000..fc2b6ed6
--- /dev/null
+++ b/util/sbase/flock.c
@@ -0,0 +1,82 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/file.h>
+#include <sys/wait.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-nosux] file cmd [arg ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int fd, status, savederrno, flags = LOCK_EX, nonblk = 0, oflag = 0;
+ pid_t pid;
+
+ ARGBEGIN {
+ case 'n':
+ nonblk = LOCK_NB;
+ break;
+ case 'o':
+ oflag = 1;
+ break;
+ case 's':
+ flags = LOCK_SH;
+ break;
+ case 'u':
+ flags = LOCK_UN;
+ break;
+ case 'x':
+ flags = LOCK_EX;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (argc < 2)
+ usage();
+
+ if ((fd = open(*argv, O_RDONLY | O_CREAT, 0644)) < 0)
+ eprintf("open %s:", *argv);
+
+ if (flock(fd, flags | nonblk)) {
+ if (nonblk && errno == EWOULDBLOCK)
+ return 1;
+ eprintf("flock:");
+ }
+
+ switch ((pid = fork())) {
+ case -1:
+ eprintf("fork:");
+ case 0:
+ if (oflag && close(fd) < 0)
+ eprintf("close:");
+ argv++;
+ execvp(*argv, argv);
+ savederrno = errno;
+ weprintf("execvp %s:", *argv);
+ _exit(126 + (savederrno == ENOENT));
+ default:
+ break;
+ }
+ if (waitpid(pid, &status, 0) < 0)
+ eprintf("waitpid:");
+
+ if (close(fd) < 0)
+ eprintf("close:");
+
+ if (WIFSIGNALED(status))
+ return 128 + WTERMSIG(status);
+ if (WIFEXITED(status))
+ return WEXITSTATUS(status);
+
+ return 0;
+}
diff --git a/util/sbase/fold.1 b/util/sbase/fold.1
new file mode 100644
index 00000000..3c2a3e80
--- /dev/null
+++ b/util/sbase/fold.1
@@ -0,0 +1,39 @@
+.Dd October 8, 2015
+.Dt FOLD 1
+.Os sbase
+.Sh NAME
+.Nm fold
+.Nd wrap lines to width
+.Sh SYNOPSIS
+.Nm
+.Op Fl bs
+.Op Fl w Ar num | Fl Ns Ar num
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+reads each
+.Ar file
+and prints its lines wrapped such that no line
+exceeds a certain width.
+If no file is given,
+.Nm
+reads from stdin.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl b
+Count bytes rather than characters.
+.It Fl s
+If a line contains spaces, break
+at the last space within width.
+.It Fl w Ar num | Fl Ns Ar num
+Break at
+.Ar num
+characters.
+The default is 80.
+.El
+.Sh STANDARDS
+POSIX.1-2013.
+.Pp
+The
+.Op Fl Ns Ar num
+syntax is an extension to that specification.
diff --git a/util/sbase/fold.c b/util/sbase/fold.c
new file mode 100644
index 00000000..6c7b9e7b
--- /dev/null
+++ b/util/sbase/fold.c
@@ -0,0 +1,130 @@
+/* See LICENSE file for copyright and license details. */
+#include <ctype.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "text.h"
+#include "util.h"
+#include "utf.h"
+
+static int bflag = 0;
+static int sflag = 0;
+static size_t width = 80;
+
+static void
+foldline(struct line *l, const char *fname) {
+ size_t i, col, last, spacesect, len;
+ Rune r;
+ int runelen;
+
+ for (i = 0, last = 0, col = 0, spacesect = 0; i < l->len; i += runelen) {
+ if (col >= width && ((l->data[i] != '\r' && l->data[i] != '\b') || bflag)) {
+ if (bflag && col > width)
+ i -= runelen; /* never split a character */
+ len = ((sflag && spacesect) ? spacesect : i) - last;
+ if (fwrite(l->data + last, 1, len, stdout) != len)
+ eprintf("fwrite <stdout>:");
+ if (l->data[i] != '\n')
+ putchar('\n');
+ if (sflag && spacesect)
+ i = spacesect;
+ last = i;
+ col = 0;
+ spacesect = 0;
+ }
+ runelen = charntorune(&r, l->data + i, l->len - i);
+ if (!runelen || r == Runeerror)
+ eprintf("charntorune: %s: invalid utf\n", fname);
+ if (sflag && isblankrune(r))
+ spacesect = i + runelen;
+ if (!bflag && iscntrl(l->data[i])) {
+ switch(l->data[i]) {
+ case '\b':
+ col -= (col > 0);
+ break;
+ case '\r':
+ col = 0;
+ break;
+ case '\t':
+ col += (8 - (col % 8));
+ if (col >= width)
+ i--;
+ break;
+ }
+ } else {
+ col += bflag ? runelen : 1;
+ }
+ }
+ if (l->len - last)
+ fwrite(l->data + last, 1, l->len - last, stdout);
+}
+
+static void
+fold(FILE *fp, const char *fname)
+{
+ static struct line line;
+ static size_t size = 0;
+ ssize_t len;
+
+ while ((len = getline(&line.data, &size, fp)) > 0) {
+ line.len = len;
+ foldline(&line, fname);
+ }
+ if (ferror(fp))
+ eprintf("getline %s:", fname);
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-bs] [-w num | -num] [FILE ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ FILE *fp;
+ int ret = 0;
+
+ ARGBEGIN {
+ case 'b':
+ bflag = 1;
+ break;
+ case 's':
+ sflag = 1;
+ break;
+ case 'w':
+ width = estrtonum(EARGF(usage()), 1, MIN(LLONG_MAX, SIZE_MAX));
+ break;
+ ARGNUM:
+ if (!(width = ARGNUMF()))
+ eprintf("illegal width value, too small\n");
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc) {
+ fold(stdin, "<stdin>");
+ } else {
+ for (; *argv; argc--, argv++) {
+ if (!strcmp(*argv, "-")) {
+ *argv = "<stdin>";
+ fp = stdin;
+ } else if (!(fp = fopen(*argv, "r"))) {
+ weprintf("fopen %s:", *argv);
+ ret = 1;
+ continue;
+ }
+ fold(fp, *argv);
+ if (fp != stdin && fshut(fp, *argv))
+ ret = 1;
+ }
+ }
+
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/fs.h b/util/sbase/fs.h
new file mode 100644
index 00000000..fd647bb7
--- /dev/null
+++ b/util/sbase/fs.h
@@ -0,0 +1,47 @@
+/* See LICENSE file for copyright and license details. */
+#include <limits.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+struct history {
+ struct history *prev;
+ dev_t dev;
+ ino_t ino;
+};
+
+struct recursor {
+ void (*fn)(int, const char *, struct stat *, void *, struct recursor *);
+ char path[PATH_MAX];
+ size_t pathlen;
+ struct history *hist;
+ int depth;
+ int maxdepth;
+ int follow;
+ int flags;
+};
+
+enum {
+ SAMEDEV = 1 << 0,
+ DIRFIRST = 1 << 1,
+ SILENT = 1 << 2,
+ CONFIRM = 1 << 3,
+ IGNORE = 1 << 4,
+};
+
+extern int cp_aflag;
+extern int cp_fflag;
+extern int cp_iflag;
+extern int cp_pflag;
+extern int cp_rflag;
+extern int cp_vflag;
+extern int cp_follow;
+extern int cp_status;
+
+extern int rm_status;
+
+extern int recurse_status;
+
+void recurse(int, const char *, void *, struct recursor *);
+
+int cp(const char *, const char *, int);
+void rm(int, const char *, struct stat *st, void *, struct recursor *);
diff --git a/util/sbase/getconf.1 b/util/sbase/getconf.1
new file mode 100644
index 00000000..e75abbb0
--- /dev/null
+++ b/util/sbase/getconf.1
@@ -0,0 +1,57 @@
+.Dd October 8, 2015
+.Dt GETCONF 1
+.Os sbase
+.Sh NAME
+.Nm getconf
+.Nd get configuration values
+.Sh SYNOPSIS
+.Nm
+.Op Fl v Ar spec
+.Ar var
+.Ar [path]
+.Sh DESCRIPTION
+.Nm
+writes the value of the variable
+.Ar var
+to stdout.
+.sp
+If
+.Ar path
+is given,
+.Ar var
+is matched against configuration values from
+.Xr pathconf 3 .
+If
+.Ar path
+is not given,
+.Ar var
+is matched against configuration values from
+.Xr sysconf 3 ,
+.Xr confstr 3
+and limits.h (Minimum and Maximum).
+.sp
+If
+.Ar var
+is not defined,
+.Nm
+writes "undefined" to stdout.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl v Ar spec
+Ignored.
+.El
+.Sh EXIT STATUS
+.Bl -tag -width Ds
+.It 0
+.Ar var
+was matched and its value written successfully.
+.It 1
+An error occured or
+.Ar var
+was invalid.
+.El
+.Sh STANDARDS
+POSIX.1-2013.
+Except for the
+.Op Fl v
+flag.
diff --git a/util/sbase/getconf.c b/util/sbase/getconf.c
new file mode 100644
index 00000000..ca3e186f
--- /dev/null
+++ b/util/sbase/getconf.c
@@ -0,0 +1,108 @@
+/* See LICENSE file for copyright and license details. */
+#include <errno.h>
+#include <unistd.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "util.h"
+
+struct var {
+ const char *k;
+ long v;
+};
+
+#include "getconf.h"
+
+void
+usage(void)
+{
+ eprintf("usage: %s [-v spec] var [path]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ size_t len;
+ long res;
+ int i;
+ char *str;
+
+ ARGBEGIN {
+ case 'v':
+ /* ignore */
+ EARGF(usage());
+ break;
+ default:
+ usage();
+ break;
+ } ARGEND
+
+ if (argc == 1) {
+ /* sysconf */
+ for (i = 0; i < LEN(sysconf_l); i++) {
+ if (strcmp(argv[0], sysconf_l[i].k))
+ continue;
+ errno = 0;
+ if ((res = sysconf(sysconf_l[i].v)) < 0) {
+ if (errno)
+ eprintf("sysconf %ld:", sysconf_l[i].v);
+ puts("undefined");
+ } else {
+ printf("%ld\n", res);
+ }
+ return fshut(stdout, "<stdout>");
+ }
+ /* confstr */
+ for (i = 0; i < LEN(confstr_l); i++) {
+ if (strcmp(argv[0], confstr_l[i].k))
+ continue;
+ errno = 0;
+ if (!(len = confstr(confstr_l[i].v, NULL, 0))) {
+ if (errno)
+ eprintf("confstr %ld:", confstr_l[i].v);
+ puts("undefined");
+ } else {
+ str = emalloc(len);
+ errno = 0;
+ if (!confstr(confstr_l[i].v, str, len)) {
+ if (errno)
+ eprintf("confstr %ld:", confstr_l[i].v);
+ puts("undefined");
+ } else {
+ puts(str);
+ }
+ free(str);
+ }
+ return fshut(stdout, "<stdout>");
+ }
+ /* limits */
+ for (i = 0; i < LEN(limits_l); i++) {
+ if (strcmp(argv[0], limits_l[i].k))
+ continue;
+ printf("%ld\n", limits_l[i].v);
+ return fshut(stdout, "<stdout>");
+ }
+ } else if (argc == 2) {
+ /* pathconf */
+ for (i = 0; i < LEN(pathconf_l); i++) {
+ if (strcmp(argv[0], pathconf_l[i].k))
+ continue;
+ errno = 0;
+ if ((res = pathconf(argv[1], pathconf_l[i].v)) < 0) {
+ if (errno)
+ eprintf("pathconf %ld:", pathconf_l[i].v);
+ puts("undefined");
+ } else {
+ printf("%ld\n", res);
+ }
+ return fshut(stdout, "<stdout>");
+ }
+ } else {
+ usage();
+ }
+
+ eprintf("invalid variable: %s\n", argv[0]);
+
+ return 0;
+}
diff --git a/util/sbase/grep.1 b/util/sbase/grep.1
new file mode 100644
index 00000000..9de0294a
--- /dev/null
+++ b/util/sbase/grep.1
@@ -0,0 +1,94 @@
+.Dd October 8, 2015
+.Dt GREP 1
+.Os sbase
+.Sh NAME
+.Nm grep
+.Nd search files for patterns
+.Sh SYNOPSIS
+.Nm
+.Op Fl EFHchilnqsvx
+.Op Fl e Ar pattern
+.Op Fl f Ar file
+.Op Ar pattern
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+searches the input files for lines that match the
+.Ar pattern ,
+a regular expression as defined in
+.Xr regex 7 or
+.Xr re_format 7 .
+By default each matching line is printed to stdout.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl E
+Match using extended regex.
+.It Fl F
+Match using fixed strings.
+Treat each pattern specified as a string instead of a regular
+expression.
+.It Fl H
+Prefix each matching line with its filename in the output.
+This is the default when there is more than one file specified.
+.It Fl c
+Print only a count of matching lines.
+.It Fl e Ar pattern
+Specify a pattern used during the search of the input: an input
+line is selected if it matches any of the specified patterns.
+This option is most useful when multiple -e options are used to
+specify multiple patterns, or when a pattern begins with a dash.
+.It Fl f Ar file
+Read one or more patterns from the file named by the pathname file.
+Patterns in file shall be terminated by a <newline>.
+A null pattern can be specified by an empty line in pattern_file.
+Unless the -E or -F option is also specified, each pattern shall be
+treated as a BRE.
+(`-').
+.It Fl h
+Do not prefix each line with 'filename:' prefix.
+.It Fl i
+Match lines case insensitively.
+.It Fl l
+Print only the names of files with matching lines.
+.It Fl n
+Prefix each matching line with its line number in the input.
+.It Fl q
+Print nothing, only return status.
+.It Fl s
+Suppress the error messages ordinarily written for nonexistent or unreadable
+files.
+.It Fl v
+Select lines which do
+.Sy not
+match the pattern.
+.It Fl w
+The expression is searched for as a word (as if surrounded by '\\<' and '\\>').
+.It Fl x
+Consider only input lines that use all characters in the line excluding the
+terminating <newline> to match an entire fixed string or regular expression to
+be matching lines.
+.El
+.Sh EXIT STATUS
+.Bl -tag -width Ds
+.It 0
+One or more lines were matched.
+.It 1
+No lines were matched.
+.It > 1
+An error occurred.
+.El
+.Sh SEE ALSO
+.Xr sed 1 ,
+.Xr re_format 7 ,
+.Xr regex 7
+.Sh STANDARDS
+POSIX.1-2013.
+.Pp
+The
+.Op Fl Hhw
+flags are an extension to that specification.
diff --git a/util/sbase/grep.c b/util/sbase/grep.c
new file mode 100644
index 00000000..1c978070
--- /dev/null
+++ b/util/sbase/grep.c
@@ -0,0 +1,290 @@
+/* See LICENSE file for copyright and license details. */
+#include <regex.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+
+#include "queue.h"
+#include "util.h"
+
+enum { Match = 0, NoMatch = 1, Error = 2 };
+
+static void addpattern(const char *, size_t);
+static void addpatternfile(FILE *);
+static int grep(FILE *, const char *);
+
+static int Eflag;
+static int Fflag;
+static int Hflag;
+static int eflag;
+static int fflag;
+static int hflag;
+static int iflag;
+static int sflag;
+static int vflag;
+static int wflag;
+static int xflag;
+static int many;
+static int mode;
+
+struct pattern {
+ char *pattern;
+ regex_t preg;
+ SLIST_ENTRY(pattern) entry;
+};
+
+static SLIST_HEAD(phead, pattern) phead;
+
+static void
+addpattern(const char *pattern, size_t patlen)
+{
+ struct pattern *pnode;
+ char *tmp;
+ int bol, eol;
+ size_t len;
+
+ if (!patlen)
+ return;
+
+ /* a null BRE/ERE matches every line */
+ if (!Fflag)
+ if (pattern[0] == '\0')
+ pattern = "^";
+
+ if (!Fflag && xflag) {
+ tmp = enmalloc(Error, patlen + 3);
+ snprintf(tmp, patlen + 3, "%s%s%s",
+ pattern[0] == '^' ? "" : "^",
+ pattern,
+ pattern[patlen - 1] == '$' ? "" : "$");
+ } else if (!Fflag && wflag) {
+ len = patlen + 5 + (Eflag ? 2 : 4);
+ tmp = enmalloc(Error, len);
+
+ bol = eol = 0;
+ if (pattern[0] == '^')
+ bol = 1;
+ if (pattern[patlen - 1] == '$')
+ eol = 1;
+
+ snprintf(tmp, len, "%s\\<%s%.*s%s\\>%s",
+ bol ? "^" : "",
+ Eflag ? "(" : "\\(",
+ (int)patlen - bol - eol, pattern + bol,
+ Eflag ? ")" : "\\)",
+ eol ? "$" : "");
+ } else {
+ tmp = enstrdup(Error, pattern);
+ }
+
+ pnode = enmalloc(Error, sizeof(*pnode));
+ pnode->pattern = tmp;
+ SLIST_INSERT_HEAD(&phead, pnode, entry);
+}
+
+static void
+addpatternfile(FILE *fp)
+{
+ static char *buf = NULL;
+ static size_t size = 0;
+ ssize_t len = 0;
+
+ while ((len = getline(&buf, &size, fp)) > 0) {
+ if (len > 0 && buf[len - 1] == '\n')
+ buf[len - 1] = '\0';
+ addpattern(buf, (size_t)len);
+ }
+ if (ferror(fp))
+ enprintf(Error, "read error:");
+}
+
+static int
+grep(FILE *fp, const char *str)
+{
+ static char *buf = NULL;
+ static size_t size = 0;
+ ssize_t len = 0;
+ long c = 0, n;
+ struct pattern *pnode;
+ int match, result = NoMatch;
+
+ for (n = 1; (len = getline(&buf, &size, fp)) > 0; n++) {
+ /* Remove the trailing newline if one is present. */
+ if (len && buf[len - 1] == '\n')
+ buf[len - 1] = '\0';
+ match = 0;
+ SLIST_FOREACH(pnode, &phead, entry) {
+ if (Fflag) {
+ if (xflag) {
+ if (!(iflag ? strcasecmp : strcmp)(buf, pnode->pattern)) {
+ match = 1;
+ break;
+ }
+ } else {
+ if ((iflag ? strcasestr : strstr)(buf, pnode->pattern)) {
+ match = 1;
+ break;
+ }
+ }
+ } else {
+ if (regexec(&pnode->preg, buf, 0, NULL, 0) == 0) {
+ match = 1;
+ break;
+ }
+ }
+ }
+ if (match != vflag) {
+ result = Match;
+ switch (mode) {
+ case 'c':
+ c++;
+ break;
+ case 'l':
+ puts(str);
+ goto end;
+ case 'q':
+ exit(Match);
+ default:
+ if (!hflag && (many || Hflag))
+ printf("%s:", str);
+ if (mode == 'n')
+ printf("%ld:", n);
+ puts(buf);
+ break;
+ }
+ }
+ }
+ if (mode == 'c')
+ printf("%ld\n", c);
+end:
+ if (ferror(fp)) {
+ weprintf("%s: read error:", str);
+ result = Error;
+ }
+ return result;
+}
+
+static void
+usage(void)
+{
+ enprintf(Error, "usage: %s [-EFHchilnqsvwx] [-e pattern] [-f file] "
+ "[pattern] [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct pattern *pnode;
+ int m, flags = REG_NOSUB, match = NoMatch;
+ FILE *fp;
+ char *arg;
+
+ SLIST_INIT(&phead);
+
+ ARGBEGIN {
+ case 'E':
+ Eflag = 1;
+ Fflag = 0;
+ flags |= REG_EXTENDED;
+ break;
+ case 'F':
+ Fflag = 1;
+ Eflag = 0;
+ flags &= ~REG_EXTENDED;
+ break;
+ case 'H':
+ Hflag = 1;
+ hflag = 0;
+ break;
+ case 'e':
+ arg = EARGF(usage());
+ if (!(fp = fmemopen(arg, strlen(arg) + 1, "r")))
+ eprintf("fmemopen:");
+ addpatternfile(fp);
+ efshut(fp, arg);
+ eflag = 1;
+ break;
+ case 'f':
+ arg = EARGF(usage());
+ fp = fopen(arg, "r");
+ if (!fp)
+ enprintf(Error, "fopen %s:", arg);
+ addpatternfile(fp);
+ efshut(fp, arg);
+ fflag = 1;
+ break;
+ case 'h':
+ hflag = 1;
+ Hflag = 0;
+ break;
+ case 'c':
+ case 'l':
+ case 'n':
+ case 'q':
+ mode = ARGC();
+ break;
+ case 'i':
+ flags |= REG_ICASE;
+ iflag = 1;
+ break;
+ case 's':
+ sflag = 1;
+ break;
+ case 'v':
+ vflag = 1;
+ break;
+ case 'w':
+ wflag = 1;
+ break;
+ case 'x':
+ xflag = 1;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (argc == 0 && !eflag && !fflag)
+ usage(); /* no pattern */
+
+ /* just add literal pattern to list */
+ if (!eflag && !fflag) {
+ if (!(fp = fmemopen(argv[0], strlen(argv[0]) + 1, "r")))
+ eprintf("fmemopen:");
+ addpatternfile(fp);
+ efshut(fp, argv[0]);
+ argc--;
+ argv++;
+ }
+
+ if (!Fflag)
+ /* Compile regex for all search patterns */
+ SLIST_FOREACH(pnode, &phead, entry)
+ enregcomp(Error, &pnode->preg, pnode->pattern, flags);
+ many = (argc > 1);
+ if (argc == 0) {
+ match = grep(stdin, "<stdin>");
+ } else {
+ for (; *argv; argc--, argv++) {
+ if (!strcmp(*argv, "-")) {
+ *argv = "<stdin>";
+ fp = stdin;
+ } else if (!(fp = fopen(*argv, "r"))) {
+ if (!sflag)
+ weprintf("fopen %s:", *argv);
+ match = Error;
+ continue;
+ }
+ m = grep(fp, *argv);
+ if (m == Error || (match != Error && m == Match))
+ match = m;
+ if (fp != stdin && fshut(fp, *argv))
+ match = Error;
+ }
+ }
+
+ if (fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>"))
+ match = Error;
+
+ return match;
+}
diff --git a/util/sbase/head.1 b/util/sbase/head.1
new file mode 100644
index 00000000..0bf3127f
--- /dev/null
+++ b/util/sbase/head.1
@@ -0,0 +1,40 @@
+.Dd October 8, 2015
+.Dt HEAD 1
+.Os sbase
+.Sh NAME
+.Nm head
+.Nd display initial lines of files
+.Sh SYNOPSIS
+.Nm
+.Op Fl n Ar num | Fl Ns Ar num
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+writes
+.Ar num
+lines of each
+.Ar file
+to stdout.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl n Ar num | Fl Ns Ar num
+Display initial
+.Ar num
+|
+.Sy N
+lines.
+Default is 10.
+.El
+.Sh SEE ALSO
+.Xr tail 1
+.Sh STANDARDS
+POSIX.1-2013.
+.Pp
+The
+.Op Fl Ns num
+syntax is an extension to that specification.
diff --git a/util/sbase/head.c b/util/sbase/head.c
new file mode 100644
index 00000000..ae550c01
--- /dev/null
+++ b/util/sbase/head.c
@@ -0,0 +1,77 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "util.h"
+
+static void
+head(FILE *fp, const char *fname, size_t n)
+{
+ char *buf = NULL;
+ size_t i = 0, size = 0;
+ ssize_t len;
+
+ while (i < n && (len = getline(&buf, &size, fp)) > 0) {
+ fwrite(buf, 1, len, stdout);
+ i += (len && (buf[len - 1] == '\n'));
+ }
+ free(buf);
+ if (ferror(fp))
+ eprintf("getline %s:", fname);
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-num | -n num] [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ size_t n = 10;
+ FILE *fp;
+ int ret = 0, newline = 0, many = 0;
+
+ ARGBEGIN {
+ case 'n':
+ n = estrtonum(EARGF(usage()), 0, MIN(LLONG_MAX, SIZE_MAX));
+ break;
+ ARGNUM:
+ n = ARGNUMF();
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc) {
+ head(stdin, "<stdin>", n);
+ } else {
+ many = argc > 1;
+ for (newline = 0; *argv; argc--, argv++) {
+ if (!strcmp(*argv, "-")) {
+ *argv = "<stdin>";
+ fp = stdin;
+ } else if (!(fp = fopen(*argv, "r"))) {
+ weprintf("fopen %s:", *argv);
+ ret = 1;
+ continue;
+ }
+ if (many) {
+ if (newline)
+ putchar('\n');
+ printf("==> %s <==\n", *argv);
+ }
+ newline = 1;
+ head(fp, *argv, n);
+ if (fp != stdin && fshut(fp, *argv))
+ ret = 1;
+ }
+ }
+
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/hostname.1 b/util/sbase/hostname.1
new file mode 100644
index 00000000..601aef9d
--- /dev/null
+++ b/util/sbase/hostname.1
@@ -0,0 +1,18 @@
+.Dd October 8, 2015
+.Dt HOSTNAME 1
+.Os sbase
+.Sh NAME
+.Nm hostname
+.Nd set or print host name
+.Sh SYNOPSIS
+.Nm
+.Op Ar name
+.Sh DESCRIPTION
+.Nm
+sets the current host name to
+.Ar name .
+If no
+.Ar name
+is given, the current host name is written to stdout.
+.Sh SEE ALSO
+.Xr hostname 7
diff --git a/util/sbase/hostname.c b/util/sbase/hostname.c
new file mode 100644
index 00000000..2532ec8d
--- /dev/null
+++ b/util/sbase/hostname.c
@@ -0,0 +1,36 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [name]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ char host[HOST_NAME_MAX + 1];
+
+ ARGBEGIN {
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc) {
+ if (gethostname(host, sizeof(host)) < 0)
+ eprintf("gethostname:");
+ puts(host);
+ } else if (argc == 1) {
+ if (sethostname(argv[0], strlen(argv[0])) < 0)
+ eprintf("sethostname:");
+ } else {
+ usage();
+ }
+
+ return fshut(stdout, "<stdout>");
+}
diff --git a/util/sbase/join.1 b/util/sbase/join.1
new file mode 100644
index 00000000..6d1f4be1
--- /dev/null
+++ b/util/sbase/join.1
@@ -0,0 +1,105 @@
+.Dd October 8, 2015
+.Dt JOIN 1
+.Os sbase
+.Sh NAME
+.Nm join
+.Nd relational database operator
+.Sh SYNOPSIS
+.Nm
+.Op Fl 1 Ar field
+.Op Fl 2 Ar field
+.Op Fl o Ar list
+.Op Fl e Ar string
+.Op Fl a Ar fileno | Fl v Ar fileno
+.Op Fl t Ar delim
+.Ar file1 file2
+.Sh DESCRIPTION
+.Nm
+lines from
+.Ar file1
+and
+.Ar file2
+on a matching field.
+If one of the input files is '-', standard input is read for that file.
+.Pp
+Files are read sequentially and are assumed to be sorted on the join
+field.
+.Nm
+does not check the order of input, and joining two unsorted files will
+produce unexpected output.
+.Pp
+By default, input lines are matched on the first blank-separated
+field; output lines are space-separated and consist of the join field
+followed by the remaining fields from
+.Ar file1 ,
+then the remaining fields from
+.Ar file2 .
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl 1 Ar field
+Join on the
+.Ar field Ns th
+field of file 1.
+.It Fl 2 Ar field
+Join on the
+.Ar field Ns th
+field of file 2.
+.It Fl a Ar fileno
+Print unpairable lines from file
+.Ar fileno
+in addition to normal output.
+.It Fl e Ar string
+When used with
+.Fl o ,
+replace empty fields in the output list with
+.Ar string .
+.It Fl o Ar list
+Format output according to the string
+.Ar list .
+Each element of
+.Ar list
+may be either
+.Ar fileno.field
+or 0 (representing the join field).
+Elements in
+.Ar list
+may be separated by blanks or commas.
+For example,
+.Bd -literal -offset indent
+join -o "0 2.1 1.3"
+.Ed
+.Pp
+would print the join field, the first field of
+.Ar file2 ,
+then the third field of
+.Ar file1 .
+.Pp
+Only paired lines are formatted with the
+.Fl o
+option.
+Unpairable lines (selected with
+.Fl a
+or
+.Fl v )
+are printed raw.
+.It Fl t Ar delim
+Use the arbitrary string
+.Ar delim
+as field delimiter for both input and output.
+.It Fl v Ar fileno
+Print unpairable lines from file
+.Ar fileno
+instead of normal output.
+.El
+.Sh STANDARDS
+POSIX.1-2013.
+.Pp
+With the following exception:
+.Bl -bullet -offset indent
+.It
+Unpairable lines ignore formatting specified with
+.Fl o .
+.El
+.Pp
+The possibility of specifying multibyte delimiters of arbitrary
+length is an extension to the specification.
diff --git a/util/sbase/join.c b/util/sbase/join.c
new file mode 100644
index 00000000..d3e23439
--- /dev/null
+++ b/util/sbase/join.c
@@ -0,0 +1,529 @@
+/* See LICENSE file for copyright and license details. */
+#include <ctype.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "text.h"
+#include "utf.h"
+#include "util.h"
+
+enum {
+ INIT = 1,
+ GROW = 2,
+};
+
+enum {
+ EXPAND = 0,
+ RESET = 1,
+};
+
+enum { FIELD_ERROR = -2, };
+
+struct field {
+ char *s;
+ size_t len;
+};
+
+struct jline {
+ struct line text;
+ size_t nf;
+ size_t maxf;
+ struct field *fields;
+};
+
+struct spec {
+ size_t fileno;
+ size_t fldno;
+};
+
+struct outlist {
+ size_t ns;
+ size_t maxs;
+ struct spec **specs;
+};
+
+struct span {
+ size_t nl;
+ size_t maxl;
+ struct jline **lines;
+};
+
+static char *sep = NULL;
+static char *replace = NULL;
+static const char defaultofs = ' ';
+static const int jfield = 1; /* POSIX default join field */
+static int unpairsa = 0, unpairsb = 0;
+static int oflag = 0;
+static int pairs = 1;
+static size_t seplen;
+static struct outlist output;
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-1 field] [-2 field] [-o list] [-e string] "
+ "[-a | -v fileno] [-t delim] file1 file2\n", argv0);
+}
+
+static void
+prfield(struct field *fp)
+{
+ if (fwrite(fp->s, 1, fp->len, stdout) != fp->len)
+ eprintf("fwrite:");
+}
+
+static void
+prsep(void)
+{
+ if (sep)
+ fwrite(sep, 1, seplen, stdout);
+ else
+ putchar(defaultofs);
+}
+
+static void
+swaplines(struct jline *la, struct jline *lb)
+{
+ struct jline tmp;
+
+ tmp = *la;
+ *la = *lb;
+ *lb = tmp;
+}
+
+static void
+prjoin(struct jline *la, struct jline *lb, size_t jfa, size_t jfb)
+{
+ struct spec *sp;
+ struct field *joinfield;
+ size_t i;
+
+ if (jfa >= la->nf || jfb >= lb->nf)
+ return;
+
+ joinfield = &la->fields[jfa];
+
+ if (oflag) {
+ for (i = 0; i < output.ns; i++) {
+ sp = output.specs[i];
+
+ if (sp->fileno == 1) {
+ if (sp->fldno < la->nf)
+ prfield(&la->fields[sp->fldno]);
+ else if (replace)
+ fputs(replace, stdout);
+ } else if (sp->fileno == 2) {
+ if (sp->fldno < lb->nf)
+ prfield(&lb->fields[sp->fldno]);
+ else if (replace)
+ fputs(replace, stdout);
+ } else if (sp->fileno == 0) {
+ prfield(joinfield);
+ }
+
+ if (i < output.ns - 1)
+ prsep();
+ }
+ } else {
+ prfield(joinfield);
+ prsep();
+
+ for (i = 0; i < la->nf; i++) {
+ if (i != jfa) {
+ prfield(&la->fields[i]);
+ prsep();
+ }
+ }
+ for (i = 0; i < lb->nf; i++) {
+ if (i != jfb) {
+ prfield(&lb->fields[i]);
+ if (i < lb->nf - 1)
+ prsep();
+ }
+ }
+ }
+ putchar('\n');
+}
+
+static void
+prline(struct jline *lp)
+{
+ if (fwrite(lp->text.data, 1, lp->text.len, stdout) != lp->text.len)
+ eprintf("fwrite:");
+ putchar('\n');
+}
+
+static int
+jlinecmp(struct jline *la, struct jline *lb, size_t jfa, size_t jfb)
+{
+ int status;
+
+ /* return FIELD_ERROR if both lines are short */
+ if (jfa >= la->nf) {
+ status = (jfb >= lb->nf) ? FIELD_ERROR : -1;
+ } else if (jfb >= lb->nf) {
+ status = 1;
+ } else {
+ status = memcmp(la->fields[jfa].s, lb->fields[jfb].s,
+ MAX(la->fields[jfa].len, lb->fields[jfb].len));
+ LIMIT(status, -1, 1);
+ }
+
+ return status;
+}
+
+static void
+addfield(struct jline *lp, char *sp, size_t len)
+{
+ if (lp->nf >= lp->maxf) {
+ lp->fields = ereallocarray(lp->fields, (GROW * lp->maxf),
+ sizeof(struct field));
+ lp->maxf *= GROW;
+ }
+ lp->fields[lp->nf].s = sp;
+ lp->fields[lp->nf].len = len;
+ lp->nf++;
+}
+
+static void
+prspanjoin(struct span *spa, struct span *spb, size_t jfa, size_t jfb)
+{
+ size_t i, j;
+
+ for (i = 0; i < (spa->nl - 1); i++)
+ for (j = 0; j < (spb->nl - 1); j++)
+ prjoin(spa->lines[i], spb->lines[j], jfa, jfb);
+}
+
+static struct jline *
+makeline(char *s, size_t len)
+{
+ struct jline *lp;
+ char *tmp;
+ size_t i, end;
+
+ if (s[len - 1] == '\n')
+ s[--len] = '\0';
+
+ lp = ereallocarray(NULL, INIT, sizeof(struct jline));
+ lp->text.data = s;
+ lp->text.len = len;
+ lp->fields = ereallocarray(NULL, INIT, sizeof(struct field));
+ lp->nf = 0;
+ lp->maxf = INIT;
+
+ for (i = 0; i < lp->text.len && isblank(lp->text.data[i]); i++)
+ ;
+ while (i < lp->text.len) {
+ if (sep) {
+ if ((lp->text.len - i) < seplen ||
+ !(tmp = memmem(lp->text.data + i,
+ lp->text.len - i, sep, seplen))) {
+ goto eol;
+ }
+ end = tmp - lp->text.data;
+ addfield(lp, lp->text.data + i, end - i);
+ i = end + seplen;
+ } else {
+ for (end = i; !(isblank(lp->text.data[end])); end++) {
+ if (end + 1 == lp->text.len)
+ goto eol;
+ }
+ addfield(lp, lp->text.data + i, end - i);
+ for (i = end; isblank(lp->text.data[i]); i++)
+ ;
+ }
+ }
+eol:
+ addfield(lp, lp->text.data + i, lp->text.len - i);
+
+ return lp;
+}
+
+static int
+addtospan(struct span *sp, FILE *fp, int reset)
+{
+ char *newl = NULL;
+ ssize_t len;
+ size_t size = 0;
+
+ if ((len = getline(&newl, &size, fp)) < 0) {
+ if (ferror(fp))
+ eprintf("getline:");
+ else
+ return 0;
+ }
+
+ if (reset)
+ sp->nl = 0;
+
+ if (sp->nl >= sp->maxl) {
+ sp->lines = ereallocarray(sp->lines, (GROW * sp->maxl),
+ sizeof(struct jline *));
+ sp->maxl *= GROW;
+ }
+
+ sp->lines[sp->nl] = makeline(newl, len);
+ sp->nl++;
+ return 1;
+}
+
+static void
+initspan(struct span *sp)
+{
+ sp->nl = 0;
+ sp->maxl = INIT;
+ sp->lines = ereallocarray(NULL, INIT, sizeof(struct jline *));
+}
+
+static void
+freespan(struct span *sp)
+{
+ size_t i;
+
+ for (i = 0; i < sp->nl; i++) {
+ free(sp->lines[i]->fields);
+ free(sp->lines[i]->text.data);
+ }
+ free(sp->lines);
+}
+
+static void
+initolist(struct outlist *olp)
+{
+ olp->ns = 0;
+ olp->maxs = 1;
+ olp->specs = ereallocarray(NULL, INIT, sizeof(struct spec *));
+}
+
+static void
+addspec(struct outlist *olp, struct spec *sp)
+{
+ if (olp->ns >= olp->maxs) {
+ olp->specs = ereallocarray(olp->specs, (GROW * olp->maxs),
+ sizeof(struct spec *));
+ olp->maxs *= GROW;
+ }
+ olp->specs[olp->ns] = sp;
+ olp->ns++;
+}
+
+static struct spec *
+makespec(char *s)
+{
+ struct spec *sp;
+ int fileno;
+ size_t fldno;
+
+ if (!strcmp(s, "0")) { /* join field must be 0 and nothing else */
+ fileno = 0;
+ fldno = 0;
+ } else if ((s[0] == '1' || s[0] == '2') && s[1] == '.') {
+ fileno = s[0] - '0';
+ fldno = estrtonum(&s[2], 1, MIN(LLONG_MAX, SIZE_MAX)) - 1;
+ } else {
+ eprintf("%s: invalid format\n", s);
+ }
+
+ sp = ereallocarray(NULL, INIT, sizeof(struct spec));
+ sp->fileno = fileno;
+ sp->fldno = fldno;
+ return sp;
+}
+
+static void
+makeolist(struct outlist *olp, char *s)
+{
+ char *item, *sp;
+ sp = s;
+
+ while (sp) {
+ item = sp;
+ sp = strpbrk(sp, ", \t");
+ if (sp)
+ *sp++ = '\0';
+ addspec(olp, makespec(item));
+ }
+}
+
+static void
+freespecs(struct outlist *olp)
+{
+ size_t i;
+
+ for (i = 0; i < olp->ns; i++)
+ free(olp->specs[i]);
+}
+
+static void
+join(FILE *fa, FILE *fb, size_t jfa, size_t jfb)
+{
+ struct span spa, spb;
+ int cmp, eofa, eofb;
+
+ initspan(&spa);
+ initspan(&spb);
+ cmp = eofa = eofb = 0;
+
+ addtospan(&spa, fa, RESET);
+ addtospan(&spb, fb, RESET);
+
+ while (spa.nl && spb.nl) {
+ if ((cmp = jlinecmp(spa.lines[0], spb.lines[0], jfa, jfb)) < 0) {
+ if (unpairsa)
+ prline(spa.lines[0]);
+ if (!addtospan(&spa, fa, RESET)) {
+ if (unpairsb) { /* a is EOF'd; print the rest of b */
+ do
+ prline(spb.lines[0]);
+ while (addtospan(&spb, fb, RESET));
+ }
+ eofa = eofb = 1;
+ } else {
+ continue;
+ }
+ } else if (cmp > 0) {
+ if (unpairsb)
+ prline(spb.lines[0]);
+ if (!addtospan(&spb, fb, RESET)) {
+ if (unpairsa) { /* b is EOF'd; print the rest of a */
+ do
+ prline(spa.lines[0]);
+ while (addtospan(&spa, fa, RESET));
+ }
+ eofa = eofb = 1;
+ } else {
+ continue;
+ }
+ } else if (cmp == 0) {
+ /* read all consecutive matching lines from a */
+ do {
+ if (!addtospan(&spa, fa, EXPAND)) {
+ eofa = 1;
+ spa.nl++;
+ break;
+ }
+ } while (jlinecmp(spa.lines[spa.nl-1], spb.lines[0], jfa, jfb) == 0);
+
+ /* read all consecutive matching lines from b */
+ do {
+ if (!addtospan(&spb, fb, EXPAND)) {
+ eofb = 1;
+ spb.nl++;
+ break;
+ }
+ } while (jlinecmp(spa.lines[0], spb.lines[spb.nl-1], jfa, jfb) == 0);
+
+ if (pairs)
+ prspanjoin(&spa, &spb, jfa, jfb);
+
+ } else { /* FIELD_ERROR: both lines lacked join fields */
+ if (unpairsa)
+ prline(spa.lines[0]);
+ if (unpairsb)
+ prline(spb.lines[0]);
+ eofa = addtospan(&spa, fa, RESET) ? 0 : 1;
+ eofb = addtospan(&spb, fb, RESET) ? 0 : 1;
+ if (!eofa && !eofb)
+ continue;
+ }
+
+ if (eofa) {
+ spa.nl = 0;
+ } else {
+ swaplines(spa.lines[0], spa.lines[spa.nl - 1]); /* ugly */
+ spa.nl = 1;
+ }
+
+ if (eofb) {
+ spb.nl = 0;
+ } else {
+ swaplines(spb.lines[0], spb.lines[spb.nl - 1]); /* ugly */
+ spb.nl = 1;
+ }
+ }
+ freespan(&spa);
+ freespan(&spb);
+}
+
+
+int
+main(int argc, char *argv[])
+{
+ size_t jf[2] = { jfield, jfield, };
+ FILE *fp[2];
+ int ret = 0, n;
+ char *fno;
+
+ ARGBEGIN {
+ case '1':
+ jf[0] = estrtonum(EARGF(usage()), 1, MIN(LLONG_MAX, SIZE_MAX));
+ break;
+ case '2':
+ jf[1] = estrtonum(EARGF(usage()), 1, MIN(LLONG_MAX, SIZE_MAX));
+ break;
+ case 'a':
+ fno = EARGF(usage());
+ if (strcmp(fno, "1") == 0)
+ unpairsa = 1;
+ else if (strcmp(fno, "2") == 0)
+ unpairsb = 1;
+ else
+ usage();
+ break;
+ case 'e':
+ replace = EARGF(usage());
+ break;
+ case 'o':
+ oflag = 1;
+ initolist(&output);
+ makeolist(&output, EARGF(usage()));
+ break;
+ case 't':
+ sep = EARGF(usage());
+ break;
+ case 'v':
+ pairs = 0;
+ fno = EARGF(usage());
+ if (strcmp(fno, "1") == 0)
+ unpairsa = 1;
+ else if (strcmp(fno, "2") == 0)
+ unpairsb = 1;
+ else
+ usage();
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (sep)
+ seplen = unescape(sep);
+
+ if (argc != 2)
+ usage();
+
+ for (n = 0; n < 2; n++) {
+ if (!strcmp(argv[n], "-")) {
+ argv[n] = "<stdin>";
+ fp[n] = stdin;
+ } else if (!(fp[n] = fopen(argv[n], "r"))) {
+ eprintf("fopen %s:", argv[n]);
+ }
+ }
+
+ jf[0]--;
+ jf[1]--;
+
+ join(fp[0], fp[1], jf[0], jf[1]);
+
+ if (oflag)
+ freespecs(&output);
+
+ if (fshut(fp[0], argv[0]) | (fp[0] != fp[1] && fshut(fp[1], argv[1])) |
+ fshut(stdout, "<stdout>"))
+ ret = 2;
+
+ return ret;
+}
diff --git a/util/sbase/kill.1 b/util/sbase/kill.1
new file mode 100644
index 00000000..37ec4dac
--- /dev/null
+++ b/util/sbase/kill.1
@@ -0,0 +1,39 @@
+.Dd October 8, 2015
+.Dt KILL 1
+.Os sbase
+.Sh NAME
+.Nm kill
+.Nd signal processes
+.Sh SYNOPSIS
+.Nm
+.Op Fl s Ar signame | Fl num | Fl signame
+.Ar pid ...
+.Nm
+.Fl l Op Ar num
+.Sh DESCRIPTION
+.Nm
+signals TERM to each process or process group specified by
+.Ar pid .
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl l Op Ar num
+List all available signals or the signal name of
+.Ar num .
+.It Fl s Ar signame | Fl num | Fl signame
+Send signal corresponding to
+.Ar signame
+|
+.Ar num .
+The default is TERM.
+.El
+.Sh SEE ALSO
+.Xr kill 2 ,
+.Xr signal 7
+.Sh STANDARDS
+POSIX.1-2013.
+.Pp
+The
+.Fl Ar signame
+and
+.Fl Ar num
+syntax is marked by POSIX.1-2013 as an X/OPEN System Interfaces option.
diff --git a/util/sbase/kill.c b/util/sbase/kill.c
new file mode 100644
index 00000000..b09f55cb
--- /dev/null
+++ b/util/sbase/kill.c
@@ -0,0 +1,131 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/wait.h>
+
+#include <ctype.h>
+#include <limits.h>
+#include <signal.h>
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>
+
+#include "util.h"
+
+struct {
+ const char *name;
+ const int sig;
+} sigs[] = {
+ { "0", 0 },
+#define SIG(n) { #n, SIG##n }
+ SIG(ABRT), SIG(ALRM), SIG(BUS), SIG(CHLD), SIG(CONT), SIG(FPE), SIG(HUP),
+ SIG(ILL), SIG(INT), SIG(KILL), SIG(PIPE), SIG(QUIT), SIG(SEGV), SIG(STOP),
+ SIG(TERM), SIG(TRAP), SIG(TSTP), SIG(TTIN), SIG(TTOU), SIG(USR1), SIG(USR2),
+ SIG(URG),
+#undef SIG
+};
+
+const char *
+sig2name(const int sig)
+{
+ size_t i;
+
+ for (i = 0; i < LEN(sigs); i++)
+ if (sigs[i].sig == sig)
+ return sigs[i].name;
+ eprintf("%d: bad signal number\n", sig);
+
+ return NULL; /* not reached */
+}
+
+int
+name2sig(const char *name)
+{
+ size_t i;
+
+ for (i = 0; i < LEN(sigs); i++)
+ if (!strcasecmp(sigs[i].name, name))
+ return sigs[i].sig;
+ eprintf("%s: bad signal name\n", name);
+
+ return -1; /* not reached */
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-s signame | -num | -signame] pid ...\n"
+ " %s -l [num]\n", argv0, argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ pid_t pid;
+ size_t i;
+ int ret = 0, sig = SIGTERM;
+
+ argv0 = *argv, argv0 ? (argc--, argv++) : (void *)0;
+
+ if (!argc)
+ usage();
+
+ if ((*argv)[0] == '-') {
+ switch ((*argv)[1]) {
+ case 'l':
+ if ((*argv)[2])
+ goto longopt;
+ argc--, argv++;
+ if (!argc) {
+ for (i = 0; i < LEN(sigs); i++)
+ puts(sigs[i].name);
+ } else if (argc == 1) {
+ sig = estrtonum(*argv, 0, INT_MAX);
+ if (sig > 128)
+ sig = WTERMSIG(sig);
+ puts(sig2name(sig));
+ } else {
+ usage();
+ }
+ return fshut(stdout, "<stdout>");
+ case 's':
+ if ((*argv)[2])
+ goto longopt;
+ argc--, argv++;
+ if (!argc)
+ usage();
+ sig = name2sig(*argv);
+ argc--, argv++;
+ break;
+ case '-':
+ if ((*argv)[2])
+ goto longopt;
+ argc--, argv++;
+ break;
+ default:
+ longopt:
+ /* XSI-extensions -argnum and -argname*/
+ if (isdigit((*argv)[1])) {
+ sig = estrtonum((*argv) + 1, 0, INT_MAX);
+ sig2name(sig);
+ } else {
+ sig = name2sig((*argv) + 1);
+ }
+ argc--, argv++;
+ }
+ }
+
+ if (argc && !strcmp(*argv, "--"))
+ argc--, argv++;
+
+ if (!argc)
+ usage();
+
+ for (; *argv; argc--, argv++) {
+ pid = estrtonum(*argv, INT_MIN, INT_MAX);
+ if (kill(pid, sig) < 0) {
+ weprintf("kill %d:", pid);
+ ret = 1;
+ }
+ }
+
+ return ret;
+}
diff --git a/util/sbase/libutf/Makefile b/util/sbase/libutf/Makefile
new file mode 100644
index 00000000..aac2d2e6
--- /dev/null
+++ b/util/sbase/libutf/Makefile
@@ -0,0 +1,6 @@
+AWK = awk
+UNICODE = http://unicode.org/Public/UCD/latest/ucd/UnicodeData.txt
+
+default:
+ @echo Downloading and parsing $(UNICODE)
+ @curl -\# $(UNICODE) | $(AWK) -f mkrunetype.awk
diff --git a/util/sbase/libutf/fgetrune.c b/util/sbase/libutf/fgetrune.c
new file mode 100644
index 00000000..8cd78c64
--- /dev/null
+++ b/util/sbase/libutf/fgetrune.c
@@ -0,0 +1,36 @@
+/* See LICENSE file for copyright and license details. */
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "../utf.h"
+
+int
+fgetrune(Rune *r, FILE *fp)
+{
+ char buf[UTFmax];
+ int i = 0, c;
+
+ while (i < UTFmax && (c = fgetc(fp)) != EOF) {
+ buf[i++] = c;
+ if (charntorune(r, buf, i) > 0)
+ break;
+ }
+ if (ferror(fp))
+ return -1;
+
+ return i;
+}
+
+int
+efgetrune(Rune *r, FILE *fp, const char *file)
+{
+ int ret;
+
+ if ((ret = fgetrune(r, fp)) < 0) {
+ fprintf(stderr, "fgetrune %s: %s\n", file, strerror(errno));
+ exit(1);
+ }
+ return ret;
+}
diff --git a/util/sbase/libutf/fputrune.c b/util/sbase/libutf/fputrune.c
new file mode 100644
index 00000000..6a393b5a
--- /dev/null
+++ b/util/sbase/libutf/fputrune.c
@@ -0,0 +1,27 @@
+/* See LICENSE file for copyright and license details. */
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "../utf.h"
+
+int
+fputrune(const Rune *r, FILE *fp)
+{
+ char buf[UTFmax];
+
+ return fwrite(buf, runetochar(buf, r), 1, fp);
+}
+
+int
+efputrune(const Rune *r, FILE *fp, const char *file)
+{
+ int ret;
+
+ if ((ret = fputrune(r, fp)) < 0) {
+ fprintf(stderr, "fputrune %s: %s\n", file, strerror(errno));
+ exit(1);
+ }
+ return ret;
+}
diff --git a/util/sbase/libutf/isalnumrune.c b/util/sbase/libutf/isalnumrune.c
new file mode 100644
index 00000000..e4720d00
--- /dev/null
+++ b/util/sbase/libutf/isalnumrune.c
@@ -0,0 +1,9 @@
+/* Automatically generated by mkrunetype.awk */
+#include "../utf.h"
+#include "runetype.h"
+
+int
+isalnumrune(Rune r)
+{
+ return isalpharune(r) || isdigitrune(r);
+}
diff --git a/util/sbase/libutf/isalpharune.c b/util/sbase/libutf/isalpharune.c
new file mode 100644
index 00000000..9d1faffe
--- /dev/null
+++ b/util/sbase/libutf/isalpharune.c
@@ -0,0 +1,830 @@
+/* Automatically generated by mkrunetype.awk */
+#include <stdlib.h>
+
+#include "../utf.h"
+#include "runetype.h"
+
+static const Rune alpha3[][2] = {
+ { 0x00D6, 0x00D8 },
+ { 0x00F6, 0x00F8 },
+ { 0x02EC, 0x02EE },
+ { 0x0374, 0x0376 },
+ { 0x037D, 0x037F },
+ { 0x0386, 0x0388 },
+ { 0x038A, 0x038E },
+ { 0x03A1, 0x03A3 },
+ { 0x03F5, 0x03F7 },
+ { 0x052F, 0x0531 },
+ { 0x066F, 0x0671 },
+ { 0x06D3, 0x06D5 },
+ { 0x0710, 0x0712 },
+ { 0x0887, 0x0889 },
+ { 0x09A8, 0x09AA },
+ { 0x09B0, 0x09B2 },
+ { 0x09DD, 0x09DF },
+ { 0x0A28, 0x0A2A },
+ { 0x0A30, 0x0A32 },
+ { 0x0A33, 0x0A35 },
+ { 0x0A36, 0x0A38 },
+ { 0x0A5C, 0x0A5E },
+ { 0x0A8D, 0x0A8F },
+ { 0x0A91, 0x0A93 },
+ { 0x0AA8, 0x0AAA },
+ { 0x0AB0, 0x0AB2 },
+ { 0x0AB3, 0x0AB5 },
+ { 0x0B28, 0x0B2A },
+ { 0x0B30, 0x0B32 },
+ { 0x0B33, 0x0B35 },
+ { 0x0B5D, 0x0B5F },
+ { 0x0B83, 0x0B85 },
+ { 0x0B90, 0x0B92 },
+ { 0x0B9A, 0x0B9E },
+ { 0x0C0C, 0x0C0E },
+ { 0x0C10, 0x0C12 },
+ { 0x0C28, 0x0C2A },
+ { 0x0C8C, 0x0C8E },
+ { 0x0C90, 0x0C92 },
+ { 0x0CA8, 0x0CAA },
+ { 0x0CB3, 0x0CB5 },
+ { 0x0CDE, 0x0CE0 },
+ { 0x0D0C, 0x0D0E },
+ { 0x0D10, 0x0D12 },
+ { 0x0DB1, 0x0DB3 },
+ { 0x0DBB, 0x0DBD },
+ { 0x0E30, 0x0E32 },
+ { 0x0E82, 0x0E86 },
+ { 0x0E8A, 0x0E8C },
+ { 0x0EA3, 0x0EA7 },
+ { 0x0EB0, 0x0EB2 },
+ { 0x0EC4, 0x0EC6 },
+ { 0x0F47, 0x0F49 },
+ { 0x10C5, 0x10C7 },
+ { 0x10FA, 0x10FC },
+ { 0x1248, 0x124A },
+ { 0x1256, 0x125A },
+ { 0x1288, 0x128A },
+ { 0x12B0, 0x12B2 },
+ { 0x12BE, 0x12C2 },
+ { 0x12D6, 0x12D8 },
+ { 0x1310, 0x1312 },
+ { 0x167F, 0x1681 },
+ { 0x176C, 0x176E },
+ { 0x18A8, 0x18AA },
+ { 0x1CEC, 0x1CEE },
+ { 0x1CF3, 0x1CF5 },
+ { 0x1F57, 0x1F5F },
+ { 0x1FB4, 0x1FB6 },
+ { 0x1FBC, 0x1FBE },
+ { 0x1FC4, 0x1FC6 },
+ { 0x1FF4, 0x1FF6 },
+ { 0x2113, 0x2115 },
+ { 0x2124, 0x212A },
+ { 0x212D, 0x212F },
+ { 0x2D25, 0x2D27 },
+ { 0x2DA6, 0x2DA8 },
+ { 0x2DAE, 0x2DB0 },
+ { 0x2DB6, 0x2DB8 },
+ { 0x2DBE, 0x2DC0 },
+ { 0x2DC6, 0x2DC8 },
+ { 0x2DCE, 0x2DD0 },
+ { 0x2DD6, 0x2DD8 },
+ { 0x309F, 0x30A1 },
+ { 0x30FA, 0x30FC },
+ { 0x312F, 0x3131 },
+ { 0xA7D1, 0xA7D5 },
+ { 0xA801, 0xA803 },
+ { 0xA805, 0xA807 },
+ { 0xA80A, 0xA80C },
+ { 0xA8FB, 0xA8FD },
+ { 0xA9E4, 0xA9E6 },
+ { 0xA9FE, 0xAA00 },
+ { 0xAA42, 0xAA44 },
+ { 0xAAAF, 0xAAB1 },
+ { 0xAAC0, 0xAAC2 },
+ { 0xAB26, 0xAB28 },
+ { 0xAB2E, 0xAB30 },
+ { 0xAB5A, 0xAB5C },
+ { 0xFB1D, 0xFB1F },
+ { 0xFB28, 0xFB2A },
+ { 0xFB36, 0xFB38 },
+ { 0xFB3C, 0xFB40 },
+ { 0xFB41, 0xFB43 },
+ { 0xFB44, 0xFB46 },
+ { 0xFE74, 0xFE76 },
+ { 0x1000B, 0x1000D },
+ { 0x10026, 0x10028 },
+ { 0x1003A, 0x1003C },
+ { 0x1003D, 0x1003F },
+ { 0x10340, 0x10342 },
+ { 0x1057A, 0x1057C },
+ { 0x1058A, 0x1058C },
+ { 0x10592, 0x10594 },
+ { 0x10595, 0x10597 },
+ { 0x105A1, 0x105A3 },
+ { 0x105B1, 0x105B3 },
+ { 0x105B9, 0x105BB },
+ { 0x10785, 0x10787 },
+ { 0x107B0, 0x107B2 },
+ { 0x10808, 0x1080A },
+ { 0x10835, 0x10837 },
+ { 0x108F2, 0x108F4 },
+ { 0x10A13, 0x10A15 },
+ { 0x10A17, 0x10A19 },
+ { 0x10AC7, 0x10AC9 },
+ { 0x111DA, 0x111DC },
+ { 0x11211, 0x11213 },
+ { 0x11286, 0x1128A },
+ { 0x1128D, 0x1128F },
+ { 0x1129D, 0x1129F },
+ { 0x11328, 0x1132A },
+ { 0x11330, 0x11332 },
+ { 0x11333, 0x11335 },
+ { 0x114C5, 0x114C7 },
+ { 0x11913, 0x11915 },
+ { 0x11916, 0x11918 },
+ { 0x1193F, 0x11941 },
+ { 0x119E1, 0x119E3 },
+ { 0x11C08, 0x11C0A },
+ { 0x11D06, 0x11D08 },
+ { 0x11D09, 0x11D0B },
+ { 0x11D65, 0x11D67 },
+ { 0x11D68, 0x11D6A },
+ { 0x11F02, 0x11F04 },
+ { 0x11F10, 0x11F12 },
+ { 0x16FE1, 0x16FE3 },
+ { 0x1AFF3, 0x1AFF5 },
+ { 0x1AFFB, 0x1AFFD },
+ { 0x1AFFE, 0x1B000 },
+ { 0x1D454, 0x1D456 },
+ { 0x1D49C, 0x1D49E },
+ { 0x1D4AC, 0x1D4AE },
+ { 0x1D4B9, 0x1D4BD },
+ { 0x1D4C3, 0x1D4C5 },
+ { 0x1D505, 0x1D507 },
+ { 0x1D514, 0x1D516 },
+ { 0x1D51C, 0x1D51E },
+ { 0x1D539, 0x1D53B },
+ { 0x1D53E, 0x1D540 },
+ { 0x1D544, 0x1D546 },
+ { 0x1D550, 0x1D552 },
+ { 0x1D6C0, 0x1D6C2 },
+ { 0x1D6DA, 0x1D6DC },
+ { 0x1D6FA, 0x1D6FC },
+ { 0x1D714, 0x1D716 },
+ { 0x1D734, 0x1D736 },
+ { 0x1D74E, 0x1D750 },
+ { 0x1D76E, 0x1D770 },
+ { 0x1D788, 0x1D78A },
+ { 0x1D7A8, 0x1D7AA },
+ { 0x1D7C2, 0x1D7C4 },
+ { 0x1E7E6, 0x1E7E8 },
+ { 0x1E7EB, 0x1E7ED },
+ { 0x1E7EE, 0x1E7F0 },
+ { 0x1E7FE, 0x1E800 },
+ { 0x1EE03, 0x1EE05 },
+ { 0x1EE1F, 0x1EE21 },
+ { 0x1EE22, 0x1EE24 },
+ { 0x1EE27, 0x1EE29 },
+ { 0x1EE32, 0x1EE34 },
+ { 0x1EE37, 0x1EE3B },
+ { 0x1EE47, 0x1EE4D },
+ { 0x1EE4F, 0x1EE51 },
+ { 0x1EE52, 0x1EE54 },
+ { 0x1EE57, 0x1EE61 },
+ { 0x1EE62, 0x1EE64 },
+ { 0x1EE6A, 0x1EE6C },
+ { 0x1EE72, 0x1EE74 },
+ { 0x1EE77, 0x1EE79 },
+ { 0x1EE7C, 0x1EE80 },
+ { 0x1EE89, 0x1EE8B },
+ { 0x1EEA3, 0x1EEA5 },
+ { 0x1EEA9, 0x1EEAB },
+};
+
+static const Rune alpha2[][2] = {
+ { 0x0041, 0x005A },
+ { 0x0061, 0x007A },
+ { 0x00C0, 0x00D6 },
+ { 0x00D8, 0x00F6 },
+ { 0x00F8, 0x02C1 },
+ { 0x02C6, 0x02D1 },
+ { 0x02E0, 0x02E4 },
+ { 0x0370, 0x0374 },
+ { 0x0376, 0x0377 },
+ { 0x037A, 0x037D },
+ { 0x0388, 0x038A },
+ { 0x038E, 0x03A1 },
+ { 0x03A3, 0x03F5 },
+ { 0x03F7, 0x0481 },
+ { 0x048A, 0x052F },
+ { 0x0531, 0x0556 },
+ { 0x0560, 0x0588 },
+ { 0x05D0, 0x05EA },
+ { 0x05EF, 0x05F2 },
+ { 0x0620, 0x064A },
+ { 0x066E, 0x066F },
+ { 0x0671, 0x06D3 },
+ { 0x06E5, 0x06E6 },
+ { 0x06EE, 0x06EF },
+ { 0x06FA, 0x06FC },
+ { 0x0712, 0x072F },
+ { 0x074D, 0x07A5 },
+ { 0x07CA, 0x07EA },
+ { 0x07F4, 0x07F5 },
+ { 0x0800, 0x0815 },
+ { 0x0840, 0x0858 },
+ { 0x0860, 0x086A },
+ { 0x0870, 0x0887 },
+ { 0x0889, 0x088E },
+ { 0x08A0, 0x08C9 },
+ { 0x0904, 0x0939 },
+ { 0x0958, 0x0961 },
+ { 0x0971, 0x0980 },
+ { 0x0985, 0x098C },
+ { 0x098F, 0x0990 },
+ { 0x0993, 0x09A8 },
+ { 0x09AA, 0x09B0 },
+ { 0x09B6, 0x09B9 },
+ { 0x09DC, 0x09DD },
+ { 0x09DF, 0x09E1 },
+ { 0x09F0, 0x09F1 },
+ { 0x0A05, 0x0A0A },
+ { 0x0A0F, 0x0A10 },
+ { 0x0A13, 0x0A28 },
+ { 0x0A2A, 0x0A30 },
+ { 0x0A32, 0x0A33 },
+ { 0x0A35, 0x0A36 },
+ { 0x0A38, 0x0A39 },
+ { 0x0A59, 0x0A5C },
+ { 0x0A72, 0x0A74 },
+ { 0x0A85, 0x0A8D },
+ { 0x0A8F, 0x0A91 },
+ { 0x0A93, 0x0AA8 },
+ { 0x0AAA, 0x0AB0 },
+ { 0x0AB2, 0x0AB3 },
+ { 0x0AB5, 0x0AB9 },
+ { 0x0AE0, 0x0AE1 },
+ { 0x0B05, 0x0B0C },
+ { 0x0B0F, 0x0B10 },
+ { 0x0B13, 0x0B28 },
+ { 0x0B2A, 0x0B30 },
+ { 0x0B32, 0x0B33 },
+ { 0x0B35, 0x0B39 },
+ { 0x0B5C, 0x0B5D },
+ { 0x0B5F, 0x0B61 },
+ { 0x0B85, 0x0B8A },
+ { 0x0B8E, 0x0B90 },
+ { 0x0B92, 0x0B95 },
+ { 0x0B99, 0x0B9A },
+ { 0x0B9E, 0x0B9F },
+ { 0x0BA3, 0x0BA4 },
+ { 0x0BA8, 0x0BAA },
+ { 0x0BAE, 0x0BB9 },
+ { 0x0C05, 0x0C0C },
+ { 0x0C0E, 0x0C10 },
+ { 0x0C12, 0x0C28 },
+ { 0x0C2A, 0x0C39 },
+ { 0x0C58, 0x0C5A },
+ { 0x0C60, 0x0C61 },
+ { 0x0C85, 0x0C8C },
+ { 0x0C8E, 0x0C90 },
+ { 0x0C92, 0x0CA8 },
+ { 0x0CAA, 0x0CB3 },
+ { 0x0CB5, 0x0CB9 },
+ { 0x0CDD, 0x0CDE },
+ { 0x0CE0, 0x0CE1 },
+ { 0x0CF1, 0x0CF2 },
+ { 0x0D04, 0x0D0C },
+ { 0x0D0E, 0x0D10 },
+ { 0x0D12, 0x0D3A },
+ { 0x0D54, 0x0D56 },
+ { 0x0D5F, 0x0D61 },
+ { 0x0D7A, 0x0D7F },
+ { 0x0D85, 0x0D96 },
+ { 0x0D9A, 0x0DB1 },
+ { 0x0DB3, 0x0DBB },
+ { 0x0DC0, 0x0DC6 },
+ { 0x0E01, 0x0E30 },
+ { 0x0E32, 0x0E33 },
+ { 0x0E40, 0x0E46 },
+ { 0x0E81, 0x0E82 },
+ { 0x0E86, 0x0E8A },
+ { 0x0E8C, 0x0EA3 },
+ { 0x0EA7, 0x0EB0 },
+ { 0x0EB2, 0x0EB3 },
+ { 0x0EC0, 0x0EC4 },
+ { 0x0EDC, 0x0EDF },
+ { 0x0F40, 0x0F47 },
+ { 0x0F49, 0x0F6C },
+ { 0x0F88, 0x0F8C },
+ { 0x1000, 0x102A },
+ { 0x1050, 0x1055 },
+ { 0x105A, 0x105D },
+ { 0x1065, 0x1066 },
+ { 0x106E, 0x1070 },
+ { 0x1075, 0x1081 },
+ { 0x10A0, 0x10C5 },
+ { 0x10D0, 0x10FA },
+ { 0x10FC, 0x1248 },
+ { 0x124A, 0x124D },
+ { 0x1250, 0x1256 },
+ { 0x125A, 0x125D },
+ { 0x1260, 0x1288 },
+ { 0x128A, 0x128D },
+ { 0x1290, 0x12B0 },
+ { 0x12B2, 0x12B5 },
+ { 0x12B8, 0x12BE },
+ { 0x12C2, 0x12C5 },
+ { 0x12C8, 0x12D6 },
+ { 0x12D8, 0x1310 },
+ { 0x1312, 0x1315 },
+ { 0x1318, 0x135A },
+ { 0x1380, 0x138F },
+ { 0x13A0, 0x13F5 },
+ { 0x13F8, 0x13FD },
+ { 0x1401, 0x166C },
+ { 0x166F, 0x167F },
+ { 0x1681, 0x169A },
+ { 0x16A0, 0x16EA },
+ { 0x16F1, 0x16F8 },
+ { 0x1700, 0x1711 },
+ { 0x171F, 0x1731 },
+ { 0x1740, 0x1751 },
+ { 0x1760, 0x176C },
+ { 0x176E, 0x1770 },
+ { 0x1780, 0x17B3 },
+ { 0x1820, 0x1878 },
+ { 0x1880, 0x1884 },
+ { 0x1887, 0x18A8 },
+ { 0x18B0, 0x18F5 },
+ { 0x1900, 0x191E },
+ { 0x1950, 0x196D },
+ { 0x1970, 0x1974 },
+ { 0x1980, 0x19AB },
+ { 0x19B0, 0x19C9 },
+ { 0x1A00, 0x1A16 },
+ { 0x1A20, 0x1A54 },
+ { 0x1B05, 0x1B33 },
+ { 0x1B45, 0x1B4C },
+ { 0x1B83, 0x1BA0 },
+ { 0x1BAE, 0x1BAF },
+ { 0x1BBA, 0x1BE5 },
+ { 0x1C00, 0x1C23 },
+ { 0x1C4D, 0x1C4F },
+ { 0x1C5A, 0x1C7D },
+ { 0x1C80, 0x1C88 },
+ { 0x1C90, 0x1CBA },
+ { 0x1CBD, 0x1CBF },
+ { 0x1CE9, 0x1CEC },
+ { 0x1CEE, 0x1CF3 },
+ { 0x1CF5, 0x1CF6 },
+ { 0x1D00, 0x1DBF },
+ { 0x1E00, 0x1F15 },
+ { 0x1F18, 0x1F1D },
+ { 0x1F20, 0x1F45 },
+ { 0x1F48, 0x1F4D },
+ { 0x1F50, 0x1F57 },
+ { 0x1F5F, 0x1F7D },
+ { 0x1F80, 0x1FB4 },
+ { 0x1FB6, 0x1FBC },
+ { 0x1FC2, 0x1FC4 },
+ { 0x1FC6, 0x1FCC },
+ { 0x1FD0, 0x1FD3 },
+ { 0x1FD6, 0x1FDB },
+ { 0x1FE0, 0x1FEC },
+ { 0x1FF2, 0x1FF4 },
+ { 0x1FF6, 0x1FFC },
+ { 0x2090, 0x209C },
+ { 0x210A, 0x2113 },
+ { 0x2119, 0x211D },
+ { 0x212A, 0x212D },
+ { 0x212F, 0x2139 },
+ { 0x213C, 0x213F },
+ { 0x2145, 0x2149 },
+ { 0x2183, 0x2184 },
+ { 0x2C00, 0x2CE4 },
+ { 0x2CEB, 0x2CEE },
+ { 0x2CF2, 0x2CF3 },
+ { 0x2D00, 0x2D25 },
+ { 0x2D30, 0x2D67 },
+ { 0x2D80, 0x2D96 },
+ { 0x2DA0, 0x2DA6 },
+ { 0x2DA8, 0x2DAE },
+ { 0x2DB0, 0x2DB6 },
+ { 0x2DB8, 0x2DBE },
+ { 0x2DC0, 0x2DC6 },
+ { 0x2DC8, 0x2DCE },
+ { 0x2DD0, 0x2DD6 },
+ { 0x2DD8, 0x2DDE },
+ { 0x3005, 0x3006 },
+ { 0x3031, 0x3035 },
+ { 0x303B, 0x303C },
+ { 0x3041, 0x3096 },
+ { 0x309D, 0x309F },
+ { 0x30A1, 0x30FA },
+ { 0x30FC, 0x30FF },
+ { 0x3105, 0x312F },
+ { 0x3131, 0x318E },
+ { 0x31A0, 0x31BF },
+ { 0x31F0, 0x31FF },
+ { 0x9FFF, 0xA48C },
+ { 0xA4D0, 0xA4FD },
+ { 0xA500, 0xA60C },
+ { 0xA610, 0xA61F },
+ { 0xA62A, 0xA62B },
+ { 0xA640, 0xA66E },
+ { 0xA67F, 0xA69D },
+ { 0xA6A0, 0xA6E5 },
+ { 0xA717, 0xA71F },
+ { 0xA722, 0xA788 },
+ { 0xA78B, 0xA7CA },
+ { 0xA7D0, 0xA7D1 },
+ { 0xA7D5, 0xA7D9 },
+ { 0xA7F2, 0xA801 },
+ { 0xA803, 0xA805 },
+ { 0xA807, 0xA80A },
+ { 0xA80C, 0xA822 },
+ { 0xA840, 0xA873 },
+ { 0xA882, 0xA8B3 },
+ { 0xA8F2, 0xA8F7 },
+ { 0xA8FD, 0xA8FE },
+ { 0xA90A, 0xA925 },
+ { 0xA930, 0xA946 },
+ { 0xA960, 0xA97C },
+ { 0xA984, 0xA9B2 },
+ { 0xA9E0, 0xA9E4 },
+ { 0xA9E6, 0xA9EF },
+ { 0xA9FA, 0xA9FE },
+ { 0xAA00, 0xAA28 },
+ { 0xAA40, 0xAA42 },
+ { 0xAA44, 0xAA4B },
+ { 0xAA60, 0xAA76 },
+ { 0xAA7E, 0xAAAF },
+ { 0xAAB5, 0xAAB6 },
+ { 0xAAB9, 0xAABD },
+ { 0xAADB, 0xAADD },
+ { 0xAAE0, 0xAAEA },
+ { 0xAAF2, 0xAAF4 },
+ { 0xAB01, 0xAB06 },
+ { 0xAB09, 0xAB0E },
+ { 0xAB11, 0xAB16 },
+ { 0xAB20, 0xAB26 },
+ { 0xAB28, 0xAB2E },
+ { 0xAB30, 0xAB5A },
+ { 0xAB5C, 0xAB69 },
+ { 0xAB70, 0xABE2 },
+ { 0xD7B0, 0xD7C6 },
+ { 0xD7CB, 0xD7FB },
+ { 0xF900, 0xFA6D },
+ { 0xFA70, 0xFAD9 },
+ { 0xFB00, 0xFB06 },
+ { 0xFB13, 0xFB17 },
+ { 0xFB1F, 0xFB28 },
+ { 0xFB2A, 0xFB36 },
+ { 0xFB38, 0xFB3C },
+ { 0xFB40, 0xFB41 },
+ { 0xFB43, 0xFB44 },
+ { 0xFB46, 0xFBB1 },
+ { 0xFBD3, 0xFD3D },
+ { 0xFD50, 0xFD8F },
+ { 0xFD92, 0xFDC7 },
+ { 0xFDF0, 0xFDFB },
+ { 0xFE70, 0xFE74 },
+ { 0xFE76, 0xFEFC },
+ { 0xFF21, 0xFF3A },
+ { 0xFF41, 0xFF5A },
+ { 0xFF66, 0xFFBE },
+ { 0xFFC2, 0xFFC7 },
+ { 0xFFCA, 0xFFCF },
+ { 0xFFD2, 0xFFD7 },
+ { 0xFFDA, 0xFFDC },
+ { 0x10000, 0x1000B },
+ { 0x1000D, 0x10026 },
+ { 0x10028, 0x1003A },
+ { 0x1003C, 0x1003D },
+ { 0x1003F, 0x1004D },
+ { 0x10050, 0x1005D },
+ { 0x10080, 0x100FA },
+ { 0x10280, 0x1029C },
+ { 0x102A0, 0x102D0 },
+ { 0x10300, 0x1031F },
+ { 0x1032D, 0x10340 },
+ { 0x10342, 0x10349 },
+ { 0x10350, 0x10375 },
+ { 0x10380, 0x1039D },
+ { 0x103A0, 0x103C3 },
+ { 0x103C8, 0x103CF },
+ { 0x10400, 0x1049D },
+ { 0x104B0, 0x104D3 },
+ { 0x104D8, 0x104FB },
+ { 0x10500, 0x10527 },
+ { 0x10530, 0x10563 },
+ { 0x10570, 0x1057A },
+ { 0x1057C, 0x1058A },
+ { 0x1058C, 0x10592 },
+ { 0x10594, 0x10595 },
+ { 0x10597, 0x105A1 },
+ { 0x105A3, 0x105B1 },
+ { 0x105B3, 0x105B9 },
+ { 0x105BB, 0x105BC },
+ { 0x10600, 0x10736 },
+ { 0x10740, 0x10755 },
+ { 0x10760, 0x10767 },
+ { 0x10780, 0x10785 },
+ { 0x10787, 0x107B0 },
+ { 0x107B2, 0x107BA },
+ { 0x10800, 0x10805 },
+ { 0x1080A, 0x10835 },
+ { 0x10837, 0x10838 },
+ { 0x1083F, 0x10855 },
+ { 0x10860, 0x10876 },
+ { 0x10880, 0x1089E },
+ { 0x108E0, 0x108F2 },
+ { 0x108F4, 0x108F5 },
+ { 0x10900, 0x10915 },
+ { 0x10920, 0x10939 },
+ { 0x10980, 0x109B7 },
+ { 0x109BE, 0x109BF },
+ { 0x10A10, 0x10A13 },
+ { 0x10A15, 0x10A17 },
+ { 0x10A19, 0x10A35 },
+ { 0x10A60, 0x10A7C },
+ { 0x10A80, 0x10A9C },
+ { 0x10AC0, 0x10AC7 },
+ { 0x10AC9, 0x10AE4 },
+ { 0x10B00, 0x10B35 },
+ { 0x10B40, 0x10B55 },
+ { 0x10B60, 0x10B72 },
+ { 0x10B80, 0x10B91 },
+ { 0x10C00, 0x10C48 },
+ { 0x10C80, 0x10CB2 },
+ { 0x10CC0, 0x10CF2 },
+ { 0x10D00, 0x10D23 },
+ { 0x10E80, 0x10EA9 },
+ { 0x10EB0, 0x10EB1 },
+ { 0x10F00, 0x10F1C },
+ { 0x10F30, 0x10F45 },
+ { 0x10F70, 0x10F81 },
+ { 0x10FB0, 0x10FC4 },
+ { 0x10FE0, 0x10FF6 },
+ { 0x11003, 0x11037 },
+ { 0x11071, 0x11072 },
+ { 0x11083, 0x110AF },
+ { 0x110D0, 0x110E8 },
+ { 0x11103, 0x11126 },
+ { 0x11150, 0x11172 },
+ { 0x11183, 0x111B2 },
+ { 0x111C1, 0x111C4 },
+ { 0x11200, 0x11211 },
+ { 0x11213, 0x1122B },
+ { 0x1123F, 0x11240 },
+ { 0x11280, 0x11286 },
+ { 0x1128A, 0x1128D },
+ { 0x1128F, 0x1129D },
+ { 0x1129F, 0x112A8 },
+ { 0x112B0, 0x112DE },
+ { 0x11305, 0x1130C },
+ { 0x1130F, 0x11310 },
+ { 0x11313, 0x11328 },
+ { 0x1132A, 0x11330 },
+ { 0x11332, 0x11333 },
+ { 0x11335, 0x11339 },
+ { 0x1135D, 0x11361 },
+ { 0x11400, 0x11434 },
+ { 0x11447, 0x1144A },
+ { 0x1145F, 0x11461 },
+ { 0x11480, 0x114AF },
+ { 0x114C4, 0x114C5 },
+ { 0x11580, 0x115AE },
+ { 0x115D8, 0x115DB },
+ { 0x11600, 0x1162F },
+ { 0x11680, 0x116AA },
+ { 0x11700, 0x1171A },
+ { 0x11740, 0x11746 },
+ { 0x11800, 0x1182B },
+ { 0x118A0, 0x118DF },
+ { 0x118FF, 0x11906 },
+ { 0x1190C, 0x11913 },
+ { 0x11915, 0x11916 },
+ { 0x11918, 0x1192F },
+ { 0x119A0, 0x119A7 },
+ { 0x119AA, 0x119D0 },
+ { 0x11A0B, 0x11A32 },
+ { 0x11A5C, 0x11A89 },
+ { 0x11AB0, 0x11AF8 },
+ { 0x11C00, 0x11C08 },
+ { 0x11C0A, 0x11C2E },
+ { 0x11C72, 0x11C8F },
+ { 0x11D00, 0x11D06 },
+ { 0x11D08, 0x11D09 },
+ { 0x11D0B, 0x11D30 },
+ { 0x11D60, 0x11D65 },
+ { 0x11D67, 0x11D68 },
+ { 0x11D6A, 0x11D89 },
+ { 0x11EE0, 0x11EF2 },
+ { 0x11F04, 0x11F10 },
+ { 0x11F12, 0x11F33 },
+ { 0x12000, 0x12399 },
+ { 0x12480, 0x12543 },
+ { 0x12F90, 0x12FF0 },
+ { 0x13000, 0x1342F },
+ { 0x13441, 0x13446 },
+ { 0x14400, 0x14646 },
+ { 0x16800, 0x16A38 },
+ { 0x16A40, 0x16A5E },
+ { 0x16A70, 0x16ABE },
+ { 0x16AD0, 0x16AED },
+ { 0x16B00, 0x16B2F },
+ { 0x16B40, 0x16B43 },
+ { 0x16B63, 0x16B77 },
+ { 0x16B7D, 0x16B8F },
+ { 0x16E40, 0x16E7F },
+ { 0x16F00, 0x16F4A },
+ { 0x16F93, 0x16F9F },
+ { 0x16FE0, 0x16FE1 },
+ { 0x18800, 0x18CD5 },
+ { 0x1AFF0, 0x1AFF3 },
+ { 0x1AFF5, 0x1AFFB },
+ { 0x1AFFD, 0x1AFFE },
+ { 0x1B000, 0x1B122 },
+ { 0x1B150, 0x1B152 },
+ { 0x1B164, 0x1B167 },
+ { 0x1B170, 0x1B2FB },
+ { 0x1BC00, 0x1BC6A },
+ { 0x1BC70, 0x1BC7C },
+ { 0x1BC80, 0x1BC88 },
+ { 0x1BC90, 0x1BC99 },
+ { 0x1D400, 0x1D454 },
+ { 0x1D456, 0x1D49C },
+ { 0x1D49E, 0x1D49F },
+ { 0x1D4A5, 0x1D4A6 },
+ { 0x1D4A9, 0x1D4AC },
+ { 0x1D4AE, 0x1D4B9 },
+ { 0x1D4BD, 0x1D4C3 },
+ { 0x1D4C5, 0x1D505 },
+ { 0x1D507, 0x1D50A },
+ { 0x1D50D, 0x1D514 },
+ { 0x1D516, 0x1D51C },
+ { 0x1D51E, 0x1D539 },
+ { 0x1D53B, 0x1D53E },
+ { 0x1D540, 0x1D544 },
+ { 0x1D54A, 0x1D550 },
+ { 0x1D552, 0x1D6A5 },
+ { 0x1D6A8, 0x1D6C0 },
+ { 0x1D6C2, 0x1D6DA },
+ { 0x1D6DC, 0x1D6FA },
+ { 0x1D6FC, 0x1D714 },
+ { 0x1D716, 0x1D734 },
+ { 0x1D736, 0x1D74E },
+ { 0x1D750, 0x1D76E },
+ { 0x1D770, 0x1D788 },
+ { 0x1D78A, 0x1D7A8 },
+ { 0x1D7AA, 0x1D7C2 },
+ { 0x1D7C4, 0x1D7CB },
+ { 0x1DF00, 0x1DF1E },
+ { 0x1DF25, 0x1DF2A },
+ { 0x1E030, 0x1E06D },
+ { 0x1E100, 0x1E12C },
+ { 0x1E137, 0x1E13D },
+ { 0x1E290, 0x1E2AD },
+ { 0x1E2C0, 0x1E2EB },
+ { 0x1E4D0, 0x1E4EB },
+ { 0x1E7E0, 0x1E7E6 },
+ { 0x1E7E8, 0x1E7EB },
+ { 0x1E7ED, 0x1E7EE },
+ { 0x1E7F0, 0x1E7FE },
+ { 0x1E800, 0x1E8C4 },
+ { 0x1E900, 0x1E943 },
+ { 0x1EE00, 0x1EE03 },
+ { 0x1EE05, 0x1EE1F },
+ { 0x1EE21, 0x1EE22 },
+ { 0x1EE29, 0x1EE32 },
+ { 0x1EE34, 0x1EE37 },
+ { 0x1EE4D, 0x1EE4F },
+ { 0x1EE51, 0x1EE52 },
+ { 0x1EE61, 0x1EE62 },
+ { 0x1EE67, 0x1EE6A },
+ { 0x1EE6C, 0x1EE72 },
+ { 0x1EE74, 0x1EE77 },
+ { 0x1EE79, 0x1EE7C },
+ { 0x1EE80, 0x1EE89 },
+ { 0x1EE8B, 0x1EE9B },
+ { 0x1EEA1, 0x1EEA3 },
+ { 0x1EEA5, 0x1EEA9 },
+ { 0x1EEAB, 0x1EEBB },
+ { 0x2F800, 0x2FA1D },
+};
+
+static const Rune alpha1[] = {
+ 0x00AA,
+ 0x00B5,
+ 0x00BA,
+ 0x0559,
+ 0x06FF,
+ 0x07B1,
+ 0x07FA,
+ 0x081A,
+ 0x0824,
+ 0x0828,
+ 0x093D,
+ 0x0950,
+ 0x09BD,
+ 0x09CE,
+ 0x09FC,
+ 0x0ABD,
+ 0x0AD0,
+ 0x0AF9,
+ 0x0B3D,
+ 0x0B71,
+ 0x0BD0,
+ 0x0C3D,
+ 0x0C5D,
+ 0x0C80,
+ 0x0CBD,
+ 0x0D3D,
+ 0x0D4E,
+ 0x0EBD,
+ 0x0F00,
+ 0x103F,
+ 0x1061,
+ 0x108E,
+ 0x10CD,
+ 0x17D7,
+ 0x17DC,
+ 0x1AA7,
+ 0x1CFA,
+ 0x2071,
+ 0x207F,
+ 0x2102,
+ 0x2107,
+ 0x214E,
+ 0x2D2D,
+ 0x2D6F,
+ 0x2E2F,
+ 0x3400,
+ 0x4DBF,
+ 0x4E00,
+ 0xA9CF,
+ 0xAA7A,
+ 0xAC00,
+ 0xD7A3,
+ 0x1083C,
+ 0x10A00,
+ 0x10F27,
+ 0x11075,
+ 0x11144,
+ 0x11147,
+ 0x11176,
+ 0x1133D,
+ 0x11350,
+ 0x11644,
+ 0x116B8,
+ 0x11909,
+ 0x11A00,
+ 0x11A3A,
+ 0x11A50,
+ 0x11A9D,
+ 0x11C40,
+ 0x11D46,
+ 0x11D98,
+ 0x11FB0,
+ 0x16F50,
+ 0x17000,
+ 0x187F7,
+ 0x18D00,
+ 0x18D08,
+ 0x1B132,
+ 0x1B155,
+ 0x1D4A2,
+ 0x1E14E,
+ 0x1E94B,
+ 0x1EE42,
+ 0x20000,
+ 0x2A6DF,
+ 0x2A700,
+ 0x2B739,
+ 0x2B740,
+ 0x2B81D,
+ 0x2B820,
+ 0x2CEA1,
+ 0x2CEB0,
+ 0x2EBE0,
+ 0x30000,
+ 0x3134A,
+ 0x31350,
+ 0x323AF,
+};
+
+int
+isalpharune(Rune r)
+{
+ const Rune *match;
+
+ if((match = bsearch(&r, alpha3, nelem(alpha3), sizeof *alpha3, &rune2cmp)))
+ return !((r - match[0]) % 2);
+ if(bsearch(&r, alpha2, nelem(alpha2), sizeof *alpha2, &rune2cmp))
+ return 1;
+ if(bsearch(&r, alpha1, nelem(alpha1), sizeof *alpha1, &rune1cmp))
+ return 1;
+ return 0;
+}
diff --git a/util/sbase/libutf/isblankrune.c b/util/sbase/libutf/isblankrune.c
new file mode 100644
index 00000000..7cf91597
--- /dev/null
+++ b/util/sbase/libutf/isblankrune.c
@@ -0,0 +1,9 @@
+/* Automatically generated by mkrunetype.awk */
+#include "../utf.h"
+#include "runetype.h"
+
+int
+isblankrune(Rune r)
+{
+ return r == ' ' || r == '\t';
+}
diff --git a/util/sbase/libutf/iscntrlrune.c b/util/sbase/libutf/iscntrlrune.c
new file mode 100644
index 00000000..603e57cb
--- /dev/null
+++ b/util/sbase/libutf/iscntrlrune.c
@@ -0,0 +1,18 @@
+/* Automatically generated by mkrunetype.awk */
+#include <stdlib.h>
+
+#include "../utf.h"
+#include "runetype.h"
+
+static const Rune cntrl2[][2] = {
+ { 0x0000, 0x001F },
+ { 0x007F, 0x009F },
+};
+
+int
+iscntrlrune(Rune r)
+{
+ if(bsearch(&r, cntrl2, nelem(cntrl2), sizeof *cntrl2, &rune2cmp))
+ return 1;
+ return 0;
+}
diff --git a/util/sbase/libutf/isdigitrune.c b/util/sbase/libutf/isdigitrune.c
new file mode 100644
index 00000000..c8901bc4
--- /dev/null
+++ b/util/sbase/libutf/isdigitrune.c
@@ -0,0 +1,80 @@
+/* Automatically generated by mkrunetype.awk */
+#include <stdlib.h>
+
+#include "../utf.h"
+#include "runetype.h"
+
+static const Rune digit2[][2] = {
+ { 0x0030, 0x0039 },
+ { 0x0660, 0x0669 },
+ { 0x06F0, 0x06F9 },
+ { 0x07C0, 0x07C9 },
+ { 0x0966, 0x096F },
+ { 0x09E6, 0x09EF },
+ { 0x0A66, 0x0A6F },
+ { 0x0AE6, 0x0AEF },
+ { 0x0B66, 0x0B6F },
+ { 0x0BE6, 0x0BEF },
+ { 0x0C66, 0x0C6F },
+ { 0x0CE6, 0x0CEF },
+ { 0x0D66, 0x0D6F },
+ { 0x0DE6, 0x0DEF },
+ { 0x0E50, 0x0E59 },
+ { 0x0ED0, 0x0ED9 },
+ { 0x0F20, 0x0F29 },
+ { 0x1040, 0x1049 },
+ { 0x1090, 0x1099 },
+ { 0x17E0, 0x17E9 },
+ { 0x1810, 0x1819 },
+ { 0x1946, 0x194F },
+ { 0x19D0, 0x19D9 },
+ { 0x1A80, 0x1A89 },
+ { 0x1A90, 0x1A99 },
+ { 0x1B50, 0x1B59 },
+ { 0x1BB0, 0x1BB9 },
+ { 0x1C40, 0x1C49 },
+ { 0x1C50, 0x1C59 },
+ { 0xA620, 0xA629 },
+ { 0xA8D0, 0xA8D9 },
+ { 0xA900, 0xA909 },
+ { 0xA9D0, 0xA9D9 },
+ { 0xA9F0, 0xA9F9 },
+ { 0xAA50, 0xAA59 },
+ { 0xABF0, 0xABF9 },
+ { 0xFF10, 0xFF19 },
+ { 0x104A0, 0x104A9 },
+ { 0x10D30, 0x10D39 },
+ { 0x11066, 0x1106F },
+ { 0x110F0, 0x110F9 },
+ { 0x11136, 0x1113F },
+ { 0x111D0, 0x111D9 },
+ { 0x112F0, 0x112F9 },
+ { 0x11450, 0x11459 },
+ { 0x114D0, 0x114D9 },
+ { 0x11650, 0x11659 },
+ { 0x116C0, 0x116C9 },
+ { 0x11730, 0x11739 },
+ { 0x118E0, 0x118E9 },
+ { 0x11950, 0x11959 },
+ { 0x11C50, 0x11C59 },
+ { 0x11D50, 0x11D59 },
+ { 0x11DA0, 0x11DA9 },
+ { 0x11F50, 0x11F59 },
+ { 0x16A60, 0x16A69 },
+ { 0x16AC0, 0x16AC9 },
+ { 0x16B50, 0x16B59 },
+ { 0x1D7CE, 0x1D7FF },
+ { 0x1E140, 0x1E149 },
+ { 0x1E2F0, 0x1E2F9 },
+ { 0x1E4F0, 0x1E4F9 },
+ { 0x1E950, 0x1E959 },
+ { 0x1FBF0, 0x1FBF9 },
+};
+
+int
+isdigitrune(Rune r)
+{
+ if(bsearch(&r, digit2, nelem(digit2), sizeof *digit2, &rune2cmp))
+ return 1;
+ return 0;
+}
diff --git a/util/sbase/libutf/isgraphrune.c b/util/sbase/libutf/isgraphrune.c
new file mode 100644
index 00000000..08770f64
--- /dev/null
+++ b/util/sbase/libutf/isgraphrune.c
@@ -0,0 +1,9 @@
+/* Automatically generated by mkrunetype.awk */
+#include "../utf.h"
+#include "runetype.h"
+
+int
+isgraphrune(Rune r)
+{
+ return !isspacerune(r) && isprintrune(r);
+}
diff --git a/util/sbase/libutf/isprintrune.c b/util/sbase/libutf/isprintrune.c
new file mode 100644
index 00000000..f6e2fa48
--- /dev/null
+++ b/util/sbase/libutf/isprintrune.c
@@ -0,0 +1,10 @@
+/* Automatically generated by mkrunetype.awk */
+#include "../utf.h"
+#include "runetype.h"
+
+int
+isprintrune(Rune r)
+{
+ return !iscntrlrune(r) && (r != 0x2028) && (r != 0x2029) &&
+ ((r < 0xFFF9) || (r > 0xFFFB));
+}
diff --git a/util/sbase/libutf/ispunctrune.c b/util/sbase/libutf/ispunctrune.c
new file mode 100644
index 00000000..d73cb25b
--- /dev/null
+++ b/util/sbase/libutf/ispunctrune.c
@@ -0,0 +1,9 @@
+/* Automatically generated by mkrunetype.awk */
+#include "../utf.h"
+#include "runetype.h"
+
+int
+ispunctrune(Rune r)
+{
+ return isgraphrune(r) && !isalnumrune(r);
+}
diff --git a/util/sbase/libutf/isspacerune.c b/util/sbase/libutf/isspacerune.c
new file mode 100644
index 00000000..8583f932
--- /dev/null
+++ b/util/sbase/libutf/isspacerune.c
@@ -0,0 +1,31 @@
+/* Automatically generated by mkrunetype.awk */
+#include <stdlib.h>
+
+#include "../utf.h"
+#include "runetype.h"
+
+static const Rune space2[][2] = {
+ { 0x0009, 0x000D },
+ { 0x001C, 0x0020 },
+ { 0x2000, 0x200A },
+ { 0x2028, 0x2029 },
+};
+
+static const Rune space1[] = {
+ 0x0085,
+ 0x00A0,
+ 0x1680,
+ 0x202F,
+ 0x205F,
+ 0x3000,
+};
+
+int
+isspacerune(Rune r)
+{
+ if(bsearch(&r, space2, nelem(space2), sizeof *space2, &rune2cmp))
+ return 1;
+ if(bsearch(&r, space1, nelem(space1), sizeof *space1, &rune1cmp))
+ return 1;
+ return 0;
+}
diff --git a/util/sbase/libutf/istitlerune.c b/util/sbase/libutf/istitlerune.c
new file mode 100644
index 00000000..36b38d12
--- /dev/null
+++ b/util/sbase/libutf/istitlerune.c
@@ -0,0 +1,31 @@
+/* Automatically generated by mkrunetype.awk */
+#include <stdlib.h>
+
+#include "../utf.h"
+#include "runetype.h"
+
+static const Rune title2[][2] = {
+ { 0x1F88, 0x1F8F },
+ { 0x1F98, 0x1F9F },
+ { 0x1FA8, 0x1FAF },
+};
+
+static const Rune title1[] = {
+ 0x01C5,
+ 0x01C8,
+ 0x01CB,
+ 0x01F2,
+ 0x1FBC,
+ 0x1FCC,
+ 0x1FFC,
+};
+
+int
+istitlerune(Rune r)
+{
+ if(bsearch(&r, title2, nelem(title2), sizeof *title2, &rune2cmp))
+ return 1;
+ if(bsearch(&r, title1, nelem(title1), sizeof *title1, &rune1cmp))
+ return 1;
+ return 0;
+}
diff --git a/util/sbase/libutf/isxdigitrune.c b/util/sbase/libutf/isxdigitrune.c
new file mode 100644
index 00000000..0797240a
--- /dev/null
+++ b/util/sbase/libutf/isxdigitrune.c
@@ -0,0 +1,9 @@
+/* Automatically generated by mkrunetype.awk */
+#include "../utf.h"
+#include "runetype.h"
+
+int
+isxdigitrune(Rune r)
+{
+ return (r >= '0' && (r - '0') < 10) || (r >= 'a' && (r - 'a') < 6);
+}
diff --git a/util/sbase/libutf/lowerrune.c b/util/sbase/libutf/lowerrune.c
new file mode 100644
index 00000000..d91a364b
--- /dev/null
+++ b/util/sbase/libutf/lowerrune.c
@@ -0,0 +1,356 @@
+/* Automatically generated by mkrunetype.awk */
+#include <stdlib.h>
+
+#include "../utf.h"
+#include "runetype.h"
+
+static const Rune lower4[][2] = {
+ { 0x0101, 0x012F },
+ { 0x0133, 0x0137 },
+ { 0x013A, 0x0148 },
+ { 0x014B, 0x0177 },
+ { 0x017A, 0x017E },
+ { 0x0183, 0x0185 },
+ { 0x01A1, 0x01A5 },
+ { 0x01B4, 0x01B6 },
+ { 0x01CE, 0x01DC },
+ { 0x01DF, 0x01EF },
+ { 0x01F9, 0x021F },
+ { 0x0223, 0x0233 },
+ { 0x0247, 0x024F },
+ { 0x0371, 0x0373 },
+ { 0x03D9, 0x03EF },
+ { 0x0461, 0x0481 },
+ { 0x048B, 0x04BF },
+ { 0x04C2, 0x04CE },
+ { 0x04D1, 0x052F },
+ { 0x1E01, 0x1E95 },
+ { 0x1EA1, 0x1EFF },
+ { 0x2C68, 0x2C6C },
+ { 0x2C81, 0x2CE3 },
+ { 0x2CEC, 0x2CEE },
+ { 0xA641, 0xA66D },
+ { 0xA681, 0xA69B },
+ { 0xA723, 0xA72F },
+ { 0xA733, 0xA76F },
+ { 0xA77A, 0xA77C },
+ { 0xA77F, 0xA787 },
+ { 0xA791, 0xA793 },
+ { 0xA797, 0xA7A9 },
+ { 0xA7B5, 0xA7C3 },
+ { 0xA7C8, 0xA7CA },
+ { 0xA7D7, 0xA7D9 },
+};
+
+static const Rune lower2[][3] = {
+ { 0x0061, 0x007A, 0x0041 },
+ { 0x00E0, 0x00F6, 0x00C0 },
+ { 0x00F8, 0x00FE, 0x00D8 },
+ { 0x01AA, 0x01AB, 0x01AA },
+ { 0x0234, 0x0239, 0x0234 },
+ { 0x023F, 0x0240, 0x2C7E },
+ { 0x0256, 0x0257, 0x0189 },
+ { 0x025D, 0x025F, 0x025D },
+ { 0x026D, 0x026E, 0x026D },
+ { 0x0273, 0x0274, 0x0273 },
+ { 0x0276, 0x027C, 0x0276 },
+ { 0x027E, 0x027F, 0x027E },
+ { 0x0284, 0x0286, 0x0284 },
+ { 0x028A, 0x028B, 0x01B1 },
+ { 0x028D, 0x0291, 0x028D },
+ { 0x0295, 0x029C, 0x0295 },
+ { 0x029F, 0x02AF, 0x029F },
+ { 0x037B, 0x037D, 0x03FD },
+ { 0x03AD, 0x03AF, 0x0388 },
+ { 0x03B1, 0x03C1, 0x0391 },
+ { 0x03C3, 0x03CB, 0x03A3 },
+ { 0x03CD, 0x03CE, 0x038E },
+ { 0x0430, 0x044F, 0x0410 },
+ { 0x0450, 0x045F, 0x0400 },
+ { 0x0561, 0x0586, 0x0531 },
+ { 0x0587, 0x0588, 0x0587 },
+ { 0x10D0, 0x10FA, 0x1C90 },
+ { 0x10FD, 0x10FF, 0x1CBD },
+ { 0x13F8, 0x13FD, 0x13F0 },
+ { 0x1C83, 0x1C84, 0x0421 },
+ { 0x1D00, 0x1D2B, 0x1D00 },
+ { 0x1D6B, 0x1D77, 0x1D6B },
+ { 0x1D7A, 0x1D7C, 0x1D7A },
+ { 0x1D7E, 0x1D8D, 0x1D7E },
+ { 0x1D8F, 0x1D9A, 0x1D8F },
+ { 0x1E96, 0x1E9A, 0x1E96 },
+ { 0x1E9C, 0x1E9D, 0x1E9C },
+ { 0x1F00, 0x1F07, 0x1F08 },
+ { 0x1F10, 0x1F15, 0x1F18 },
+ { 0x1F20, 0x1F27, 0x1F28 },
+ { 0x1F30, 0x1F37, 0x1F38 },
+ { 0x1F40, 0x1F45, 0x1F48 },
+ { 0x1F60, 0x1F67, 0x1F68 },
+ { 0x1F70, 0x1F71, 0x1FBA },
+ { 0x1F72, 0x1F75, 0x1FC8 },
+ { 0x1F76, 0x1F77, 0x1FDA },
+ { 0x1F78, 0x1F79, 0x1FF8 },
+ { 0x1F7A, 0x1F7B, 0x1FEA },
+ { 0x1F7C, 0x1F7D, 0x1FFA },
+ { 0x1F80, 0x1F87, 0x1F88 },
+ { 0x1F90, 0x1F97, 0x1F98 },
+ { 0x1FA0, 0x1FA7, 0x1FA8 },
+ { 0x1FB0, 0x1FB1, 0x1FB8 },
+ { 0x1FB6, 0x1FB7, 0x1FB6 },
+ { 0x1FC6, 0x1FC7, 0x1FC6 },
+ { 0x1FD0, 0x1FD1, 0x1FD8 },
+ { 0x1FD2, 0x1FD3, 0x1FD2 },
+ { 0x1FD6, 0x1FD7, 0x1FD6 },
+ { 0x1FE0, 0x1FE1, 0x1FE8 },
+ { 0x1FE2, 0x1FE4, 0x1FE2 },
+ { 0x1FE6, 0x1FE7, 0x1FE6 },
+ { 0x1FF6, 0x1FF7, 0x1FF6 },
+ { 0x210E, 0x210F, 0x210E },
+ { 0x213C, 0x213D, 0x213C },
+ { 0x2146, 0x2149, 0x2146 },
+ { 0x2C30, 0x2C5F, 0x2C00 },
+ { 0x2C77, 0x2C7B, 0x2C77 },
+ { 0x2D00, 0x2D25, 0x10A0 },
+ { 0xA730, 0xA731, 0xA730 },
+ { 0xA771, 0xA778, 0xA771 },
+ { 0xAB30, 0xAB52, 0xAB30 },
+ { 0xAB54, 0xAB5A, 0xAB54 },
+ { 0xAB60, 0xAB68, 0xAB60 },
+ { 0xAB70, 0xABBF, 0x13A0 },
+ { 0xFB00, 0xFB06, 0xFB00 },
+ { 0xFB13, 0xFB17, 0xFB13 },
+ { 0xFF41, 0xFF5A, 0xFF21 },
+ { 0x10428, 0x1044F, 0x10400 },
+ { 0x104D8, 0x104FB, 0x104B0 },
+ { 0x10597, 0x105A1, 0x10570 },
+ { 0x105A3, 0x105B1, 0x1057C },
+ { 0x105B3, 0x105B9, 0x1058C },
+ { 0x105BB, 0x105BC, 0x10594 },
+ { 0x10CC0, 0x10CF2, 0x10C80 },
+ { 0x118C0, 0x118DF, 0x118A0 },
+ { 0x16E60, 0x16E7F, 0x16E40 },
+ { 0x1D41A, 0x1D433, 0x1D41A },
+ { 0x1D44E, 0x1D454, 0x1D44E },
+ { 0x1D456, 0x1D467, 0x1D456 },
+ { 0x1D482, 0x1D49B, 0x1D482 },
+ { 0x1D4B6, 0x1D4B9, 0x1D4B6 },
+ { 0x1D4BD, 0x1D4C3, 0x1D4BD },
+ { 0x1D4C5, 0x1D4CF, 0x1D4C5 },
+ { 0x1D4EA, 0x1D503, 0x1D4EA },
+ { 0x1D51E, 0x1D537, 0x1D51E },
+ { 0x1D552, 0x1D56B, 0x1D552 },
+ { 0x1D586, 0x1D59F, 0x1D586 },
+ { 0x1D5BA, 0x1D5D3, 0x1D5BA },
+ { 0x1D5EE, 0x1D607, 0x1D5EE },
+ { 0x1D622, 0x1D63B, 0x1D622 },
+ { 0x1D656, 0x1D66F, 0x1D656 },
+ { 0x1D68A, 0x1D6A5, 0x1D68A },
+ { 0x1D6C2, 0x1D6DA, 0x1D6C2 },
+ { 0x1D6DC, 0x1D6E1, 0x1D6DC },
+ { 0x1D6FC, 0x1D714, 0x1D6FC },
+ { 0x1D716, 0x1D71B, 0x1D716 },
+ { 0x1D736, 0x1D74E, 0x1D736 },
+ { 0x1D750, 0x1D755, 0x1D750 },
+ { 0x1D770, 0x1D788, 0x1D770 },
+ { 0x1D78A, 0x1D78F, 0x1D78A },
+ { 0x1D7AA, 0x1D7C2, 0x1D7AA },
+ { 0x1D7C4, 0x1D7C9, 0x1D7C4 },
+ { 0x1DF00, 0x1DF09, 0x1DF00 },
+ { 0x1DF0B, 0x1DF1E, 0x1DF0B },
+ { 0x1DF25, 0x1DF2A, 0x1DF25 },
+ { 0x1E922, 0x1E943, 0x1E900 },
+};
+
+static const Rune lower1[][2] = {
+ { 0x00B5, 0x039C },
+ { 0x00DF, 0x00DF },
+ { 0x00FF, 0x0178 },
+ { 0x0131, 0x0049 },
+ { 0x0138, 0x0138 },
+ { 0x0149, 0x0149 },
+ { 0x017F, 0x0053 },
+ { 0x0180, 0x0243 },
+ { 0x0188, 0x0187 },
+ { 0x018C, 0x018B },
+ { 0x018D, 0x018D },
+ { 0x0192, 0x0191 },
+ { 0x0195, 0x01F6 },
+ { 0x0199, 0x0198 },
+ { 0x019A, 0x023D },
+ { 0x019B, 0x019B },
+ { 0x019E, 0x0220 },
+ { 0x01A8, 0x01A7 },
+ { 0x01AD, 0x01AC },
+ { 0x01B0, 0x01AF },
+ { 0x01B9, 0x01B8 },
+ { 0x01BA, 0x01BA },
+ { 0x01BD, 0x01BC },
+ { 0x01BE, 0x01BE },
+ { 0x01BF, 0x01F7 },
+ { 0x01C6, 0x01C4 },
+ { 0x01C9, 0x01C7 },
+ { 0x01CC, 0x01CA },
+ { 0x01DD, 0x018E },
+ { 0x01F0, 0x01F0 },
+ { 0x01F3, 0x01F1 },
+ { 0x01F5, 0x01F4 },
+ { 0x0221, 0x0221 },
+ { 0x023C, 0x023B },
+ { 0x0242, 0x0241 },
+ { 0x0250, 0x2C6F },
+ { 0x0251, 0x2C6D },
+ { 0x0252, 0x2C70 },
+ { 0x0253, 0x0181 },
+ { 0x0254, 0x0186 },
+ { 0x0255, 0x0255 },
+ { 0x0258, 0x0258 },
+ { 0x0259, 0x018F },
+ { 0x025A, 0x025A },
+ { 0x025B, 0x0190 },
+ { 0x025C, 0xA7AB },
+ { 0x0260, 0x0193 },
+ { 0x0261, 0xA7AC },
+ { 0x0262, 0x0262 },
+ { 0x0263, 0x0194 },
+ { 0x0264, 0x0264 },
+ { 0x0265, 0xA78D },
+ { 0x0266, 0xA7AA },
+ { 0x0267, 0x0267 },
+ { 0x0268, 0x0197 },
+ { 0x0269, 0x0196 },
+ { 0x026A, 0xA7AE },
+ { 0x026B, 0x2C62 },
+ { 0x026C, 0xA7AD },
+ { 0x026F, 0x019C },
+ { 0x0270, 0x0270 },
+ { 0x0271, 0x2C6E },
+ { 0x0272, 0x019D },
+ { 0x0275, 0x019F },
+ { 0x027D, 0x2C64 },
+ { 0x0280, 0x01A6 },
+ { 0x0281, 0x0281 },
+ { 0x0282, 0xA7C5 },
+ { 0x0283, 0x01A9 },
+ { 0x0287, 0xA7B1 },
+ { 0x0288, 0x01AE },
+ { 0x0289, 0x0244 },
+ { 0x028C, 0x0245 },
+ { 0x0292, 0x01B7 },
+ { 0x0293, 0x0293 },
+ { 0x029D, 0xA7B2 },
+ { 0x029E, 0xA7B0 },
+ { 0x0377, 0x0376 },
+ { 0x0390, 0x0390 },
+ { 0x03AC, 0x0386 },
+ { 0x03B0, 0x03B0 },
+ { 0x03C2, 0x03A3 },
+ { 0x03CC, 0x038C },
+ { 0x03D0, 0x0392 },
+ { 0x03D1, 0x0398 },
+ { 0x03D5, 0x03A6 },
+ { 0x03D6, 0x03A0 },
+ { 0x03D7, 0x03CF },
+ { 0x03F0, 0x039A },
+ { 0x03F1, 0x03A1 },
+ { 0x03F2, 0x03F9 },
+ { 0x03F3, 0x037F },
+ { 0x03F5, 0x0395 },
+ { 0x03F8, 0x03F7 },
+ { 0x03FB, 0x03FA },
+ { 0x03FC, 0x03FC },
+ { 0x04CF, 0x04C0 },
+ { 0x0560, 0x0560 },
+ { 0x1C80, 0x0412 },
+ { 0x1C81, 0x0414 },
+ { 0x1C82, 0x041E },
+ { 0x1C85, 0x0422 },
+ { 0x1C86, 0x042A },
+ { 0x1C87, 0x0462 },
+ { 0x1C88, 0xA64A },
+ { 0x1D79, 0xA77D },
+ { 0x1D7D, 0x2C63 },
+ { 0x1D8E, 0xA7C6 },
+ { 0x1E9B, 0x1E60 },
+ { 0x1E9F, 0x1E9F },
+ { 0x1F50, 0x1F50 },
+ { 0x1F51, 0x1F59 },
+ { 0x1F52, 0x1F52 },
+ { 0x1F53, 0x1F5B },
+ { 0x1F54, 0x1F54 },
+ { 0x1F55, 0x1F5D },
+ { 0x1F56, 0x1F56 },
+ { 0x1F57, 0x1F5F },
+ { 0x1FB2, 0x1FB2 },
+ { 0x1FB3, 0x1FBC },
+ { 0x1FB4, 0x1FB4 },
+ { 0x1FBE, 0x0399 },
+ { 0x1FC2, 0x1FC2 },
+ { 0x1FC3, 0x1FCC },
+ { 0x1FC4, 0x1FC4 },
+ { 0x1FE5, 0x1FEC },
+ { 0x1FF2, 0x1FF2 },
+ { 0x1FF3, 0x1FFC },
+ { 0x1FF4, 0x1FF4 },
+ { 0x210A, 0x210A },
+ { 0x2113, 0x2113 },
+ { 0x212F, 0x212F },
+ { 0x2134, 0x2134 },
+ { 0x2139, 0x2139 },
+ { 0x214E, 0x2132 },
+ { 0x2184, 0x2183 },
+ { 0x2C61, 0x2C60 },
+ { 0x2C65, 0x023A },
+ { 0x2C66, 0x023E },
+ { 0x2C71, 0x2C71 },
+ { 0x2C73, 0x2C72 },
+ { 0x2C74, 0x2C74 },
+ { 0x2C76, 0x2C75 },
+ { 0x2CE4, 0x2CE4 },
+ { 0x2CF3, 0x2CF2 },
+ { 0x2D27, 0x10C7 },
+ { 0x2D2D, 0x10CD },
+ { 0xA78C, 0xA78B },
+ { 0xA78E, 0xA78E },
+ { 0xA794, 0xA7C4 },
+ { 0xA795, 0xA795 },
+ { 0xA7AF, 0xA7AF },
+ { 0xA7D1, 0xA7D0 },
+ { 0xA7D3, 0xA7D3 },
+ { 0xA7D5, 0xA7D5 },
+ { 0xA7F6, 0xA7F5 },
+ { 0xA7FA, 0xA7FA },
+ { 0xAB53, 0xA7B3 },
+ { 0x1D4BB, 0x1D4BB },
+ { 0x1D7CB, 0x1D7CB },
+};
+
+int
+islowerrune(Rune r)
+{
+ const Rune *match;
+
+ if((match = bsearch(&r, lower4, nelem(lower4), sizeof *lower4, &rune2cmp)))
+ return !((r - match[0]) % 2);
+ if(bsearch(&r, lower2, nelem(lower2), sizeof *lower2, &rune2cmp))
+ return 1;
+ if(bsearch(&r, lower1, nelem(lower1), sizeof *lower1, &rune1cmp))
+ return 1;
+ return 0;
+}
+
+int
+toupperrune(Rune r)
+{
+ Rune *match;
+
+ match = bsearch(&r, lower4, nelem(lower4), sizeof *lower4, &rune2cmp);
+ if (match)
+ return ((r - match[0]) % 2) ? r : r - 1;
+ match = bsearch(&r, lower2, nelem(lower2), sizeof *lower2, &rune2cmp);
+ if (match)
+ return match[2] + (r - match[0]);
+ match = bsearch(&r, lower1, nelem(lower1), sizeof *lower1, &rune1cmp);
+ if (match)
+ return match[1];
+ return r;
+}
diff --git a/util/sbase/libutf/mkrunetype.awk b/util/sbase/libutf/mkrunetype.awk
new file mode 100644
index 00000000..e01ea2cc
--- /dev/null
+++ b/util/sbase/libutf/mkrunetype.awk
@@ -0,0 +1,240 @@
+# See LICENSE file for copyright and license details.
+
+BEGIN {
+ FS = ";"
+ # set up hexadecimal lookup table
+ for(i = 0; i < 16; i++)
+ hex[sprintf("%X",i)] = i;
+ HEADER = "/* Automatically generated by mkrunetype.awk */\n#include <stdlib.h>\n\n#include \"../utf.h\"\n#include \"runetype.h\"\n"
+ HEADER_OTHER = "/* Automatically generated by mkrunetype.awk */\n#include \"../utf.h\"\n#include \"runetype.h\"\n"
+}
+
+$3 ~ /^L/ { alphav[alphac++] = $1; }
+($3 ~ /^Z/) || ($5 == "WS") || ($5 == "S") || ($5 == "B") { spacev[spacec++] = $1; }
+$3 == "Cc" { cntrlv[cntrlc++] = $1; }
+$3 == "Lu" { upperv[upperc++] = $1; tolowerv[uppercc++] = ($14 == "") ? $1 : $14; }
+$3 == "Ll" { lowerv[lowerc++] = $1; toupperv[lowercc++] = ($13 == "") ? $1 : $13; }
+$3 == "Lt" { titlev[titlec++] = $1; }
+$3 == "Nd" { digitv[digitc++] = $1; }
+
+END {
+ system("rm -f isalpharune.c isspacerune.c iscntrlrune.c upperrune.c lowerrune.c istitlerune.c isdigitrune.c");
+
+ mkis("alpha", alphav, alphac, "isalpharune.c", q, "");
+ mkis("space", spacev, spacec, "isspacerune.c", q, "");
+ mkis("cntrl", cntrlv, cntrlc, "iscntrlrune.c", q, "");
+ mkis("upper", upperv, upperc, "upperrune.c", tolowerv, "lower");
+ mkis("lower", lowerv, lowerc, "lowerrune.c", toupperv, "upper");
+ mkis("title", titlev, titlec, "istitlerune.c", q, "");
+ mkis("digit", digitv, digitc, "isdigitrune.c", q, "");
+
+ system("rm -f isalnumrune.c isblankrune.c isprintrune.c isgraphrune.c ispunctrune.c isxdigitrune.c");
+
+ otheris();
+}
+
+# parse hexadecimal rune index to int
+function code(s) {
+ x = 0;
+ for(i = 1; i <= length(s); i++) {
+ c = substr(s, i, 1);
+ x = (x*16) + hex[c];
+ }
+ return x;
+}
+
+# generate 'is<name>rune' unicode lookup function
+function mkis(name, runev, runec, file, casev, casename) {
+ rune1c = 0;
+ rune2c = 0;
+ rune3c = 0;
+ rune4c = 0;
+ mode = 1;
+
+ #sort rune groups into singletons, ranges and laces
+ for(j = 0; j < runec; j++) {
+ # range
+ if(code(runev[j+1]) == code(runev[j])+1 && ((length(casev) == 0) ||
+ code(casev[j+1]) == code(casev[j])+1) && j+1 < runec) {
+ if (mode == 2) {
+ continue;
+ } else if (mode == 3) {
+ rune3v1[rune3c] = runev[j];
+ rune3c++;
+ } else if (mode == 4) {
+ rune4v1[rune4c] = runev[j];
+ rune4c++;
+ }
+ mode = 2;
+ rune2v0[rune2c] = runev[j];
+ if(length(casev) > 0) {
+ case2v[rune2c] = casev[j];
+ }
+ continue;
+ }
+ # lace 1
+ if(code(runev[j+1]) == code(runev[j])+2 && ((length(casev) == 0) ||
+ (code(casev[j+1]) == code(runev[j+1])+1 && code(casev[j]) == code(runev[j])+1)) &&
+ j+1 < runec) {
+ if (mode == 3) {
+ continue;
+ } else if (mode == 2) {
+ rune2v1[rune2c] = runev[j];
+ rune2c++;
+ } else if (mode == 4) {
+ rune4v1[rune2c] = runev[j];
+ rune4c++;
+ }
+ mode = 3;
+ rune3v0[rune3c] = runev[j];
+ continue;
+ }
+ # lace 2
+ if(code(runev[j+1]) == code(runev[j])+2 && ((length(casev) == 0) ||
+ (code(casev[j+1]) == code(runev[j+1])-1 && code(casev[j]) == code(runev[j])-1)) &&
+ j+1 < runec) {
+ if (mode == 4) {
+ continue;
+ } else if (mode == 2) {
+ rune2v1[rune2c] = runev[j];
+ rune2c++;
+ } else if (mode == 3) {
+ rune3v1[rune2c] = runev[j];
+ rune3c++;
+ }
+ mode = 4;
+ rune4v0[rune4c] = runev[j];
+ continue;
+ }
+ # terminating case
+ if (mode == 1) {
+ rune1v[rune1c] = runev[j];
+ if (length(casev) > 0) {
+ case1v[rune1c] = casev[j];
+ }
+ rune1c++;
+ } else if (mode == 2) {
+ rune2v1[rune2c] = runev[j];
+ rune2c++;
+ } else if (mode == 3) {
+ rune3v1[rune3c] = runev[j];
+ rune3c++;
+ } else { #lace 2
+ rune4v1[rune4c] = runev[j];
+ rune4c++;
+ }
+ mode = 1;
+ }
+ print HEADER > file;
+
+ #generate list of laces 1
+ if(rune3c > 0) {
+ print "static const Rune "name"3[][2] = {" > file;
+ for(j = 0; j < rune3c; j++) {
+ print "\t{ 0x"rune3v0[j]", 0x"rune3v1[j]" }," > file;
+ }
+ print "};\n" > file;
+ }
+
+ #generate list of laces 2
+ if(rune4c > 0) {
+ print "static const Rune "name"4[][2] = {" > file;
+ for(j = 0; j < rune4c; j++) {
+ print "\t{ 0x"rune4v0[j]", 0x"rune4v1[j]" }," > file;
+ }
+ print "};\n" > file;
+ }
+
+ # generate list of ranges
+ if(rune2c > 0) {
+ if(length(casev) > 0) {
+ print "static const Rune "name"2[][3] = {" > file;
+ for(j = 0; j < rune2c; j++) {
+ print "\t{ 0x"rune2v0[j]", 0x"rune2v1[j]", 0x"case2v[j]" }," > file;
+ }
+ } else {
+ print "static const Rune "name"2[][2] = {" > file
+ for(j = 0; j < rune2c; j++) {
+ print "\t{ 0x"rune2v0[j]", 0x"rune2v1[j]" }," > file;
+ }
+ }
+ print "};\n" > file;
+ }
+
+ # generate list of singletons
+ if(rune1c > 0) {
+ if(length(casev) > 0) {
+ print "static const Rune "name"1[][2] = {" > file;
+ for(j = 0; j < rune1c; j++) {
+ print "\t{ 0x"rune1v[j]", 0x"case1v[j]" }," > file;
+ }
+ } else {
+ print "static const Rune "name"1[] = {" > file;
+ for(j = 0; j < rune1c; j++) {
+ print "\t0x"rune1v[j]"," > file;
+ }
+ }
+ print "};\n" > file;
+ }
+ # generate lookup function
+ print "int\nis"name"rune(Rune r)\n{" > file;
+ if(rune4c > 0 || rune3c > 0)
+ print "\tconst Rune *match;\n" > file;
+ if(rune4c > 0) {
+ print "\tif((match = bsearch(&r, "name"4, nelem("name"4), sizeof *"name"4, &rune2cmp)))" > file;
+ print "\t\treturn !((r - match[0]) % 2);" > file;
+ }
+ if(rune3c > 0) {
+ print "\tif((match = bsearch(&r, "name"3, nelem("name"3), sizeof *"name"3, &rune2cmp)))" > file;
+ print "\t\treturn !((r - match[0]) % 2);" > file;
+ }
+ if(rune2c > 0) {
+ print "\tif(bsearch(&r, "name"2, nelem("name"2), sizeof *"name"2, &rune2cmp))\n\t\treturn 1;" > file;
+ }
+ if(rune1c > 0) {
+ print "\tif(bsearch(&r, "name"1, nelem("name"1), sizeof *"name"1, &rune1cmp))\n\t\treturn 1;" > file;
+ }
+ print "\treturn 0;\n}" > file;
+
+ # generate case conversion function
+ if(length(casev) > 0) {
+ print "\nint\nto"casename"rune(Rune r)\n{\n\tRune *match;\n" > file;
+ if(rune4c > 0) {
+ print "\tmatch = bsearch(&r, "name"4, nelem("name"4), sizeof *"name"4, &rune2cmp);" > file;
+ print "\tif (match)" > file;
+ print "\t\treturn ((r - match[0]) % 2) ? r : r - 1;" > file;
+ }
+ if(rune3c > 0) {
+ print "\tmatch = bsearch(&r, "name"3, nelem("name"3), sizeof *"name"3, &rune2cmp);" > file;
+ print "\tif (match)" > file;
+ print "\t\treturn ((r - match[0]) % 2) ? r : r + 1;" > file;
+ }
+ if(rune2c > 0) {
+ print "\tmatch = bsearch(&r, "name"2, nelem("name"2), sizeof *"name"2, &rune2cmp);" > file;
+ print "\tif (match)" > file;
+ print "\t\treturn match[2] + (r - match[0]);" > file;
+ }
+ if(rune1c > 0) {
+ print "\tmatch = bsearch(&r, "name"1, nelem("name"1), sizeof *"name"1, &rune1cmp);" > file;
+ print "\tif (match)" > file;
+ print "\t\treturn match[1];" > file;
+ }
+ print "\treturn r;\n}" > file;
+ }
+}
+
+function otheris() {
+ print HEADER_OTHER > "isalnumrune.c";
+ print "int\nisalnumrune(Rune r)\n{\n\treturn isalpharune(r) || isdigitrune(r);\n}" > "isalnumrune.c";
+ print HEADER_OTHER > "isblankrune.c";
+ print "int\nisblankrune(Rune r)\n{\n\treturn r == ' ' || r == '\\t';\n}" > "isblankrune.c";
+ print HEADER_OTHER > "isprintrune.c";
+ print "int\nisprintrune(Rune r)\n{\n\treturn !iscntrlrune(r) && (r != 0x2028) && (r != 0x2029) &&" > "isprintrune.c";
+ print "\t ((r < 0xFFF9) || (r > 0xFFFB));\n}" > "isprintrune.c";
+ print HEADER_OTHER > "isgraphrune.c";
+ print "int\nisgraphrune(Rune r)\n{\n\treturn !isspacerune(r) && isprintrune(r);\n}" > "isgraphrune.c";
+ print HEADER_OTHER > "ispunctrune.c";
+ print "int\nispunctrune(Rune r)\n{\n\treturn isgraphrune(r) && !isalnumrune(r);\n}" > "ispunctrune.c";
+ print HEADER_OTHER > "isxdigitrune.c";
+ print "int\nisxdigitrune(Rune r)\n{\n\treturn (r >= '0' && (r - '0') < 10) || (r >= 'a' && (r - 'a') < 6);\n}" > "isxdigitrune.c";
+}
diff --git a/util/sbase/libutf/rune.c b/util/sbase/libutf/rune.c
new file mode 100644
index 00000000..1273f451
--- /dev/null
+++ b/util/sbase/libutf/rune.c
@@ -0,0 +1,148 @@
+/* MIT/X Consortium Copyright (c) 2012 Connor Lane Smith <cls@lubutu.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+#include "../utf.h"
+
+#define MIN(x,y) ((x) < (y) ? (x) : (y))
+
+#define UTFSEQ(x) ((((x) & 0x80) == 0x00) ? 1 /* 0xxxxxxx */ \
+ : (((x) & 0xC0) == 0x80) ? 0 /* 10xxxxxx */ \
+ : (((x) & 0xE0) == 0xC0) ? 2 /* 110xxxxx */ \
+ : (((x) & 0xF0) == 0xE0) ? 3 /* 1110xxxx */ \
+ : (((x) & 0xF8) == 0xF0) ? 4 /* 11110xxx */ \
+ : (((x) & 0xFC) == 0xF8) ? 5 /* 111110xx */ \
+ : (((x) & 0xFE) == 0xFC) ? 6 /* 1111110x */ \
+ : 0 )
+
+#define BADRUNE(x) ((x) < 0 || (x) > Runemax \
+ || ((x) & 0xFFFE) == 0xFFFE \
+ || ((x) >= 0xD800 && (x) <= 0xDFFF) \
+ || ((x) >= 0xFDD0 && (x) <= 0xFDEF))
+
+int
+runetochar(char *s, const Rune *p)
+{
+ Rune r = *p;
+
+ switch(runelen(r)) {
+ case 1: /* 0aaaaaaa */
+ s[0] = r;
+ return 1;
+ case 2: /* 00000aaa aabbbbbb */
+ s[0] = 0xC0 | ((r & 0x0007C0) >> 6); /* 110aaaaa */
+ s[1] = 0x80 | (r & 0x00003F); /* 10bbbbbb */
+ return 2;
+ case 3: /* aaaabbbb bbcccccc */
+ s[0] = 0xE0 | ((r & 0x00F000) >> 12); /* 1110aaaa */
+ s[1] = 0x80 | ((r & 0x000FC0) >> 6); /* 10bbbbbb */
+ s[2] = 0x80 | (r & 0x00003F); /* 10cccccc */
+ return 3;
+ case 4: /* 000aaabb bbbbcccc ccdddddd */
+ s[0] = 0xF0 | ((r & 0x1C0000) >> 18); /* 11110aaa */
+ s[1] = 0x80 | ((r & 0x03F000) >> 12); /* 10bbbbbb */
+ s[2] = 0x80 | ((r & 0x000FC0) >> 6); /* 10cccccc */
+ s[3] = 0x80 | (r & 0x00003F); /* 10dddddd */
+ return 4;
+ default:
+ return 0; /* error */
+ }
+}
+
+int
+chartorune(Rune *p, const char *s)
+{
+ return charntorune(p, s, UTFmax);
+}
+
+int
+charntorune(Rune *p, const char *s, size_t len)
+{
+ unsigned int i, n;
+ Rune r;
+
+ if(len == 0) /* can't even look at s[0] */
+ return 0;
+
+ switch((n = UTFSEQ(s[0]))) {
+ case 1: r = s[0]; break; /* 0xxxxxxx */
+ case 2: r = s[0] & 0x1F; break; /* 110xxxxx */
+ case 3: r = s[0] & 0x0F; break; /* 1110xxxx */
+ case 4: r = s[0] & 0x07; break; /* 11110xxx */
+ case 5: r = s[0] & 0x03; break; /* 111110xx */
+ case 6: r = s[0] & 0x01; break; /* 1111110x */
+ default: /* invalid sequence */
+ *p = Runeerror;
+ return 1;
+ }
+ /* add values from continuation bytes */
+ for(i = 1; i < MIN(n, len); i++)
+ if((s[i] & 0xC0) == 0x80) {
+ /* add bits from continuation byte to rune value
+ * cannot overflow: 6 byte sequences contain 31 bits */
+ r = (r << 6) | (s[i] & 0x3F); /* 10xxxxxx */
+ }
+ else { /* expected continuation */
+ *p = Runeerror;
+ return i;
+ }
+
+ if(i < n) /* must have reached len limit */
+ return 0;
+
+ /* reject invalid or overlong sequences */
+ if(BADRUNE(r) || runelen(r) < (int)n)
+ r = Runeerror;
+
+ *p = r;
+ return n;
+}
+
+int
+runelen(Rune r)
+{
+ if(BADRUNE(r))
+ return 0; /* error */
+ else if(r <= 0x7F)
+ return 1;
+ else if(r <= 0x07FF)
+ return 2;
+ else if(r <= 0xFFFF)
+ return 3;
+ else
+ return 4;
+}
+
+size_t
+runenlen(const Rune *p, size_t len)
+{
+ size_t i, n = 0;
+
+ for(i = 0; i < len; i++)
+ n += runelen(p[i]);
+ return n;
+}
+
+int
+fullrune(const char *s, size_t len)
+{
+ Rune r;
+
+ return charntorune(&r, s, len) > 0;
+}
diff --git a/util/sbase/libutf/runetype.c b/util/sbase/libutf/runetype.c
new file mode 100644
index 00000000..9e8ede8a
--- /dev/null
+++ b/util/sbase/libutf/runetype.c
@@ -0,0 +1,41 @@
+/* MIT/X Consortium Copyright (c) 2012 Connor Lane Smith <cls@lubutu.com>
+ * (c) 2015 Laslo Hunhold <dev@frign.de>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+#include "../utf.h"
+
+int
+rune1cmp(const void *v1, const void *v2)
+{
+ Rune r1 = *(Rune *)v1, r2 = *(Rune *)v2;
+
+ return r1 - r2;
+}
+
+int
+rune2cmp(const void *v1, const void *v2)
+{
+ Rune r = *(Rune *)v1, *p = (Rune *)v2;
+
+ if(r >= p[0] && r <= p[1])
+ return 0;
+ else
+ return r - p[0];
+}
diff --git a/util/sbase/libutf/runetype.h b/util/sbase/libutf/runetype.h
new file mode 100644
index 00000000..8d09c347
--- /dev/null
+++ b/util/sbase/libutf/runetype.h
@@ -0,0 +1,26 @@
+/* MIT/X Consortium Copyright (c) 2012 Connor Lane Smith <cls@lubutu.com>
+ * (c) 2015 Laslo Hunhold <dev@frign.de>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#define nelem(x) (sizeof (x) / sizeof *(x))
+
+int rune1cmp(const void *, const void *);
+int rune2cmp(const void *, const void *);
diff --git a/util/sbase/libutf/upperrune.c b/util/sbase/libutf/upperrune.c
new file mode 100644
index 00000000..0c874a85
--- /dev/null
+++ b/util/sbase/libutf/upperrune.c
@@ -0,0 +1,265 @@
+/* Automatically generated by mkrunetype.awk */
+#include <stdlib.h>
+
+#include "../utf.h"
+#include "runetype.h"
+
+static const Rune upper3[][2] = {
+ { 0x0100, 0x012E },
+ { 0x0132, 0x0136 },
+ { 0x0139, 0x0147 },
+ { 0x014A, 0x0176 },
+ { 0x0179, 0x017D },
+ { 0x0182, 0x0184 },
+ { 0x01A0, 0x01A4 },
+ { 0x01B3, 0x01B5 },
+ { 0x01CD, 0x01DB },
+ { 0x01DE, 0x01EE },
+ { 0x01F8, 0x021E },
+ { 0x0222, 0x0232 },
+ { 0x0246, 0x024E },
+ { 0x0370, 0x0372 },
+ { 0x03D8, 0x03EE },
+ { 0x0460, 0x0480 },
+ { 0x048A, 0x04BE },
+ { 0x04C1, 0x04CD },
+ { 0x04D0, 0x052E },
+ { 0x1E00, 0x1E94 },
+ { 0x1EA0, 0x1EFE },
+ { 0x2C67, 0x2C6B },
+ { 0x2C80, 0x2CE2 },
+ { 0x2CEB, 0x2CED },
+ { 0xA640, 0xA66C },
+ { 0xA680, 0xA69A },
+ { 0xA722, 0xA72E },
+ { 0xA732, 0xA76E },
+ { 0xA779, 0xA77B },
+ { 0xA77E, 0xA786 },
+ { 0xA790, 0xA792 },
+ { 0xA796, 0xA7A8 },
+ { 0xA7B4, 0xA7C2 },
+ { 0xA7C7, 0xA7C9 },
+ { 0xA7D6, 0xA7D8 },
+};
+
+static const Rune upper2[][3] = {
+ { 0x0041, 0x005A, 0x0061 },
+ { 0x00C0, 0x00D6, 0x00E0 },
+ { 0x00D8, 0x00DE, 0x00F8 },
+ { 0x0189, 0x018A, 0x0256 },
+ { 0x01B1, 0x01B2, 0x028A },
+ { 0x0388, 0x038A, 0x03AD },
+ { 0x038E, 0x038F, 0x03CD },
+ { 0x0391, 0x03A1, 0x03B1 },
+ { 0x03A3, 0x03AB, 0x03C3 },
+ { 0x03D2, 0x03D4, 0x03D2 },
+ { 0x03FD, 0x03FF, 0x037B },
+ { 0x0400, 0x040F, 0x0450 },
+ { 0x0410, 0x042F, 0x0430 },
+ { 0x0531, 0x0556, 0x0561 },
+ { 0x10A0, 0x10C5, 0x2D00 },
+ { 0x13A0, 0x13EF, 0xAB70 },
+ { 0x13F0, 0x13F5, 0x13F8 },
+ { 0x1C90, 0x1CBA, 0x10D0 },
+ { 0x1CBD, 0x1CBF, 0x10FD },
+ { 0x1F08, 0x1F0F, 0x1F00 },
+ { 0x1F18, 0x1F1D, 0x1F10 },
+ { 0x1F28, 0x1F2F, 0x1F20 },
+ { 0x1F38, 0x1F3F, 0x1F30 },
+ { 0x1F48, 0x1F4D, 0x1F40 },
+ { 0x1F68, 0x1F6F, 0x1F60 },
+ { 0x1FB8, 0x1FB9, 0x1FB0 },
+ { 0x1FBA, 0x1FBB, 0x1F70 },
+ { 0x1FC8, 0x1FCB, 0x1F72 },
+ { 0x1FD8, 0x1FD9, 0x1FD0 },
+ { 0x1FDA, 0x1FDB, 0x1F76 },
+ { 0x1FE8, 0x1FE9, 0x1FE0 },
+ { 0x1FEA, 0x1FEB, 0x1F7A },
+ { 0x1FF8, 0x1FF9, 0x1F78 },
+ { 0x1FFA, 0x1FFB, 0x1F7C },
+ { 0x210B, 0x210D, 0x210B },
+ { 0x2110, 0x2112, 0x2110 },
+ { 0x2119, 0x211D, 0x2119 },
+ { 0x212C, 0x212D, 0x212C },
+ { 0x2130, 0x2131, 0x2130 },
+ { 0x213E, 0x213F, 0x213E },
+ { 0x2C00, 0x2C2F, 0x2C30 },
+ { 0x2C7E, 0x2C7F, 0x023F },
+ { 0xFF21, 0xFF3A, 0xFF41 },
+ { 0x10400, 0x10427, 0x10428 },
+ { 0x104B0, 0x104D3, 0x104D8 },
+ { 0x10570, 0x1057A, 0x10597 },
+ { 0x1057C, 0x1058A, 0x105A3 },
+ { 0x1058C, 0x10592, 0x105B3 },
+ { 0x10594, 0x10595, 0x105BB },
+ { 0x10C80, 0x10CB2, 0x10CC0 },
+ { 0x118A0, 0x118BF, 0x118C0 },
+ { 0x16E40, 0x16E5F, 0x16E60 },
+ { 0x1D400, 0x1D419, 0x1D400 },
+ { 0x1D434, 0x1D44D, 0x1D434 },
+ { 0x1D468, 0x1D481, 0x1D468 },
+ { 0x1D49E, 0x1D49F, 0x1D49E },
+ { 0x1D4A5, 0x1D4A6, 0x1D4A5 },
+ { 0x1D4A9, 0x1D4AC, 0x1D4A9 },
+ { 0x1D4AE, 0x1D4B5, 0x1D4AE },
+ { 0x1D4D0, 0x1D4E9, 0x1D4D0 },
+ { 0x1D504, 0x1D505, 0x1D504 },
+ { 0x1D507, 0x1D50A, 0x1D507 },
+ { 0x1D50D, 0x1D514, 0x1D50D },
+ { 0x1D516, 0x1D51C, 0x1D516 },
+ { 0x1D538, 0x1D539, 0x1D538 },
+ { 0x1D53B, 0x1D53E, 0x1D53B },
+ { 0x1D540, 0x1D544, 0x1D540 },
+ { 0x1D54A, 0x1D550, 0x1D54A },
+ { 0x1D56C, 0x1D585, 0x1D56C },
+ { 0x1D5A0, 0x1D5B9, 0x1D5A0 },
+ { 0x1D5D4, 0x1D5ED, 0x1D5D4 },
+ { 0x1D608, 0x1D621, 0x1D608 },
+ { 0x1D63C, 0x1D655, 0x1D63C },
+ { 0x1D670, 0x1D689, 0x1D670 },
+ { 0x1D6A8, 0x1D6C0, 0x1D6A8 },
+ { 0x1D6E2, 0x1D6FA, 0x1D6E2 },
+ { 0x1D71C, 0x1D734, 0x1D71C },
+ { 0x1D756, 0x1D76E, 0x1D756 },
+ { 0x1D790, 0x1D7A8, 0x1D790 },
+ { 0x1E900, 0x1E921, 0x1E922 },
+};
+
+static const Rune upper1[][2] = {
+ { 0x0130, 0x0069 },
+ { 0x0178, 0x00FF },
+ { 0x0181, 0x0253 },
+ { 0x0186, 0x0254 },
+ { 0x0187, 0x0188 },
+ { 0x018B, 0x018C },
+ { 0x018E, 0x01DD },
+ { 0x018F, 0x0259 },
+ { 0x0190, 0x025B },
+ { 0x0191, 0x0192 },
+ { 0x0193, 0x0260 },
+ { 0x0194, 0x0263 },
+ { 0x0196, 0x0269 },
+ { 0x0197, 0x0268 },
+ { 0x0198, 0x0199 },
+ { 0x019C, 0x026F },
+ { 0x019D, 0x0272 },
+ { 0x019F, 0x0275 },
+ { 0x01A6, 0x0280 },
+ { 0x01A7, 0x01A8 },
+ { 0x01A9, 0x0283 },
+ { 0x01AC, 0x01AD },
+ { 0x01AE, 0x0288 },
+ { 0x01AF, 0x01B0 },
+ { 0x01B7, 0x0292 },
+ { 0x01B8, 0x01B9 },
+ { 0x01BC, 0x01BD },
+ { 0x01C4, 0x01C6 },
+ { 0x01C7, 0x01C9 },
+ { 0x01CA, 0x01CC },
+ { 0x01F1, 0x01F3 },
+ { 0x01F4, 0x01F5 },
+ { 0x01F6, 0x0195 },
+ { 0x01F7, 0x01BF },
+ { 0x0220, 0x019E },
+ { 0x023A, 0x2C65 },
+ { 0x023B, 0x023C },
+ { 0x023D, 0x019A },
+ { 0x023E, 0x2C66 },
+ { 0x0241, 0x0242 },
+ { 0x0243, 0x0180 },
+ { 0x0244, 0x0289 },
+ { 0x0245, 0x028C },
+ { 0x0376, 0x0377 },
+ { 0x037F, 0x03F3 },
+ { 0x0386, 0x03AC },
+ { 0x038C, 0x03CC },
+ { 0x03CF, 0x03D7 },
+ { 0x03F4, 0x03B8 },
+ { 0x03F7, 0x03F8 },
+ { 0x03F9, 0x03F2 },
+ { 0x03FA, 0x03FB },
+ { 0x04C0, 0x04CF },
+ { 0x10C7, 0x2D27 },
+ { 0x10CD, 0x2D2D },
+ { 0x1E9E, 0x00DF },
+ { 0x1F59, 0x1F51 },
+ { 0x1F5B, 0x1F53 },
+ { 0x1F5D, 0x1F55 },
+ { 0x1F5F, 0x1F57 },
+ { 0x1FEC, 0x1FE5 },
+ { 0x2102, 0x2102 },
+ { 0x2107, 0x2107 },
+ { 0x2115, 0x2115 },
+ { 0x2124, 0x2124 },
+ { 0x2126, 0x03C9 },
+ { 0x2128, 0x2128 },
+ { 0x212A, 0x006B },
+ { 0x212B, 0x00E5 },
+ { 0x2132, 0x214E },
+ { 0x2133, 0x2133 },
+ { 0x2145, 0x2145 },
+ { 0x2183, 0x2184 },
+ { 0x2C60, 0x2C61 },
+ { 0x2C62, 0x026B },
+ { 0x2C63, 0x1D7D },
+ { 0x2C64, 0x027D },
+ { 0x2C6D, 0x0251 },
+ { 0x2C6E, 0x0271 },
+ { 0x2C6F, 0x0250 },
+ { 0x2C70, 0x0252 },
+ { 0x2C72, 0x2C73 },
+ { 0x2C75, 0x2C76 },
+ { 0x2CF2, 0x2CF3 },
+ { 0xA77D, 0x1D79 },
+ { 0xA78B, 0xA78C },
+ { 0xA78D, 0x0265 },
+ { 0xA7AA, 0x0266 },
+ { 0xA7AB, 0x025C },
+ { 0xA7AC, 0x0261 },
+ { 0xA7AD, 0x026C },
+ { 0xA7AE, 0x026A },
+ { 0xA7B0, 0x029E },
+ { 0xA7B1, 0x0287 },
+ { 0xA7B2, 0x029D },
+ { 0xA7B3, 0xAB53 },
+ { 0xA7C4, 0xA794 },
+ { 0xA7C5, 0x0282 },
+ { 0xA7C6, 0x1D8E },
+ { 0xA7D0, 0xA7D1 },
+ { 0xA7F5, 0xA7F6 },
+ { 0x1D49C, 0x1D49C },
+ { 0x1D4A2, 0x1D4A2 },
+ { 0x1D546, 0x1D546 },
+ { 0x1D7CA, 0x1D7CA },
+};
+
+int
+isupperrune(Rune r)
+{
+ const Rune *match;
+
+ if((match = bsearch(&r, upper3, nelem(upper3), sizeof *upper3, &rune2cmp)))
+ return !((r - match[0]) % 2);
+ if(bsearch(&r, upper2, nelem(upper2), sizeof *upper2, &rune2cmp))
+ return 1;
+ if(bsearch(&r, upper1, nelem(upper1), sizeof *upper1, &rune1cmp))
+ return 1;
+ return 0;
+}
+
+int
+tolowerrune(Rune r)
+{
+ Rune *match;
+
+ match = bsearch(&r, upper3, nelem(upper3), sizeof *upper3, &rune2cmp);
+ if (match)
+ return ((r - match[0]) % 2) ? r : r + 1;
+ match = bsearch(&r, upper2, nelem(upper2), sizeof *upper2, &rune2cmp);
+ if (match)
+ return match[2] + (r - match[0]);
+ match = bsearch(&r, upper1, nelem(upper1), sizeof *upper1, &rune1cmp);
+ if (match)
+ return match[1];
+ return r;
+}
diff --git a/util/sbase/libutf/utf.c b/util/sbase/libutf/utf.c
new file mode 100644
index 00000000..492e020f
--- /dev/null
+++ b/util/sbase/libutf/utf.c
@@ -0,0 +1,142 @@
+/* MIT/X Consortium Copyright (c) 2012 Connor Lane Smith <cls@lubutu.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+#include <string.h>
+#include "../utf.h"
+
+char *
+utfecpy(char *to, char *end, const char *from)
+{
+ Rune r = Runeerror;
+ size_t i, n;
+
+ /* seek through to find final full rune */
+ for(i = 0; r != '\0' && (n = charntorune(&r, &from[i], end - &to[i])); i += n)
+ ;
+ memcpy(to, from, i); /* copy over bytes up to this rune */
+
+ if(i > 0 && r != '\0')
+ to[i] = '\0'; /* terminate if unterminated */
+ return &to[i];
+}
+
+size_t
+utflen(const char *s)
+{
+ const char *p = s;
+ size_t i;
+ Rune r;
+
+ for(i = 0; *p != '\0'; i++)
+ p += chartorune(&r, p);
+ return i;
+}
+
+size_t
+utfnlen(const char *s, size_t len)
+{
+ const char *p = s;
+ size_t i;
+ Rune r;
+ int n;
+
+ for(i = 0; (n = charntorune(&r, p, len-(p-s))) && r != '\0'; i++)
+ p += n;
+ return i;
+}
+
+size_t
+utfmemlen(const char *s, size_t len)
+{
+ const char *p = s;
+ size_t i;
+ Rune r;
+ int n;
+
+ for(i = 0; (n = charntorune(&r, p, len-(p-s))); i++)
+ p += n;
+ return i;
+}
+
+char *
+utfrune(const char *s, Rune r)
+{
+ if(r < Runeself) {
+ return strchr(s, r);
+ }
+ else if(r == Runeerror) {
+ Rune r0;
+ int n;
+
+ for(; *s != '\0'; s += n) {
+ n = chartorune(&r0, s);
+ if(r == r0)
+ return (char *)s;
+ }
+ }
+ else {
+ char buf[UTFmax+1];
+ int n;
+
+ if(!(n = runetochar(buf, &r)))
+ return NULL;
+ buf[n] = '\0';
+ return strstr(s, buf);
+ }
+ return NULL;
+}
+
+char *
+utfrrune(const char *s, Rune r)
+{
+ const char *p = NULL;
+ Rune r0;
+ int n;
+
+ if(r < Runeself)
+ return strrchr(s, r);
+
+ for(; *s != '\0'; s += n) {
+ n = chartorune(&r0, s);
+ if(r == r0)
+ p = s;
+ }
+ return (char *)p;
+}
+
+char *
+utfutf(const char *s, const char *t)
+{
+ const char *p, *q;
+ Rune r0, r1, r2;
+ int n, m;
+
+ for(chartorune(&r0, t); (s = utfrune(s, r0)); s++) {
+ for(p = s, q = t; *q && *p; p += n, q += m) {
+ n = chartorune(&r1, p);
+ m = chartorune(&r2, q);
+ if(r1 != r2)
+ break;
+ }
+ if(!*q)
+ return (char *)s;
+ }
+ return NULL;
+}
diff --git a/util/sbase/libutf/utftorunestr.c b/util/sbase/libutf/utftorunestr.c
new file mode 100644
index 00000000..e182bc15
--- /dev/null
+++ b/util/sbase/libutf/utftorunestr.c
@@ -0,0 +1,27 @@
+/* See LICENSE file for copyright and license details. */
+#include "../utf.h"
+
+size_t
+utftorunestr(const char *str, Rune *r)
+{
+ size_t i;
+ int n;
+
+ for (i = 0; (n = chartorune(&r[i], str)) && r[i]; i++)
+ str += n;
+
+ return i;
+}
+
+size_t
+utfntorunestr(const char *str, size_t len, Rune *r)
+{
+ size_t i;
+ int n;
+ const char *end = str + len;
+
+ for (i = 0; (n = charntorune(&r[i], str, end - str)); i++)
+ str += n;
+
+ return i;
+}
diff --git a/util/sbase/libutil/concat.c b/util/sbase/libutil/concat.c
new file mode 100644
index 00000000..2e9aa521
--- /dev/null
+++ b/util/sbase/libutil/concat.c
@@ -0,0 +1,23 @@
+/* See LICENSE file for copyright and license details. */
+#include <unistd.h>
+
+#include "../util.h"
+
+int
+concat(int f1, const char *s1, int f2, const char *s2)
+{
+ char buf[BUFSIZ];
+ ssize_t n;
+
+ while ((n = read(f1, buf, sizeof(buf))) > 0) {
+ if (writeall(f2, buf, n) < 0) {
+ weprintf("write %s:", s2);
+ return -2;
+ }
+ }
+ if (n < 0) {
+ weprintf("read %s:", s1);
+ return -1;
+ }
+ return 0;
+}
diff --git a/util/sbase/libutil/confirm.c b/util/sbase/libutil/confirm.c
new file mode 100644
index 00000000..44396af9
--- /dev/null
+++ b/util/sbase/libutil/confirm.c
@@ -0,0 +1,22 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdarg.h>
+#include <ctype.h>
+
+#include "../util.h"
+
+int
+confirm(const char *fmt, ...)
+{
+ int c, ans;
+ va_list ap;
+
+ va_start(ap, fmt);
+ xvprintf(fmt, ap);
+ va_end(ap);
+
+ c = getchar();
+ ans = (c == 'y' || c == 'Y');
+ while (c != '\n' && c != EOF)
+ c = getchar();
+ return ans;
+}
diff --git a/util/sbase/libutil/cp.c b/util/sbase/libutil/cp.c
new file mode 100644
index 00000000..2ab32a03
--- /dev/null
+++ b/util/sbase/libutil/cp.c
@@ -0,0 +1,176 @@
+/* See LICENSE file for copyright and license details. */
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <utime.h>
+
+#include "../fs.h"
+#include "../util.h"
+
+int cp_aflag = 0;
+int cp_fflag = 0;
+int cp_iflag = 0;
+int cp_pflag = 0;
+int cp_rflag = 0;
+int cp_vflag = 0;
+int cp_status = 0;
+int cp_follow;
+
+int
+cp(const char *s1, const char *s2, int depth)
+{
+ DIR *dp;
+ int f1, f2, flags = 0;
+ struct dirent *d;
+ struct stat st;
+ struct timespec times[2];
+ ssize_t r;
+ char target[PATH_MAX], ns1[PATH_MAX], ns2[PATH_MAX];
+
+ if (cp_follow == 'P' || (cp_follow == 'H' && depth))
+ flags |= AT_SYMLINK_NOFOLLOW;
+
+ if (fstatat(AT_FDCWD, s1, &st, flags) < 0) {
+ weprintf("stat %s:", s1);
+ cp_status = 1;
+ return 0;
+ }
+
+ if (cp_iflag && access(s2, F_OK) == 0) {
+ if (!confirm("overwrite '%s'? ", s2))
+ return 0;
+ }
+
+ if (cp_vflag)
+ printf("%s -> %s\n", s1, s2);
+
+ if (S_ISLNK(st.st_mode)) {
+ if ((r = readlink(s1, target, sizeof(target) - 1)) >= 0) {
+ target[r] = '\0';
+ if (cp_fflag && unlink(s2) < 0 && errno != ENOENT) {
+ weprintf("unlink %s:", s2);
+ cp_status = 1;
+ return 0;
+ } else if (symlink(target, s2) < 0) {
+ weprintf("symlink %s -> %s:", s2, target);
+ cp_status = 1;
+ return 0;
+ }
+ }
+ } else if (S_ISDIR(st.st_mode)) {
+ if (!cp_rflag) {
+ weprintf("%s is a directory\n", s1);
+ cp_status = 1;
+ return 0;
+ }
+ if (!(dp = opendir(s1))) {
+ weprintf("opendir %s:", s1);
+ cp_status = 1;
+ return 0;
+ }
+ if (mkdir(s2, st.st_mode) < 0 && errno != EEXIST) {
+ weprintf("mkdir %s:", s2);
+ cp_status = 1;
+ closedir(dp);
+ return 0;
+ }
+
+ while ((d = readdir(dp))) {
+ if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
+ continue;
+
+ estrlcpy(ns1, s1, sizeof(ns1));
+ if (s1[strlen(s1) - 1] != '/')
+ estrlcat(ns1, "/", sizeof(ns1));
+ estrlcat(ns1, d->d_name, sizeof(ns1));
+
+ estrlcpy(ns2, s2, sizeof(ns2));
+ if (s2[strlen(s2) - 1] != '/')
+ estrlcat(ns2, "/", sizeof(ns2));
+ estrlcat(ns2, d->d_name, sizeof(ns2));
+
+ fnck(ns1, ns2, cp, depth + 1);
+ }
+
+ closedir(dp);
+ } else if (cp_aflag && (S_ISBLK(st.st_mode) || S_ISCHR(st.st_mode) ||
+ S_ISSOCK(st.st_mode) || S_ISFIFO(st.st_mode))) {
+ if (cp_fflag && unlink(s2) < 0 && errno != ENOENT) {
+ weprintf("unlink %s:", s2);
+ cp_status = 1;
+ return 0;
+ } else if (mknod(s2, st.st_mode, st.st_rdev) < 0) {
+ weprintf("mknod %s:", s2);
+ cp_status = 1;
+ return 0;
+ }
+ } else {
+ if ((f1 = open(s1, O_RDONLY)) < 0) {
+ weprintf("open %s:", s1);
+ cp_status = 1;
+ return 0;
+ }
+ if ((f2 = creat(s2, st.st_mode)) < 0 && cp_fflag) {
+ if (unlink(s2) < 0 && errno != ENOENT) {
+ weprintf("unlink %s:", s2);
+ cp_status = 1;
+ close(f1);
+ return 0;
+ }
+ f2 = creat(s2, st.st_mode);
+ }
+ if (f2 < 0) {
+ weprintf("creat %s:", s2);
+ cp_status = 1;
+ close(f1);
+ return 0;
+ }
+ if (concat(f1, s1, f2, s2) < 0) {
+ cp_status = 1;
+ close(f1);
+ close(f2);
+ return 0;
+ }
+
+ close(f1);
+ close(f2);
+ }
+
+ if (cp_aflag || cp_pflag) {
+ /* atime and mtime */
+ times[0] = st.st_atim;
+ times[1] = st.st_mtim;
+ if (utimensat(AT_FDCWD, s2, times, AT_SYMLINK_NOFOLLOW) < 0) {
+ weprintf("utimensat %s:", s2);
+ cp_status = 1;
+ }
+
+ /* owner and mode */
+ if (!S_ISLNK(st.st_mode)) {
+ if (chown(s2, st.st_uid, st.st_gid) < 0) {
+ weprintf("chown %s:", s2);
+ cp_status = 1;
+ st.st_mode &= ~(S_ISUID | S_ISGID);
+ }
+ if (chmod(s2, st.st_mode) < 0) {
+ weprintf("chmod %s:", s2);
+ cp_status = 1;
+ }
+ } else {
+ if (lchown(s2, st.st_uid, st.st_gid) < 0) {
+ weprintf("lchown %s:", s2);
+ cp_status = 1;
+ return 0;
+ }
+ }
+ }
+
+ return 0;
+}
diff --git a/util/sbase/libutil/crypt.c b/util/sbase/libutil/crypt.c
new file mode 100644
index 00000000..e285614b
--- /dev/null
+++ b/util/sbase/libutil/crypt.c
@@ -0,0 +1,184 @@
+/* See LICENSE file for copyright and license details. */
+#include <fcntl.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "../crypt.h"
+#include "../text.h"
+#include "../util.h"
+
+static int
+hexdec(int c)
+{
+ if (c >= '0' && c <= '9')
+ return c - '0';
+ else if (c >= 'A' && c <= 'F')
+ return c - 'A' + 10;
+ else if (c >= 'a' && c <= 'f')
+ return c - 'a' + 10;
+ return -1; /* unknown character */
+}
+
+static int
+mdcheckline(const char *s, uint8_t *md, size_t sz)
+{
+ size_t i;
+ int b1, b2;
+
+ for (i = 0; i < sz; i++) {
+ if (!*s || (b1 = hexdec(*s++)) < 0)
+ return -1; /* invalid format */
+ if (!*s || (b2 = hexdec(*s++)) < 0)
+ return -1; /* invalid format */
+ if ((uint8_t)((b1 << 4) | b2) != md[i])
+ return 0; /* value mismatch */
+ }
+ return (i == sz) ? 1 : 0;
+}
+
+static void
+mdchecklist(FILE *listfp, struct crypt_ops *ops, uint8_t *md, size_t sz,
+ int *formatsucks, int *noread, int *nonmatch)
+{
+ int fd;
+ size_t bufsiz = 0;
+ int r;
+ char *line = NULL, *file, *p;
+
+ while (getline(&line, &bufsiz, listfp) > 0) {
+ if (!(file = strstr(line, " "))) {
+ (*formatsucks)++;
+ continue;
+ }
+ if ((file - line) / 2 != sz) {
+ (*formatsucks)++; /* checksum length mismatch */
+ continue;
+ }
+ *file = '\0';
+ file += 2;
+ for (p = file; *p && *p != '\n' && *p != '\r'; p++); /* strip newline */
+ *p = '\0';
+ if ((fd = open(file, O_RDONLY)) < 0) {
+ weprintf("open %s:", file);
+ (*noread)++;
+ continue;
+ }
+ if (cryptsum(ops, fd, file, md)) {
+ (*noread)++;
+ continue;
+ }
+ r = mdcheckline(line, md, sz);
+ if (r == 1) {
+ printf("%s: OK\n", file);
+ } else if (r == 0) {
+ printf("%s: FAILED\n", file);
+ (*nonmatch)++;
+ } else {
+ (*formatsucks)++;
+ }
+ close(fd);
+ }
+ free(line);
+}
+
+int
+cryptcheck(int argc, char *argv[], struct crypt_ops *ops, uint8_t *md, size_t sz)
+{
+ FILE *fp;
+ int formatsucks = 0, noread = 0, nonmatch = 0, ret = 0;
+
+ if (argc == 0) {
+ mdchecklist(stdin, ops, md, sz, &formatsucks, &noread, &nonmatch);
+ } else {
+ for (; *argv; argc--, argv++) {
+ if ((*argv)[0] == '-' && !(*argv)[1]) {
+ fp = stdin;
+ } else if (!(fp = fopen(*argv, "r"))) {
+ weprintf("fopen %s:", *argv);
+ ret = 1;
+ continue;
+ }
+ mdchecklist(fp, ops, md, sz, &formatsucks, &noread, &nonmatch);
+ if (fp != stdin)
+ fclose(fp);
+ }
+ }
+
+ if (formatsucks) {
+ weprintf("%d lines are improperly formatted\n", formatsucks);
+ ret = 1;
+ }
+ if (noread) {
+ weprintf("%d listed file could not be read\n", noread);
+ ret = 1;
+ }
+ if (nonmatch) {
+ weprintf("%d computed checksums did NOT match\n", nonmatch);
+ ret = 1;
+ }
+
+ return ret;
+}
+
+int
+cryptmain(int argc, char *argv[], struct crypt_ops *ops, uint8_t *md, size_t sz)
+{
+ int fd;
+ int ret = 0;
+
+ if (argc == 0) {
+ if (cryptsum(ops, 0, "<stdin>", md))
+ ret = 1;
+ else
+ mdprint(md, "<stdin>", sz);
+ } else {
+ for (; *argv; argc--, argv++) {
+ if ((*argv)[0] == '-' && !(*argv)[1]) {
+ *argv = "<stdin>";
+ fd = 0;
+ } else if ((fd = open(*argv, O_RDONLY)) < 0) {
+ weprintf("open %s:", *argv);
+ ret = 1;
+ continue;
+ }
+ if (cryptsum(ops, fd, *argv, md))
+ ret = 1;
+ else
+ mdprint(md, *argv, sz);
+ if (fd != 0)
+ close(fd);
+ }
+ }
+
+ return ret;
+}
+
+int
+cryptsum(struct crypt_ops *ops, int fd, const char *f, uint8_t *md)
+{
+ uint8_t buf[BUFSIZ];
+ ssize_t n;
+
+ ops->init(ops->s);
+ while ((n = read(fd, buf, sizeof(buf))) > 0)
+ ops->update(ops->s, buf, n);
+ if (n < 0) {
+ weprintf("%s: read error:", f);
+ return 1;
+ }
+ ops->sum(ops->s, md);
+ return 0;
+}
+
+void
+mdprint(const uint8_t *md, const char *f, size_t len)
+{
+ size_t i;
+
+ for (i = 0; i < len; i++)
+ printf("%02x", md[i]);
+ printf(" %s\n", f);
+}
diff --git a/util/sbase/libutil/ealloc.c b/util/sbase/libutil/ealloc.c
new file mode 100644
index 00000000..320865da
--- /dev/null
+++ b/util/sbase/libutil/ealloc.c
@@ -0,0 +1,88 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdlib.h>
+#include <string.h>
+
+#include "../util.h"
+
+void *
+ecalloc(size_t nmemb, size_t size)
+{
+ return encalloc(1, nmemb, size);
+}
+
+void *
+emalloc(size_t size)
+{
+ return enmalloc(1, size);
+}
+
+void *
+erealloc(void *p, size_t size)
+{
+ return enrealloc(1, p, size);
+}
+
+char *
+estrdup(const char *s)
+{
+ return enstrdup(1, s);
+}
+
+char *
+estrndup(const char *s, size_t n)
+{
+ return enstrndup(1, s, n);
+}
+
+void *
+encalloc(int status, size_t nmemb, size_t size)
+{
+ void *p;
+
+ p = calloc(nmemb, size);
+ if (!p)
+ enprintf(status, "calloc: out of memory\n");
+ return p;
+}
+
+void *
+enmalloc(int status, size_t size)
+{
+ void *p;
+
+ p = malloc(size);
+ if (!p)
+ enprintf(status, "malloc: out of memory\n");
+ return p;
+}
+
+void *
+enrealloc(int status, void *p, size_t size)
+{
+ p = realloc(p, size);
+ if (!p)
+ enprintf(status, "realloc: out of memory\n");
+ return p;
+}
+
+char *
+enstrdup(int status, const char *s)
+{
+ char *p;
+
+ p = strdup(s);
+ if (!p)
+ enprintf(status, "strdup: out of memory\n");
+ return p;
+}
+
+char *
+enstrndup(int status, const char *s, size_t n)
+{
+ char *p;
+
+ p = strndup(s, n);
+ if (!p)
+ enprintf(status, "strndup: out of memory\n");
+ return p;
+}
diff --git a/util/sbase/libutil/enmasse.c b/util/sbase/libutil/enmasse.c
new file mode 100644
index 00000000..a2e225ab
--- /dev/null
+++ b/util/sbase/libutil/enmasse.c
@@ -0,0 +1,38 @@
+/* See LICENSE file for copyright and license details. */
+#include <libgen.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "../util.h"
+
+void
+enmasse(int argc, char *argv[], int (*fn)(const char *, const char *, int))
+{
+ struct stat st;
+ char buf[PATH_MAX], *dir;
+ int i, len;
+ size_t dlen;
+
+ if (argc == 2 && !(stat(argv[1], &st) == 0 && S_ISDIR(st.st_mode))) {
+ fnck(argv[0], argv[1], fn, 0);
+ return;
+ } else {
+ dir = (argc == 1) ? "." : argv[--argc];
+ }
+
+ for (i = 0; i < argc; i++) {
+ dlen = strlen(dir);
+ if (dlen > 0 && dir[dlen - 1] == '/')
+ len = snprintf(buf, sizeof(buf), "%s%s", dir, basename(argv[i]));
+ else
+ len = snprintf(buf, sizeof(buf), "%s/%s", dir, basename(argv[i]));
+ if (len < 0 || len >= sizeof(buf)) {
+ eprintf("%s/%s: filename too long\n", dir,
+ basename(argv[i]));
+ }
+ fnck(argv[i], buf, fn, 0);
+ }
+}
diff --git a/util/sbase/libutil/eprintf.c b/util/sbase/libutil/eprintf.c
new file mode 100644
index 00000000..7197fbb9
--- /dev/null
+++ b/util/sbase/libutil/eprintf.c
@@ -0,0 +1,57 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "../util.h"
+
+char *argv0;
+
+void
+eprintf(const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ xvprintf(fmt, ap);
+ va_end(ap);
+
+ exit(1);
+}
+
+void
+enprintf(int status, const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ xvprintf(fmt, ap);
+ va_end(ap);
+
+ exit(status);
+}
+
+void
+weprintf(const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ xvprintf(fmt, ap);
+ va_end(ap);
+}
+
+void
+xvprintf(const char *fmt, va_list ap)
+{
+ if (argv0 && strncmp(fmt, "usage", strlen("usage")))
+ fprintf(stderr, "%s: ", argv0);
+
+ vfprintf(stderr, fmt, ap);
+
+ if (fmt[0] && fmt[strlen(fmt)-1] == ':') {
+ fputc(' ', stderr);
+ perror(NULL);
+ }
+}
diff --git a/util/sbase/libutil/eregcomp.c b/util/sbase/libutil/eregcomp.c
new file mode 100644
index 00000000..02c8698c
--- /dev/null
+++ b/util/sbase/libutil/eregcomp.c
@@ -0,0 +1,27 @@
+#include <sys/types.h>
+
+#include <regex.h>
+#include <stdio.h>
+
+#include "../util.h"
+
+int
+enregcomp(int status, regex_t *preg, const char *regex, int cflags)
+{
+ char errbuf[BUFSIZ] = "";
+ int r;
+
+ if ((r = regcomp(preg, regex, cflags)) == 0)
+ return r;
+
+ regerror(r, preg, errbuf, sizeof(errbuf));
+ enprintf(status, "invalid regex: %s\n", errbuf);
+
+ return r;
+}
+
+int
+eregcomp(regex_t *preg, const char *regex, int cflags)
+{
+ return enregcomp(1, preg, regex, cflags);
+}
diff --git a/util/sbase/libutil/estrtod.c b/util/sbase/libutil/estrtod.c
new file mode 100644
index 00000000..24e4fdce
--- /dev/null
+++ b/util/sbase/libutil/estrtod.c
@@ -0,0 +1,18 @@
+/* See LICENSE file for copyright and license details. */
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "../util.h"
+
+double
+estrtod(const char *s)
+{
+ char *end;
+ double d;
+
+ d = strtod(s, &end);
+ if (end == s || *end != '\0')
+ eprintf("%s: not a real number\n", s);
+ return d;
+}
diff --git a/util/sbase/libutil/fnck.c b/util/sbase/libutil/fnck.c
new file mode 100644
index 00000000..4f8875ba
--- /dev/null
+++ b/util/sbase/libutil/fnck.c
@@ -0,0 +1,22 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+
+#include "../util.h"
+
+void
+fnck(const char *a, const char *b,
+ int (*fn)(const char *, const char *, int), int depth)
+{
+ struct stat sta, stb;
+
+ if (!stat(a, &sta)
+ && !stat(b, &stb)
+ && sta.st_dev == stb.st_dev
+ && sta.st_ino == stb.st_ino) {
+ weprintf("%s -> %s: same file\n", a, b);
+ return;
+ }
+
+ if (fn(a, b, depth) < 0)
+ eprintf("%s -> %s:", a, b);
+}
diff --git a/util/sbase/libutil/fshut.c b/util/sbase/libutil/fshut.c
new file mode 100644
index 00000000..e596f074
--- /dev/null
+++ b/util/sbase/libutil/fshut.c
@@ -0,0 +1,43 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "../util.h"
+
+int
+fshut(FILE *fp, const char *fname)
+{
+ int ret = 0;
+
+ /* fflush() is undefined for input streams by ISO C,
+ * but not POSIX 2008 if you ignore ISO C overrides.
+ * Leave it unchecked and rely on the following
+ * functions to detect errors.
+ */
+ fflush(fp);
+
+ if (ferror(fp) && !ret) {
+ weprintf("ferror %s:", fname);
+ ret = 1;
+ }
+
+ if (fclose(fp) && !ret) {
+ weprintf("fclose %s:", fname);
+ ret = 1;
+ }
+
+ return ret;
+}
+
+void
+enfshut(int status, FILE *fp, const char *fname)
+{
+ if (fshut(fp, fname))
+ exit(status);
+}
+
+void
+efshut(FILE *fp, const char *fname)
+{
+ enfshut(1, fp, fname);
+}
diff --git a/util/sbase/libutil/getlines.c b/util/sbase/libutil/getlines.c
new file mode 100644
index 00000000..cef9a612
--- /dev/null
+++ b/util/sbase/libutil/getlines.c
@@ -0,0 +1,32 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "../text.h"
+#include "../util.h"
+
+void
+getlines(FILE *fp, struct linebuf *b)
+{
+ char *line = NULL;
+ size_t size = 0, linelen = 0;
+ ssize_t len;
+
+ while ((len = getline(&line, &size, fp)) > 0) {
+ if (++b->nlines > b->capacity) {
+ b->capacity += 512;
+ b->lines = ereallocarray(b->lines, b->capacity, sizeof(*b->lines));
+ }
+ linelen = len;
+ b->lines[b->nlines - 1].data = memcpy(emalloc(linelen + 1), line, linelen + 1);
+ b->lines[b->nlines - 1].len = linelen;
+ }
+ free(line);
+ if (b->lines && b->nlines && linelen && b->lines[b->nlines - 1].data[linelen - 1] != '\n') {
+ b->lines[b->nlines - 1].data = erealloc(b->lines[b->nlines - 1].data, linelen + 2);
+ b->lines[b->nlines - 1].data[linelen] = '\n';
+ b->lines[b->nlines - 1].data[linelen + 1] = '\0';
+ b->lines[b->nlines - 1].len++;
+ }
+}
diff --git a/util/sbase/libutil/human.c b/util/sbase/libutil/human.c
new file mode 100644
index 00000000..7e39ba5f
--- /dev/null
+++ b/util/sbase/libutil/human.c
@@ -0,0 +1,25 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdio.h>
+#include <string.h>
+#include <stdint.h>
+
+#include "../util.h"
+
+char *
+humansize(off_t n)
+{
+ static char buf[16];
+ const char postfixes[] = "BKMGTPE";
+ double size;
+ int i;
+
+ for (size = n, i = 0; size >= 1024 && i < strlen(postfixes); i++)
+ size /= 1024;
+
+ if (!i)
+ snprintf(buf, sizeof(buf), "%ju", (uintmax_t)n);
+ else
+ snprintf(buf, sizeof(buf), "%.1f%c", size, postfixes[i]);
+
+ return buf;
+}
diff --git a/util/sbase/libutil/linecmp.c b/util/sbase/libutil/linecmp.c
new file mode 100644
index 00000000..08fc0e3a
--- /dev/null
+++ b/util/sbase/libutil/linecmp.c
@@ -0,0 +1,17 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdio.h>
+#include <string.h>
+
+#include "../text.h"
+#include "../util.h"
+
+int
+linecmp(struct line *a, struct line *b)
+{
+ int res = 0;
+
+ if (!(res = memcmp(a->data, b->data, MIN(a->len, b->len))))
+ res = a->len - b->len;
+
+ return res;
+}
diff --git a/util/sbase/libutil/md5.c b/util/sbase/libutil/md5.c
new file mode 100644
index 00000000..c7483ac6
--- /dev/null
+++ b/util/sbase/libutil/md5.c
@@ -0,0 +1,148 @@
+/* public domain md5 implementation based on rfc1321 and libtomcrypt */
+#include <stdint.h>
+#include <string.h>
+
+#include "../md5.h"
+
+static uint32_t rol(uint32_t n, int k) { return (n << k) | (n >> (32-k)); }
+#define F(x,y,z) (z ^ (x & (y ^ z)))
+#define G(x,y,z) (y ^ (z & (y ^ x)))
+#define H(x,y,z) (x ^ y ^ z)
+#define I(x,y,z) (y ^ (x | ~z))
+#define FF(a,b,c,d,w,s,t) a += F(b,c,d) + w + t; a = rol(a,s) + b
+#define GG(a,b,c,d,w,s,t) a += G(b,c,d) + w + t; a = rol(a,s) + b
+#define HH(a,b,c,d,w,s,t) a += H(b,c,d) + w + t; a = rol(a,s) + b
+#define II(a,b,c,d,w,s,t) a += I(b,c,d) + w + t; a = rol(a,s) + b
+
+static const uint32_t tab[64] = {
+ 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
+ 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
+ 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
+ 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
+ 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
+ 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
+ 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
+ 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391
+};
+
+static void
+processblock(struct md5 *s, const uint8_t *buf)
+{
+ uint32_t i, W[16], a, b, c, d;
+
+ for (i = 0; i < 16; i++) {
+ W[i] = buf[4*i];
+ W[i] |= (uint32_t)buf[4*i+1]<<8;
+ W[i] |= (uint32_t)buf[4*i+2]<<16;
+ W[i] |= (uint32_t)buf[4*i+3]<<24;
+ }
+
+ a = s->h[0];
+ b = s->h[1];
+ c = s->h[2];
+ d = s->h[3];
+
+ i = 0;
+ while (i < 16) {
+ FF(a,b,c,d, W[i], 7, tab[i]); i++;
+ FF(d,a,b,c, W[i], 12, tab[i]); i++;
+ FF(c,d,a,b, W[i], 17, tab[i]); i++;
+ FF(b,c,d,a, W[i], 22, tab[i]); i++;
+ }
+ while (i < 32) {
+ GG(a,b,c,d, W[(5*i+1)%16], 5, tab[i]); i++;
+ GG(d,a,b,c, W[(5*i+1)%16], 9, tab[i]); i++;
+ GG(c,d,a,b, W[(5*i+1)%16], 14, tab[i]); i++;
+ GG(b,c,d,a, W[(5*i+1)%16], 20, tab[i]); i++;
+ }
+ while (i < 48) {
+ HH(a,b,c,d, W[(3*i+5)%16], 4, tab[i]); i++;
+ HH(d,a,b,c, W[(3*i+5)%16], 11, tab[i]); i++;
+ HH(c,d,a,b, W[(3*i+5)%16], 16, tab[i]); i++;
+ HH(b,c,d,a, W[(3*i+5)%16], 23, tab[i]); i++;
+ }
+ while (i < 64) {
+ II(a,b,c,d, W[7*i%16], 6, tab[i]); i++;
+ II(d,a,b,c, W[7*i%16], 10, tab[i]); i++;
+ II(c,d,a,b, W[7*i%16], 15, tab[i]); i++;
+ II(b,c,d,a, W[7*i%16], 21, tab[i]); i++;
+ }
+
+ s->h[0] += a;
+ s->h[1] += b;
+ s->h[2] += c;
+ s->h[3] += d;
+}
+
+static void
+pad(struct md5 *s)
+{
+ unsigned r = s->len % 64;
+
+ s->buf[r++] = 0x80;
+ if (r > 56) {
+ memset(s->buf + r, 0, 64 - r);
+ r = 0;
+ processblock(s, s->buf);
+ }
+ memset(s->buf + r, 0, 56 - r);
+ s->len *= 8;
+ s->buf[56] = s->len;
+ s->buf[57] = s->len >> 8;
+ s->buf[58] = s->len >> 16;
+ s->buf[59] = s->len >> 24;
+ s->buf[60] = s->len >> 32;
+ s->buf[61] = s->len >> 40;
+ s->buf[62] = s->len >> 48;
+ s->buf[63] = s->len >> 56;
+ processblock(s, s->buf);
+}
+
+void
+md5_init(void *ctx)
+{
+ struct md5 *s = ctx;
+ s->len = 0;
+ s->h[0] = 0x67452301;
+ s->h[1] = 0xefcdab89;
+ s->h[2] = 0x98badcfe;
+ s->h[3] = 0x10325476;
+}
+
+void
+md5_sum(void *ctx, uint8_t md[MD5_DIGEST_LENGTH])
+{
+ struct md5 *s = ctx;
+ int i;
+
+ pad(s);
+ for (i = 0; i < 4; i++) {
+ md[4*i] = s->h[i];
+ md[4*i+1] = s->h[i] >> 8;
+ md[4*i+2] = s->h[i] >> 16;
+ md[4*i+3] = s->h[i] >> 24;
+ }
+}
+
+void
+md5_update(void *ctx, const void *m, unsigned long len)
+{
+ struct md5 *s = ctx;
+ const uint8_t *p = m;
+ unsigned r = s->len % 64;
+
+ s->len += len;
+ if (r) {
+ if (len < 64 - r) {
+ memcpy(s->buf + r, p, len);
+ return;
+ }
+ memcpy(s->buf + r, p, 64 - r);
+ len -= 64 - r;
+ p += 64 - r;
+ processblock(s, s->buf);
+ }
+ for (; len >= 64; len -= 64, p += 64)
+ processblock(s, p);
+ memcpy(s->buf, p, len);
+}
diff --git a/util/sbase/libutil/memmem.c b/util/sbase/libutil/memmem.c
new file mode 100644
index 00000000..7dfef34b
--- /dev/null
+++ b/util/sbase/libutil/memmem.c
@@ -0,0 +1,66 @@
+/* $OpenBSD: memmem.c,v 1.4 2015/08/31 02:53:57 guenther Exp $ */
+
+/*
+ * Copyright (c) 2005 Pascal Gloor <pascal.gloor@spale.com>
+ *
+ * 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.
+ * 3. The name of the author may not be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * 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 <string.h>
+
+#include "../util.h"
+
+/*
+ * Find the first occurrence of the byte string s in byte string l.
+ */
+
+void *
+memmem(const void *l, size_t l_len, const void *s, size_t s_len)
+{
+ const char *cur, *last;
+ const char *cl = l;
+ const char *cs = s;
+
+ /* a zero length needle should just return the haystack */
+ if (s_len == 0)
+ return (void *)cl;
+
+ /* "s" must be smaller or equal to "l" */
+ if (l_len < s_len)
+ return NULL;
+
+ /* special case where s_len == 1 */
+ if (s_len == 1)
+ return memchr(l, *cs, l_len);
+
+ /* the last position where its possible to find "s" in "l" */
+ last = cl + l_len - s_len;
+
+ for (cur = cl; cur <= last; cur++)
+ if (cur[0] == cs[0] && memcmp(cur, cs, s_len) == 0)
+ return (void *)cur;
+
+ return NULL;
+}
diff --git a/util/sbase/libutil/mkdirp.c b/util/sbase/libutil/mkdirp.c
new file mode 100644
index 00000000..c3c678c0
--- /dev/null
+++ b/util/sbase/libutil/mkdirp.c
@@ -0,0 +1,39 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+
+#include <errno.h>
+#include <limits.h>
+
+#include "../util.h"
+
+int
+mkdirp(const char *path, mode_t mode, mode_t pmode)
+{
+ char tmp[PATH_MAX], *p;
+ struct stat st;
+
+ if (stat(path, &st) == 0) {
+ if (S_ISDIR(st.st_mode))
+ return 0;
+ errno = ENOTDIR;
+ weprintf("%s:", path);
+ return -1;
+ }
+
+ estrlcpy(tmp, path, sizeof(tmp));
+ for (p = tmp + (tmp[0] == '/'); *p; p++) {
+ if (*p != '/')
+ continue;
+ *p = '\0';
+ if (mkdir(tmp, pmode) < 0 && errno != EEXIST) {
+ weprintf("mkdir %s:", tmp);
+ return -1;
+ }
+ *p = '/';
+ }
+ if (mkdir(tmp, mode) < 0 && errno != EEXIST) {
+ weprintf("mkdir %s:", tmp);
+ return -1;
+ }
+ return 0;
+}
diff --git a/util/sbase/libutil/mode.c b/util/sbase/libutil/mode.c
new file mode 100644
index 00000000..2754be79
--- /dev/null
+++ b/util/sbase/libutil/mode.c
@@ -0,0 +1,152 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "../util.h"
+
+mode_t
+getumask(void)
+{
+ mode_t mask = umask(0);
+ umask(mask);
+ return mask;
+}
+
+mode_t
+parsemode(const char *str, mode_t mode, mode_t mask)
+{
+ char *end;
+ const char *p = str;
+ int octal, op;
+ mode_t who, perm, clear;
+
+ octal = strtol(str, &end, 8);
+ if (*end == '\0') {
+ if (octal < 0 || octal > 07777)
+ eprintf("%s: invalid mode\n", str);
+ return octal;
+ }
+next:
+ /* first, determine which bits we will be modifying */
+ for (who = 0; *p; p++) {
+ switch (*p) {
+ /* masks */
+ case 'u':
+ who |= S_IRWXU|S_ISUID;
+ continue;
+ case 'g':
+ who |= S_IRWXG|S_ISGID;
+ continue;
+ case 'o':
+ who |= S_IRWXO|S_ISVTX;
+ continue;
+ case 'a':
+ who |= S_IRWXU|S_ISUID|S_IRWXG|S_ISGID|S_IRWXO|S_ISVTX;
+ continue;
+ }
+ break;
+ }
+ if (who) {
+ clear = who;
+ } else {
+ clear = S_ISUID|S_ISGID|S_ISVTX|S_IRWXU|S_IRWXG|S_IRWXO;
+ who = ~mask;
+ }
+ while (*p) {
+ switch (*p) {
+ /* opers */
+ case '=':
+ case '+':
+ case '-':
+ op = (int)*p;
+ break;
+ default:
+ eprintf("%s: invalid mode\n", str);
+ }
+
+ perm = 0;
+ switch (*++p) {
+ /* copy */
+ case 'u':
+ if (mode & S_IRUSR)
+ perm |= S_IRUSR|S_IRGRP|S_IROTH;
+ if (mode & S_IWUSR)
+ perm |= S_IWUSR|S_IWGRP|S_IWOTH;
+ if (mode & S_IXUSR)
+ perm |= S_IXUSR|S_IXGRP|S_IXOTH;
+ if (mode & S_ISUID)
+ perm |= S_ISUID|S_ISGID;
+ p++;
+ break;
+ case 'g':
+ if (mode & S_IRGRP)
+ perm |= S_IRUSR|S_IRGRP|S_IROTH;
+ if (mode & S_IWGRP)
+ perm |= S_IWUSR|S_IWGRP|S_IWOTH;
+ if (mode & S_IXGRP)
+ perm |= S_IXUSR|S_IXGRP|S_IXOTH;
+ if (mode & S_ISGID)
+ perm |= S_ISUID|S_ISGID;
+ p++;
+ break;
+ case 'o':
+ if (mode & S_IROTH)
+ perm |= S_IRUSR|S_IRGRP|S_IROTH;
+ if (mode & S_IWOTH)
+ perm |= S_IWUSR|S_IWGRP|S_IWOTH;
+ if (mode & S_IXOTH)
+ perm |= S_IXUSR|S_IXGRP|S_IXOTH;
+ p++;
+ break;
+ default:
+ for (; *p; p++) {
+ switch (*p) {
+ /* modes */
+ case 'r':
+ perm |= S_IRUSR|S_IRGRP|S_IROTH;
+ break;
+ case 'w':
+ perm |= S_IWUSR|S_IWGRP|S_IWOTH;
+ break;
+ case 'x':
+ perm |= S_IXUSR|S_IXGRP|S_IXOTH;
+ break;
+ case 'X':
+ if (S_ISDIR(mode) || mode & (S_IXUSR|S_IXGRP|S_IXOTH))
+ perm |= S_IXUSR|S_IXGRP|S_IXOTH;
+ break;
+ case 's':
+ perm |= S_ISUID|S_ISGID;
+ break;
+ case 't':
+ perm |= S_ISVTX;
+ break;
+ default:
+ goto apply;
+ }
+ }
+ }
+
+apply:
+ /* apply */
+ switch (op) {
+ case '=':
+ mode &= ~clear;
+ /* fallthrough */
+ case '+':
+ mode |= perm & who;
+ break;
+ case '-':
+ mode &= ~(perm & who);
+ break;
+ }
+ /* if we hit a comma, move on to the next clause */
+ if (*p == ',') {
+ p++;
+ goto next;
+ }
+ }
+ return mode & ~S_IFMT;
+}
diff --git a/util/sbase/libutil/parseoffset.c b/util/sbase/libutil/parseoffset.c
new file mode 100644
index 00000000..362a7829
--- /dev/null
+++ b/util/sbase/libutil/parseoffset.c
@@ -0,0 +1,61 @@
+/* See LICENSE file for copyright and license details. */
+#include <ctype.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "../util.h"
+
+off_t
+parseoffset(const char *str)
+{
+ off_t res, scale = 1;
+ char *end;
+
+ /* strictly check what strtol() usually would let pass */
+ if (!str || !*str || isspace(*str) || *str == '+' || *str == '-') {
+ weprintf("parseoffset %s: invalid value\n", str);
+ return -1;
+ }
+
+ errno = 0;
+ res = strtol(str, &end, 0);
+ if (errno) {
+ weprintf("parseoffset %s: invalid value\n", str);
+ return -1;
+ }
+ if (res < 0) {
+ weprintf("parseoffset %s: negative value\n", str);
+ return -1;
+ }
+
+ /* suffix */
+ if (*end) {
+ switch (toupper((int)*end)) {
+ case 'B':
+ scale = 512L;
+ break;
+ case 'K':
+ scale = 1024L;
+ break;
+ case 'M':
+ scale = 1024L * 1024L;
+ break;
+ case 'G':
+ scale = 1024L * 1024L * 1024L;
+ break;
+ default:
+ weprintf("parseoffset %s: invalid suffix '%s'\n", str, end);
+ return -1;
+ }
+ }
+
+ /* prevent overflow */
+ if (res > (SSIZE_MAX / scale)) {
+ weprintf("parseoffset %s: out of range\n", str);
+ return -1;
+ }
+
+ return res * scale;
+}
diff --git a/util/sbase/libutil/putword.c b/util/sbase/libutil/putword.c
new file mode 100644
index 00000000..80a9860a
--- /dev/null
+++ b/util/sbase/libutil/putword.c
@@ -0,0 +1,16 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdio.h>
+
+#include "../util.h"
+
+void
+putword(FILE *fp, const char *s)
+{
+ static int first = 1;
+
+ if (!first)
+ fputc(' ', fp);
+
+ fputs(s, fp);
+ first = 0;
+}
diff --git a/util/sbase/libutil/reallocarray.c b/util/sbase/libutil/reallocarray.c
new file mode 100644
index 00000000..31ff6c31
--- /dev/null
+++ b/util/sbase/libutil/reallocarray.c
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2008 Otto Moerbeek <otto@drijf.net>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <errno.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#include "../util.h"
+
+/*
+ * This is sqrt(SIZE_MAX+1), as s1*s2 <= SIZE_MAX
+ * if both s1 < MUL_NO_OVERFLOW and s2 < MUL_NO_OVERFLOW
+ */
+#define MUL_NO_OVERFLOW (1UL << (sizeof(size_t) * 4))
+
+void *
+reallocarray(void *optr, size_t nmemb, size_t size)
+{
+ if ((nmemb >= MUL_NO_OVERFLOW || size >= MUL_NO_OVERFLOW) &&
+ nmemb > 0 && SIZE_MAX / nmemb < size) {
+ errno = ENOMEM;
+ return NULL;
+ }
+ return realloc(optr, size * nmemb);
+}
+
+void *
+ereallocarray(void *optr, size_t nmemb, size_t size)
+{
+ return enreallocarray(1, optr, nmemb, size);
+}
+
+void *
+enreallocarray(int status, void *optr, size_t nmemb, size_t size)
+{
+ void *p;
+
+ if (!(p = reallocarray(optr, nmemb, size)))
+ enprintf(status, "reallocarray: out of memory\n");
+
+ return p;
+}
diff --git a/util/sbase/libutil/recurse.c b/util/sbase/libutil/recurse.c
new file mode 100644
index 00000000..e66efaf5
--- /dev/null
+++ b/util/sbase/libutil/recurse.c
@@ -0,0 +1,108 @@
+/* See LICENSE file for copyright and license details. */
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "../fs.h"
+#include "../util.h"
+
+int recurse_status = 0;
+
+void
+recurse(int dirfd, const char *name, void *data, struct recursor *r)
+{
+ struct dirent *d;
+ struct history *new, *h;
+ struct stat st, dst;
+ DIR *dp;
+ int flags = 0, fd;
+ size_t pathlen = r->pathlen;
+
+ if (dirfd == AT_FDCWD)
+ pathlen = estrlcpy(r->path, name, sizeof(r->path));
+
+ if (r->follow == 'P' || (r->follow == 'H' && r->depth))
+ flags |= AT_SYMLINK_NOFOLLOW;
+
+ if (fstatat(dirfd, name, &st, flags) < 0) {
+ if (!(r->flags & SILENT)) {
+ weprintf("stat %s:", r->path);
+ recurse_status = 1;
+ }
+ return;
+ }
+ if (!S_ISDIR(st.st_mode)) {
+ r->fn(dirfd, name, &st, data, r);
+ return;
+ }
+
+ new = emalloc(sizeof(struct history));
+ new->prev = r->hist;
+ r->hist = new;
+ new->dev = st.st_dev;
+ new->ino = st.st_ino;
+
+ for (h = new->prev; h; h = h->prev)
+ if (h->ino == st.st_ino && h->dev == st.st_dev)
+ return;
+
+ if (!r->depth && (r->flags & DIRFIRST))
+ r->fn(dirfd, name, &st, data, r);
+
+ if (!r->maxdepth || r->depth + 1 < r->maxdepth) {
+ fd = openat(dirfd, name, O_RDONLY | O_CLOEXEC | O_DIRECTORY);
+ if (fd < 0) {
+ weprintf("open %s:", r->path);
+ recurse_status = 1;
+ }
+ if (!(dp = fdopendir(fd))) {
+ if (!(r->flags & SILENT)) {
+ weprintf("fdopendir:");
+ recurse_status = 1;
+ }
+ return;
+ }
+ if (r->path[pathlen - 1] != '/')
+ r->path[pathlen++] = '/';
+ if (r->follow == 'H')
+ flags |= AT_SYMLINK_NOFOLLOW;
+ while ((d = readdir(dp))) {
+ if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
+ continue;
+ r->pathlen = pathlen + estrlcpy(r->path + pathlen, d->d_name, sizeof(r->path) - pathlen);
+ if (fstatat(fd, d->d_name, &dst, flags) < 0) {
+ if (!(r->flags & SILENT)) {
+ weprintf("stat %s:", r->path);
+ recurse_status = 1;
+ }
+ } else if ((r->flags & SAMEDEV) && dst.st_dev != st.st_dev) {
+ continue;
+ } else {
+ r->depth++;
+ r->fn(fd, d->d_name, &dst, data, r);
+ r->depth--;
+ }
+ }
+ r->path[pathlen - 1] = '\0';
+ r->pathlen = pathlen - 1;
+ closedir(dp);
+ }
+
+ if (!r->depth) {
+ if (!(r->flags & DIRFIRST))
+ r->fn(dirfd, name, &st, data, r);
+
+ while (r->hist) {
+ h = r->hist;
+ r->hist = r->hist->prev;
+ free(h);
+ }
+ }
+}
diff --git a/util/sbase/libutil/rm.c b/util/sbase/libutil/rm.c
new file mode 100644
index 00000000..fb99840d
--- /dev/null
+++ b/util/sbase/libutil/rm.c
@@ -0,0 +1,49 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include "../fs.h"
+#include "../util.h"
+
+int rm_status = 0;
+
+void
+rm(int dirfd, const char *name, struct stat *st, void *data, struct recursor *r)
+{
+ int quiet, ask, write, flags, ignore;
+
+ ignore = r->flags & IGNORE;
+ quiet = r->flags & SILENT;
+ ask = r->flags & CONFIRM;
+ write = faccessat(dirfd, name, W_OK, 0) == 0;
+ flags = 0;
+
+ if (S_ISDIR(st->st_mode) && r->maxdepth) {
+ errno = EISDIR;
+ goto err;
+ }
+
+ if (!quiet && (!write && isatty(0) || ask)) {
+ if (!confirm("remove file '%s'", r->path));
+ return;
+ }
+
+ if (S_ISDIR(st->st_mode)) {
+ flags = AT_REMOVEDIR;
+ recurse(dirfd, name, NULL, r);
+ }
+
+ if (unlinkat(dirfd, name, flags) < 0)
+ goto err;
+ return;
+
+err:
+ if (!ignore) {
+ weprintf("cannot remove '%s':", r->path);
+ rm_status = 1;
+ }
+}
diff --git a/util/sbase/libutil/sha1.c b/util/sbase/libutil/sha1.c
new file mode 100644
index 00000000..3d76a1be
--- /dev/null
+++ b/util/sbase/libutil/sha1.c
@@ -0,0 +1,144 @@
+/* public domain sha1 implementation based on rfc3174 and libtomcrypt */
+#include <stdint.h>
+#include <string.h>
+
+#include "../sha1.h"
+
+static uint32_t rol(uint32_t n, int k) { return (n << k) | (n >> (32-k)); }
+#define F0(b,c,d) (d ^ (b & (c ^ d)))
+#define F1(b,c,d) (b ^ c ^ d)
+#define F2(b,c,d) ((b & c) | (d & (b | c)))
+#define F3(b,c,d) (b ^ c ^ d)
+#define G0(a,b,c,d,e,i) e += rol(a,5)+F0(b,c,d)+W[i]+0x5A827999; b = rol(b,30)
+#define G1(a,b,c,d,e,i) e += rol(a,5)+F1(b,c,d)+W[i]+0x6ED9EBA1; b = rol(b,30)
+#define G2(a,b,c,d,e,i) e += rol(a,5)+F2(b,c,d)+W[i]+0x8F1BBCDC; b = rol(b,30)
+#define G3(a,b,c,d,e,i) e += rol(a,5)+F3(b,c,d)+W[i]+0xCA62C1D6; b = rol(b,30)
+
+static void
+processblock(struct sha1 *s, const uint8_t *buf)
+{
+ uint32_t W[80], a, b, c, d, e;
+ int i;
+
+ for (i = 0; i < 16; i++) {
+ W[i] = (uint32_t)buf[4*i]<<24;
+ W[i] |= (uint32_t)buf[4*i+1]<<16;
+ W[i] |= (uint32_t)buf[4*i+2]<<8;
+ W[i] |= buf[4*i+3];
+ }
+ for (; i < 80; i++)
+ W[i] = rol(W[i-3] ^ W[i-8] ^ W[i-14] ^ W[i-16], 1);
+ a = s->h[0];
+ b = s->h[1];
+ c = s->h[2];
+ d = s->h[3];
+ e = s->h[4];
+ for (i = 0; i < 20; ) {
+ G0(a,b,c,d,e,i++);
+ G0(e,a,b,c,d,i++);
+ G0(d,e,a,b,c,i++);
+ G0(c,d,e,a,b,i++);
+ G0(b,c,d,e,a,i++);
+ }
+ while (i < 40) {
+ G1(a,b,c,d,e,i++);
+ G1(e,a,b,c,d,i++);
+ G1(d,e,a,b,c,i++);
+ G1(c,d,e,a,b,i++);
+ G1(b,c,d,e,a,i++);
+ }
+ while (i < 60) {
+ G2(a,b,c,d,e,i++);
+ G2(e,a,b,c,d,i++);
+ G2(d,e,a,b,c,i++);
+ G2(c,d,e,a,b,i++);
+ G2(b,c,d,e,a,i++);
+ }
+ while (i < 80) {
+ G3(a,b,c,d,e,i++);
+ G3(e,a,b,c,d,i++);
+ G3(d,e,a,b,c,i++);
+ G3(c,d,e,a,b,i++);
+ G3(b,c,d,e,a,i++);
+ }
+ s->h[0] += a;
+ s->h[1] += b;
+ s->h[2] += c;
+ s->h[3] += d;
+ s->h[4] += e;
+}
+
+static void
+pad(struct sha1 *s)
+{
+ unsigned r = s->len % 64;
+
+ s->buf[r++] = 0x80;
+ if (r > 56) {
+ memset(s->buf + r, 0, 64 - r);
+ r = 0;
+ processblock(s, s->buf);
+ }
+ memset(s->buf + r, 0, 56 - r);
+ s->len *= 8;
+ s->buf[56] = s->len >> 56;
+ s->buf[57] = s->len >> 48;
+ s->buf[58] = s->len >> 40;
+ s->buf[59] = s->len >> 32;
+ s->buf[60] = s->len >> 24;
+ s->buf[61] = s->len >> 16;
+ s->buf[62] = s->len >> 8;
+ s->buf[63] = s->len;
+ processblock(s, s->buf);
+}
+
+void
+sha1_init(void *ctx)
+{
+ struct sha1 *s = ctx;
+
+ s->len = 0;
+ s->h[0] = 0x67452301;
+ s->h[1] = 0xEFCDAB89;
+ s->h[2] = 0x98BADCFE;
+ s->h[3] = 0x10325476;
+ s->h[4] = 0xC3D2E1F0;
+}
+
+void
+sha1_sum(void *ctx, uint8_t md[SHA1_DIGEST_LENGTH])
+{
+ struct sha1 *s = ctx;
+ int i;
+
+ pad(s);
+ for (i = 0; i < 5; i++) {
+ md[4*i] = s->h[i] >> 24;
+ md[4*i+1] = s->h[i] >> 16;
+ md[4*i+2] = s->h[i] >> 8;
+ md[4*i+3] = s->h[i];
+ }
+}
+
+void
+sha1_update(void *ctx, const void *m, unsigned long len)
+{
+ struct sha1 *s = ctx;
+ const uint8_t *p = m;
+ unsigned r = s->len % 64;
+
+ s->len += len;
+ if (r) {
+ if (len < 64 - r) {
+ memcpy(s->buf + r, p, len);
+ return;
+ }
+ memcpy(s->buf + r, p, 64 - r);
+ len -= 64 - r;
+ p += 64 - r;
+ processblock(s, s->buf);
+ }
+ for (; len >= 64; len -= 64, p += 64)
+ processblock(s, p);
+ memcpy(s->buf, p, len);
+}
diff --git a/util/sbase/libutil/sha224.c b/util/sbase/libutil/sha224.c
new file mode 100644
index 00000000..fce520f5
--- /dev/null
+++ b/util/sbase/libutil/sha224.c
@@ -0,0 +1,26 @@
+/* public domain sha224 implementation based on fips180-3 */
+#include <stdint.h>
+#include "../sha224.h"
+
+extern void sha256_sum_n(void *, uint8_t *, int n);
+
+void
+sha224_init(void *ctx)
+{
+ struct sha224 *s = ctx;
+ s->len = 0;
+ s->h[0] = 0xc1059ed8;
+ s->h[1] = 0x367cd507;
+ s->h[2] = 0x3070dd17;
+ s->h[3] = 0xf70e5939;
+ s->h[4] = 0xffc00b31;
+ s->h[5] = 0x68581511;
+ s->h[6] = 0x64f98fa7;
+ s->h[7] = 0xbefa4fa4;
+}
+
+void
+sha224_sum(void *ctx, uint8_t md[SHA224_DIGEST_LENGTH])
+{
+ sha256_sum_n(ctx, md, 8);
+}
diff --git a/util/sbase/libutil/sha256.c b/util/sbase/libutil/sha256.c
new file mode 100644
index 00000000..266cfecb
--- /dev/null
+++ b/util/sbase/libutil/sha256.c
@@ -0,0 +1,154 @@
+/* public domain sha256 implementation based on fips180-3 */
+#include <ctype.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "../sha256.h"
+
+static uint32_t ror(uint32_t n, int k) { return (n >> k) | (n << (32-k)); }
+#define Ch(x,y,z) (z ^ (x & (y ^ z)))
+#define Maj(x,y,z) ((x & y) | (z & (x | y)))
+#define S0(x) (ror(x,2) ^ ror(x,13) ^ ror(x,22))
+#define S1(x) (ror(x,6) ^ ror(x,11) ^ ror(x,25))
+#define R0(x) (ror(x,7) ^ ror(x,18) ^ (x>>3))
+#define R1(x) (ror(x,17) ^ ror(x,19) ^ (x>>10))
+
+static const uint32_t K[64] = {
+0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
+0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
+0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
+0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
+0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
+0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
+0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
+0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
+};
+
+static void
+processblock(struct sha256 *s, const uint8_t *buf)
+{
+ uint32_t W[64], t1, t2, a, b, c, d, e, f, g, h;
+ int i;
+
+ for (i = 0; i < 16; i++) {
+ W[i] = (uint32_t)buf[4*i]<<24;
+ W[i] |= (uint32_t)buf[4*i+1]<<16;
+ W[i] |= (uint32_t)buf[4*i+2]<<8;
+ W[i] |= buf[4*i+3];
+ }
+ for (; i < 64; i++)
+ W[i] = R1(W[i-2]) + W[i-7] + R0(W[i-15]) + W[i-16];
+ a = s->h[0];
+ b = s->h[1];
+ c = s->h[2];
+ d = s->h[3];
+ e = s->h[4];
+ f = s->h[5];
+ g = s->h[6];
+ h = s->h[7];
+ for (i = 0; i < 64; i++) {
+ t1 = h + S1(e) + Ch(e,f,g) + K[i] + W[i];
+ t2 = S0(a) + Maj(a,b,c);
+ h = g;
+ g = f;
+ f = e;
+ e = d + t1;
+ d = c;
+ c = b;
+ b = a;
+ a = t1 + t2;
+ }
+ s->h[0] += a;
+ s->h[1] += b;
+ s->h[2] += c;
+ s->h[3] += d;
+ s->h[4] += e;
+ s->h[5] += f;
+ s->h[6] += g;
+ s->h[7] += h;
+}
+
+static void
+pad(struct sha256 *s)
+{
+ unsigned r = s->len % 64;
+
+ s->buf[r++] = 0x80;
+ if (r > 56) {
+ memset(s->buf + r, 0, 64 - r);
+ r = 0;
+ processblock(s, s->buf);
+ }
+ memset(s->buf + r, 0, 56 - r);
+ s->len *= 8;
+ s->buf[56] = s->len >> 56;
+ s->buf[57] = s->len >> 48;
+ s->buf[58] = s->len >> 40;
+ s->buf[59] = s->len >> 32;
+ s->buf[60] = s->len >> 24;
+ s->buf[61] = s->len >> 16;
+ s->buf[62] = s->len >> 8;
+ s->buf[63] = s->len;
+ processblock(s, s->buf);
+}
+
+void
+sha256_init(void *ctx)
+{
+ struct sha256 *s = ctx;
+ s->len = 0;
+ s->h[0] = 0x6a09e667;
+ s->h[1] = 0xbb67ae85;
+ s->h[2] = 0x3c6ef372;
+ s->h[3] = 0xa54ff53a;
+ s->h[4] = 0x510e527f;
+ s->h[5] = 0x9b05688c;
+ s->h[6] = 0x1f83d9ab;
+ s->h[7] = 0x5be0cd19;
+}
+
+void
+sha256_sum_n(void *ctx, uint8_t *md, int n)
+{
+ struct sha256 *s = ctx;
+ int i;
+
+ pad(s);
+ for (i = 0; i < n; i++) {
+ md[4*i] = s->h[i] >> 24;
+ md[4*i+1] = s->h[i] >> 16;
+ md[4*i+2] = s->h[i] >> 8;
+ md[4*i+3] = s->h[i];
+ }
+}
+
+void
+sha256_sum(void *ctx, uint8_t md[SHA256_DIGEST_LENGTH])
+{
+ sha256_sum_n(ctx, md, 8);
+}
+
+void
+sha256_update(void *ctx, const void *m, unsigned long len)
+{
+ struct sha256 *s = ctx;
+ const uint8_t *p = m;
+ unsigned r = s->len % 64;
+
+ s->len += len;
+ if (r) {
+ if (len < 64 - r) {
+ memcpy(s->buf + r, p, len);
+ return;
+ }
+ memcpy(s->buf + r, p, 64 - r);
+ len -= 64 - r;
+ p += 64 - r;
+ processblock(s, s->buf);
+ }
+ for (; len >= 64; len -= 64, p += 64)
+ processblock(s, p);
+ memcpy(s->buf, p, len);
+}
diff --git a/util/sbase/libutil/sha384.c b/util/sbase/libutil/sha384.c
new file mode 100644
index 00000000..0a0e7777
--- /dev/null
+++ b/util/sbase/libutil/sha384.c
@@ -0,0 +1,26 @@
+/* public domain sha384 implementation based on fips180-3 */
+#include <stdint.h>
+#include "../sha384.h"
+
+extern void sha512_sum_n(void *, uint8_t *, int n);
+
+void
+sha384_init(void *ctx)
+{
+ struct sha384 *s = ctx;
+ s->len = 0;
+ s->h[0] = 0xcbbb9d5dc1059ed8ULL;
+ s->h[1] = 0x629a292a367cd507ULL;
+ s->h[2] = 0x9159015a3070dd17ULL;
+ s->h[3] = 0x152fecd8f70e5939ULL;
+ s->h[4] = 0x67332667ffc00b31ULL;
+ s->h[5] = 0x8eb44a8768581511ULL;
+ s->h[6] = 0xdb0c2e0d64f98fa7ULL;
+ s->h[7] = 0x47b5481dbefa4fa4ULL;
+}
+
+void
+sha384_sum(void *ctx, uint8_t md[SHA384_DIGEST_LENGTH])
+{
+ sha512_sum_n(ctx, md, 6);
+}
diff --git a/util/sbase/libutil/sha512-224.c b/util/sbase/libutil/sha512-224.c
new file mode 100644
index 00000000..a5636c13
--- /dev/null
+++ b/util/sbase/libutil/sha512-224.c
@@ -0,0 +1,26 @@
+/* public domain sha512/224 implementation based on fips180-3 */
+#include <stdint.h>
+#include "../sha512-224.h"
+
+extern void sha512_sum_n(void *, uint8_t *, int n);
+
+void
+sha512_224_init(void *ctx)
+{
+ struct sha512_224 *s = ctx;
+ s->len = 0;
+ s->h[0] = 0x8c3d37c819544da2ULL;
+ s->h[1] = 0x73e1996689dcd4d6ULL;
+ s->h[2] = 0x1dfab7ae32ff9c82ULL;
+ s->h[3] = 0x679dd514582f9fcfULL;
+ s->h[4] = 0x0f6d2b697bd44da8ULL;
+ s->h[5] = 0x77e36f7304c48942ULL;
+ s->h[6] = 0x3f9d85a86a1d36c8ULL;
+ s->h[7] = 0x1112e6ad91d692a1ULL;
+}
+
+void
+sha512_224_sum(void *ctx, uint8_t md[SHA512_224_DIGEST_LENGTH])
+{
+ sha512_sum_n(ctx, md, 4);
+}
diff --git a/util/sbase/libutil/sha512-256.c b/util/sbase/libutil/sha512-256.c
new file mode 100644
index 00000000..d4b84495
--- /dev/null
+++ b/util/sbase/libutil/sha512-256.c
@@ -0,0 +1,26 @@
+/* public domain sha512/256 implementation based on fips180-3 */
+#include <stdint.h>
+#include "../sha512-256.h"
+
+extern void sha512_sum_n(void *, uint8_t *, int n);
+
+void
+sha512_256_init(void *ctx)
+{
+ struct sha512_256 *s = ctx;
+ s->len = 0;
+ s->h[0] = 0x22312194fc2bf72cULL;
+ s->h[1] = 0x9f555fa3c84c64c2ULL;
+ s->h[2] = 0x2393b86b6f53b151ULL;
+ s->h[3] = 0x963877195940eabdULL;
+ s->h[4] = 0x96283ee2a88effe3ULL;
+ s->h[5] = 0xbe5e1e2553863992ULL;
+ s->h[6] = 0x2b0199fc2c85b8aaULL;
+ s->h[7] = 0x0eb72ddc81c52ca2ULL;
+}
+
+void
+sha512_256_sum(void *ctx, uint8_t md[SHA512_256_DIGEST_LENGTH])
+{
+ sha512_sum_n(ctx, md, 4);
+}
diff --git a/util/sbase/libutil/sha512.c b/util/sbase/libutil/sha512.c
new file mode 100644
index 00000000..25264c78
--- /dev/null
+++ b/util/sbase/libutil/sha512.c
@@ -0,0 +1,175 @@
+/* public domain sha256 implementation based on fips180-3 */
+
+#include <ctype.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "../sha512.h"
+
+static uint64_t ror(uint64_t n, int k) { return (n >> k) | (n << (64-k)); }
+#define Ch(x,y,z) (z ^ (x & (y ^ z)))
+#define Maj(x,y,z) ((x & y) | (z & (x | y)))
+#define S0(x) (ror(x,28) ^ ror(x,34) ^ ror(x,39))
+#define S1(x) (ror(x,14) ^ ror(x,18) ^ ror(x,41))
+#define R0(x) (ror(x,1) ^ ror(x,8) ^ (x>>7))
+#define R1(x) (ror(x,19) ^ ror(x,61) ^ (x>>6))
+
+static const uint64_t K[80] = {
+0x428a2f98d728ae22ULL, 0x7137449123ef65cdULL, 0xb5c0fbcfec4d3b2fULL, 0xe9b5dba58189dbbcULL,
+0x3956c25bf348b538ULL, 0x59f111f1b605d019ULL, 0x923f82a4af194f9bULL, 0xab1c5ed5da6d8118ULL,
+0xd807aa98a3030242ULL, 0x12835b0145706fbeULL, 0x243185be4ee4b28cULL, 0x550c7dc3d5ffb4e2ULL,
+0x72be5d74f27b896fULL, 0x80deb1fe3b1696b1ULL, 0x9bdc06a725c71235ULL, 0xc19bf174cf692694ULL,
+0xe49b69c19ef14ad2ULL, 0xefbe4786384f25e3ULL, 0x0fc19dc68b8cd5b5ULL, 0x240ca1cc77ac9c65ULL,
+0x2de92c6f592b0275ULL, 0x4a7484aa6ea6e483ULL, 0x5cb0a9dcbd41fbd4ULL, 0x76f988da831153b5ULL,
+0x983e5152ee66dfabULL, 0xa831c66d2db43210ULL, 0xb00327c898fb213fULL, 0xbf597fc7beef0ee4ULL,
+0xc6e00bf33da88fc2ULL, 0xd5a79147930aa725ULL, 0x06ca6351e003826fULL, 0x142929670a0e6e70ULL,
+0x27b70a8546d22ffcULL, 0x2e1b21385c26c926ULL, 0x4d2c6dfc5ac42aedULL, 0x53380d139d95b3dfULL,
+0x650a73548baf63deULL, 0x766a0abb3c77b2a8ULL, 0x81c2c92e47edaee6ULL, 0x92722c851482353bULL,
+0xa2bfe8a14cf10364ULL, 0xa81a664bbc423001ULL, 0xc24b8b70d0f89791ULL, 0xc76c51a30654be30ULL,
+0xd192e819d6ef5218ULL, 0xd69906245565a910ULL, 0xf40e35855771202aULL, 0x106aa07032bbd1b8ULL,
+0x19a4c116b8d2d0c8ULL, 0x1e376c085141ab53ULL, 0x2748774cdf8eeb99ULL, 0x34b0bcb5e19b48a8ULL,
+0x391c0cb3c5c95a63ULL, 0x4ed8aa4ae3418acbULL, 0x5b9cca4f7763e373ULL, 0x682e6ff3d6b2b8a3ULL,
+0x748f82ee5defb2fcULL, 0x78a5636f43172f60ULL, 0x84c87814a1f0ab72ULL, 0x8cc702081a6439ecULL,
+0x90befffa23631e28ULL, 0xa4506cebde82bde9ULL, 0xbef9a3f7b2c67915ULL, 0xc67178f2e372532bULL,
+0xca273eceea26619cULL, 0xd186b8c721c0c207ULL, 0xeada7dd6cde0eb1eULL, 0xf57d4f7fee6ed178ULL,
+0x06f067aa72176fbaULL, 0x0a637dc5a2c898a6ULL, 0x113f9804bef90daeULL, 0x1b710b35131c471bULL,
+0x28db77f523047d84ULL, 0x32caab7b40c72493ULL, 0x3c9ebe0a15c9bebcULL, 0x431d67c49c100d4cULL,
+0x4cc5d4becb3e42b6ULL, 0x597f299cfc657e2aULL, 0x5fcb6fab3ad6faecULL, 0x6c44198c4a475817ULL
+};
+
+static void
+processblock(struct sha512 *s, const uint8_t *buf)
+{
+ uint64_t W[80], t1, t2, a, b, c, d, e, f, g, h;
+ int i;
+
+ for (i = 0; i < 16; i++) {
+ W[i] = (uint64_t)buf[8*i]<<56;
+ W[i] |= (uint64_t)buf[8*i+1]<<48;
+ W[i] |= (uint64_t)buf[8*i+2]<<40;
+ W[i] |= (uint64_t)buf[8*i+3]<<32;
+ W[i] |= (uint64_t)buf[8*i+4]<<24;
+ W[i] |= (uint64_t)buf[8*i+5]<<16;
+ W[i] |= (uint64_t)buf[8*i+6]<<8;
+ W[i] |= buf[8*i+7];
+ }
+ for (; i < 80; i++)
+ W[i] = R1(W[i-2]) + W[i-7] + R0(W[i-15]) + W[i-16];
+ a = s->h[0];
+ b = s->h[1];
+ c = s->h[2];
+ d = s->h[3];
+ e = s->h[4];
+ f = s->h[5];
+ g = s->h[6];
+ h = s->h[7];
+ for (i = 0; i < 80; i++) {
+ t1 = h + S1(e) + Ch(e,f,g) + K[i] + W[i];
+ t2 = S0(a) + Maj(a,b,c);
+ h = g;
+ g = f;
+ f = e;
+ e = d + t1;
+ d = c;
+ c = b;
+ b = a;
+ a = t1 + t2;
+ }
+ s->h[0] += a;
+ s->h[1] += b;
+ s->h[2] += c;
+ s->h[3] += d;
+ s->h[4] += e;
+ s->h[5] += f;
+ s->h[6] += g;
+ s->h[7] += h;
+}
+
+static void
+pad(struct sha512 *s)
+{
+ unsigned r = s->len % 128;
+
+ s->buf[r++] = 0x80;
+ if (r > 112) {
+ memset(s->buf + r, 0, 128 - r);
+ r = 0;
+ processblock(s, s->buf);
+ }
+ memset(s->buf + r, 0, 120 - r);
+ s->len *= 8;
+ s->buf[120] = s->len >> 56;
+ s->buf[121] = s->len >> 48;
+ s->buf[122] = s->len >> 40;
+ s->buf[123] = s->len >> 32;
+ s->buf[124] = s->len >> 24;
+ s->buf[125] = s->len >> 16;
+ s->buf[126] = s->len >> 8;
+ s->buf[127] = s->len;
+ processblock(s, s->buf);
+}
+
+void
+sha512_init(void *ctx)
+{
+ struct sha512 *s = ctx;
+ s->len = 0;
+ s->h[0] = 0x6a09e667f3bcc908ULL;
+ s->h[1] = 0xbb67ae8584caa73bULL;
+ s->h[2] = 0x3c6ef372fe94f82bULL;
+ s->h[3] = 0xa54ff53a5f1d36f1ULL;
+ s->h[4] = 0x510e527fade682d1ULL;
+ s->h[5] = 0x9b05688c2b3e6c1fULL;
+ s->h[6] = 0x1f83d9abfb41bd6bULL;
+ s->h[7] = 0x5be0cd19137e2179ULL;
+}
+
+void
+sha512_sum_n(void *ctx, uint8_t *md, int n)
+{
+ struct sha512 *s = ctx;
+ int i;
+
+ pad(s);
+ for (i = 0; i < n; i++) {
+ md[8*i] = s->h[i] >> 56;
+ md[8*i+1] = s->h[i] >> 48;
+ md[8*i+2] = s->h[i] >> 40;
+ md[8*i+3] = s->h[i] >> 32;
+ md[8*i+4] = s->h[i] >> 24;
+ md[8*i+5] = s->h[i] >> 16;
+ md[8*i+6] = s->h[i] >> 8;
+ md[8*i+7] = s->h[i];
+ }
+}
+
+void
+sha512_sum(void *ctx, uint8_t md[SHA512_DIGEST_LENGTH])
+{
+ sha512_sum_n(ctx, md, 8);
+}
+
+void
+sha512_update(void *ctx, const void *m, unsigned long len)
+{
+ struct sha512 *s = ctx;
+ const uint8_t *p = m;
+ unsigned r = s->len % 128;
+
+ s->len += len;
+ if (r) {
+ if (len < 128 - r) {
+ memcpy(s->buf + r, p, len);
+ return;
+ }
+ memcpy(s->buf + r, p, 128 - r);
+ len -= 128 - r;
+ p += 128 - r;
+ processblock(s, s->buf);
+ }
+ for (; len >= 128; len -= 128, p += 128)
+ processblock(s, p);
+ memcpy(s->buf, p, len);
+}
diff --git a/util/sbase/libutil/strcasestr.c b/util/sbase/libutil/strcasestr.c
new file mode 100644
index 00000000..26eb6bbd
--- /dev/null
+++ b/util/sbase/libutil/strcasestr.c
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2005-2014 Rich Felker, et al.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+#include <string.h>
+#include <strings.h>
+
+#include "../util.h"
+
+char *
+strcasestr(const char *h, const char *n)
+{
+ size_t l = strlen(n);
+
+ for (; *h; h++)
+ if (!strncasecmp(h, n, l))
+ return (char *)h;
+
+ return 0;
+}
diff --git a/util/sbase/libutil/strlcat.c b/util/sbase/libutil/strlcat.c
new file mode 100644
index 00000000..bf263b87
--- /dev/null
+++ b/util/sbase/libutil/strlcat.c
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 1998 Todd C. Miller <Todd.Miller@courtesan.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <string.h>
+#include <sys/types.h>
+
+#include "../util.h"
+
+/*
+ * Appends src to string dst of size siz (unlike strncat, siz is the
+ * full size of dst, not space left). At most siz-1 characters
+ * will be copied. Always NUL terminates (unless siz <= strlen(dst)).
+ * Returns strlen(src) + MIN(siz, strlen(initial dst)).
+ * If retval >= siz, truncation occurred.
+ */
+size_t
+strlcat(char *dst, const char *src, size_t siz)
+{
+ char *d = dst;
+ const char *s = src;
+ size_t n = siz;
+ size_t dlen;
+ /* Find the end of dst and adjust bytes left but don't go past end */
+ while (n-- != 0 && *d != '\0')
+ d++;
+ dlen = d - dst;
+ n = siz - dlen;
+ if (n == 0)
+ return(dlen + strlen(s));
+ while (*s != '\0') {
+ if (n != 1) {
+ *d++ = *s;
+ n--;
+ }
+ s++;
+ }
+ *d = '\0';
+ return(dlen + (s - src)); /* count does not include NUL */
+}
+
+size_t
+estrlcat(char *dst, const char *src, size_t siz)
+{
+ size_t ret;
+
+ if ((ret = strlcat(dst, src, siz)) >= siz)
+ eprintf("strlcat: input string too long\n");
+
+ return ret;
+}
diff --git a/util/sbase/libutil/strlcpy.c b/util/sbase/libutil/strlcpy.c
new file mode 100644
index 00000000..44b618a0
--- /dev/null
+++ b/util/sbase/libutil/strlcpy.c
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 1998 Todd C. Miller <Todd.Miller@courtesan.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <string.h>
+#include <sys/types.h>
+
+#include "../util.h"
+
+/*
+ * Copy src to string dst of size siz. At most siz-1 characters
+ * will be copied. Always NUL terminates (unless siz == 0).
+ * Returns strlen(src); if retval >= siz, truncation occurred.
+ */
+size_t
+strlcpy(char *dst, const char *src, size_t siz)
+{
+ char *d = dst;
+ const char *s = src;
+ size_t n = siz;
+ /* Copy as many bytes as will fit */
+ if (n != 0) {
+ while (--n != 0) {
+ if ((*d++ = *s++) == '\0')
+ break;
+ }
+ }
+ /* Not enough room in dst, add NUL and traverse rest of src */
+ if (n == 0) {
+ if (siz != 0)
+ *d = '\0'; /* NUL-terminate dst */
+ while (*s++)
+ ;
+ }
+ return(s - src - 1); /* count does not include NUL */
+}
+
+size_t
+estrlcpy(char *dst, const char *src, size_t siz)
+{
+ size_t ret;
+
+ if ((ret = strlcpy(dst, src, siz)) >= siz)
+ eprintf("strlcpy: input string too long\n");
+
+ return ret;
+}
diff --git a/util/sbase/libutil/strnsubst.c b/util/sbase/libutil/strnsubst.c
new file mode 100644
index 00000000..2da54aba
--- /dev/null
+++ b/util/sbase/libutil/strnsubst.c
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2002 J. Mallett. All rights reserved.
+ * You may do whatever you want with this file as long as
+ * the above copyright and this notice remain intact, along
+ * with the following statement:
+ * For the man who taught me vi, and who got too old, too young.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "../util.h"
+
+/*
+ * Replaces str with a string consisting of str with match replaced with
+ * replstr as many times as can be done before the constructed string is
+ * maxsize bytes large. It does not free the string pointed to by str, it
+ * is up to the calling program to be sure that the original contents of
+ * str as well as the new contents are handled in an appropriate manner.
+ * If replstr is NULL, then that internally is changed to a nil-string, so
+ * that we can still pretend to do somewhat meaningful substitution.
+ * No value is returned.
+ */
+void
+strnsubst(char **str, const char *match, const char *replstr, size_t maxsize)
+{
+ char *s1, *s2, *this;
+ size_t matchlen, s2len;
+ int n;
+
+ if ((s1 = *str) == NULL)
+ return;
+ s2 = emalloc(maxsize);
+
+ if (replstr == NULL)
+ replstr = "";
+
+ if (match == NULL || *match == '\0' || strlen(s1) >= maxsize) {
+ strlcpy(s2, s1, maxsize);
+ goto done;
+ }
+
+ *s2 = '\0';
+ s2len = 0;
+ matchlen = strlen(match);
+ for (;;) {
+ if ((this = strstr(s1, match)) == NULL)
+ break;
+ n = snprintf(s2 + s2len, maxsize - s2len, "%.*s%s",
+ (int)(this - s1), s1, replstr);
+ if (n == -1 || n + s2len + strlen(this + matchlen) >= maxsize)
+ break; /* out of room */
+ s2len += n;
+ s1 = this + matchlen;
+ }
+ strlcpy(s2 + s2len, s1, maxsize - s2len);
+done:
+ *str = s2;
+ return;
+}
diff --git a/util/sbase/libutil/strsep.c b/util/sbase/libutil/strsep.c
new file mode 100644
index 00000000..d9f06444
--- /dev/null
+++ b/util/sbase/libutil/strsep.c
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2005-2014 Rich Felker, et al.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+#include <string.h>
+
+#include "../util.h"
+
+char *
+strsep(char **str, const char *sep)
+{
+ char *s = *str, *end;
+ if (!s) return NULL;
+ end = s + strcspn(s, sep);
+ if (*end) *end++ = 0;
+ else end = 0;
+ *str = end;
+ return s;
+}
diff --git a/util/sbase/libutil/strtonum.c b/util/sbase/libutil/strtonum.c
new file mode 100644
index 00000000..c0ac401f
--- /dev/null
+++ b/util/sbase/libutil/strtonum.c
@@ -0,0 +1,85 @@
+/* $OpenBSD: strtonum.c,v 1.7 2013/04/17 18:40:58 tedu Exp $ */
+
+/*
+ * Copyright (c) 2004 Ted Unangst and Todd Miller
+ * All rights reserved.
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <errno.h>
+#include <limits.h>
+#include <stdlib.h>
+
+#include "../util.h"
+
+#define INVALID 1
+#define TOOSMALL 2
+#define TOOLARGE 3
+
+long long
+strtonum(const char *numstr, long long minval, long long maxval,
+ const char **errstrp)
+{
+ long long ll = 0;
+ int error = 0;
+ char *ep;
+ struct errval {
+ const char *errstr;
+ int err;
+ } ev[4] = {
+ { NULL, 0 },
+ { "invalid", EINVAL },
+ { "too small", ERANGE },
+ { "too large", ERANGE },
+ };
+
+ ev[0].err = errno;
+ errno = 0;
+ if (minval > maxval) {
+ error = INVALID;
+ } else {
+ ll = strtoll(numstr, &ep, 10);
+ if (numstr == ep || *ep != '\0')
+ error = INVALID;
+ else if ((ll == LLONG_MIN && errno == ERANGE) || ll < minval)
+ error = TOOSMALL;
+ else if ((ll == LLONG_MAX && errno == ERANGE) || ll > maxval)
+ error = TOOLARGE;
+ }
+ if (errstrp != NULL)
+ *errstrp = ev[error].errstr;
+ errno = ev[error].err;
+ if (error)
+ ll = 0;
+
+ return (ll);
+}
+
+long long
+enstrtonum(int status, const char *numstr, long long minval, long long maxval)
+{
+ const char *errstr;
+ long long ll;
+
+ ll = strtonum(numstr, minval, maxval, &errstr);
+ if (errstr)
+ enprintf(status, "strtonum %s: %s\n", numstr, errstr);
+ return ll;
+}
+
+long long
+estrtonum(const char *numstr, long long minval, long long maxval)
+{
+ return enstrtonum(1, numstr, minval, maxval);
+}
diff --git a/util/sbase/libutil/unescape.c b/util/sbase/libutil/unescape.c
new file mode 100644
index 00000000..b8f75ca9
--- /dev/null
+++ b/util/sbase/libutil/unescape.c
@@ -0,0 +1,58 @@
+/* See LICENSE file for copyright and license details. */
+#include <ctype.h>
+#include <string.h>
+
+#include "../util.h"
+
+#define is_odigit(c) ('0' <= c && c <= '7')
+
+size_t
+unescape(char *s)
+{
+ static const char escapes[256] = {
+ ['"'] = '"',
+ ['\''] = '\'',
+ ['\\'] = '\\',
+ ['a'] = '\a',
+ ['b'] = '\b',
+ ['E'] = 033,
+ ['e'] = 033,
+ ['f'] = '\f',
+ ['n'] = '\n',
+ ['r'] = '\r',
+ ['t'] = '\t',
+ ['v'] = '\v'
+ };
+ size_t m, q;
+ char *r, *w;
+
+ for (r = w = s; *r;) {
+ if (*r != '\\') {
+ *w++ = *r++;
+ continue;
+ }
+ r++;
+ if (!*r) {
+ eprintf("null escape sequence\n");
+ } else if (escapes[(unsigned char)*r]) {
+ *w++ = escapes[(unsigned char)*r++];
+ } else if (is_odigit(*r)) {
+ for (q = 0, m = 3; m && is_odigit(*r); m--, r++)
+ q = q * 8 + (*r - '0');
+ *w++ = MIN(q, 255);
+ } else if (*r == 'x' && isxdigit(r[1])) {
+ r++;
+ for (q = 0, m = 2; m && isxdigit(*r); m--, r++)
+ if (isdigit(*r))
+ q = q * 16 + (*r - '0');
+ else
+ q = q * 16 + (tolower(*r) - 'a' + 10);
+ *w++ = q;
+ } else {
+ eprintf("invalid escape sequence '\\%c'\n", *r);
+ }
+ }
+ *w = '\0';
+
+ return w - s;
+}
diff --git a/util/sbase/libutil/writeall.c b/util/sbase/libutil/writeall.c
new file mode 100644
index 00000000..4725ced8
--- /dev/null
+++ b/util/sbase/libutil/writeall.c
@@ -0,0 +1,21 @@
+/* See LICENSE file for copyright and license details. */
+#include <unistd.h>
+
+#include "../util.h"
+
+ssize_t
+writeall(int fd, const void *buf, size_t len)
+{
+ const char *p = buf;
+ ssize_t n;
+
+ while (len) {
+ n = write(fd, p, len);
+ if (n <= 0)
+ return n;
+ p += n;
+ len -= n;
+ }
+
+ return p - (const char *)buf;
+}
diff --git a/util/sbase/link.1 b/util/sbase/link.1
new file mode 100644
index 00000000..915b9d80
--- /dev/null
+++ b/util/sbase/link.1
@@ -0,0 +1,16 @@
+.Dd October 8, 2015
+.Dt LINK 1
+.Os sbase
+.Sh NAME
+.Nm link
+.Nd call the link function
+.Ar target
+.Ar name
+.Sh DESCRIPTION
+.Nm
+creates a hard link
+.Ar name
+to
+.Ar target .
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/link.c b/util/sbase/link.c
new file mode 100644
index 00000000..7cee4d0f
--- /dev/null
+++ b/util/sbase/link.c
@@ -0,0 +1,27 @@
+/* See LICENSE file for copyright and license details. */
+#include <unistd.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s target name\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ ARGBEGIN {
+ default:
+ usage();
+ } ARGEND
+
+ if (argc != 2)
+ usage();
+
+ if (link(argv[0], argv[1]) < 0)
+ eprintf("link:");
+
+ return 0;
+}
diff --git a/util/sbase/ln.1 b/util/sbase/ln.1
new file mode 100644
index 00000000..057b5a09
--- /dev/null
+++ b/util/sbase/ln.1
@@ -0,0 +1,61 @@
+.Dd October 8, 2015
+.Dt LN 1
+.Os sbase
+.Sh NAME
+.Nm ln
+.Nd link files
+.Sh SYNOPSIS
+.Nm
+.Op Fl f
+.Op Fl L | Fl P | Fl s
+.Ar target
+.Op Ar name
+.Nm
+.Op Fl f
+.Op Fl L | Fl P | Fl s
+.Ar target ...
+.Ar directory
+.Sh DESCRIPTION
+.Nm
+creates a hard link
+.Ar name
+to
+.Ar target .
+If no
+.Ar name
+is given, a hard link to
+.Ar target
+is created in the current directory.
+If more than one
+.Ar target
+is given,
+.Nm
+hardlinks them in the existing
+.Ar directory .
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl f
+If
+.Ar name
+exists and is not a
+.Ar target ,
+remove it to allow the link.
+.It Fl L | Fl P
+If
+.Ar target
+is a symbolic link, create a hard link to the (referenced file) |
+(symbolic link itself). The former is the default.
+.It Fl s
+Create symbolic links instead of hard links.
+Disables
+.Fl L
+and
+.Fl P ,
+because their purpose does not apply to symbolic links.
+.El
+.Sh SEE ALSO
+.Xr cp 1 ,
+.Xr link 2 ,
+.Xr symlink 2
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/ln.c b/util/sbase/ln.c
new file mode 100644
index 00000000..f62068a1
--- /dev/null
+++ b/util/sbase/ln.c
@@ -0,0 +1,103 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <libgen.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-f] [-L | -P | -s] target [name]\n"
+ " %s [-f] [-L | -P | -s] target ... dir\n", argv0, argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ char *targetdir = ".", *target = NULL;
+ int ret = 0, sflag = 0, fflag = 0, dirfd = AT_FDCWD,
+ hastarget = 0, flags = AT_SYMLINK_FOLLOW;
+ struct stat st, tst;
+
+ ARGBEGIN {
+ case 'f':
+ fflag = 1;
+ break;
+ case 'L':
+ flags |= AT_SYMLINK_FOLLOW;
+ break;
+ case 'P':
+ flags &= ~AT_SYMLINK_FOLLOW;
+ break;
+ case 's':
+ sflag = 1;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc)
+ usage();
+
+ if (argc > 1) {
+ if (!stat(argv[argc - 1], &st) && S_ISDIR(st.st_mode)) {
+ if ((dirfd = open(argv[argc - 1], O_RDONLY)) < 0)
+ eprintf("open %s:", argv[argc - 1]);
+ targetdir = argv[argc - 1];
+ if (targetdir[strlen(targetdir) - 1] == '/')
+ targetdir[strlen(targetdir) - 1] = '\0';
+ } else if (argc == 2) {
+ hastarget = 1;
+ target = argv[argc - 1];
+ } else {
+ eprintf("%s: not a directory\n", argv[argc - 1]);
+ }
+ argv[argc - 1] = NULL;
+ argc--;
+ }
+
+ for (; *argv; argc--, argv++) {
+ if (!hastarget)
+ target = basename(*argv);
+
+ if (!sflag) {
+ if (stat(*argv, &st) < 0) {
+ weprintf("stat %s:", *argv);
+ ret = 1;
+ continue;
+ } else if (fstatat(dirfd, target, &tst, AT_SYMLINK_NOFOLLOW) < 0) {
+ if (errno != ENOENT) {
+ weprintf("fstatat %s %s:", targetdir, target);
+ ret = 1;
+ continue;
+ }
+ } else if (st.st_dev == tst.st_dev && st.st_ino == tst.st_ino) {
+ if (!fflag) {
+ weprintf("%s and %s/%s are the same file\n",
+ *argv, targetdir, target);
+ ret = 1;
+ }
+ continue;
+ }
+ }
+
+ if (fflag && unlinkat(dirfd, target, 0) < 0 && errno != ENOENT) {
+ weprintf("unlinkat %s %s:", targetdir, target);
+ ret = 1;
+ continue;
+ }
+ if ((sflag ? symlinkat(*argv, dirfd, target) :
+ linkat(AT_FDCWD, *argv, dirfd, target, flags)) < 0) {
+ weprintf("%s %s <- %s/%s:", sflag ? "symlinkat" : "linkat",
+ *argv, targetdir, target);
+ ret = 1;
+ }
+ }
+
+ return ret;
+}
diff --git a/util/sbase/logger.1 b/util/sbase/logger.1
new file mode 100644
index 00000000..4624a163
--- /dev/null
+++ b/util/sbase/logger.1
@@ -0,0 +1,52 @@
+.Dd October 8, 2015
+.Dt LOGGER 1
+.Os sbase
+.Sh NAME
+.Nm logger
+.Nd make entries in the system log
+.Sh SYNOPSIS
+.Nm
+.Op Fl is
+.Op Fl p Ar priority
+.Op Fl t Ar tag
+.Op Ar message ...
+.Sh DESCRIPTION
+.Nm
+provides a shell command interface to the
+.Xr syslog 3
+system log module and writes each
+.Ar message
+to the log.
+If no
+.Ar message
+is given,
+.Nm
+logs stdin.
+.Sh OPTIONS
+.Bl -tag -width xxxxxxxxxxxx
+.It Fl i
+Add the logger process ID to each line in the log.
+.It Fl p Ar priority
+Set the message
+.Ar priority
+given symbolically as a
+.Dq facility.level
+pair.
+The default is
+.Dq user.notice .
+.It Fl s
+Also log to stderr.
+.It Fl t Ar tag
+Add
+.Ar tag
+to each line in the log.
+.El
+.Sh SEE ALSO
+.Xr syslogd 1 ,
+.Xr syslog 3
+.Sh STANDARDS
+POSIX.1-2013.
+.Pp
+The
+.Op Fl ipst
+flags are an extensions to that specification.
diff --git a/util/sbase/logger.c b/util/sbase/logger.c
new file mode 100644
index 00000000..603da04f
--- /dev/null
+++ b/util/sbase/logger.c
@@ -0,0 +1,91 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#define SYSLOG_NAMES
+#include <syslog.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static int
+decodetable(CODE *table, char *name)
+{
+ CODE *c;
+
+ for (c = table; c->c_name; c++)
+ if (!strcasecmp(name, c->c_name))
+ return c->c_val;
+ eprintf("invalid priority name: %s\n", name);
+
+ return -1; /* not reached */
+}
+
+static int
+decodepri(char *pri)
+{
+ char *lev, *fac = pri;
+
+ if (!(lev = strchr(pri, '.')))
+ eprintf("invalid priority name: %s\n", pri);
+ *lev++ = '\0';
+ if (!*lev)
+ eprintf("invalid priority name: %s\n", pri);
+
+ return (decodetable(facilitynames, fac) & LOG_FACMASK) |
+ (decodetable(prioritynames, lev) & LOG_PRIMASK);
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-is] [-p priority] [-t tag] [message ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ size_t sz;
+ int logflags = 0, priority = LOG_NOTICE, i;
+ char *buf = NULL, *tag = NULL;
+
+ ARGBEGIN {
+ case 'i':
+ logflags |= LOG_PID;
+ break;
+ case 'p':
+ priority = decodepri(EARGF(usage()));
+ break;
+ case 's':
+ logflags |= LOG_PERROR;
+ break;
+ case 't':
+ tag = EARGF(usage());
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ openlog(tag ? tag : getlogin(), logflags, 0);
+
+ if (!argc) {
+ while (getline(&buf, &sz, stdin) > 0)
+ syslog(priority, "%s", buf);
+ } else {
+ for (i = 0, sz = 0; i < argc; i++)
+ sz += strlen(argv[i]);
+ sz += argc;
+ buf = ecalloc(1, sz);
+ for (i = 0; i < argc; i++) {
+ estrlcat(buf, argv[i], sz);
+ if (i + 1 < argc)
+ estrlcat(buf, " ", sz);
+ }
+ syslog(priority, "%s", buf);
+ }
+
+ closelog();
+
+ return fshut(stdin, "<stdin>");
+}
diff --git a/util/sbase/logname.1 b/util/sbase/logname.1
new file mode 100644
index 00000000..1c1f16fd
--- /dev/null
+++ b/util/sbase/logname.1
@@ -0,0 +1,13 @@
+.Dd October 8, 2015
+.Dt LOGNAME 1
+.Os sbase
+.Sh NAME
+.Nm logname
+.Nd show login name
+.Sh SYNOPSIS
+.Nm
+.Sh DESCRIPTION
+.Nm
+writes the login name of the current user to stdout.
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/logname.c b/util/sbase/logname.c
new file mode 100644
index 00000000..8eb8eea5
--- /dev/null
+++ b/util/sbase/logname.c
@@ -0,0 +1,29 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdio.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ char *login;
+
+ argv0 = *argv, argv0 ? (argc--, argv++) : (void *)0;
+
+ if (argc)
+ usage();
+
+ if ((login = getlogin()))
+ puts(login);
+ else
+ eprintf("no login name\n");
+
+ return fshut(stdout, "<stdout>");
+}
diff --git a/util/sbase/ls.1 b/util/sbase/ls.1
new file mode 100644
index 00000000..26a41e62
--- /dev/null
+++ b/util/sbase/ls.1
@@ -0,0 +1,96 @@
+.Dd October 8, 2015
+.Dt LS 1
+.Os sbase
+.Sh NAME
+.Nm ls
+.Nd list directory contents
+.Sh SYNOPSIS
+.Nm
+.Op Fl iqr
+.Op Fl ln
+.Op Fl A | a
+.Op Fl 1
+.Op Fl h | F | p
+.Op Fl H | L
+.Op Fl R | d
+.Op Fl S | f | t | U
+.Op Fl c | u
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+lists each given file, and the contents of each given directory.
+If no files are given the current directory is listed.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl A
+List all entries except for '.' and '..'.
+.It Fl a
+Show hidden files (those beginning with '.').
+.It Fl c
+Use time file's status was last changed instead of last
+modification time for sorting or printing.
+.It Fl d
+List directories themselves, not their contents.
+.It Fl F
+Append a file type indicator to all special files.
+.It Fl f
+Like
+.Fl U
+but turns on
+.Fl a
+and disables
+.Fl r ,
+.Fl S
+and
+.Fl t .
+.It Fl H
+List information about the targets of symbolic links specified on the command
+line instead of the links themselves.
+.It Fl h
+Show filesizes in human\-readable format.
+.It Fl i
+Print the index number of each file.
+.It Fl L
+List information about the targets of symbolic links instead of the links
+themselves.
+.It Fl l
+List detailed information about each file, including their type, permissions,
+links, owner, group, size or major and minor numbers if the file is a
+character/block device, and last file status/modification time.
+.It Fl n
+List detailed information about each file, including their type, permissions,
+links, owner, group, size or major and minor numbers if the file is a
+character/block device, and last file status/modification time, but with
+numeric IDs.
+.It Fl p
+Append a file type indicator to directories.
+.It Fl q
+Replace non-printable characters in filenames with '?'.
+.It Fl R
+List directory content recursively.
+The
+.Fl 1
+flag is set implicitly.
+.It Fl r
+Reverse the sort order.
+.It Fl S
+Sort files by size (in decreasing order).
+.It Fl t
+Sort files by last file status/modification time instead of by name.
+.It Fl U
+Keep the list unsorted.
+.It Fl u
+Use file's last access time instead of last modification time for
+sorting or printing.
+.El
+.Sh SEE ALSO
+.Xr stat 2
+.Sh STANDARDS
+POSIX.1-2013.
+Except for the
+.Op Fl Ckmpsx
+flags.
+.Pp
+The
+.Op Fl hU
+flags are an extension to that specification.
diff --git a/util/sbase/ls.c b/util/sbase/ls.c
new file mode 100644
index 00000000..aa95fef2
--- /dev/null
+++ b/util/sbase/ls.c
@@ -0,0 +1,489 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+#include <sys/types.h>
+#ifndef major
+#include <sys/sysmacros.h>
+#endif
+
+#include <dirent.h>
+#include <grp.h>
+#include <pwd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "utf.h"
+#include "util.h"
+
+struct entry {
+ char *name;
+ mode_t mode, tmode;
+ nlink_t nlink;
+ uid_t uid;
+ gid_t gid;
+ off_t size;
+ struct timespec t;
+ dev_t dev;
+ dev_t rdev;
+ ino_t ino, tino;
+};
+
+static struct {
+ dev_t dev;
+ ino_t ino;
+} tree[PATH_MAX];
+
+static int ret = 0;
+static int Aflag = 0;
+static int aflag = 0;
+static int cflag = 0;
+static int dflag = 0;
+static int Fflag = 0;
+static int fflag = 0;
+static int Hflag = 0;
+static int hflag = 0;
+static int iflag = 0;
+static int Lflag = 0;
+static int lflag = 0;
+static int nflag = 0;
+static int pflag = 0;
+static int qflag = 0;
+static int Rflag = 0;
+static int rflag = 0;
+static int Uflag = 0;
+static int uflag = 0;
+static int first = 1;
+static char sort = 0;
+static int showdirs;
+
+static void ls(const char *, const struct entry *, int);
+
+static void
+mkent(struct entry *ent, char *path, int dostat, int follow)
+{
+ struct stat st;
+
+ ent->name = path;
+ if (!dostat)
+ return;
+ if ((follow ? stat : lstat)(path, &st) < 0)
+ eprintf("%s %s:", follow ? "stat" : "lstat", path);
+ ent->mode = st.st_mode;
+ ent->nlink = st.st_nlink;
+ ent->uid = st.st_uid;
+ ent->gid = st.st_gid;
+ ent->size = st.st_size;
+ if (cflag)
+ ent->t = st.st_ctim;
+ else if (uflag)
+ ent->t = st.st_atim;
+ else
+ ent->t = st.st_mtim;
+ ent->dev = st.st_dev;
+ ent->rdev = st.st_rdev;
+ ent->ino = st.st_ino;
+ if (S_ISLNK(ent->mode)) {
+ if (stat(path, &st) == 0) {
+ ent->tmode = st.st_mode;
+ ent->dev = st.st_dev;
+ ent->tino = st.st_ino;
+ } else {
+ ent->tmode = ent->tino = 0;
+ }
+ }
+}
+
+static char *
+indicator(mode_t mode)
+{
+ if (pflag || Fflag)
+ if (S_ISDIR(mode))
+ return "/";
+
+ if (Fflag) {
+ if (S_ISLNK(mode))
+ return "@";
+ else if (S_ISFIFO(mode))
+ return "|";
+ else if (S_ISSOCK(mode))
+ return "=";
+ else if (mode & S_IXUSR || mode & S_IXGRP || mode & S_IXOTH)
+ return "*";
+ }
+
+ return "";
+}
+
+static void
+printname(const char *name)
+{
+ const char *c;
+ Rune r;
+ size_t l;
+
+ for (c = name; *c; c += l) {
+ l = chartorune(&r, c);
+ if (!qflag || isprintrune(r))
+ fwrite(c, 1, l, stdout);
+ else
+ putchar('?');
+ }
+}
+
+static void
+output(const struct entry *ent)
+{
+ struct group *gr;
+ struct passwd *pw;
+ struct tm *tm;
+ ssize_t len;
+ char *fmt, buf[BUFSIZ], pwname[_SC_LOGIN_NAME_MAX],
+ grname[_SC_LOGIN_NAME_MAX], mode[] = "----------";
+
+ if (iflag)
+ printf("%lu ", (unsigned long)ent->ino);
+ if (!lflag) {
+ printname(ent->name);
+ puts(indicator(ent->mode));
+ return;
+ }
+ if (S_ISREG(ent->mode))
+ mode[0] = '-';
+ else if (S_ISBLK(ent->mode))
+ mode[0] = 'b';
+ else if (S_ISCHR(ent->mode))
+ mode[0] = 'c';
+ else if (S_ISDIR(ent->mode))
+ mode[0] = 'd';
+ else if (S_ISFIFO(ent->mode))
+ mode[0] = 'p';
+ else if (S_ISLNK(ent->mode))
+ mode[0] = 'l';
+ else if (S_ISSOCK(ent->mode))
+ mode[0] = 's';
+ else
+ mode[0] = '?';
+
+ if (ent->mode & S_IRUSR) mode[1] = 'r';
+ if (ent->mode & S_IWUSR) mode[2] = 'w';
+ if (ent->mode & S_IXUSR) mode[3] = 'x';
+ if (ent->mode & S_IRGRP) mode[4] = 'r';
+ if (ent->mode & S_IWGRP) mode[5] = 'w';
+ if (ent->mode & S_IXGRP) mode[6] = 'x';
+ if (ent->mode & S_IROTH) mode[7] = 'r';
+ if (ent->mode & S_IWOTH) mode[8] = 'w';
+ if (ent->mode & S_IXOTH) mode[9] = 'x';
+
+ if (ent->mode & S_ISUID) mode[3] = (mode[3] == 'x') ? 's' : 'S';
+ if (ent->mode & S_ISGID) mode[6] = (mode[6] == 'x') ? 's' : 'S';
+ if (ent->mode & S_ISVTX) mode[9] = (mode[9] == 'x') ? 't' : 'T';
+
+ if (!nflag && (pw = getpwuid(ent->uid)))
+ snprintf(pwname, sizeof(pwname), "%s", pw->pw_name);
+ else
+ snprintf(pwname, sizeof(pwname), "%d", ent->uid);
+
+ if (!nflag && (gr = getgrgid(ent->gid)))
+ snprintf(grname, sizeof(grname), "%s", gr->gr_name);
+ else
+ snprintf(grname, sizeof(grname), "%d", ent->gid);
+
+ if (time(NULL) > ent->t.tv_sec + (180 * 24 * 60 * 60)) /* 6 months ago? */
+ fmt = "%b %d %Y";
+ else
+ fmt = "%b %d %H:%M";
+
+ if ((tm = localtime(&ent->t.tv_sec)))
+ strftime(buf, sizeof(buf), fmt, tm);
+ else
+ snprintf(buf, sizeof(buf), "%lld", (long long)(ent->t.tv_sec));
+ printf("%s %4ld %-8.8s %-8.8s ", mode, (long)ent->nlink, pwname, grname);
+
+ if (S_ISBLK(ent->mode) || S_ISCHR(ent->mode))
+ printf("%4u, %4u ", major(ent->rdev), minor(ent->rdev));
+ else if (hflag)
+ printf("%10s ", humansize(ent->size));
+ else
+ printf("%10lu ", (unsigned long)ent->size);
+ printf("%s ", buf);
+ printname(ent->name);
+ fputs(indicator(ent->mode), stdout);
+ if (S_ISLNK(ent->mode)) {
+ if ((len = readlink(ent->name, buf, sizeof(buf) - 1)) < 0)
+ eprintf("readlink %s:", ent->name);
+ buf[len] = '\0';
+ printf(" -> %s%s", buf, indicator(ent->tmode));
+ }
+ putchar('\n');
+}
+
+static int
+entcmp(const void *va, const void *vb)
+{
+ int cmp = 0;
+ const struct entry *a = va, *b = vb;
+
+ switch (sort) {
+ case 'S':
+ cmp = b->size - a->size;
+ break;
+ case 't':
+ if (!(cmp = b->t.tv_sec - a->t.tv_sec))
+ cmp = b->t.tv_nsec - a->t.tv_nsec;
+ break;
+ }
+
+ if (!cmp)
+ cmp = strcmp(a->name, b->name);
+
+ return rflag ? 0 - cmp : cmp;
+}
+
+static void
+lsdir(const char *path, const struct entry *dir)
+{
+ DIR *dp;
+ struct entry *ent, *ents = NULL;
+ struct dirent *d;
+ size_t i, n = 0;
+ char prefix[PATH_MAX];
+
+ if (!(dp = opendir(dir->name))) {
+ ret = 1;
+ weprintf("opendir %s%s:", path, dir->name);
+ return;
+ }
+ if (chdir(dir->name) < 0)
+ eprintf("chdir %s:", dir->name);
+
+ while ((d = readdir(dp))) {
+ if (d->d_name[0] == '.' && !aflag && !Aflag)
+ continue;
+ else if (Aflag)
+ if (strcmp(d->d_name, ".") == 0 ||
+ strcmp(d->d_name, "..") == 0)
+ continue;
+
+ ents = ereallocarray(ents, ++n, sizeof(*ents));
+ mkent(&ents[n - 1], estrdup(d->d_name), Fflag || iflag ||
+ lflag || pflag || Rflag || sort, Lflag);
+ }
+
+ closedir(dp);
+
+ if (!Uflag)
+ qsort(ents, n, sizeof(*ents), entcmp);
+
+ if (path[0] || showdirs) {
+ fputs(path, stdout);
+ printname(dir->name);
+ puts(":");
+ }
+ for (i = 0; i < n; i++)
+ output(&ents[i]);
+
+ if (Rflag) {
+ if (snprintf(prefix, PATH_MAX, "%s%s/", path, dir->name) >=
+ PATH_MAX)
+ eprintf("path too long: %s%s\n", path, dir->name);
+
+ for (i = 0; i < n; i++) {
+ ent = &ents[i];
+ if (strcmp(ent->name, ".") == 0 ||
+ strcmp(ent->name, "..") == 0)
+ continue;
+ if (S_ISLNK(ent->mode) && S_ISDIR(ent->tmode) && !Lflag)
+ continue;
+
+ ls(prefix, ent, 1);
+ }
+ }
+
+ for (i = 0; i < n; ++i)
+ free(ents[i].name);
+ free(ents);
+}
+
+static int
+visit(const struct entry *ent)
+{
+ dev_t dev;
+ ino_t ino;
+ int i;
+
+ dev = ent->dev;
+ ino = S_ISLNK(ent->mode) ? ent->tino : ent->ino;
+
+ for (i = 0; i < PATH_MAX && tree[i].ino; ++i) {
+ if (ino == tree[i].ino && dev == tree[i].dev)
+ return -1;
+ }
+
+ tree[i].ino = ino;
+ tree[i].dev = dev;
+
+ return i;
+}
+
+static void
+ls(const char *path, const struct entry *ent, int listdir)
+{
+ int treeind;
+ char cwd[PATH_MAX];
+
+ if (!listdir) {
+ output(ent);
+ } else if (S_ISDIR(ent->mode) ||
+ (S_ISLNK(ent->mode) && S_ISDIR(ent->tmode))) {
+ if ((treeind = visit(ent)) < 0) {
+ ret = 1;
+ weprintf("%s%s: Already visited\n", path, ent->name);
+ return;
+ }
+
+ if (!getcwd(cwd, PATH_MAX))
+ eprintf("getcwd:");
+
+ if (first)
+ first = 0;
+ else
+ putchar('\n');
+
+ lsdir(path, ent);
+ tree[treeind].ino = 0;
+
+ if (chdir(cwd) < 0)
+ eprintf("chdir %s:", cwd);
+ }
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-1AacdFfHhiLlnpqRrtUu] [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct entry ent, *dents, *fents;
+ size_t i, ds, fs;
+
+ ARGBEGIN {
+ case '1':
+ /* force output to 1 entry per line */
+ qflag = 1;
+ break;
+ case 'A':
+ Aflag = 1;
+ break;
+ case 'a':
+ aflag = 1;
+ break;
+ case 'c':
+ cflag = 1;
+ uflag = 0;
+ break;
+ case 'd':
+ dflag = 1;
+ break;
+ case 'f':
+ aflag = 1;
+ fflag = 1;
+ Uflag = 1;
+ break;
+ case 'F':
+ Fflag = 1;
+ break;
+ case 'H':
+ Hflag = 1;
+ break;
+ case 'h':
+ hflag = 1;
+ break;
+ case 'i':
+ iflag = 1;
+ break;
+ case 'L':
+ Lflag = 1;
+ break;
+ case 'l':
+ lflag = 1;
+ break;
+ case 'n':
+ lflag = 1;
+ nflag = 1;
+ break;
+ case 'p':
+ pflag = 1;
+ break;
+ case 'q':
+ qflag = 1;
+ break;
+ case 'R':
+ Rflag = 1;
+ break;
+ case 'r':
+ rflag = 1;
+ break;
+ case 'S':
+ sort = 'S';
+ break;
+ case 't':
+ sort = 't';
+ break;
+ case 'U':
+ Uflag = 1;
+ break;
+ case 'u':
+ uflag = 1;
+ cflag = 0;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ switch (argc) {
+ case 0: /* fallthrough */
+ *--argv = ".", ++argc;
+ case 1:
+ mkent(&ent, argv[0], 1, Hflag || Lflag);
+ ls("", &ent, (!dflag && S_ISDIR(ent.mode)) ||
+ (S_ISLNK(ent.mode) && S_ISDIR(ent.tmode) &&
+ !(dflag || Fflag || lflag)));
+
+ break;
+ default:
+ for (i = ds = fs = 0, fents = dents = NULL; i < argc; ++i) {
+ mkent(&ent, argv[i], 1, Hflag || Lflag);
+
+ if ((!dflag && S_ISDIR(ent.mode)) ||
+ (S_ISLNK(ent.mode) && S_ISDIR(ent.tmode) &&
+ !(dflag || Fflag || lflag))) {
+ dents = ereallocarray(dents, ++ds, sizeof(*dents));
+ memcpy(&dents[ds - 1], &ent, sizeof(ent));
+ } else {
+ fents = ereallocarray(fents, ++fs, sizeof(*fents));
+ memcpy(&fents[fs - 1], &ent, sizeof(ent));
+ }
+ }
+
+ showdirs = ds > 1 || (ds && fs);
+
+ qsort(fents, fs, sizeof(ent), entcmp);
+ qsort(dents, ds, sizeof(ent), entcmp);
+
+ for (i = 0; i < fs; ++i)
+ ls("", &fents[i], 0);
+ free(fents);
+ if (fs && ds)
+ putchar('\n');
+ for (i = 0; i < ds; ++i)
+ ls("", &dents[i], 1);
+ free(dents);
+ }
+
+ return (fshut(stdout, "<stdout>") | ret);
+}
diff --git a/util/sbase/md5.h b/util/sbase/md5.h
new file mode 100644
index 00000000..0b5005e9
--- /dev/null
+++ b/util/sbase/md5.h
@@ -0,0 +1,18 @@
+/* public domain md5 implementation based on rfc1321 and libtomcrypt */
+
+struct md5 {
+ uint64_t len; /* processed message length */
+ uint32_t h[4]; /* hash state */
+ uint8_t buf[64]; /* message block buffer */
+};
+
+enum { MD5_DIGEST_LENGTH = 16 };
+
+/* reset state */
+void md5_init(void *ctx);
+/* process message */
+void md5_update(void *ctx, const void *m, unsigned long len);
+/* get message digest */
+/* state is ruined after sum, keep a copy if multiple sum is needed */
+/* part of the message might be left in s, zero it if secrecy is needed */
+void md5_sum(void *ctx, uint8_t md[MD5_DIGEST_LENGTH]);
diff --git a/util/sbase/md5sum.1 b/util/sbase/md5sum.1
new file mode 100644
index 00000000..79a37cfe
--- /dev/null
+++ b/util/sbase/md5sum.1
@@ -0,0 +1,32 @@
+.Dd October 8, 2015
+.Dt MD5SUM 1
+.Os sbase
+.Sh NAME
+.Nm md5sum
+.Nd compute or check MD5 message digests
+.Sh SYNOPSIS
+.Nm
+.Op Fl c
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+writes MD5 (128-bit) checksums of each
+.Ar file
+to stdout.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl c
+Read list of MD5 checksums from each
+.Ar file
+and check them.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.El
diff --git a/util/sbase/md5sum.c b/util/sbase/md5sum.c
new file mode 100644
index 00000000..224b20ed
--- /dev/null
+++ b/util/sbase/md5sum.c
@@ -0,0 +1,46 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+
+#include "crypt.h"
+#include "md5.h"
+#include "util.h"
+
+static struct md5 s;
+struct crypt_ops md5_ops = {
+ md5_init,
+ md5_update,
+ md5_sum,
+ &s,
+};
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-c] [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int ret = 0, (*cryptfunc)(int, char **, struct crypt_ops *, uint8_t *, size_t) = cryptmain;
+ uint8_t md[MD5_DIGEST_LENGTH];
+
+ ARGBEGIN {
+ case 'b':
+ case 't':
+ /* ignore */
+ break;
+ case 'c':
+ cryptfunc = cryptcheck;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ ret |= cryptfunc(argc, argv, &md5_ops, md, sizeof(md));
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/mkdir.1 b/util/sbase/mkdir.1
new file mode 100644
index 00000000..ec842d5b
--- /dev/null
+++ b/util/sbase/mkdir.1
@@ -0,0 +1,34 @@
+.Dd October 8, 2015
+.Dt MKDIR 1
+.Os sbase
+.Sh NAME
+.Nm mkdir
+.Nd create directories
+.Sh SYNOPSIS
+.Nm
+.Op Fl p
+.Op Fl m Ar mode
+.Ar name ...
+.Sh DESCRIPTION
+.Nm
+creates a directory for each
+.Ar name
+if it does not already exist.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl m
+Set the file
+.Ar mode
+of newly created directories.
+See
+.Xr chmod 1 .
+.It Fl p
+Also create necessary parent directories and
+do not fail if
+.Ar name
+already exists.
+.El
+.Sh SEE ALSO
+.Xr mkdir 2
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/mkdir.c b/util/sbase/mkdir.c
new file mode 100644
index 00000000..3e20b1ae
--- /dev/null
+++ b/util/sbase/mkdir.c
@@ -0,0 +1,49 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+
+#include <errno.h>
+#include <stdlib.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-p] [-m mode] name ...\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ mode_t mode, mask;
+ int pflag = 0, ret = 0;
+
+ mask = umask(0);
+ mode = 0777 & ~mask;
+
+ ARGBEGIN {
+ case 'p':
+ pflag = 1;
+ break;
+ case 'm':
+ mode = parsemode(EARGF(usage()), 0777, mask);
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc)
+ usage();
+
+ for (; *argv; argc--, argv++) {
+ if (pflag) {
+ if (mkdirp(*argv, mode, 0777 & (~mask | 0300)) < 0)
+ ret = 1;
+ } else if (mkdir(*argv, mode) < 0) {
+ weprintf("mkdir %s:", *argv);
+ ret = 1;
+ }
+ }
+
+ return ret;
+}
diff --git a/util/sbase/mkfifo.1 b/util/sbase/mkfifo.1
new file mode 100644
index 00000000..58b724aa
--- /dev/null
+++ b/util/sbase/mkfifo.1
@@ -0,0 +1,28 @@
+.Dd October 8, 2015
+.Dt MKFIFO 1
+.Os sbase
+.Sh NAME
+.Nm mkfifo
+.Nd create named pipes
+.Sh SYNOPSIS
+.Nm
+.Op Fl m Ar mode
+.Ar name ...
+.Sh DESCRIPTION
+.Nm
+creates a named pipe for each
+.Ar name
+if it does not already exist.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl m
+Set the file
+.Ar mode
+of newly created named pipes.
+See
+.Xr chmod 1 .
+.El
+.Sh SEE ALSO
+.Xr mkfifo 3
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/mkfifo.c b/util/sbase/mkfifo.c
new file mode 100644
index 00000000..2470a09d
--- /dev/null
+++ b/util/sbase/mkfifo.c
@@ -0,0 +1,39 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+
+#include <stdlib.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-m mode] name ...\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ mode_t mode = 0666;
+ int ret = 0;
+
+ ARGBEGIN {
+ case 'm':
+ mode = parsemode(EARGF(usage()), mode, umask(0));
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc)
+ usage();
+
+ for (; *argv; argc--, argv++) {
+ if (mkfifo(*argv, mode) < 0) {
+ weprintf("mkfifo %s:", *argv);
+ ret = 1;
+ }
+ }
+
+ return ret;
+}
diff --git a/util/sbase/mknod.1 b/util/sbase/mknod.1
new file mode 100644
index 00000000..1206549c
--- /dev/null
+++ b/util/sbase/mknod.1
@@ -0,0 +1,44 @@
+.Dd February 2, 2015
+.Dt MKNOD 1
+.Os sbase
+.Sh NAME
+.Nm mknod
+.Nd create a special device file
+.Sh SYNOPSIS
+.Nm
+.Op Fl m Ar mode
+.Ar name
+.Cm b Ns | Ns Cm c Ns | Ns Cm u
+.Ar major
+.Ar minor
+.Nm
+.Op Fl m Ar mode
+.Ar name
+.Cm p
+.Sh DESCRIPTION
+.Nm
+creates a special file named
+.Ar name .
+.Pp
+The following special file types are supported:
+.Bl -tag -width Ds
+.It Cm b
+A block device.
+.It Cm c | u
+A character device.
+.It Cm p
+A named pipe.
+.El
+.Pp
+Block and character devices are created with major number
+.Ar major ,
+and minor number
+.Ar minor .
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl m
+Set the mode of the new file based on the octal value of
+.Ar mode .
+.El
+.Sh SEE ALSO
+.Xr mknod 2
diff --git a/util/sbase/mknod.c b/util/sbase/mknod.c
new file mode 100644
index 00000000..a519ecb2
--- /dev/null
+++ b/util/sbase/mknod.c
@@ -0,0 +1,72 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+#include <sys/types.h>
+#ifndef makedev
+#include <sys/sysmacros.h>
+#endif
+
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-m mode] name b|c|u major minor\n"
+ " %s [-m mode] name p\n",
+ argv0, argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ mode_t mode = 0666;
+ dev_t dev;
+
+ ARGBEGIN {
+ case 'm':
+ mode = parsemode(EARGF(usage()), mode, umask(0));
+ break;
+ default:
+ usage();
+ } ARGEND;
+
+ if (argc < 2)
+ usage();
+
+ if (strlen(argv[1]) != 1)
+ goto invalid;
+ switch (argv[1][0]) {
+ case 'b':
+ mode |= S_IFBLK;
+ break;
+ case 'u':
+ case 'c':
+ mode |= S_IFCHR;
+ break;
+ case 'p':
+ mode |= S_IFIFO;
+ break;
+ default:
+ invalid:
+ eprintf("invalid type '%s'\n", argv[1]);
+ }
+
+ if (S_ISFIFO(mode)) {
+ if (argc != 2)
+ usage();
+ dev = 0;
+ } else {
+ if (argc != 4)
+ usage();
+ dev = makedev(estrtonum(argv[2], 0, LLONG_MAX), estrtonum(argv[3], 0, LLONG_MAX));
+ }
+
+ if (mknod(argv[0], mode, dev) == -1)
+ eprintf("mknod %s:", argv[0]);
+ return 0;
+}
diff --git a/util/sbase/mktemp.1 b/util/sbase/mktemp.1
new file mode 100644
index 00000000..59e27d8e
--- /dev/null
+++ b/util/sbase/mktemp.1
@@ -0,0 +1,50 @@
+.Dd October 8, 2015
+.Dt MKTEMP 1
+.Os sbase
+.Sh NAME
+.Nm mktemp
+.Nd create temporary file or directory
+.Sh SYNOPSIS
+.Nm
+.Op Fl dqtu
+.Op Fl p Ar directory
+.Op Ar template
+.Sh DESCRIPTION
+.Nm
+creates a temporary file by generating a unique filename with
+.Ar template ,
+which has to have at least six 'X's appended to it.
+If no
+.Ar template
+is specified, a default of 'tmp.XXXXXXXXXX' is used and the
+tmpdir set to '/tmp' or, if set, the TMPDIR environment variable.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl d
+Create a temporary directory instead of a file.
+.It Fl p Ar directory
+Use the specified
+.Ar directory
+as a prefix when generating the temporary filename.
+The directory will be overridden by the user's
+.Ev TMPDIR
+environment variable if it is set.
+This option implies the
+.Fl t
+flag (see below).
+.It Fl q
+Fail silently if an error occurs.
+.It Fl t
+Generate a path rooted in a temporary directory.
+.It Fl u
+Unlink file before
+.Nm
+exits.
+This is slightly better than
+.Xr mktemp 3
+but still introduces a race condition.
+Use of this option is not encouraged.
+.El
+.Sh SEE ALSO
+.Xr mkdtemp 3 ,
+.Xr mkstemp 3
diff --git a/util/sbase/mktemp.c b/util/sbase/mktemp.c
new file mode 100644
index 00000000..a3076ba2
--- /dev/null
+++ b/util/sbase/mktemp.c
@@ -0,0 +1,92 @@
+/* See LICENSE file for copyright and license details. */
+#include <libgen.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-dqtu] [-p directory] [template]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int dflag = 0, pflag = 0, qflag = 0, tflag = 0, uflag = 0, fd;
+ char *template = "tmp.XXXXXXXXXX", *tmpdir = "", *pdir,
+ *p, path[PATH_MAX], tmp[PATH_MAX];
+ size_t len;
+
+ ARGBEGIN {
+ case 'd':
+ dflag = 1;
+ break;
+ case 'p':
+ pflag = 1;
+ pdir = EARGF(usage());
+ break;
+ case 'q':
+ qflag = 1;
+ break;
+ case 't':
+ tflag = 1;
+ break;
+ case 'u':
+ uflag = 1;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (argc > 1)
+ usage();
+ else if (argc == 1)
+ template = argv[0];
+
+ if (!argc || pflag || tflag) {
+ if ((p = getenv("TMPDIR")))
+ tmpdir = p;
+ else if (pflag)
+ tmpdir = pdir;
+ else
+ tmpdir = "/tmp";
+ }
+
+ len = estrlcpy(path, tmpdir, sizeof(path));
+ if (path[0] && path[len - 1] != '/')
+ estrlcat(path, "/", sizeof(path));
+
+ estrlcpy(tmp, template, sizeof(tmp));
+ p = dirname(tmp);
+ if (!(p[0] == '.' && p[1] == '\0')) {
+ if (tflag && !pflag)
+ eprintf("template must not contain directory separators in -t mode\n");
+ }
+ estrlcat(path, template, sizeof(path));
+
+ if (dflag) {
+ if (!mkdtemp(path)) {
+ if (!qflag)
+ eprintf("mkdtemp %s:", path);
+ return 1;
+ }
+ } else {
+ if ((fd = mkstemp(path)) < 0) {
+ if (!qflag)
+ eprintf("mkstemp %s:", path);
+ return 1;
+ }
+ if (close(fd))
+ eprintf("close %s:", path);
+ }
+ if (uflag)
+ unlink(path);
+ puts(path);
+
+ efshut(stdout, "<stdout>");
+ return 0;
+}
diff --git a/util/sbase/mv.1 b/util/sbase/mv.1
new file mode 100644
index 00000000..7fb95273
--- /dev/null
+++ b/util/sbase/mv.1
@@ -0,0 +1,36 @@
+.Dd October 8, 2015
+.Dt MV 1
+.Os sbase
+.Sh NAME
+.Nm mv
+.Nd move files and directories
+.Sh SYNOPSIS
+.Nm
+.Op Fl f
+.Ar source ...
+.Ar dest
+.Sh DESCRIPTION
+.Nm
+moves each
+.Ar source
+to
+.Ar dest .
+If only one
+.Ar source
+is given and
+.Ar dest
+is not a directory,
+.Nm
+overwrites the latter with the former.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl f
+Do not prompt before overwriting.
+.Ar dest .
+Prompting has not been implemented yet.
+.El
+.Sh STANDARDS
+POSIX.1-2013.
+Except for the unsupported
+.Fl i
+flag.
diff --git a/util/sbase/mv.c b/util/sbase/mv.c
new file mode 100644
index 00000000..d24c77f5
--- /dev/null
+++ b/util/sbase/mv.c
@@ -0,0 +1,68 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+
+#include "fs.h"
+#include "util.h"
+
+static int mv_status = 0;
+
+static int
+mv(const char *s1, const char *s2, int depth)
+{
+ struct recursor r = { .fn = rm, .follow = 'P', .flags = SILENT };
+
+ if (!rename(s1, s2))
+ return 0;
+ if (errno == EXDEV) {
+ cp_aflag = cp_rflag = cp_pflag = 1;
+ cp_follow = 'P';
+ cp_status = 0;
+ rm_status = 0;
+ cp(s1, s2, depth);
+ if (cp_status == 0)
+ recurse(AT_FDCWD, s1, NULL, &r);
+ if (cp_status || rm_status)
+ mv_status = 1;
+ } else {
+ weprintf("%s -> %s:", s1, s2);
+ mv_status = 1;
+ }
+
+ return 0;
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-f] source ... dest\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct stat st;
+
+ ARGBEGIN {
+ case 'f':
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (argc < 2)
+ usage();
+
+ if (argc > 2) {
+ if (stat(argv[argc - 1], &st) < 0)
+ eprintf("stat %s:", argv[argc - 1]);
+ if (!S_ISDIR(st.st_mode))
+ eprintf("%s: not a directory\n", argv[argc - 1]);
+ }
+ enmasse(argc, argv, mv);
+
+ return mv_status;
+}
diff --git a/util/sbase/nice.1 b/util/sbase/nice.1
new file mode 100644
index 00000000..18bbe585
--- /dev/null
+++ b/util/sbase/nice.1
@@ -0,0 +1,36 @@
+.Dd October 8, 2015
+.Dt NICE 1
+.Os sbase
+.Sh NAME
+.Nm nice
+.Nd run command with altered niceness
+.Sh SYNOPSIS
+.Nm
+.Op Fl n Ar inc
+.Ar cmd
+.Op Ar arg ...
+.Sh DESCRIPTION
+.Nm
+runs
+.Ar cmd
+with the current niceness plus
+.Ar inc .
+A negative niceness is reserved to the superuser.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl n Ar inc
+Change niceness by
+.Ar inc ,
+ranging from
+.Sy -20
+(highest priority)
+to
+.Sy +20
+(lowest priority).
+Default is 10.
+.El
+.Sh SEE ALSO
+.Xr nice 2 ,
+.Xr renice 2
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/nice.c b/util/sbase/nice.c
new file mode 100644
index 00000000..d036e26c
--- /dev/null
+++ b/util/sbase/nice.c
@@ -0,0 +1,56 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/resource.h>
+
+#include <errno.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "util.h"
+
+#ifndef PRIO_MIN
+#define PRIO_MIN -NZERO
+#endif
+
+#ifndef PRIO_MAX
+#define PRIO_MAX (NZERO-1)
+#endif
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-n inc] cmd [arg ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int val = 10, r, savederrno;
+
+ ARGBEGIN {
+ case 'n':
+ val = estrtonum(EARGF(usage()), PRIO_MIN, PRIO_MAX);
+ break;
+ default:
+ usage();
+ break;
+ } ARGEND
+
+ if (!argc)
+ usage();
+
+ errno = 0;
+ r = getpriority(PRIO_PROCESS, 0);
+ if (errno)
+ weprintf("getpriority:");
+ else
+ val += r;
+ LIMIT(val, PRIO_MIN, PRIO_MAX);
+ if (setpriority(PRIO_PROCESS, 0, val) < 0)
+ weprintf("setpriority:");
+
+ execvp(argv[0], argv);
+ savederrno = errno;
+ weprintf("execvp %s:", argv[0]);
+
+ _exit(126 + (savederrno == ENOENT));
+}
diff --git a/util/sbase/nl.1 b/util/sbase/nl.1
new file mode 100644
index 00000000..26975421
--- /dev/null
+++ b/util/sbase/nl.1
@@ -0,0 +1,116 @@
+.Dd May 15, 2020
+.Dt NL 1
+.Os sbase
+.Sh NAME
+.Nm nl
+.Nd line numbering filter
+.Sh SYNOPSIS
+.Nm
+.Op Fl p
+.Op Fl b Ar type
+.Op Fl d Ar delim
+.Op Fl f Ar type
+.Op Fl h Ar type
+.Op Fl i Ar num
+.Op Fl l Ar num
+.Op Fl n Ar format
+.Op Fl s Ar sep
+.Op Fl v Ar num
+.Op Fl w Ar num
+.Op Ar file
+.Sh DESCRIPTION
+.Nm
+reads lines from
+.Ar file
+and writes them to stdout, numbering non-empty lines.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.Pp
+.Nm
+treats the input text as a collection of logical pages divided into
+logical page sections.
+Each logical page consists of a header section, a body
+section and a footer section.
+Sections may be empty.
+The start of each section is indicated by a single delimiting line, one of:
+.Bl -column "\e:\e:\e: " "header " -offset indent
+.It Em "Line" Ta Em "Start of"
+.It \e:\e:\e: header
+.It \e:\e: body
+.It \e: footer
+.El
+.Pp
+If the input text contains no delimiting line then all of the input text
+belongs to a single logical page body section.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl p
+Do not reset line number for logical pages.
+.It Fl h Ar type | Fl b Ar type | Fl f Ar type
+Define which lines to number in the head | body | footer section:
+.Bl -tag -width pstringXX
+.It a
+All lines.
+.It n
+No lines.
+.It t
+Only non-empty lines.
+This is the default.
+.It p Ns Ar expr
+Only lines matching
+.Ar expr
+according to
+.Xr regex 7 or
+.Xr re_format 7 .
+.El
+.It Fl d Ar delim
+Set
+.Ar delim
+as the delimiter for logical pages.
+If
+.Ar delim
+is only one character,
+.Nm
+appends ":" to it.
+The default is "\e:".
+.It Fl i Ar num
+Set the increment between numbered lines to
+.Ar num .
+.It Fl l Ar num
+Set the number of adjacent blank lines to be considered as one to
+.Ar num .
+The default is 1.
+.It Fl n Ar format
+Set the line number output
+.Ar format
+to one of:
+.Bl -tag -width pstringXX
+.It ln
+Left justified.
+.It rn
+Right justified.
+This is the default.
+.It rz
+Right justified with leading zeroes.
+.El
+.It Fl s Ar sep
+Use
+.Ar sep
+to separate line numbers and lines.
+The default is "\et".
+.It Fl v Ar num
+Start counting lines from
+.Ar num .
+The default is 1.
+.It Fl w Ar num
+Set the width of the line number to
+.Ar num .
+The default is 6.
+.El
+.Sh SEE ALSO
+.Xr pr 1
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/nl.c b/util/sbase/nl.c
new file mode 100644
index 00000000..9a289b02
--- /dev/null
+++ b/util/sbase/nl.c
@@ -0,0 +1,212 @@
+/* See LICENSE file for copyright and license details. */
+#include <limits.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "text.h"
+#include "utf.h"
+#include "util.h"
+
+static size_t startnum = 1;
+static size_t incr = 1;
+static size_t blines = 1;
+static size_t delimlen = 2;
+static size_t seplen = 1;
+static int width = 6;
+static int pflag = 0;
+static char type[] = { 'n', 't', 'n' }; /* footer, body, header */
+static char *delim = "\\:";
+static char format[6] = "%*ld";
+static char *sep = "\t";
+static regex_t preg[3];
+
+static int
+getsection(struct line *l, int *section)
+{
+ size_t i;
+ int sectionchanged = 0, newsection = *section;
+
+ for (i = 0; (l->len - i) >= delimlen &&
+ !memcmp(l->data + i, delim, delimlen); i += delimlen) {
+ if (!sectionchanged) {
+ sectionchanged = 1;
+ newsection = 0;
+ } else {
+ newsection = (newsection + 1) % 3;
+ }
+ }
+
+ if (!(l->len - i) || l->data[i] == '\n')
+ *section = newsection;
+ else
+ sectionchanged = 0;
+
+ return sectionchanged;
+}
+
+static void
+nl(const char *fname, FILE *fp)
+{
+ static struct line line;
+ static size_t size;
+ size_t number = startnum, bl = 1;
+ ssize_t len;
+ int donumber, oldsection, section = 1;
+
+ while ((len = getline(&line.data, &size, fp)) > 0) {
+ line.len = len;
+ donumber = 0;
+ oldsection = section;
+
+ if (getsection(&line, &section)) {
+ if ((section >= oldsection) && !pflag)
+ number = startnum;
+ continue;
+ }
+
+ switch (type[section]) {
+ case 't':
+ if (line.data[0] != '\n')
+ donumber = 1;
+ break;
+ case 'p':
+ if (!regexec(preg + section, line.data, 0, NULL, 0))
+ donumber = 1;
+ break;
+ case 'a':
+ if (line.data[0] == '\n' && bl < blines) {
+ ++bl;
+ } else {
+ donumber = 1;
+ bl = 1;
+ }
+ }
+
+ if (donumber) {
+ printf(format, width, number);
+ fwrite(sep, 1, seplen, stdout);
+ number += incr;
+ }
+ fwrite(line.data, 1, line.len, stdout);
+ }
+ free(line.data);
+ if (ferror(fp))
+ eprintf("getline %s:", fname);
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-p] [-b type] [-d delim] [-f type]\n"
+ " [-h type] [-i num] [-l num] [-n format]\n"
+ " [-s sep] [-v num] [-w num] [file]\n", argv0);
+}
+
+static char
+getlinetype(char *type, regex_t *preg)
+{
+ if (type[0] == 'p')
+ eregcomp(preg, type + 1, REG_NOSUB);
+ else if (!type[0] || !strchr("ant", type[0]))
+ usage();
+
+ return type[0];
+}
+
+int
+main(int argc, char *argv[])
+{
+ FILE *fp = NULL;
+ size_t s;
+ int ret = 0;
+ char *d, *formattype, *formatblit;
+
+ ARGBEGIN {
+ case 'd':
+ switch (utflen((d = EARGF(usage())))) {
+ case 0:
+ eprintf("empty logical page delimiter\n");
+ case 1:
+ s = strlen(d);
+ delim = emalloc(s + 1 + 1);
+ estrlcpy(delim, d, s + 1 + 1);
+ estrlcat(delim, ":", s + 1 + 1);
+ delimlen = s + 1;
+ break;
+ default:
+ delim = d;
+ delimlen = strlen(delim);
+ break;
+ }
+ break;
+ case 'f':
+ type[0] = getlinetype(EARGF(usage()), preg);
+ break;
+ case 'b':
+ type[1] = getlinetype(EARGF(usage()), preg + 1);
+ break;
+ case 'h':
+ type[2] = getlinetype(EARGF(usage()), preg + 2);
+ break;
+ case 'i':
+ incr = estrtonum(EARGF(usage()), 0, MIN(LLONG_MAX, SIZE_MAX));
+ break;
+ case 'l':
+ blines = estrtonum(EARGF(usage()), 0, MIN(LLONG_MAX, SIZE_MAX));
+ break;
+ case 'n':
+ formattype = EARGF(usage());
+ estrlcpy(format, "%", sizeof(format));
+
+ if (!strcmp(formattype, "ln")) {
+ formatblit = "-";
+ } else if (!strcmp(formattype, "rn")) {
+ formatblit = "";
+ } else if (!strcmp(formattype, "rz")) {
+ formatblit = "0";
+ } else {
+ eprintf("%s: bad format\n", formattype);
+ }
+
+ estrlcat(format, formatblit, sizeof(format));
+ estrlcat(format, "*ld", sizeof(format));
+ break;
+ case 'p':
+ pflag = 1;
+ break;
+ case 's':
+ sep = EARGF(usage());
+ seplen = unescape(sep);
+ break;
+ case 'v':
+ startnum = estrtonum(EARGF(usage()), 0, MIN(LLONG_MAX, SIZE_MAX));
+ break;
+ case 'w':
+ width = estrtonum(EARGF(usage()), 1, INT_MAX);
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (argc > 1)
+ usage();
+
+ if (!argc) {
+ nl("<stdin>", stdin);
+ } else {
+ if (!strcmp(argv[0], "-")) {
+ argv[0] = "<stdin>";
+ fp = stdin;
+ } else if (!(fp = fopen(argv[0], "r"))) {
+ eprintf("fopen %s:", argv[0]);
+ }
+ nl(argv[0], fp);
+ }
+
+ ret |= fp && fp != stdin && fshut(fp, argv[0]);
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/nohup.1 b/util/sbase/nohup.1
new file mode 100644
index 00000000..48a8fad6
--- /dev/null
+++ b/util/sbase/nohup.1
@@ -0,0 +1,40 @@
+.Dd October 8, 2015
+.Dt NOHUP 1
+.Os sbase
+.Sh NAME
+.Nm nohup
+.Nd run command immune to hangups
+.Sh SYNOPSIS
+.Nm
+.Ar cmd
+.Op Ar arg ...
+.Sh DESCRIPTION
+.Nm
+runs
+.Ar cmd
+with the
+.Em HUP
+signal set to be ignored.
+.Pp
+If stdout is a terminal, it is appended to
+.Em nohup.out
+in the current working directory.
+If stderr is a terminal, it is redirected to stdout.
+.Sh EXIT STATUS
+.Bl -tag -width Ds
+.It 0
+.Ar cmd
+executed successfully.
+.It 1
+Internal error.
+.It 126
+.Ar cmd
+was found but could not be executed.
+.It 127
+.Ar cmd
+could not be found.
+.El
+.Sh SEE ALSO
+.Xr signal 7
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/nohup.c b/util/sbase/nohup.c
new file mode 100644
index 00000000..5c1bf448
--- /dev/null
+++ b/util/sbase/nohup.c
@@ -0,0 +1,48 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s cmd [arg ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int fd, savederrno;
+
+ ARGBEGIN {
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc)
+ usage();
+
+ if (signal(SIGHUP, SIG_IGN) == SIG_ERR)
+ enprintf(127, "signal HUP:");
+
+ if (isatty(STDOUT_FILENO)) {
+ if ((fd = open("nohup.out", O_WRONLY | O_APPEND | O_CREAT, S_IRUSR | S_IWUSR)) < 0)
+ enprintf(127, "open nohup.out:");
+ if (dup2(fd, STDOUT_FILENO) < 0)
+ enprintf(127, "dup2:");
+ close(fd);
+ }
+ if (isatty(STDERR_FILENO) && dup2(STDOUT_FILENO, STDERR_FILENO) < 0)
+ enprintf(127, "dup2:");
+
+ execvp(argv[0], argv);
+ savederrno = errno;
+ weprintf("execvp %s:", argv[0]);
+
+ _exit(126 + (savederrno == ENOENT));
+}
diff --git a/util/sbase/od.1 b/util/sbase/od.1
new file mode 100644
index 00000000..622093f5
--- /dev/null
+++ b/util/sbase/od.1
@@ -0,0 +1,80 @@
+.Dd October 25, 2015
+.Dt OD 1
+.Os sbase
+.Sh NAME
+.Nm od
+.Nd octal dump
+.Sh SYNOPSIS
+.Nm
+.Op Fl bdosvx
+.Op Fl A Ar addrformat
+.Op Fl E | e
+.Op Fl j Ar skip
+.Op Fl t Ar outputformat...
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+writes an octal dump of each
+.Ar file
+to stdout.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl A Ar addressformat
+.Ar addressformat
+is one of d|o|x|n and sets the address to be
+either in \fId\fRecimal, \fIo\fRctal, he\fIx\fRadecimal or \fIn\fRot
+printed at all.
+The default is octal.
+.It Fl E | e
+Force Little Endian
+.Fl ( e )
+or Big Endian
+.Fl ( E )
+system-independently.
+.It Fl b
+Equivalent to
+.Fl t o1 .
+.It Fl d
+Equivalent to
+.Fl t u2 .
+.It Fl j Ar skip
+Ignore the first
+.Ar skip
+bytes of input.
+.It Fl o
+Equivalent to
+.Fl t o2 .
+.It Fl s
+Equivalent to
+.Fl t d2 .
+.It Fl t Ar outputformat
+.Ar outputformat
+is a list of a|c|d|o|u|x followed by a digit or C|S|I|L and sets
+the content to be in n\fIa\fRmed character, \fIc\fRharacter, signed
+\fId\fRecimal, \fIo\fRctal, \fIu\fRnsigned decimal, or
+he\fIx\fRadecimal format, processing the given amount of bytes or the length
+of \fIC\fRhar, \fIS\fRhort, \fII\fRnteger or \fIL\fRong.
+The default is octal with 4 bytes.
+.It Fl v
+Always set.
+Write all input data, including duplicate lines.
+.It Fl x
+Equivalent to
+.Fl t x2 .
+.El
+.Sh STANDARDS
+POSIX.1-2013.
+Except that the
+.Op Fl v
+flag is always enabled and the 'd' parameter for the
+.Op Fl t
+flag is interpreted as 'u'.
+.Pp
+The
+.Op Ee
+flags are an extension to that specification.
diff --git a/util/sbase/od.c b/util/sbase/od.c
new file mode 100644
index 00000000..0b1c5c60
--- /dev/null
+++ b/util/sbase/od.c
@@ -0,0 +1,332 @@
+/* See LICENSE file for copyright and license details. */
+#include <ctype.h>
+#include <fcntl.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "queue.h"
+#include "util.h"
+
+struct type {
+ unsigned char format;
+ unsigned int len;
+ TAILQ_ENTRY(type) entry;
+};
+
+static TAILQ_HEAD(head, type) head = TAILQ_HEAD_INITIALIZER(head);
+static unsigned char addr_format = 'o';
+static off_t skip = 0;
+static off_t max = -1;
+static size_t linelen = 1;
+static int big_endian;
+
+static void
+printaddress(off_t addr)
+{
+ char fmt[] = "%07j#";
+
+ if (addr_format == 'n') {
+ fputc(' ', stdout);
+ } else {
+ fmt[4] = addr_format;
+ printf(fmt, (intmax_t)addr);
+ }
+}
+
+static void
+printchunk(const unsigned char *s, unsigned char format, size_t len)
+{
+ long long res, basefac;
+ size_t i;
+ char fmt[] = " %#*ll#";
+ unsigned char c;
+
+ const char *namedict[] = {
+ "nul", "soh", "stx", "etx", "eot", "enq", "ack",
+ "bel", "bs", "ht", "nl", "vt", "ff", "cr",
+ "so", "si", "dle", "dc1", "dc2", "dc3", "dc4",
+ "nak", "syn", "etb", "can", "em", "sub", "esc",
+ "fs", "gs", "rs", "us", "sp",
+ };
+ const char *escdict[] = {
+ ['\0'] = "\\0", ['\a'] = "\\a",
+ ['\b'] = "\\b", ['\t'] = "\\t",
+ ['\n'] = "\\n", ['\v'] = "\\v",
+ ['\f'] = "\\f", ['\r'] = "\\r",
+ };
+
+ switch (format) {
+ case 'a':
+ c = *s & ~128; /* clear high bit as required by standard */
+ if (c < LEN(namedict) || c == 127) {
+ printf(" %3s", (c == 127) ? "del" : namedict[c]);
+ } else {
+ printf(" %3c", c);
+ }
+ break;
+ case 'c':
+ if (strchr("\a\b\t\n\v\f\r\0", *s)) {
+ printf(" %3s", escdict[*s]);
+ } else if (!isprint(*s)) {
+ printf(" %3o", *s);
+ } else {
+ printf(" %3c", *s);
+ }
+ break;
+ default:
+ if (big_endian) {
+ for (res = 0, basefac = 1, i = len; i; i--) {
+ res += s[i - 1] * basefac;
+ basefac <<= 8;
+ }
+ } else {
+ for (res = 0, basefac = 1, i = 0; i < len; i++) {
+ res += s[i] * basefac;
+ basefac <<= 8;
+ }
+ }
+ fmt[2] = big_endian ? '-' : ' ';
+ fmt[6] = format;
+ printf(fmt, (int)(3 * len + len - 1), res);
+ }
+}
+
+static void
+printline(const unsigned char *line, size_t len, off_t addr)
+{
+ struct type *t = NULL;
+ size_t i;
+ int first = 1;
+ unsigned char *tmp;
+
+ if (TAILQ_EMPTY(&head))
+ goto once;
+ TAILQ_FOREACH(t, &head, entry) {
+once:
+ if (first) {
+ printaddress(addr);
+ first = 0;
+ } else {
+ printf("%*c", (addr_format == 'n') ? 1 : 7, ' ');
+ }
+ for (i = 0; i < len; i += MIN(len - i, t ? t->len : 4)) {
+ if (len - i < (t ? t->len : 4)) {
+ tmp = ecalloc(t ? t->len : 4, 1);
+ memcpy(tmp, line + i, len - i);
+ printchunk(tmp, t ? t->format : 'o',
+ t ? t->len : 4);
+ free(tmp);
+ } else {
+ printchunk(line + i, t ? t->format : 'o',
+ t ? t->len : 4);
+ }
+ }
+ fputc('\n', stdout);
+ if (TAILQ_EMPTY(&head) || (!len && !first))
+ break;
+ }
+}
+
+static int
+od(int fd, char *fname, int last)
+{
+ static unsigned char *line;
+ static size_t lineoff;
+ static off_t addr;
+ unsigned char buf[BUFSIZ];
+ size_t i, size = sizeof(buf);
+ ssize_t n;
+
+ while (skip - addr > 0) {
+ n = read(fd, buf, MIN(skip - addr, sizeof(buf)));
+ if (n < 0)
+ weprintf("read %s:", fname);
+ if (n <= 0)
+ return n;
+ addr += n;
+ }
+ if (!line)
+ line = emalloc(linelen);
+
+ for (;;) {
+ if (max >= 0)
+ size = MIN(max - (addr - skip), size);
+ if ((n = read(fd, buf, size)) <= 0)
+ break;
+ for (i = 0; i < n; i++, addr++) {
+ line[lineoff++] = buf[i];
+ if (lineoff == linelen) {
+ printline(line, lineoff, addr - lineoff + 1);
+ lineoff = 0;
+ }
+ }
+ }
+ if (n < 0) {
+ weprintf("read %s:", fname);
+ return n;
+ }
+ if (lineoff && last)
+ printline(line, lineoff, addr - lineoff);
+ if (last)
+ printline((unsigned char *)"", 0, addr);
+ return 0;
+}
+
+static int
+lcm(unsigned int a, unsigned int b)
+{
+ unsigned int c, d, e;
+
+ for (c = a, d = b; c ;) {
+ e = c;
+ c = d % c;
+ d = e;
+ }
+
+ return a / d * b;
+}
+
+static void
+addtype(char format, int len)
+{
+ struct type *t;
+
+ t = emalloc(sizeof(*t));
+ t->format = format;
+ t->len = len;
+ TAILQ_INSERT_TAIL(&head, t, entry);
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-bdosvx] [-A addressformat] [-E | -e] [-j skip] "
+ "[-t outputformat] [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int fd;
+ struct type *t;
+ int ret = 0, len;
+ char *s;
+
+ big_endian = (*(uint16_t *)"\0\xff" == 0xff);
+
+ ARGBEGIN {
+ case 'A':
+ s = EARGF(usage());
+ if (strlen(s) != 1 || !strchr("doxn", s[0]))
+ usage();
+ addr_format = s[0];
+ break;
+ case 'b':
+ addtype('o', 1);
+ break;
+ case 'd':
+ addtype('u', 2);
+ break;
+ case 'E':
+ case 'e':
+ big_endian = (ARGC() == 'E');
+ break;
+ case 'j':
+ if ((skip = parseoffset(EARGF(usage()))) < 0)
+ usage();
+ break;
+ case 'N':
+ if ((max = parseoffset(EARGF(usage()))) < 0)
+ usage();
+ break;
+ case 'o':
+ addtype('o', 2);
+ break;
+ case 's':
+ addtype('d', 2);
+ break;
+ case 't':
+ s = EARGF(usage());
+ for (; *s; s++) {
+ switch (*s) {
+ case 'a':
+ case 'c':
+ addtype(*s, 1);
+ break;
+ case 'd':
+ case 'o':
+ case 'u':
+ case 'x':
+ /* todo: allow multiple digits */
+ if (*(s+1) > '0' && *(s+1) <= '9') {
+ len = *(s+1) - '0';
+ } else {
+ switch (*(s+1)) {
+ case 'C':
+ len = sizeof(char);
+ break;
+ case 'S':
+ len = sizeof(short);
+ break;
+ case 'I':
+ len = sizeof(int);
+ break;
+ case 'L':
+ len = sizeof(long);
+ break;
+ default:
+ len = sizeof(int);
+ }
+ }
+ addtype(*s++, len);
+ break;
+ default:
+ usage();
+ }
+ }
+ break;
+ case 'v':
+ /* always set - use uniq(1) to handle duplicate lines */
+ break;
+ case 'x':
+ addtype('x', 2);
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ /* line length is lcm of type lengths and >= 16 by doubling */
+ TAILQ_FOREACH(t, &head, entry)
+ linelen = lcm(linelen, t->len);
+ if (TAILQ_EMPTY(&head))
+ linelen = 16;
+ while (linelen < 16)
+ linelen *= 2;
+
+ if (!argc) {
+ if (od(0, "<stdin>", 1) < 0)
+ ret = 1;
+ } else {
+ for (; *argv; argc--, argv++) {
+ if (!strcmp(*argv, "-")) {
+ *argv = "<stdin>";
+ fd = 0;
+ } else if ((fd = open(*argv, O_RDONLY)) < 0) {
+ weprintf("open %s:", *argv);
+ ret = 1;
+ continue;
+ }
+ if (od(fd, *argv, (!*(argv + 1))) < 0)
+ ret = 1;
+ if (fd != 0)
+ close(fd);
+ }
+ }
+
+ ret |= fshut(stdout, "<stdout>") | fshut(stderr, "<stderr>");
+
+ return ret;
+}
diff --git a/util/sbase/paste.1 b/util/sbase/paste.1
new file mode 100644
index 00000000..7b26130f
--- /dev/null
+++ b/util/sbase/paste.1
@@ -0,0 +1,47 @@
+.Dd October 8, 2015
+.Dt PASTE 1
+.Os sbase
+.Sh NAME
+.Nm paste
+.Nd merge lines of files in parallel or sequentially
+.Sh SYNOPSIS
+.Nm
+.Op Fl s
+.Op Fl d Ar list
+.Ar file ...
+.Sh DESCRIPTION
+.Nm
+reads single lines from each
+.Ar file
+and writes them into one line, replacing
+.Sy \en
+with
+.Sy \et
+except from the last
+.Ar file .
+This process is repeated until each
+.Ar file
+is starved, treating zero-reads as empty lines along the way.
+.Pp
+If
+.Ar file
+is '-',
+.Nm
+interprets it as stdin.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl d Ar list
+Replace
+.Sy \en
+with escaped characters from
+.Ar list
+by cycling through it.
+.It Fl s
+Read each
+.Ar file
+sequentially instead of in parallel.
+.El
+.Sh SEE ALSO
+.Xr cut 1
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/paste.c b/util/sbase/paste.c
new file mode 100644
index 00000000..4fa9fc5a
--- /dev/null
+++ b/util/sbase/paste.c
@@ -0,0 +1,144 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdlib.h>
+#include <string.h>
+
+#include "utf.h"
+#include "util.h"
+
+struct fdescr {
+ FILE *fp;
+ const char *name;
+};
+
+static void
+sequential(struct fdescr *dsc, int fdescrlen, Rune *delim, size_t delimlen)
+{
+ Rune c, last;
+ size_t i, d;
+
+ for (i = 0; i < fdescrlen; i++) {
+ d = 0;
+ last = 0;
+
+ while (efgetrune(&c, dsc[i].fp, dsc[i].name)) {
+ if (last == '\n') {
+ if (delim[d] != '\0')
+ efputrune(&delim[d], stdout, "<stdout>");
+ d = (d + 1) % delimlen;
+ }
+
+ if (c != '\n')
+ efputrune(&c, stdout, "<stdout>");
+ last = c;
+ }
+
+ if (last == '\n')
+ efputrune(&last, stdout, "<stdout>");
+ }
+}
+
+static void
+parallel(struct fdescr *dsc, int fdescrlen, Rune *delim, size_t delimlen)
+{
+ Rune c, d;
+ size_t i, m;
+ ssize_t last;
+
+nextline:
+ last = -1;
+
+ for (i = 0; i < fdescrlen; i++) {
+ d = delim[i % delimlen];
+ c = 0;
+
+ while (efgetrune(&c, dsc[i].fp, dsc[i].name)) {
+ for (m = last + 1; m < i; m++) {
+ if (delim[m % delimlen] != '\0')
+ efputrune(&delim[m % delimlen], stdout, "<stdout>");
+ }
+ last = i;
+ if (c == '\n') {
+ if (i != fdescrlen - 1)
+ c = d;
+ efputrune(&c, stdout, "<stdout>");
+ break;
+ }
+ efputrune(&c, stdout, "<stdout>");
+ }
+
+ if (c == 0 && last != -1) {
+ if (i == fdescrlen - 1)
+ putchar('\n');
+ else if (d != '\0')
+ efputrune(&d, stdout, "<stdout>");
+ last++;
+ }
+ }
+ if (last != -1)
+ goto nextline;
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-s] [-d list] file ...\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct fdescr *dsc;
+ Rune *delim_rune = NULL;
+ size_t delim_runelen, i, delim_bytelen = 1;
+ int seq = 0, ret = 0;
+ char *delim = "\t";
+
+ ARGBEGIN {
+ case 's':
+ seq = 1;
+ break;
+ case 'd':
+ delim = EARGF(usage());
+ delim_bytelen = unescape(delim);
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc)
+ usage();
+
+ /* populate delimiters */
+ delim_rune = ereallocarray(NULL,
+ utfmemlen(delim, delim_bytelen) + 1, sizeof(*delim_rune));
+ if (!(delim_runelen = utfntorunestr(delim, delim_bytelen, delim_rune))) {
+ usage();
+ }
+
+ /* populate file list */
+ dsc = ereallocarray(NULL, argc, sizeof(*dsc));
+
+ for (i = 0; i < argc; i++) {
+ if (!strcmp(argv[i], "-")) {
+ argv[i] = "<stdin>";
+ dsc[i].fp = stdin;
+ } else if (!(dsc[i].fp = fopen(argv[i], "r"))) {
+ eprintf("fopen %s:", argv[i]);
+ }
+ dsc[i].name = argv[i];
+ }
+
+ if (seq) {
+ sequential(dsc, argc, delim_rune, delim_runelen);
+ } else {
+ parallel(dsc, argc, delim_rune, delim_runelen);
+ }
+
+ for (i = 0; i < argc; i++)
+ if (dsc[i].fp != stdin && fshut(dsc[i].fp, argv[i]))
+ ret |= fshut(dsc[i].fp, argv[i]);
+
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/pathchk.1 b/util/sbase/pathchk.1
new file mode 100644
index 00000000..e0b69b65
--- /dev/null
+++ b/util/sbase/pathchk.1
@@ -0,0 +1,31 @@
+.Dd February 3, 2016
+.Dt PATHCHK 1
+.Os sbase
+.Sh NAME
+.Nm pathchk
+.Nd validate filename validity or portability
+.Sh SYNOPSIS
+.Nm
+.Op Fl p
+.Op Fl P
+.Ar file Ar ...
+.Sh DESCRIPTION
+.Nm
+checks that filenames are valid for the system,
+and optional on other POSIX systems.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl p
+Check for most POSIX systems.
+.It Fl P
+Check for empty pathnames and leading hythens.
+.El
+.Sh EXIT STATUS
+.Bl -tag -width Ds
+.It 0
+All pathname operands passed all of the checks.
+.It > 0
+An error occurred.
+.El
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/pathchk.c b/util/sbase/pathchk.c
new file mode 100644
index 00000000..b6b0c91c
--- /dev/null
+++ b/util/sbase/pathchk.c
@@ -0,0 +1,104 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+
+#include <errno.h>
+#include <limits.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "util.h"
+
+#define PORTABLE_CHARACTER_SET "0123456789._-qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM"
+/* If your system supports more other characters, but not all non-NUL characters, define SYSTEM_CHARACTER_SET. */
+
+static int most = 0;
+static int extra = 0;
+
+static int
+pathchk(char *filename)
+{
+ char *invalid, *invalid_end, *p, *q;
+ const char *character_set;
+ size_t len, maxlen;
+ struct stat st;
+
+ /* Empty? */
+ if (extra && !*filename)
+ eprintf("empty filename\n");
+
+ /* Leading hyphen? */
+ if (extra && ((*filename == '-') || strstr(filename, "/-")))
+ eprintf("%s: leading '-' in component of filename\n", filename);
+
+ /* Nonportable character? */
+#ifdef SYSTEM_CHARACTER_SET
+ character_set = "/"SYSTEM_CHARACTER_SET;
+#else
+ character_set = 0;
+#endif
+ if (most)
+ character_set = "/"PORTABLE_CHARACTER_SET;
+ if (character_set && *(invalid = filename + strspn(filename, character_set))) {
+ for (invalid_end = invalid + 1; *invalid_end & 0x80; invalid_end++);
+ p = estrdup(filename);
+ *invalid_end = 0;
+ eprintf("%s: nonportable character '%s'\n", p, invalid);
+ }
+
+ /* Symlink error? Non-searchable directory? */
+ if (lstat(filename, &st) && errno != ENOENT) {
+ /* lstat rather than stat, so that if filename is a bad symlink, but
+ * all parents are OK, no error will be detected. */
+ eprintf("%s:", filename);
+ }
+
+ /* Too long pathname? */
+ maxlen = most ? _POSIX_PATH_MAX : PATH_MAX;
+ if (strlen(filename) >= maxlen)
+ eprintf("%s: is longer than %zu bytes\n", filename, maxlen);
+
+ /* Too long component? */
+ maxlen = most ? _POSIX_NAME_MAX : NAME_MAX;
+ for (p = filename; p; p = q) {
+ q = strchr(p, '/');
+ len = q ? (size_t)(q++ - p) : strlen(p);
+ if (len > maxlen)
+ eprintf("%s: includes component longer than %zu bytes\n",
+ filename, maxlen);
+ }
+
+ return 0;
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-pP] filename...\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int ret = 0;
+
+ ARGBEGIN {
+ case 'p':
+ most = 1;
+ break;
+ case 'P':
+ extra = 1;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc)
+ usage();
+
+ for (; argc--; argv++)
+ ret |= pathchk(*argv);
+
+ return ret;
+}
diff --git a/util/sbase/printenv.1 b/util/sbase/printenv.1
new file mode 100644
index 00000000..24c410be
--- /dev/null
+++ b/util/sbase/printenv.1
@@ -0,0 +1,30 @@
+.Dd March 30, 2016
+.Dt PRINTENV 1
+.Os sbase
+.Sh NAME
+.Nm printenv
+.Nd print the environment or values of variables
+.Sh SYNOPSIS
+.Nm
+.Op Ar var ...
+.Sh DESCRIPTION
+.Nm
+prints the entire environment as key=value pairs if no
+.Ar var
+is given.
+Otherwise,
+.Nm
+prints only the value of each
+.Ar var
+one per line in the order specified.
+.Sh EXIT STATUS
+.Bl -tag -width Ds
+.It 0
+Successful completion.
+.It 1
+One or more queried variables were not found.
+.It > 1
+An error occurred.
+.El
+.Sh SEE ALSO
+.Xr env 1
diff --git a/util/sbase/printenv.c b/util/sbase/printenv.c
new file mode 100644
index 00000000..19b5b7d2
--- /dev/null
+++ b/util/sbase/printenv.c
@@ -0,0 +1,39 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "util.h"
+
+extern char **environ;
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [var ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ char *var;
+ int ret = 0;
+
+ ARGBEGIN {
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc) {
+ for (; *environ; environ++)
+ puts(*environ);
+ } else {
+ for (; *argv; argc--, argv++) {
+ if ((var = getenv(*argv)))
+ puts(var);
+ else
+ ret = 1;
+ }
+ }
+
+ return fshut(stdout, "<stdout>") ? 2 : ret;
+}
diff --git a/util/sbase/printf.1 b/util/sbase/printf.1
new file mode 100644
index 00000000..67456e47
--- /dev/null
+++ b/util/sbase/printf.1
@@ -0,0 +1,33 @@
+.Dd October 8, 2015
+.Dt PRINTF 1
+.Os sbase
+.Sh NAME
+.Nm printf
+.Nd print formatted data
+.Sh SYNOPSIS
+.Nm
+.Ar format
+.Op Ar arg ...
+.Sh DESCRIPTION
+.Nm
+writes formatted data according to
+.Ar format
+using each
+.Ar arg
+until drained.
+.Pp
+.Nm
+interprets the standard escape sequences \e\e, \e', \e", \ea, \eb, \ee,
+\ef, \en, \er, \et, \ev, \exH[H], \eO[OOO], the sequence \ec, which
+terminates further output if it's found inside
+.Ar format
+or a %b format string, the format specification %b for an unescaped string and
+all C
+.Xr printf 3
+format specifications ending with csdiouxXaAeEfFgG, including variable width
+and precision.
+.Sh STANDARDS
+POSIX.1-2013.
+.Pp
+The possibility of specifying 4-digit octals is an extension to that
+specification.
diff --git a/util/sbase/printf.c b/util/sbase/printf.c
new file mode 100644
index 00000000..039dac71
--- /dev/null
+++ b/util/sbase/printf.c
@@ -0,0 +1,188 @@
+/* See LICENSE file for copyright and license details. */
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "utf.h"
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s format [arg ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ Rune *rarg;
+ size_t i, j, argi, lastargi, formatlen, blen;
+ long long num;
+ double dou;
+ int cooldown = 0, width, precision, ret = 0;
+ char *format, *tmp, *arg, *fmt, flag;
+
+ argv0 = argv[0];
+ if (argc < 2)
+ usage();
+
+ format = argv[1];
+ if ((tmp = strstr(format, "\\c"))) {
+ *tmp = 0;
+ cooldown = 1;
+ }
+ formatlen = unescape(format);
+ if (formatlen == 0)
+ return 0;
+ lastargi = 0;
+ for (i = 0, argi = 2; !cooldown || i < formatlen; i++, i = cooldown ? i : (i % formatlen)) {
+ if (i == 0) {
+ if (lastargi == argi)
+ break;
+ lastargi = argi;
+ }
+ if (format[i] != '%') {
+ putchar(format[i]);
+ continue;
+ }
+
+ /* flag */
+ for (flag = '\0', i++; strchr("#-+ 0", format[i]); i++) {
+ flag = format[i];
+ }
+
+ /* field width */
+ width = -1;
+ if (format[i] == '*') {
+ if (argi < argc)
+ width = estrtonum(argv[argi++], 0, INT_MAX);
+ else
+ cooldown = 1;
+ i++;
+ } else {
+ j = i;
+ for (; strchr("+-0123456789", format[i]); i++);
+ if (j != i) {
+ tmp = estrndup(format + j, i - j);
+ width = estrtonum(tmp, 0, INT_MAX);
+ free(tmp);
+ } else {
+ width = 0;
+ }
+ }
+
+ /* field precision */
+ precision = -1;
+ if (format[i] == '.') {
+ if (format[++i] == '*') {
+ if (argi < argc)
+ precision = estrtonum(argv[argi++], 0, INT_MAX);
+ else
+ cooldown = 1;
+ i++;
+ } else {
+ j = i;
+ for (; strchr("+-0123456789", format[i]); i++);
+ if (j != i) {
+ tmp = estrndup(format + j, i - j);
+ precision = estrtonum(tmp, 0, INT_MAX);
+ free(tmp);
+ } else {
+ precision = 0;
+ }
+ }
+ }
+
+ if (format[i] != '%') {
+ if (argi < argc)
+ arg = argv[argi++];
+ else {
+ arg = "";
+ cooldown = 1;
+ }
+ } else {
+ putchar('%');
+ continue;
+ }
+
+ switch (format[i]) {
+ case 'b':
+ if ((tmp = strstr(arg, "\\c"))) {
+ *tmp = 0;
+ blen = unescape(arg);
+ fwrite(arg, sizeof(*arg), blen, stdout);
+ return 0;
+ }
+ blen = unescape(arg);
+ fwrite(arg, sizeof(*arg), blen, stdout);
+ break;
+ case 'c':
+ unescape(arg);
+ rarg = ereallocarray(NULL, utflen(arg) + 1, sizeof(*rarg));
+ utftorunestr(arg, rarg);
+ efputrune(rarg, stdout, "<stdout>");
+ free(rarg);
+ break;
+ case 's':
+ fmt = estrdup(flag ? "%#*.*s" : "%*.*s");
+ if (flag)
+ fmt[1] = flag;
+ printf(fmt, width, precision, arg);
+ free(fmt);
+ break;
+ case 'd': case 'i': case 'o': case 'u': case 'x': case 'X':
+ for (j = 0; isspace(arg[j]); j++);
+ if (arg[j] == '\'' || arg[j] == '\"') {
+ arg += j + 1;
+ unescape(arg);
+ rarg = ereallocarray(NULL, utflen(arg) + 1, sizeof(*rarg));
+ utftorunestr(arg, rarg);
+ num = rarg[0];
+ } else if (arg[0]) {
+ errno = 0;
+ if (format[i] == 'd' || format[i] == 'i')
+ num = strtol(arg, &tmp, 0);
+ else
+ num = strtoul(arg, &tmp, 0);
+
+ if (tmp == arg || *tmp != '\0') {
+ ret = 1;
+ weprintf("%%%c %s: conversion error\n",
+ format[i], arg);
+ }
+ if (errno == ERANGE) {
+ ret = 1;
+ weprintf("%%%c %s: out of range\n",
+ format[i], arg);
+ }
+ } else {
+ num = 0;
+ }
+ fmt = estrdup(flag ? "%#*.*ll#" : "%*.*ll#");
+ if (flag)
+ fmt[1] = flag;
+ fmt[flag ? 7 : 6] = format[i];
+ printf(fmt, width, precision, num);
+ free(fmt);
+ break;
+ case 'a': case 'A': case 'e': case 'E': case 'f': case 'F': case 'g': case 'G':
+ fmt = estrdup(flag ? "%#*.*#" : "%*.*#");
+ if (flag)
+ fmt[1] = flag;
+ fmt[flag ? 5 : 4] = format[i];
+ dou = (strlen(arg) > 0) ? estrtod(arg) : 0;
+ printf(fmt, width, precision, dou);
+ free(fmt);
+ break;
+ default:
+ eprintf("Invalid format specifier '%c'.\n", format[i]);
+ }
+ if (argi >= argc)
+ cooldown = 1;
+ }
+
+ return fshut(stdout, "<stdout>") | ret;
+}
diff --git a/util/sbase/pwd.1 b/util/sbase/pwd.1
new file mode 100644
index 00000000..71b9d2a8
--- /dev/null
+++ b/util/sbase/pwd.1
@@ -0,0 +1,29 @@
+.Dd October 8, 2015
+.Dt PWD 1
+.Os sbase
+.Sh NAME
+.Nm pwd
+.Nd print working directory
+.Sh SYNOPSIS
+.Nm
+.Op Fl L | Fl P
+.Sh DESCRIPTION
+.Nm
+prints the path of the current working directory.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl L
+Logical path, uses $PWD.
+This is the default.
+.It Fl P
+Physical path, avoids all symlinks.
+.El
+.Sh ENVIRONMENT
+.Bl -tag -width PWD
+.It Ev PWD
+The logical path to the current working directory.
+.El
+.Sh SEE ALSO
+.Xr getcwd 3
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/pwd.c b/util/sbase/pwd.c
new file mode 100644
index 00000000..c6a4497f
--- /dev/null
+++ b/util/sbase/pwd.c
@@ -0,0 +1,50 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static const char *
+getpwd(const char *cwd)
+{
+ const char *pwd;
+ struct stat cst, pst;
+
+ if (!(pwd = getenv("PWD")) || pwd[0] != '/' || stat(pwd, &pst) < 0)
+ return cwd;
+ if (stat(cwd, &cst) < 0)
+ eprintf("stat %s:", cwd);
+
+ return (pst.st_dev == cst.st_dev && pst.st_ino == cst.st_ino) ? pwd : cwd;
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-LP]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ char cwd[PATH_MAX];
+ char mode = 'L';
+
+ ARGBEGIN {
+ case 'L':
+ case 'P':
+ mode = ARGC();
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!getcwd(cwd, sizeof(cwd)))
+ eprintf("getcwd:");
+ puts((mode == 'L') ? getpwd(cwd) : cwd);
+
+ return fshut(stdout, "<stdout>");
+}
diff --git a/util/sbase/queue.h b/util/sbase/queue.h
new file mode 100644
index 00000000..f8f09bf1
--- /dev/null
+++ b/util/sbase/queue.h
@@ -0,0 +1,648 @@
+/* $OpenBSD: queue.h,v 1.38 2013/07/03 15:05:21 fgsch Exp $ */
+/* $NetBSD: queue.h,v 1.11 1996/05/16 05:17:14 mycroft Exp $ */
+
+/*
+ * Copyright (c) 1991, 1993
+ * The Regents of the University of California. 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.
+ * 3. Neither the name of the University nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * 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.
+ *
+ * @(#)queue.h 8.5 (Berkeley) 8/20/94
+ */
+
+#ifndef _SYS_QUEUE_H_
+#define _SYS_QUEUE_H_
+
+/*
+ * This file defines five types of data structures: singly-linked lists,
+ * lists, simple queues, tail queues, and circular queues.
+ *
+ *
+ * A singly-linked list is headed by a single forward pointer. The elements
+ * are singly linked for minimum space and pointer manipulation overhead at
+ * the expense of O(n) removal for arbitrary elements. New elements can be
+ * added to the list after an existing element or at the head of the list.
+ * Elements being removed from the head of the list should use the explicit
+ * macro for this purpose for optimum efficiency. A singly-linked list may
+ * only be traversed in the forward direction. Singly-linked lists are ideal
+ * for applications with large datasets and few or no removals or for
+ * implementing a LIFO queue.
+ *
+ * A list is headed by a single forward pointer (or an array of forward
+ * pointers for a hash table header). The elements are doubly linked
+ * so that an arbitrary element can be removed without a need to
+ * traverse the list. New elements can be added to the list before
+ * or after an existing element or at the head of the list. A list
+ * may only be traversed in the forward direction.
+ *
+ * A simple queue is headed by a pair of pointers, one the head of the
+ * list and the other to the tail of the list. The elements are singly
+ * linked to save space, so elements can only be removed from the
+ * head of the list. New elements can be added to the list before or after
+ * an existing element, at the head of the list, or at the end of the
+ * list. A simple queue may only be traversed in the forward direction.
+ *
+ * A tail queue is headed by a pair of pointers, one to the head of the
+ * list and the other to the tail of the list. The elements are doubly
+ * linked so that an arbitrary element can be removed without a need to
+ * traverse the list. New elements can be added to the list before or
+ * after an existing element, at the head of the list, or at the end of
+ * the list. A tail queue may be traversed in either direction.
+ *
+ * A circle queue is headed by a pair of pointers, one to the head of the
+ * list and the other to the tail of the list. The elements are doubly
+ * linked so that an arbitrary element can be removed without a need to
+ * traverse the list. New elements can be added to the list before or after
+ * an existing element, at the head of the list, or at the end of the list.
+ * A circle queue may be traversed in either direction, but has a more
+ * complex end of list detection.
+ *
+ * For details on the use of these macros, see the queue(3) manual page.
+ */
+
+#if defined(QUEUE_MACRO_DEBUG) || (defined(_KERNEL) && defined(DIAGNOSTIC))
+#define _Q_INVALIDATE(a) (a) = ((void *)-1)
+#else
+#define _Q_INVALIDATE(a)
+#endif
+
+/*
+ * Singly-linked List definitions.
+ */
+#define SLIST_HEAD(name, type) \
+struct name { \
+ struct type *slh_first; /* first element */ \
+}
+
+#define SLIST_HEAD_INITIALIZER(head) \
+ { NULL }
+
+#define SLIST_ENTRY(type) \
+struct { \
+ struct type *sle_next; /* next element */ \
+}
+
+/*
+ * Singly-linked List access methods.
+ */
+#define SLIST_FIRST(head) ((head)->slh_first)
+#define SLIST_END(head) NULL
+#define SLIST_EMPTY(head) (SLIST_FIRST(head) == SLIST_END(head))
+#define SLIST_NEXT(elm, field) ((elm)->field.sle_next)
+
+#define SLIST_FOREACH(var, head, field) \
+ for((var) = SLIST_FIRST(head); \
+ (var) != SLIST_END(head); \
+ (var) = SLIST_NEXT(var, field))
+
+#define SLIST_FOREACH_SAFE(var, head, field, tvar) \
+ for ((var) = SLIST_FIRST(head); \
+ (var) && ((tvar) = SLIST_NEXT(var, field), 1); \
+ (var) = (tvar))
+
+/*
+ * Singly-linked List functions.
+ */
+#define SLIST_INIT(head) { \
+ SLIST_FIRST(head) = SLIST_END(head); \
+}
+
+#define SLIST_INSERT_AFTER(slistelm, elm, field) do { \
+ (elm)->field.sle_next = (slistelm)->field.sle_next; \
+ (slistelm)->field.sle_next = (elm); \
+} while (0)
+
+#define SLIST_INSERT_HEAD(head, elm, field) do { \
+ (elm)->field.sle_next = (head)->slh_first; \
+ (head)->slh_first = (elm); \
+} while (0)
+
+#define SLIST_REMOVE_AFTER(elm, field) do { \
+ (elm)->field.sle_next = (elm)->field.sle_next->field.sle_next; \
+} while (0)
+
+#define SLIST_REMOVE_HEAD(head, field) do { \
+ (head)->slh_first = (head)->slh_first->field.sle_next; \
+} while (0)
+
+#define SLIST_REMOVE(head, elm, type, field) do { \
+ if ((head)->slh_first == (elm)) { \
+ SLIST_REMOVE_HEAD((head), field); \
+ } else { \
+ struct type *curelm = (head)->slh_first; \
+ \
+ while (curelm->field.sle_next != (elm)) \
+ curelm = curelm->field.sle_next; \
+ curelm->field.sle_next = \
+ curelm->field.sle_next->field.sle_next; \
+ _Q_INVALIDATE((elm)->field.sle_next); \
+ } \
+} while (0)
+
+/*
+ * List definitions.
+ */
+#define LIST_HEAD(name, type) \
+struct name { \
+ struct type *lh_first; /* first element */ \
+}
+
+#define LIST_HEAD_INITIALIZER(head) \
+ { NULL }
+
+#define LIST_ENTRY(type) \
+struct { \
+ struct type *le_next; /* next element */ \
+ struct type **le_prev; /* address of previous next element */ \
+}
+
+/*
+ * List access methods
+ */
+#define LIST_FIRST(head) ((head)->lh_first)
+#define LIST_END(head) NULL
+#define LIST_EMPTY(head) (LIST_FIRST(head) == LIST_END(head))
+#define LIST_NEXT(elm, field) ((elm)->field.le_next)
+
+#define LIST_FOREACH(var, head, field) \
+ for((var) = LIST_FIRST(head); \
+ (var)!= LIST_END(head); \
+ (var) = LIST_NEXT(var, field))
+
+#define LIST_FOREACH_SAFE(var, head, field, tvar) \
+ for ((var) = LIST_FIRST(head); \
+ (var) && ((tvar) = LIST_NEXT(var, field), 1); \
+ (var) = (tvar))
+
+/*
+ * List functions.
+ */
+#define LIST_INIT(head) do { \
+ LIST_FIRST(head) = LIST_END(head); \
+} while (0)
+
+#define LIST_INSERT_AFTER(listelm, elm, field) do { \
+ if (((elm)->field.le_next = (listelm)->field.le_next) != NULL) \
+ (listelm)->field.le_next->field.le_prev = \
+ &(elm)->field.le_next; \
+ (listelm)->field.le_next = (elm); \
+ (elm)->field.le_prev = &(listelm)->field.le_next; \
+} while (0)
+
+#define LIST_INSERT_BEFORE(listelm, elm, field) do { \
+ (elm)->field.le_prev = (listelm)->field.le_prev; \
+ (elm)->field.le_next = (listelm); \
+ *(listelm)->field.le_prev = (elm); \
+ (listelm)->field.le_prev = &(elm)->field.le_next; \
+} while (0)
+
+#define LIST_INSERT_HEAD(head, elm, field) do { \
+ if (((elm)->field.le_next = (head)->lh_first) != NULL) \
+ (head)->lh_first->field.le_prev = &(elm)->field.le_next;\
+ (head)->lh_first = (elm); \
+ (elm)->field.le_prev = &(head)->lh_first; \
+} while (0)
+
+#define LIST_REMOVE(elm, field) do { \
+ if ((elm)->field.le_next != NULL) \
+ (elm)->field.le_next->field.le_prev = \
+ (elm)->field.le_prev; \
+ *(elm)->field.le_prev = (elm)->field.le_next; \
+ _Q_INVALIDATE((elm)->field.le_prev); \
+ _Q_INVALIDATE((elm)->field.le_next); \
+} while (0)
+
+#define LIST_REPLACE(elm, elm2, field) do { \
+ if (((elm2)->field.le_next = (elm)->field.le_next) != NULL) \
+ (elm2)->field.le_next->field.le_prev = \
+ &(elm2)->field.le_next; \
+ (elm2)->field.le_prev = (elm)->field.le_prev; \
+ *(elm2)->field.le_prev = (elm2); \
+ _Q_INVALIDATE((elm)->field.le_prev); \
+ _Q_INVALIDATE((elm)->field.le_next); \
+} while (0)
+
+/*
+ * Simple queue definitions.
+ */
+#define SIMPLEQ_HEAD(name, type) \
+struct name { \
+ struct type *sqh_first; /* first element */ \
+ struct type **sqh_last; /* addr of last next element */ \
+}
+
+#define SIMPLEQ_HEAD_INITIALIZER(head) \
+ { NULL, &(head).sqh_first }
+
+#define SIMPLEQ_ENTRY(type) \
+struct { \
+ struct type *sqe_next; /* next element */ \
+}
+
+/*
+ * Simple queue access methods.
+ */
+#define SIMPLEQ_FIRST(head) ((head)->sqh_first)
+#define SIMPLEQ_END(head) NULL
+#define SIMPLEQ_EMPTY(head) (SIMPLEQ_FIRST(head) == SIMPLEQ_END(head))
+#define SIMPLEQ_NEXT(elm, field) ((elm)->field.sqe_next)
+
+#define SIMPLEQ_FOREACH(var, head, field) \
+ for((var) = SIMPLEQ_FIRST(head); \
+ (var) != SIMPLEQ_END(head); \
+ (var) = SIMPLEQ_NEXT(var, field))
+
+#define SIMPLEQ_FOREACH_SAFE(var, head, field, tvar) \
+ for ((var) = SIMPLEQ_FIRST(head); \
+ (var) && ((tvar) = SIMPLEQ_NEXT(var, field), 1); \
+ (var) = (tvar))
+
+/*
+ * Simple queue functions.
+ */
+#define SIMPLEQ_INIT(head) do { \
+ (head)->sqh_first = NULL; \
+ (head)->sqh_last = &(head)->sqh_first; \
+} while (0)
+
+#define SIMPLEQ_INSERT_HEAD(head, elm, field) do { \
+ if (((elm)->field.sqe_next = (head)->sqh_first) == NULL) \
+ (head)->sqh_last = &(elm)->field.sqe_next; \
+ (head)->sqh_first = (elm); \
+} while (0)
+
+#define SIMPLEQ_INSERT_TAIL(head, elm, field) do { \
+ (elm)->field.sqe_next = NULL; \
+ *(head)->sqh_last = (elm); \
+ (head)->sqh_last = &(elm)->field.sqe_next; \
+} while (0)
+
+#define SIMPLEQ_INSERT_AFTER(head, listelm, elm, field) do { \
+ if (((elm)->field.sqe_next = (listelm)->field.sqe_next) == NULL)\
+ (head)->sqh_last = &(elm)->field.sqe_next; \
+ (listelm)->field.sqe_next = (elm); \
+} while (0)
+
+#define SIMPLEQ_REMOVE_HEAD(head, field) do { \
+ if (((head)->sqh_first = (head)->sqh_first->field.sqe_next) == NULL) \
+ (head)->sqh_last = &(head)->sqh_first; \
+} while (0)
+
+#define SIMPLEQ_REMOVE_AFTER(head, elm, field) do { \
+ if (((elm)->field.sqe_next = (elm)->field.sqe_next->field.sqe_next) \
+ == NULL) \
+ (head)->sqh_last = &(elm)->field.sqe_next; \
+} while (0)
+
+/*
+ * XOR Simple queue definitions.
+ */
+#define XSIMPLEQ_HEAD(name, type) \
+struct name { \
+ struct type *sqx_first; /* first element */ \
+ struct type **sqx_last; /* addr of last next element */ \
+ unsigned long sqx_cookie; \
+}
+
+#define XSIMPLEQ_ENTRY(type) \
+struct { \
+ struct type *sqx_next; /* next element */ \
+}
+
+/*
+ * XOR Simple queue access methods.
+ */
+#define XSIMPLEQ_XOR(head, ptr) ((__typeof(ptr))((head)->sqx_cookie ^ \
+ (unsigned long)(ptr)))
+#define XSIMPLEQ_FIRST(head) XSIMPLEQ_XOR(head, ((head)->sqx_first))
+#define XSIMPLEQ_END(head) NULL
+#define XSIMPLEQ_EMPTY(head) (XSIMPLEQ_FIRST(head) == XSIMPLEQ_END(head))
+#define XSIMPLEQ_NEXT(head, elm, field) XSIMPLEQ_XOR(head, ((elm)->field.sqx_next))
+
+
+#define XSIMPLEQ_FOREACH(var, head, field) \
+ for ((var) = XSIMPLEQ_FIRST(head); \
+ (var) != XSIMPLEQ_END(head); \
+ (var) = XSIMPLEQ_NEXT(head, var, field))
+
+#define XSIMPLEQ_FOREACH_SAFE(var, head, field, tvar) \
+ for ((var) = XSIMPLEQ_FIRST(head); \
+ (var) && ((tvar) = XSIMPLEQ_NEXT(head, var, field), 1); \
+ (var) = (tvar))
+
+/*
+ * XOR Simple queue functions.
+ */
+#define XSIMPLEQ_INIT(head) do { \
+ arc4random_buf(&(head)->sqx_cookie, sizeof((head)->sqx_cookie)); \
+ (head)->sqx_first = XSIMPLEQ_XOR(head, NULL); \
+ (head)->sqx_last = XSIMPLEQ_XOR(head, &(head)->sqx_first); \
+} while (0)
+
+#define XSIMPLEQ_INSERT_HEAD(head, elm, field) do { \
+ if (((elm)->field.sqx_next = (head)->sqx_first) == \
+ XSIMPLEQ_XOR(head, NULL)) \
+ (head)->sqx_last = XSIMPLEQ_XOR(head, &(elm)->field.sqx_next); \
+ (head)->sqx_first = XSIMPLEQ_XOR(head, (elm)); \
+} while (0)
+
+#define XSIMPLEQ_INSERT_TAIL(head, elm, field) do { \
+ (elm)->field.sqx_next = XSIMPLEQ_XOR(head, NULL); \
+ *(XSIMPLEQ_XOR(head, (head)->sqx_last)) = XSIMPLEQ_XOR(head, (elm)); \
+ (head)->sqx_last = XSIMPLEQ_XOR(head, &(elm)->field.sqx_next); \
+} while (0)
+
+#define XSIMPLEQ_INSERT_AFTER(head, listelm, elm, field) do { \
+ if (((elm)->field.sqx_next = (listelm)->field.sqx_next) == \
+ XSIMPLEQ_XOR(head, NULL)) \
+ (head)->sqx_last = XSIMPLEQ_XOR(head, &(elm)->field.sqx_next); \
+ (listelm)->field.sqx_next = XSIMPLEQ_XOR(head, (elm)); \
+} while (0)
+
+#define XSIMPLEQ_REMOVE_HEAD(head, field) do { \
+ if (((head)->sqx_first = XSIMPLEQ_XOR(head, \
+ (head)->sqx_first)->field.sqx_next) == XSIMPLEQ_XOR(head, NULL)) \
+ (head)->sqx_last = XSIMPLEQ_XOR(head, &(head)->sqx_first); \
+} while (0)
+
+#define XSIMPLEQ_REMOVE_AFTER(head, elm, field) do { \
+ if (((elm)->field.sqx_next = XSIMPLEQ_XOR(head, \
+ (elm)->field.sqx_next)->field.sqx_next) \
+ == XSIMPLEQ_XOR(head, NULL)) \
+ (head)->sqx_last = \
+ XSIMPLEQ_XOR(head, &(elm)->field.sqx_next); \
+} while (0)
+
+
+/*
+ * Tail queue definitions.
+ */
+#define TAILQ_HEAD(name, type) \
+struct name { \
+ struct type *tqh_first; /* first element */ \
+ struct type **tqh_last; /* addr of last next element */ \
+}
+
+#define TAILQ_HEAD_INITIALIZER(head) \
+ { NULL, &(head).tqh_first }
+
+#define TAILQ_ENTRY(type) \
+struct { \
+ struct type *tqe_next; /* next element */ \
+ struct type **tqe_prev; /* address of previous next element */ \
+}
+
+/*
+ * tail queue access methods
+ */
+#define TAILQ_FIRST(head) ((head)->tqh_first)
+#define TAILQ_END(head) NULL
+#define TAILQ_NEXT(elm, field) ((elm)->field.tqe_next)
+#define TAILQ_LAST(head, headname) \
+ (*(((struct headname *)((head)->tqh_last))->tqh_last))
+/* XXX */
+#define TAILQ_PREV(elm, headname, field) \
+ (*(((struct headname *)((elm)->field.tqe_prev))->tqh_last))
+#define TAILQ_EMPTY(head) \
+ (TAILQ_FIRST(head) == TAILQ_END(head))
+
+#define TAILQ_FOREACH(var, head, field) \
+ for((var) = TAILQ_FIRST(head); \
+ (var) != TAILQ_END(head); \
+ (var) = TAILQ_NEXT(var, field))
+
+#define TAILQ_FOREACH_SAFE(var, head, field, tvar) \
+ for ((var) = TAILQ_FIRST(head); \
+ (var) != TAILQ_END(head) && \
+ ((tvar) = TAILQ_NEXT(var, field), 1); \
+ (var) = (tvar))
+
+
+#define TAILQ_FOREACH_REVERSE(var, head, headname, field) \
+ for((var) = TAILQ_LAST(head, headname); \
+ (var) != TAILQ_END(head); \
+ (var) = TAILQ_PREV(var, headname, field))
+
+#define TAILQ_FOREACH_REVERSE_SAFE(var, head, headname, field, tvar) \
+ for ((var) = TAILQ_LAST(head, headname); \
+ (var) != TAILQ_END(head) && \
+ ((tvar) = TAILQ_PREV(var, headname, field), 1); \
+ (var) = (tvar))
+
+/*
+ * Tail queue functions.
+ */
+#define TAILQ_INIT(head) do { \
+ (head)->tqh_first = NULL; \
+ (head)->tqh_last = &(head)->tqh_first; \
+} while (0)
+
+#define TAILQ_INSERT_HEAD(head, elm, field) do { \
+ if (((elm)->field.tqe_next = (head)->tqh_first) != NULL) \
+ (head)->tqh_first->field.tqe_prev = \
+ &(elm)->field.tqe_next; \
+ else \
+ (head)->tqh_last = &(elm)->field.tqe_next; \
+ (head)->tqh_first = (elm); \
+ (elm)->field.tqe_prev = &(head)->tqh_first; \
+} while (0)
+
+#define TAILQ_INSERT_TAIL(head, elm, field) do { \
+ (elm)->field.tqe_next = NULL; \
+ (elm)->field.tqe_prev = (head)->tqh_last; \
+ *(head)->tqh_last = (elm); \
+ (head)->tqh_last = &(elm)->field.tqe_next; \
+} while (0)
+
+#define TAILQ_INSERT_AFTER(head, listelm, elm, field) do { \
+ if (((elm)->field.tqe_next = (listelm)->field.tqe_next) != NULL)\
+ (elm)->field.tqe_next->field.tqe_prev = \
+ &(elm)->field.tqe_next; \
+ else \
+ (head)->tqh_last = &(elm)->field.tqe_next; \
+ (listelm)->field.tqe_next = (elm); \
+ (elm)->field.tqe_prev = &(listelm)->field.tqe_next; \
+} while (0)
+
+#define TAILQ_INSERT_BEFORE(listelm, elm, field) do { \
+ (elm)->field.tqe_prev = (listelm)->field.tqe_prev; \
+ (elm)->field.tqe_next = (listelm); \
+ *(listelm)->field.tqe_prev = (elm); \
+ (listelm)->field.tqe_prev = &(elm)->field.tqe_next; \
+} while (0)
+
+#define TAILQ_REMOVE(head, elm, field) do { \
+ if (((elm)->field.tqe_next) != NULL) \
+ (elm)->field.tqe_next->field.tqe_prev = \
+ (elm)->field.tqe_prev; \
+ else \
+ (head)->tqh_last = (elm)->field.tqe_prev; \
+ *(elm)->field.tqe_prev = (elm)->field.tqe_next; \
+ _Q_INVALIDATE((elm)->field.tqe_prev); \
+ _Q_INVALIDATE((elm)->field.tqe_next); \
+} while (0)
+
+#define TAILQ_REPLACE(head, elm, elm2, field) do { \
+ if (((elm2)->field.tqe_next = (elm)->field.tqe_next) != NULL) \
+ (elm2)->field.tqe_next->field.tqe_prev = \
+ &(elm2)->field.tqe_next; \
+ else \
+ (head)->tqh_last = &(elm2)->field.tqe_next; \
+ (elm2)->field.tqe_prev = (elm)->field.tqe_prev; \
+ *(elm2)->field.tqe_prev = (elm2); \
+ _Q_INVALIDATE((elm)->field.tqe_prev); \
+ _Q_INVALIDATE((elm)->field.tqe_next); \
+} while (0)
+
+/*
+ * Circular queue definitions.
+ */
+#define CIRCLEQ_HEAD(name, type) \
+struct name { \
+ struct type *cqh_first; /* first element */ \
+ struct type *cqh_last; /* last element */ \
+}
+
+#define CIRCLEQ_HEAD_INITIALIZER(head) \
+ { CIRCLEQ_END(&head), CIRCLEQ_END(&head) }
+
+#define CIRCLEQ_ENTRY(type) \
+struct { \
+ struct type *cqe_next; /* next element */ \
+ struct type *cqe_prev; /* previous element */ \
+}
+
+/*
+ * Circular queue access methods
+ */
+#define CIRCLEQ_FIRST(head) ((head)->cqh_first)
+#define CIRCLEQ_LAST(head) ((head)->cqh_last)
+#define CIRCLEQ_END(head) ((void *)(head))
+#define CIRCLEQ_NEXT(elm, field) ((elm)->field.cqe_next)
+#define CIRCLEQ_PREV(elm, field) ((elm)->field.cqe_prev)
+#define CIRCLEQ_EMPTY(head) \
+ (CIRCLEQ_FIRST(head) == CIRCLEQ_END(head))
+
+#define CIRCLEQ_FOREACH(var, head, field) \
+ for((var) = CIRCLEQ_FIRST(head); \
+ (var) != CIRCLEQ_END(head); \
+ (var) = CIRCLEQ_NEXT(var, field))
+
+#define CIRCLEQ_FOREACH_SAFE(var, head, field, tvar) \
+ for ((var) = CIRCLEQ_FIRST(head); \
+ (var) != CIRCLEQ_END(head) && \
+ ((tvar) = CIRCLEQ_NEXT(var, field), 1); \
+ (var) = (tvar))
+
+#define CIRCLEQ_FOREACH_REVERSE(var, head, field) \
+ for((var) = CIRCLEQ_LAST(head); \
+ (var) != CIRCLEQ_END(head); \
+ (var) = CIRCLEQ_PREV(var, field))
+
+#define CIRCLEQ_FOREACH_REVERSE_SAFE(var, head, headname, field, tvar) \
+ for ((var) = CIRCLEQ_LAST(head, headname); \
+ (var) != CIRCLEQ_END(head) && \
+ ((tvar) = CIRCLEQ_PREV(var, headname, field), 1); \
+ (var) = (tvar))
+
+/*
+ * Circular queue functions.
+ */
+#define CIRCLEQ_INIT(head) do { \
+ (head)->cqh_first = CIRCLEQ_END(head); \
+ (head)->cqh_last = CIRCLEQ_END(head); \
+} while (0)
+
+#define CIRCLEQ_INSERT_AFTER(head, listelm, elm, field) do { \
+ (elm)->field.cqe_next = (listelm)->field.cqe_next; \
+ (elm)->field.cqe_prev = (listelm); \
+ if ((listelm)->field.cqe_next == CIRCLEQ_END(head)) \
+ (head)->cqh_last = (elm); \
+ else \
+ (listelm)->field.cqe_next->field.cqe_prev = (elm); \
+ (listelm)->field.cqe_next = (elm); \
+} while (0)
+
+#define CIRCLEQ_INSERT_BEFORE(head, listelm, elm, field) do { \
+ (elm)->field.cqe_next = (listelm); \
+ (elm)->field.cqe_prev = (listelm)->field.cqe_prev; \
+ if ((listelm)->field.cqe_prev == CIRCLEQ_END(head)) \
+ (head)->cqh_first = (elm); \
+ else \
+ (listelm)->field.cqe_prev->field.cqe_next = (elm); \
+ (listelm)->field.cqe_prev = (elm); \
+} while (0)
+
+#define CIRCLEQ_INSERT_HEAD(head, elm, field) do { \
+ (elm)->field.cqe_next = (head)->cqh_first; \
+ (elm)->field.cqe_prev = CIRCLEQ_END(head); \
+ if ((head)->cqh_last == CIRCLEQ_END(head)) \
+ (head)->cqh_last = (elm); \
+ else \
+ (head)->cqh_first->field.cqe_prev = (elm); \
+ (head)->cqh_first = (elm); \
+} while (0)
+
+#define CIRCLEQ_INSERT_TAIL(head, elm, field) do { \
+ (elm)->field.cqe_next = CIRCLEQ_END(head); \
+ (elm)->field.cqe_prev = (head)->cqh_last; \
+ if ((head)->cqh_first == CIRCLEQ_END(head)) \
+ (head)->cqh_first = (elm); \
+ else \
+ (head)->cqh_last->field.cqe_next = (elm); \
+ (head)->cqh_last = (elm); \
+} while (0)
+
+#define CIRCLEQ_REMOVE(head, elm, field) do { \
+ if ((elm)->field.cqe_next == CIRCLEQ_END(head)) \
+ (head)->cqh_last = (elm)->field.cqe_prev; \
+ else \
+ (elm)->field.cqe_next->field.cqe_prev = \
+ (elm)->field.cqe_prev; \
+ if ((elm)->field.cqe_prev == CIRCLEQ_END(head)) \
+ (head)->cqh_first = (elm)->field.cqe_next; \
+ else \
+ (elm)->field.cqe_prev->field.cqe_next = \
+ (elm)->field.cqe_next; \
+ _Q_INVALIDATE((elm)->field.cqe_prev); \
+ _Q_INVALIDATE((elm)->field.cqe_next); \
+} while (0)
+
+#define CIRCLEQ_REPLACE(head, elm, elm2, field) do { \
+ if (((elm2)->field.cqe_next = (elm)->field.cqe_next) == \
+ CIRCLEQ_END(head)) \
+ (head)->cqh_last = (elm2); \
+ else \
+ (elm2)->field.cqe_next->field.cqe_prev = (elm2); \
+ if (((elm2)->field.cqe_prev = (elm)->field.cqe_prev) == \
+ CIRCLEQ_END(head)) \
+ (head)->cqh_first = (elm2); \
+ else \
+ (elm2)->field.cqe_prev->field.cqe_next = (elm2); \
+ _Q_INVALIDATE((elm)->field.cqe_prev); \
+ _Q_INVALIDATE((elm)->field.cqe_next); \
+} while (0)
+
+#endif /* !_SYS_QUEUE_H_ */
diff --git a/util/sbase/readlink.1 b/util/sbase/readlink.1
new file mode 100644
index 00000000..ac19710d
--- /dev/null
+++ b/util/sbase/readlink.1
@@ -0,0 +1,32 @@
+.Dd November 16, 2015
+.Dt READLINK 1
+.Os sbase
+.Sh NAME
+.Nm readlink
+.Nd print symbolic link target or canonical file name
+.Sh SYNOPSIS
+.Nm
+.Op Fl f
+.Op Fl n
+.Ar path
+.Sh DESCRIPTION
+.Nm
+writes the target of
+.Ar path ,
+if it is a symbolic link, to stdout.
+If not,
+.Nm
+exits with a non-zero return value.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl f
+Canonicalize
+.Ar path ,
+which needn't be a symlink,
+by recursively following every symlink in its path components.
+.It Fl n
+Do not print the terminating newline.
+.El
+.Sh SEE ALSO
+.Xr readlink 2 ,
+.Xr realpath 3
diff --git a/util/sbase/readlink.c b/util/sbase/readlink.c
new file mode 100644
index 00000000..d059584f
--- /dev/null
+++ b/util/sbase/readlink.c
@@ -0,0 +1,54 @@
+/* See LICENSE file for copyright and license details. */
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-fn] path\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ char buf[PATH_MAX];
+ ssize_t n;
+ int nflag = 0, fflag = 0;
+
+ ARGBEGIN {
+ case 'f':
+ fflag = ARGC();
+ break;
+ case 'n':
+ nflag = 1;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (argc != 1)
+ usage();
+
+ if (strlen(argv[0]) >= PATH_MAX)
+ eprintf("path too long\n");
+
+ if (fflag) {
+ if (!realpath(argv[0], buf))
+ eprintf("realpath %s:", argv[0]);
+ } else {
+ if ((n = readlink(argv[0], buf, PATH_MAX - 1)) < 0)
+ eprintf("readlink %s:", argv[0]);
+ buf[n] = '\0';
+ }
+
+ fputs(buf, stdout);
+ if (!nflag)
+ putchar('\n');
+
+ return fshut(stdout, "<stdout>");
+}
diff --git a/util/sbase/renice.1 b/util/sbase/renice.1
new file mode 100644
index 00000000..c3ec7f8a
--- /dev/null
+++ b/util/sbase/renice.1
@@ -0,0 +1,38 @@
+.Dd October 8, 2015
+.Dt RENICE 1
+.Os sbase
+.Sh NAME
+.Nm renice
+.Nd change niceness of processes
+.Sh SYNOPSIS
+.Nm
+.Fl n Ar num
+.Op Fl g | Fl p | Fl u
+.Ar id ...
+.Sh DESCRIPTION
+.Nm
+changes the niceness of each process with the given
+.Ar id .
+Only the superuser can lower the niceness.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl g | Fl p | Fl u
+Interpret each
+.Ar id
+as a process group ID | process ID | user name or ID.
+The middle option is default.
+.It Fl n Ar num
+Change niceness by
+.Ar num ,
+with niceness ranging from
+.Sy -20
+(highest priority)
+to
+.Sy +20
+(lowest priority).
+.El
+.Sh SEE ALSO
+.Xr nice 2 ,
+.Xr renice 2
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/renice.c b/util/sbase/renice.c
new file mode 100644
index 00000000..358c5604
--- /dev/null
+++ b/util/sbase/renice.c
@@ -0,0 +1,93 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/resource.h>
+
+#include <errno.h>
+#include <pwd.h>
+#include <stdlib.h>
+
+#include "util.h"
+
+#ifndef PRIO_MIN
+#define PRIO_MIN -NZERO
+#endif
+
+#ifndef PRIO_MAX
+#define PRIO_MAX (NZERO-1)
+#endif
+
+static int
+renice(int which, int who, long adj)
+{
+ errno = 0;
+ adj += getpriority(which, who);
+ if (errno) {
+ weprintf("getpriority %d:", who);
+ return 0;
+ }
+
+ adj = MAX(PRIO_MIN, MIN(adj, PRIO_MAX));
+ if (setpriority(which, who, (int)adj) < 0) {
+ weprintf("setpriority %d:", who);
+ return 0;
+ }
+
+ return 1;
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s -n num [-g | -p | -u] id ...\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ const char *adj = NULL;
+ long val;
+ int which = PRIO_PROCESS, ret = 0;
+ struct passwd *pw;
+ int who;
+
+ ARGBEGIN {
+ case 'n':
+ adj = EARGF(usage());
+ break;
+ case 'g':
+ which = PRIO_PGRP;
+ break;
+ case 'p':
+ which = PRIO_PROCESS;
+ break;
+ case 'u':
+ which = PRIO_USER;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc || !adj)
+ usage();
+
+ val = estrtonum(adj, PRIO_MIN, PRIO_MAX);
+ for (; *argv; argc--, argv++) {
+ if (which == PRIO_USER) {
+ errno = 0;
+ if (!(pw = getpwnam(*argv))) {
+ if (errno)
+ weprintf("getpwnam %s:", *argv);
+ else
+ weprintf("getpwnam %s: no user found\n", *argv);
+ ret = 1;
+ continue;
+ }
+ who = pw->pw_uid;
+ } else {
+ who = estrtonum(*argv, 1, INT_MAX);
+ }
+ if (!renice(which, who, val))
+ ret = 1;
+ }
+
+ return ret;
+}
diff --git a/util/sbase/rev.1 b/util/sbase/rev.1
new file mode 100644
index 00000000..e56b920b
--- /dev/null
+++ b/util/sbase/rev.1
@@ -0,0 +1,22 @@
+.Dd March 26, 2016
+.Dt REV 1
+.Os sbase
+.Sh NAME
+.Nm rev
+.Nd reverse each line
+.Sh SYNOPSIS
+.Nm
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+reads each
+.Ar file
+in sequence and writes it to stdout, but with all characters in each
+line in reverse order.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.Sh SEE ALSO
+.Xr tac 1
diff --git a/util/sbase/rev.c b/util/sbase/rev.c
new file mode 100644
index 00000000..9ac1da65
--- /dev/null
+++ b/util/sbase/rev.c
@@ -0,0 +1,74 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "text.h"
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [file ...]\n", argv0);
+}
+
+static void
+rev(FILE *fp)
+{
+ static char *line = NULL;
+ static size_t size = 0;
+ size_t i;
+ ssize_t n;
+ int lf;
+
+ while ((n = getline(&line, &size, fp)) > 0) {
+ lf = n && line[n - 1] == '\n';
+ i = n -= lf;
+ for (n = 0; i--;) {
+ if (UTF8_POINT(line[i])) {
+ n++;
+ } else {
+ fwrite(line + i, 1, n + 1, stdout);
+ n = 0;
+ }
+ }
+ if (n)
+ fwrite(line, 1, n, stdout);
+ if (lf)
+ fputc('\n', stdout);
+ }
+}
+
+int
+main(int argc, char *argv[])
+{
+ FILE *fp;
+ int ret = 0;
+
+ ARGBEGIN {
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc) {
+ rev(stdin);
+ } else {
+ for (; *argv; argc--, argv++) {
+ if (!strcmp(*argv, "-")) {
+ *argv = "<stdin>";
+ fp = stdin;
+ } else if (!(fp = fopen(*argv, "r"))) {
+ weprintf("fopen %s:", *argv);
+ ret = 1;
+ continue;
+ }
+ rev(fp);
+ if (fp != stdin && fshut(fp, *argv))
+ ret = 1;
+ }
+ }
+
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/rm.1 b/util/sbase/rm.1
new file mode 100644
index 00000000..604407da
--- /dev/null
+++ b/util/sbase/rm.1
@@ -0,0 +1,37 @@
+.Dd April 24, 2025
+.Dt RM 1
+.Os sbase
+.Sh NAME
+.Nm rm
+.Nd remove directory entries
+.Sh SYNOPSIS
+.Nm
+.Op Fl f
+.Op Fl Rr
+.Ar file ...
+.Sh DESCRIPTION
+.Nm
+removes each
+.Ar file .
+If
+.Ar file
+is a directory, it has to be empty unless
+.Fl R
+or
+.Fl r
+is specified.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl f
+Do not prompt before removing
+.Ar file .
+Do not report when
+.Ar file
+doesn't exist or couldn't be removed.
+.It Fl Rr
+Remove directories recursively.
+.El
+.Sh SEE ALSO
+.Xr remove 3
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/rm.c b/util/sbase/rm.c
new file mode 100644
index 00000000..3adfc8cd
--- /dev/null
+++ b/util/sbase/rm.c
@@ -0,0 +1,87 @@
+/* See LICENSE file for copyright and license details. */
+#include <fcntl.h>
+#include <string.h>
+
+#include "fs.h"
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-f] [-iRr] file ...\n", argv0);
+}
+
+static int
+forbidden(char *path, struct stat *root)
+{
+ char *s, *t;
+ size_t n;
+ struct stat st;
+ static int w1, w2;
+
+ n = strlen(path);
+ for (t = path + n; t > path && t[-1] == '/'; --t)
+ ;
+ for (s = t; s > path && s[-1] != '/'; --s)
+ ;
+ n = t - s;
+ if (n == 1 && *s == '.' || n == 2 && s[0] == '.' && s[1] == '.') {
+ if (!w1)
+ weprintf("\".\" and \"..\" may not be removed\n");
+ w1 = 1;
+ return 1;
+ }
+
+ if (stat(path, &st) < 0)
+ return 0;
+ if (st.st_dev == root->st_dev && st.st_ino == root->st_ino) {
+ if (!w2)
+ weprintf("\"/\" may not be removed\n");
+ w2 = 1;
+ return 1;
+ }
+
+ return 0;
+}
+
+int
+main(int argc, char *argv[])
+{
+ char *s;
+ struct stat st;
+ struct recursor r = { .fn = rm, .maxdepth = 1, .follow = 'P' };
+
+ ARGBEGIN {
+ case 'f':
+ r.flags |= SILENT | IGNORE;
+ break;
+ case 'i':
+ r.flags |= CONFIRM;
+ break;
+ case 'R':
+ case 'r':
+ r.maxdepth = 0;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc) {
+ if (!(r.flags & IGNORE))
+ usage();
+ else
+ return 0;
+ }
+
+ if (stat("/", &st) < 0)
+ eprintf("stat root:");
+ for (; *argv; argc--, argv++) {
+ if (forbidden(*argv, &st)) {
+ rm_status = 1;
+ continue;
+ }
+ recurse(AT_FDCWD, *argv, NULL, &r);
+ }
+
+ return rm_status || recurse_status;
+}
diff --git a/util/sbase/rmdir.1 b/util/sbase/rmdir.1
new file mode 100644
index 00000000..b1631cf9
--- /dev/null
+++ b/util/sbase/rmdir.1
@@ -0,0 +1,29 @@
+.Dd October 8, 2015
+.Dt RMDIR 1
+.Os sbase
+.Sh NAME
+.Nm rmdir
+.Nd remove empty directories
+.Sh SYNOPSIS
+.Nm
+.Op Fl p
+.Ar dir ...
+.Sh DESCRIPTION
+.Nm
+removes each empty
+.Ar dir .
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl p
+Remove
+.Ar dir
+and its parents in the pathname
+.Ar dir .
+.El
+.Sh SEE ALSO
+.Xr rm 1 ,
+.Xr unlink 1 ,
+.Xr rmdir 2 ,
+.Xr unlink 2
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/rmdir.c b/util/sbase/rmdir.c
new file mode 100644
index 00000000..44224547
--- /dev/null
+++ b/util/sbase/rmdir.c
@@ -0,0 +1,49 @@
+/* See LICENSE file for copyright and license details. */
+#include <libgen.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-p] dir ...\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int pflag = 0, ret = 0;
+ char *d;
+
+ ARGBEGIN {
+ case 'p':
+ pflag = 1;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc)
+ usage();
+
+ for (; *argv; argc--, argv++) {
+ if (rmdir(*argv) < 0) {
+ weprintf("rmdir %s:", *argv);
+ ret = 1;
+ } else if (pflag) {
+ d = dirname(*argv);
+ for (; strcmp(d, "/") && strcmp(d, ".") ;) {
+ if (rmdir(d) < 0) {
+ weprintf("rmdir %s:", d);
+ ret = 1;
+ break;
+ }
+ d = dirname(d);
+ }
+ }
+ }
+
+ return ret;
+}
diff --git a/util/sbase/scripts/getconf.sh b/util/sbase/scripts/getconf.sh
new file mode 100755
index 00000000..70902a30
--- /dev/null
+++ b/util/sbase/scripts/getconf.sh
@@ -0,0 +1,218 @@
+#!/bin/sh
+
+ifdef() {
+ printf 'static const struct var %s[] = {\n' "$1"
+ awk '{printf("#ifdef %s\n\t{\"%s\",\t%s},\n#endif\n", $2, $1, $2)}'
+ echo '};'
+}
+
+ifdef confstr_l << EOF
+PATH _CS_PATH
+POSIX_V7_ILP32_OFF32_CFLAGS _CS_POSIX_V7_ILP32_OFF32_CFLAGS
+POSIX_V7_ILP32_OFF32_LDFLAGS _CS_POSIX_V7_ILP32_OFF32_LDFLAGS
+POSIX_V7_ILP32_OFF32_LIBS _CS_POSIX_V7_ILP32_OFF32_LIBS
+POSIX_V7_ILP32_OFFBIG_CFLAGS _CS_POSIX_V7_ILP32_OFFBIG_CFLAGS
+POSIX_V7_ILP32_OFFBIG_LDFLAGS _CS_POSIX_V7_ILP32_OFFBIG_LDFLAGS
+POSIX_V7_ILP32_OFFBIG_LIBS _CS_POSIX_V7_ILP32_OFFBIG_LIBS
+POSIX_V7_LP64_OFF64_CFLAGS _CS_POSIX_V7_LP64_OFF64_CFLAGS
+POSIX_V7_LP64_OFF64_LDFLAGS _CS_POSIX_V7_LP64_OFF64_LDFLAGS
+POSIX_V7_LP64_OFF64_LIBS _CS_POSIX_V7_LP64_OFF64_LIBS
+POSIX_V7_LPBIG_OFFBIG_CFLAGS _CS_POSIX_V7_LPBIG_OFFBIG_CFLAGS
+POSIX_V7_LPBIG_OFFBIG_LDFLAGS _CS_POSIX_V7_LPBIG_OFFBIG_LDFLAGS
+POSIX_V7_LPBIG_OFFBIG_LIBS _CS_POSIX_V7_LPBIG_OFFBIG_LIBS
+POSIX_V7_THREADS_CFLAGS _CS_POSIX_V7_THREADS_CFLAGS
+POSIX_V7_THREADS_LDFLAGS _CS_POSIX_V7_THREADS_LDFLAGS
+POSIX_V7_WIDTH_RESTRICTED_ENVS _CS_POSIX_V7_WIDTH_RESTRICTED_ENVS
+V7_ENV _CS_V7_ENV
+EOF
+
+ifdef limits_l << EOF
+_POSIX_CLOCKRES_MIN _POSIX_CLOCKRES_MIN
+_POSIX_AIO_LISTIO_MAX _POSIX_AIO_LISTIO_MAX
+_POSIX_AIO_MAX _POSIX_AIO_MAX
+_POSIX_ARG_MAX _POSIX_ARG_MAX
+_POSIX_CHILD_MAX _POSIX_CHILD_MAX
+_POSIX_DELAYTIMER_MAX _POSIX_DELAYTIMER_MAX
+_POSIX_HOST_NAME_MAX _POSIX_HOST_NAME_MAX
+_POSIX_LINK_MAX _POSIX_LINK_MAX
+_POSIX_LOGIN_NAME_MAX _POSIX_LOGIN_NAME_MAX
+_POSIX_MAX_CANON _POSIX_MAX_CANON
+_POSIX_MAX_INPUT _POSIX_MAX_INPUT
+_POSIX_MQ_OPEN_MAX _POSIX_MQ_OPEN_MAX
+_POSIX_MQ_PRIO_MAX _POSIX_MQ_PRIO_MAX
+_POSIX_NAME_MAX _POSIX_NAME_MAX
+_POSIX_NGROUPS_MAX _POSIX_NGROUPS_MAX
+_POSIX_OPEN_MAX _POSIX_OPEN_MAX
+_POSIX_PATH_MAX _POSIX_PATH_MAX
+_POSIX_PIPE_BUF _POSIX_PIPE_BUF
+_POSIX_RE_DUP_MAX _POSIX_RE_DUP_MAX
+_POSIX_RTSIG_MAX _POSIX_RTSIG_MAX
+_POSIX_SEM_NSEMS_MAX _POSIX_SEM_NSEMS_MAX
+_POSIX_SEM_VALUE_MAX _POSIX_SEM_VALUE_MAX
+_POSIX_SIGQUEUE_MAX _POSIX_SIGQUEUE_MAX
+_POSIX_SSIZE_MAX _POSIX_SSIZE_MAX
+_POSIX_SS_REPL_MAX _POSIX_SS_REPL_MAX
+_POSIX_STREAM_MAX _POSIX_STREAM_MAX
+_POSIX_SYMLINK_MAX _POSIX_SYMLINK_MAX
+_POSIX_SYMLOOP_MAX _POSIX_SYMLOOP_MAX
+_POSIX_THREAD_DESTRUCTOR_ITERATIONS _POSIX_THREAD_DESTRUCTOR_ITERATIONS
+_POSIX_THREAD_KEYS_MAX _POSIX_THREAD_KEYS_MAX
+_POSIX_THREAD_THREADS_MAX _POSIX_THREAD_THREADS_MAX
+_POSIX_TIMER_MAX _POSIX_TIMER_MAX
+_POSIX_TTY_NAME_MAX _POSIX_TTY_NAME_MAX
+_POSIX_TZNAME_MAX _POSIX_TZNAME_MAX
+_POSIX2_BC_BASE_MAX _POSIX2_BC_BASE_MAX
+_POSIX2_BC_DIM_MAX _POSIX2_BC_DIM_MAX
+_POSIX2_BC_SCALE_MAX _POSIX2_BC_SCALE_MAX
+_POSIX2_BC_STRING_MAX _POSIX2_BC_STRING_MAX
+_POSIX2_CHARCLASS_NAME_MAX _POSIX2_CHARCLASS_NAME_MAX
+_POSIX2_COLL_WEIGHTS_MAX _POSIX2_COLL_WEIGHTS_MAX
+_POSIX2_EXPR_NEST_MAX _POSIX2_EXPR_NEST_MAX
+_POSIX2_LINE_MAX _POSIX2_LINE_MAX
+_POSIX2_RE_DUP_MAX _POSIX2_RE_DUP_MAX
+EOF
+
+ifdef sysconf_l << EOF
+AIO_LISTIO_MAX _SC_AIO_LISTIO_MAX
+AIO_MAX _SC_AIO_MAX
+AIO_PRIO_DELTA_MAX _SC_AIO_PRIO_DELTA_MAX
+ARG_MAX _SC_ARG_MAX
+ATEXIT_MAX _SC_ATEXIT_MAX
+BC_BASE_MAX _SC_BC_BASE_MAX
+BC_DIM_MAX _SC_BC_DIM_MAX
+BC_SCALE_MAX _SC_BC_SCALE_MAX
+BC_STRING_MAX _SC_BC_STRING_MAX
+CHILD_MAX _SC_CHILD_MAX
+COLL_WEIGHTS_MAX _SC_COLL_WEIGHTS_MAX
+DELAYTIMER_MAX _SC_DELAYTIMER_MAX
+EXPR_NEST_MAX _SC_EXPR_NEST_MAX
+HOST_NAME_MAX _SC_HOST_NAME_MAX
+IOV_MAX _SC_IOV_MAX
+LINE_MAX _SC_LINE_MAX
+LOGIN_NAME_MAX _SC_LOGIN_NAME_MAX
+NGROUPS_MAX _SC_NGROUPS_MAX
+MQ_OPEN_MAX _SC_MQ_OPEN_MAX
+MQ_PRIO_MAX _SC_MQ_PRIO_MAX
+OPEN_MAX _SC_OPEN_MAX
+_POSIX_ADVISORY_INFO _SC_ADVISORY_INFO
+_POSIX_BARRIERS _SC_BARRIERS
+_POSIX_ASYNCHRONOUS_IO _SC_ASYNCHRONOUS_IO
+_POSIX_CLOCK_SELECTION _SC_CLOCK_SELECTION
+_POSIX_CPUTIME _SC_CPUTIME
+_POSIX_FSYNC _SC_FSYNC
+_POSIX_IPV6 _SC_IPV6
+_POSIX_JOB_CONTROL _SC_JOB_CONTROL
+_POSIX_MAPPED_FILES _SC_MAPPED_FILES
+_POSIX_MEMLOCK _SC_MEMLOCK
+_POSIX_MEMLOCK_RANGE _SC_MEMLOCK_RANGE
+_POSIX_MEMORY_PROTECTION _SC_MEMORY_PROTECTION
+_POSIX_MESSAGE_PASSING _SC_MESSAGE_PASSING
+_POSIX_MONOTONIC_CLOCK _SC_MONOTONIC_CLOCK
+_POSIX_PRIORITIZED_IO _SC_PRIORITIZED_IO
+_POSIX_PRIORITY_SCHEDULING _SC_PRIORITY_SCHEDULING
+_POSIX_RAW_SOCKETS _SC_RAW_SOCKETS
+_POSIX_READER_WRITER_LOCKS _SC_READER_WRITER_LOCKS
+_POSIX_REALTIME_SIGNALS _SC_REALTIME_SIGNALS
+_POSIX_REGEXP _SC_REGEXP
+_POSIX_SAVED_IDS _SC_SAVED_IDS
+_POSIX_SEMAPHORES _SC_SEMAPHORES
+_POSIX_SHARED_MEMORY_OBJECTS _SC_SHARED_MEMORY_OBJECTS
+_POSIX_SHELL _SC_SHELL
+_POSIX_SPAWN _SC_SPAWN
+_POSIX_SPIN_LOCKS _SC_SPIN_LOCKS
+_POSIX_SPORADIC_SERVER _SC_SPORADIC_SERVER
+_POSIX_SS_REPL_MAX _SC_SS_REPL_MAX
+_POSIX_SYNCHRONIZED_IO _SC_SYNCHRONIZED_IO
+_POSIX_THREAD_ATTR_STACKADDR _SC_THREAD_ATTR_STACKADDR
+_POSIX_THREAD_ATTR_STACKSIZE _SC_THREAD_ATTR_STACKSIZE
+_POSIX_THREAD_CPUTIME _SC_THREAD_CPUTIME
+_POSIX_THREAD_PRIO_INHERIT _SC_THREAD_PRIO_INHERIT
+_POSIX_THREAD_PRIO_PROTECT _SC_THREAD_PRIO_PROTECT
+_POSIX_THREAD_PRIORITY_SCHEDULING _SC_THREAD_PRIORITY_SCHEDULING
+_POSIX_THREAD_PROCESS_SHARED _SC_THREAD_PROCESS_SHARED
+_POSIX_THREAD_ROBUST_PRIO_INHERIT _SC_THREAD_ROBUST_PRIO_INHERIT
+_POSIX_THREAD_ROBUST_PRIO_PROTECT _SC_THREAD_ROBUST_PRIO_PROTECT
+_POSIX_THREAD_SAFE_FUNCTIONS _SC_THREAD_SAFE_FUNCTIONS
+_POSIX_THREAD_SPORADIC_SERVER _SC_THREAD_SPORADIC_SERVER
+_POSIX_THREADS _SC_THREADS
+_POSIX_TIMEOUTS _SC_TIMEOUTS
+_POSIX_TIMERS _SC_TIMERS
+_POSIX_TRACE _SC_TRACE
+_POSIX_TRACE_EVENT_FILTER _SC_TRACE_EVENT_FILTER
+_POSIX_TRACE_EVENT_NAME_MAX _SC_TRACE_EVENT_NAME_MAX
+_POSIX_TRACE_INHERIT _SC_TRACE_INHERIT
+_POSIX_TRACE_LOG _SC_TRACE_LOG
+_POSIX_TRACE_NAME_MAX _SC_TRACE_NAME_MAX
+_POSIX_TRACE_SYS_MAX _SC_TRACE_SYS_MAX
+_POSIX_TRACE_USER_EVENT_MAX _SC_TRACE_USER_EVENT_MAX
+_POSIX_TYPED_MEMORY_OBJECTS _SC_TYPED_MEMORY_OBJECTS
+_POSIX_VERSION _SC_VERSION
+_POSIX_V7_ILP32_OFF32 _SC_V7_ILP32_OFF32
+_POSIX_V7_ILP32_OFFBIG _SC_V7_ILP32_OFFBIG
+_POSIX_V7_LP64_OFF64 _SC_V7_LP64_OFF64
+_POSIX_V7_LPBIG_OFFBIG _SC_V7_LPBIG_OFFBIG
+_POSIX2_C_BIND _SC_2_C_BIND
+_POSIX2_C_DEV _SC_2_C_DEV
+_POSIX2_CHAR_TERM _SC_2_CHAR_TERM
+_POSIX2_FORT_DEV _SC_2_FORT_DEV
+_POSIX2_FORT_RUN _SC_2_FORT_RUN
+_POSIX2_LOCALEDEF _SC_2_LOCALEDEF
+_POSIX2_PBS _SC_2_PBS
+_POSIX2_PBS_ACCOUNTING _SC_2_PBS_ACCOUNTING
+_POSIX2_PBS_CHECKPOINT _SC_2_PBS_CHECKPOINT
+_POSIX2_PBS_LOCATE _SC_2_PBS_LOCATE
+_POSIX2_PBS_MESSAGE _SC_2_PBS_MESSAGE
+_POSIX2_PBS_TRACK _SC_2_PBS_TRACK
+_POSIX2_SW_DEV _SC_2_SW_DEV
+_POSIX2_UPE _SC_2_UPE
+_POSIX2_VERSION _SC_2_VERSION
+PAGE_SIZE _SC_PAGE_SIZE
+PAGESIZE _SC_PAGESIZE
+PTHREAD_DESTRUCTOR_ITERATIONS _SC_THREAD_DESTRUCTOR_ITERATIONS
+PTHREAD_KEYS_MAX _SC_THREAD_KEYS_MAX
+PTHREAD_STACK_MIN _SC_THREAD_STACK_MIN
+PTHREAD_THREADS_MAX _SC_THREAD_THREADS_MAX
+RE_DUP_MAX _SC_RE_DUP_MAX
+RTSIG_MAX _SC_RTSIG_MAX
+SEM_NSEMS_MAX _SC_SEM_NSEMS_MAX
+SEM_VALUE_MAX _SC_SEM_VALUE_MAX
+SIGQUEUE_MAX _SC_SIGQUEUE_MAX
+STREAM_MAX _SC_STREAM_MAX
+SYMLOOP_MAX _SC_SYMLOOP_MAX
+TIMER_MAX _SC_TIMER_MAX
+TTY_NAME_MAX _SC_TTY_NAME_MAX
+TZNAME_MAX _SC_TZNAME_MAX
+_XOPEN_CRYPT _SC_XOPEN_CRYPT
+_XOPEN_ENH_I18N _SC_XOPEN_ENH_I18N
+_XOPEN_REALTIME _SC_XOPEN_REALTIME
+_XOPEN_REALTIME_THREADS _SC_XOPEN_REALTIME_THREADS
+_XOPEN_SHM _SC_XOPEN_SHM
+_XOPEN_STREAMS _SC_XOPEN_STREAMS
+_XOPEN_UNIX _SC_XOPEN_UNIX
+_XOPEN_UUCP _SC_XOPEN_UUCP
+_XOPEN_VERSION _SC_XOPEN_VERSION
+EOF
+
+ifdef pathconf_l << EOF
+FILESIZEBITS _PC_FILESIZEBITS
+LINK_MAX _PC_LINK_MAX
+MAX_CANON _PC_MAX_CANON
+MAX_INPUT _PC_MAX_INPUT
+NAME_MAX _PC_NAME_MAX
+PATH_MAX _PC_PATH_MAX
+PIPE_BUF _PC_PIPE_BUF
+POSIX2_SYMLINKS _PC_2_SYMLINKS
+POSIX_ALLOC_SIZE_MIN _PC_ALLOC_SIZE_MIN
+POSIX_REC_INCR_XFER_SIZE _PC_REC_INCR_XFER_SIZE
+POSIX_REC_MAX_XFER_SIZE _PC_REC_MAX_XFER_SIZE
+POSIX_REC_MIN_XFER_SIZE _PC_REC_MIN_XFER_SIZE
+POSIX_REC_XFER_ALIGN _PC_REC_XFER_ALIGN
+SYMLINK_MAX _PC_SYMLINK_MAX
+_POSIX_CHOWN_RESTRICTED _PC_CHOWN_RESTRICTED
+_POSIX_NO_TRUNC _PC_NO_TRUNC
+_POSIX_VDISABLE _PC_VDISABLE
+_POSIX_ASYNC_IO _PC_ASYNC_IO
+_POSIX_PRIO_IO _PC_PRIO_IO
+_POSIX_SYNC_IO _PC_SYNC_IO
+_POSIX_TIMESTAMP_RESOLUTION _PC_TIMESTAMP_RESOLUTION
+EOF
diff --git a/util/sbase/scripts/install b/util/sbase/scripts/install
new file mode 100755
index 00000000..ce78c1da
--- /dev/null
+++ b/util/sbase/scripts/install
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+set -e
+
+while read type src dst perm
+do
+ case $type in
+ d)
+ mkdir -p $src
+ ;;
+ c)
+ cp -f $src $dst
+ ;;
+ *)
+ echo install: wrong entry type >&2
+ exit 1
+ ;;
+ esac
+
+ chmod $perm $dst
+done < $1
diff --git a/util/sbase/scripts/mkbox b/util/sbase/scripts/mkbox
new file mode 100755
index 00000000..99e40441
--- /dev/null
+++ b/util/sbase/scripts/mkbox
@@ -0,0 +1,103 @@
+#!/bin/sh
+
+trap "rm -rf build" INT QUIT TERM
+
+rm -rf build
+mkdir -p build
+
+cp *.h build
+
+cat > build/sbase-box.c <<EOF
+#include <unistd.h>
+
+#include <libgen.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "util.h"
+#include "sbase-box.h"
+
+struct cmd {
+ char *name;
+ int (*fn)(int, char **);
+} cmds[] = {
+ {"install", xinstall_main},
+ {"[", test_main},
+$(grep -l ^main *.c |
+while read f
+do
+ sed -n '
+ /^main/ {
+ s/main/'${f%.c}'_main/
+ s/-/_/g
+ w build/'$f'
+ s/\(^.*\)(.*)/ {"'${f%.c}'", \1},/p
+ d
+ }
+ w 'build/$f $f
+done)
+ {NULL},
+};
+
+static void
+install(char *path)
+{
+ int r;
+ struct cmd *bp;
+ char fname[FILENAME_MAX];
+
+ if (path == NULL) {
+ fputs("sbase-box [-i path] [command]\n", stderr);
+ exit(1);
+ }
+
+ for (bp = cmds; bp->name; ++bp) {
+ r = snprintf(fname, sizeof(fname), "%s/%s", path, bp->name);
+ if (r < 0 || r >= sizeof(fname)) {
+ fprintf(stderr,
+ "sbase-box: destination path truncated for '%s'\n",
+ bp->name);
+ exit(1);
+ }
+ remove(fname);
+ if (symlink("sbase-box", fname) < 0) {
+ fprintf(stderr,
+ "sbase-box: %s: %s\n",
+ bp->name, strerror(errno));
+ exit(1);
+ }
+ }
+}
+
+int
+main(int argc, char *argv[])
+{
+ char *s = basename(argv[0]);
+ struct cmd *bp;
+
+ if (!strcmp(s, "sbase-box") && argc > 1) {
+ argc--; argv++;
+ if (strcmp(argv[0], "-i") == 0) {
+ install(argv[1]);
+ exit(0);
+ }
+ s = basename(argv[0]);
+ }
+
+ for (bp = cmds; bp->name; ++bp) {
+ if (strcmp(bp->name, s) == 0)
+ return (*bp->fn)(argc, argv);
+ }
+
+ for (bp = cmds; bp->name; ++bp)
+ printf("%s ", bp->name);
+ putchar('\n');
+
+ return 0;
+}
+EOF
+
+sed -n 's/.* \(.*_main\).*/int \1(int, char **);/p'\
+ build/sbase-box.c > build/sbase-box.h
diff --git a/util/sbase/scripts/mkproto b/util/sbase/scripts/mkproto
new file mode 100755
index 00000000..192fe56b
--- /dev/null
+++ b/util/sbase/scripts/mkproto
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+usage()
+{
+ echo mkproto: prefix manprefix proto>&2
+ exit 1
+}
+
+prefix=${1?$(usage)}
+manprefix=${2?$(usage)}
+proto=${3?$(usage)}
+
+trap "rm -f scripts/proto" EXIT INT QUIT TERM
+
+(set -e
+echo d $prefix/bin $prefix/bin 755
+find . ! -name . -prune -type f \( -perm -u+x -o -perm -g+x -o -perm o+x \) |
+sed "s@.*@c & $prefix/bin/& 755@"
+
+echo d $manprefix/man1 $manprefix/man1 755
+find . ! -name . -prune -name '*.1' |
+sed "s@.*@c & $manprefix/man1/& 644@") > $proto
+
+trap "" EXIT INT QUIT TERM
diff --git a/util/sbase/scripts/uninstall b/util/sbase/scripts/uninstall
new file mode 100755
index 00000000..e9c74f2d
--- /dev/null
+++ b/util/sbase/scripts/uninstall
@@ -0,0 +1,32 @@
+#!/bin/sh
+
+set -e
+
+while read type src dst perm
+do
+ case $type in
+ d)
+ echo $type $src $dst $perm
+ continue
+ ;;
+ c)
+ rm -f $dst
+ ;;
+ *)
+ echo uninstall: wrong entry type >&2
+ exit 1
+ ;;
+ esac
+done < $1 |
+sort -r |
+while read type src dst perm
+do
+ case $type in
+ d)
+ if test `ls $dst | wc -l` -eq 0
+ then
+ rmdir $dst
+ fi
+ ;;
+ esac
+done
diff --git a/util/sbase/sed.1 b/util/sbase/sed.1
new file mode 100644
index 00000000..18981aa7
--- /dev/null
+++ b/util/sbase/sed.1
@@ -0,0 +1,173 @@
+.Dd October 8, 2015
+.Dt SED 1
+.Os sbase
+.Sh NAME
+.Nm sed
+.Nd stream editor
+.Sh SYNOPSIS
+.Nm
+.Op Fl nrE
+.Ar script
+.Op Ar file ...
+.Nm
+.Op Fl nrE
+.Fl e Ar script
+.Op Fl e Ar script
+.Ar ...
+.Op Fl f Ar scriptfile
+.Ar ...
+.Op Ar file ...
+.Nm
+.Op Fl nrE
+.Op Fl e Ar script
+.Ar ...
+.Fl f Ar scriptfile
+.Op Fl f Ar scriptfile
+.Ar ...
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+reads line oriented output from
+.Ar file
+or stdin, applies the editing commands supplied by
+.Ar script
+or
+.Ar scriptfile
+and writes the edited stream to stdout.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl n
+Suppress default printing at the end of each cycle.
+.It Fl r E
+Use extended regular expressions
+.It Fl e Ar script
+Append
+.Ar script
+to the list of editing commands.
+.It Fl f Ar scriptfile
+Append the commands from
+.Ar scriptfile
+to the list of editing commands.
+.El
+.Sh EXTENDED DESCRIPTION
+Editing commands take the form:
+.Pp
+[address[,address]]function
+.Pp
+Commands can be separated by ';' or by a new line.
+.Pp
+Multiple functions for the specified address (or address-range) can be enclosed
+in blocks with '{' and '}':
+.Pp
+[address[,address]] { function ; function }
+.Ss Addresses
+Addresses are either blank, a positive decimal integer denoting a line
+number, the character '$' denoting the last line of input, or a regular
+expression (in the format
+.No / Ns
+.Ar regexp Ns /).
+A command with no addresses matches every line, one address matches
+individual lines, and two addresses matches a range of lines from the
+first to the second address inclusive.
+.Pp
+The character '!' may be appended after the addresses,
+in which case the function is executed only if the addresses
+.Em don't
+match.
+.Ss Functions
+.Bl -tag -width Ds
+.It Ar a Op Ar text
+Append text to output after end of current cycle.
+.It Ar b Op Ar label
+Branch to label.
+If no label is provided branch to end of script.
+.It Ar c Op Ar text
+Change.
+Delete addressed range and output text after end of current cycle.
+.It Ar d
+Delete pattern space and begin next cycle.
+.It Ar D
+Delete pattern space up to and including first newline and begin new
+cycle without reading input.
+If there is no newline, behave like d.
+.It Ar g
+Get.
+Replace the pattern space with the hold space.
+.It Ar G
+Get.
+Append a newline and the hold space to the pattern space.
+.It Ar h
+Hold.
+Replace the hold space with the pattern space.
+.It Ar H
+Hold.
+Append a newline and the pattern space to the hold space.
+.It Ar i Op Ar text
+Insert text in output.
+.It Ar l
+List? Write the pattern space replacing known non printing characters with
+backslash escaped versions (\\\\, \\a, \\b, \\f, \\r, \\t, \\v).
+Print bad UTF-8 sequences as \\ooo where ooo is a three digit octal
+number.
+Mark end of lines with '$'.
+.It Ar n
+Next.
+Write pattern space (unless
+.Fl n ) ,
+read next line into pattern space, and continue current cycle.
+If there is no next line, quit.
+.It Ar N
+Next.
+Read next line, append newline and next line to pattern space, and
+continue cycle.
+If there is no next line, quit without printing current pattern space.
+.It Ar p
+Print current pattern space.
+.It Ar P
+Print current pattern space up to first newline.
+.It Ar q
+Quit.
+.It Ar r file
+Read file and write contents to output.
+.It Ar s/re/text/flags
+Find occurences of regular expression re in the pattern space and
+replace with text.
+A '&' in text is replaced with the entire match.
+A \\d where d is a decimal digit 1-9 is replaced with the corresponding
+match group from the regular expression.
+\\n represents a newline in both the regular expression and replacement
+text.
+A literal newline in the replacement text must be preceded by a \\.
+.Pp
+Flags are
+.Bl -tag -width Ds
+.It Ar n
+A positive decimal number denoting which match in the pattern space
+to replace.
+.It Ar g
+Global.
+Replace all matches in the pattern space.
+.It Ar p
+Print the pattern if a replacement was made.
+.It Ar w file
+Write the pattern space to file if a replacement was made.
+.El
+.It Ar t Op Ar label
+Test.
+Branch to corresponding label if a substitution has been made since the
+last line was read or last t command was executed.
+If no label is provided branch to end of script.
+.It Ar w file
+Write pattern space to file.
+.It Ar x
+Exchange hold space and pattern space.
+.It Ar y/set1/set2/
+Replace each occurrence of a character from set 1 with the corresponding
+character from set 2.
+.It Ar :label
+Create a label for b and t commands.
+.It Ar #comment
+The comment extends until the next newline.
+.It Ar =
+Write current input line number to output.
+.El
diff --git a/util/sbase/sed.c b/util/sbase/sed.c
new file mode 100644
index 00000000..08471943
--- /dev/null
+++ b/util/sbase/sed.c
@@ -0,0 +1,1738 @@
+/* FIXME: summary
+ * decide whether we enforce valid UTF-8, right now it's enforced in certain
+ * parts of the script, but not the input...
+ * nul bytes cause explosions due to use of libc string functions. thoughts?
+ * lack of newline at end of file, currently we add one. what should we do?
+ * allow "\\t" for "\t" etc. in regex? in replacement text?
+ * POSIX says don't flush on N when out of input, but GNU and busybox do.
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <regex.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "utf.h"
+#include "util.h"
+
+/* Types */
+
+/* used as queue for writes and stack for {,:,b,t */
+typedef struct {
+ void **data;
+ size_t size;
+ size_t cap;
+} Vec;
+
+/* used for arbitrary growth, str is a C string
+ * FIXME: does it make sense to keep track of length? or just rely on libc
+ * string functions? If we want to support nul bytes everything changes
+ */
+typedef struct {
+ char *str;
+ size_t cap;
+} String;
+
+typedef struct Cmd Cmd;
+typedef struct {
+ void (*fn)(Cmd *);
+ char *(*getarg)(Cmd *, char *);
+ void (*freearg)(Cmd *);
+ unsigned char naddr;
+} Fninfo;
+
+typedef struct {
+ union {
+ size_t lineno;
+ regex_t *re;
+ } u;
+ enum {
+ IGNORE, /* empty address, ignore */
+ EVERY , /* every line */
+ LINE , /* line number */
+ LAST , /* last line ($) */
+ REGEX , /* use included regex */
+ LASTRE, /* use most recently used regex */
+ } type;
+} Addr;
+
+/* DISCUSS: naddr is not strictly necessary, but very helpful
+ * naddr == 0 iff beg.type == EVERY && end.type == IGNORE
+ * naddr == 1 iff beg.type != IGNORE && end.type == IGNORE
+ * naddr == 2 iff beg.type != IGNORE && end.type != IGNORE
+ */
+typedef struct {
+ Addr beg;
+ Addr end;
+ unsigned char naddr;
+} Range;
+
+typedef struct {
+ regex_t *re; /* if NULL use last regex */
+ String repl;
+ FILE *file;
+ size_t occurrence; /* 0 for all (g flag) */
+ Rune delim;
+ unsigned int p:1;
+} Sarg;
+
+typedef struct {
+ Rune *set1;
+ Rune *set2;
+} Yarg;
+
+typedef struct {
+ String str; /* a,c,i text. r file path */
+ void (*print)(char *, FILE *); /* check_puts for a, write_file for r, unused for c,i */
+} ACIRarg;
+
+struct Cmd {
+ Range range;
+ Fninfo *fninfo;
+ union {
+ Cmd *jump; /* used for b,t when running */
+ char *label; /* used for :,b,t when building */
+ ptrdiff_t offset; /* used for { (pointers break during realloc) */
+ FILE *file; /* used for w */
+
+ /* FIXME: Should the following be in the union? or pointers and malloc? */
+ Sarg s;
+ Yarg y;
+ ACIRarg acir;
+ } u; /* I find your lack of anonymous unions disturbing */
+ unsigned int in_match:1;
+ unsigned int negate :1;
+};
+
+/* Files for w command (and s' w flag) */
+typedef struct {
+ char *path;
+ FILE *file;
+} Wfile;
+
+/*
+ * Function Declarations
+ */
+
+/* Dynamically allocated arrays and strings */
+static void resize(void **ptr, size_t *nmemb, size_t size, size_t new_nmemb, void **next);
+static void *pop(Vec *v);
+static void push(Vec *v, void *p);
+static void stracat(String *dst, char *src);
+static void strnacat(String *dst, char *src, size_t n);
+static void stracpy(String *dst, char *src);
+
+/* Cleanup and errors */
+static void usage(void);
+
+/* Parsing functions and related utilities */
+static void compile(char *s, int isfile);
+static int read_line(FILE *f, String *s);
+static char *make_range(Range *range, char *s);
+static char *make_addr(Addr *addr, char *s);
+static char *find_delim(char *s, Rune delim, int do_brackets);
+static char *chompr(char *s, Rune rune);
+static char *chomp(char *s);
+static Rune *strtorunes(char *s, size_t nrunes);
+static long stol(char *s, char **endp);
+static size_t escapes(char *beg, char *end, Rune delim, int n_newline);
+static size_t echarntorune(Rune *r, char *s, size_t n);
+static void insert_labels(void);
+
+/* Get and Free arg and related utilities */
+static char *get_aci_arg(Cmd *c, char *s);
+static void aci_append(Cmd *c, char *s);
+static void free_acir_arg(Cmd *c);
+static char *get_bt_arg(Cmd *c, char *s);
+static char *get_r_arg(Cmd *c, char *s);
+static char *get_s_arg(Cmd *c, char *s);
+static void free_s_arg(Cmd *c);
+static char *get_w_arg(Cmd *c, char *s);
+static char *get_y_arg(Cmd *c, char *s);
+static void free_y_arg(Cmd *c);
+static char *get_colon_arg(Cmd *c, char *s);
+static char *get_lbrace_arg(Cmd *c, char *s);
+static char *get_rbrace_arg(Cmd *c, char *s);
+static char *semicolon_arg(char *s);
+
+/* Running */
+static void run(void);
+static int in_range(Cmd *c);
+static int match_addr(Addr *a);
+static int next_file(void);
+static int is_eof(FILE *f);
+static void do_writes(void);
+static void write_file(char *path, FILE *out);
+static void check_puts(char *s, FILE *f);
+static void update_ranges(Cmd *beg, Cmd *end);
+
+/* Sed functions */
+static void cmd_y(Cmd *c);
+static void cmd_x(Cmd *c);
+static void cmd_w(Cmd *c);
+static void cmd_t(Cmd *c);
+static void cmd_s(Cmd *c);
+static void cmd_r(Cmd *c);
+static void cmd_q(Cmd *c);
+static void cmd_P(Cmd *c);
+static void cmd_p(Cmd *c);
+static void cmd_N(Cmd *c);
+static void cmd_n(Cmd *c);
+static void cmd_l(Cmd *c);
+static void cmd_i(Cmd *c);
+static void cmd_H(Cmd *c);
+static void cmd_h(Cmd *c);
+static void cmd_G(Cmd *c);
+static void cmd_g(Cmd *c);
+static void cmd_D(Cmd *c);
+static void cmd_d(Cmd *c);
+static void cmd_c(Cmd *c);
+static void cmd_b(Cmd *c);
+static void cmd_a(Cmd *c);
+static void cmd_colon(Cmd *c);
+static void cmd_equal(Cmd *c);
+static void cmd_lbrace(Cmd *c);
+static void cmd_rbrace(Cmd *c);
+static void cmd_last(Cmd *c);
+
+/* Actions */
+static void new_line(void);
+static void app_line(void);
+static void new_next(void);
+static void old_next(void);
+
+/*
+ * Globals
+ */
+static Vec braces, labels, branches; /* holds ptrdiff_t. addrs of {, :, bt */
+static Vec writes; /* holds cmd*. writes scheduled by a and r commands */
+static Vec wfiles; /* holds Wfile*. files for w and s///w commands */
+
+static Cmd *prog, *pc; /* Program, program counter */
+static size_t pcap;
+static size_t lineno;
+
+static regex_t *lastre; /* last used regex for empty regex search */
+static char **files; /* list of file names from argv */
+static FILE *file; /* current file we are reading */
+static int ret; /* exit status */
+
+static String patt, hold, genbuf;
+
+static struct {
+ unsigned int n :1; /* -n (no print) */
+ unsigned int E :1; /* -E (extended re) */
+ unsigned int s :1; /* s/// replacement happened */
+ unsigned int aci_cont:1; /* a,c,i text continuation */
+ unsigned int s_cont :1; /* s/// replacement text continuation */
+ unsigned int halt :1; /* halt execution */
+} gflags;
+
+/* FIXME: move character inside Fninfo and only use 26*sizeof(Fninfo) instead of 127*sizeof(Fninfo) bytes */
+static Fninfo fns[] = {
+ ['a'] = { cmd_a , get_aci_arg , free_acir_arg , 1 }, /* schedule write of text for later */
+ ['b'] = { cmd_b , get_bt_arg , NULL , 2 }, /* branch to label char *label when building, Cmd *jump when running */
+ ['c'] = { cmd_c , get_aci_arg , free_acir_arg , 2 }, /* delete pattern space. at 0 or 1 addr or end of 2 addr, write text */
+ ['d'] = { cmd_d , NULL , NULL , 2 }, /* delete pattern space */
+ ['D'] = { cmd_D , NULL , NULL , 2 }, /* delete to first newline and start new cycle without reading (if no newline, d) */
+ ['g'] = { cmd_g , NULL , NULL , 2 }, /* replace pattern space with hold space */
+ ['G'] = { cmd_G , NULL , NULL , 2 }, /* append newline and hold space to pattern space */
+ ['h'] = { cmd_h , NULL , NULL , 2 }, /* replace hold space with pattern space */
+ ['H'] = { cmd_H , NULL , NULL , 2 }, /* append newline and pattern space to hold space */
+ ['i'] = { cmd_i , get_aci_arg , free_acir_arg , 1 }, /* write text */
+ ['l'] = { cmd_l , NULL , NULL , 2 }, /* write pattern space in 'visually unambiguous form' */
+ ['n'] = { cmd_n , NULL , NULL , 2 }, /* write pattern space (unless -n) read to replace pattern space (if no input, quit) */
+ ['N'] = { cmd_N , NULL , NULL , 2 }, /* append to pattern space separated by newline, line number changes (if no input, quit) */
+ ['p'] = { cmd_p , NULL , NULL , 2 }, /* write pattern space */
+ ['P'] = { cmd_P , NULL , NULL , 2 }, /* write pattern space up to first newline */
+ ['q'] = { cmd_q , NULL , NULL , 1 }, /* quit */
+ ['r'] = { cmd_r , get_r_arg , free_acir_arg , 1 }, /* write contents of file (unable to open/read treated as empty file) */
+ ['s'] = { cmd_s , get_s_arg , free_s_arg , 2 }, /* find/replace/all that crazy s stuff */
+ ['t'] = { cmd_t , get_bt_arg , NULL , 2 }, /* if s/// succeeded (since input or last t) branch to label (branch to end if no label) */
+ ['w'] = { cmd_w , get_w_arg , NULL , 2 }, /* append pattern space to file */
+ ['x'] = { cmd_x , NULL , NULL , 2 }, /* exchange pattern and hold spaces */
+ ['y'] = { cmd_y , get_y_arg , free_y_arg , 2 }, /* replace runes in set1 with runes in set2 */
+ [':'] = { cmd_colon , get_colon_arg , NULL , 0 }, /* defines label for later b and t commands */
+ ['='] = { cmd_equal , NULL , NULL , 1 }, /* printf("%d\n", line_number); */
+ ['{'] = { cmd_lbrace, get_lbrace_arg, NULL , 2 }, /* if we match, run commands, otherwise jump to close */
+ ['}'] = { cmd_rbrace, get_rbrace_arg, NULL , 0 }, /* noop, hold onto open for ease of building scripts */
+
+ [0x7f] = { NULL, NULL, NULL, 0 }, /* index is checked with isascii(3p). fill out rest of array */
+};
+
+/*
+ * Function Definitions
+ */
+
+/* given memory pointed to by *ptr that currently holds *nmemb members of size
+ * size, realloc to hold new_nmemb members, return new_nmemb in *memb and one
+ * past old end in *next. if realloc fails...explode
+ */
+static void
+resize(void **ptr, size_t *nmemb, size_t size, size_t new_nmemb, void **next)
+{
+ void *n, *tmp;
+
+ if (new_nmemb) {
+ tmp = ereallocarray(*ptr, new_nmemb, size);
+ } else { /* turns out realloc(*ptr, 0) != free(*ptr) */
+ free(*ptr);
+ tmp = NULL;
+ }
+ n = (char *)tmp + *nmemb * size;
+ *nmemb = new_nmemb;
+ *ptr = tmp;
+ if (next)
+ *next = n;
+}
+
+static void *
+pop(Vec *v)
+{
+ if (!v->size)
+ return NULL;
+ return v->data[--v->size];
+}
+
+static void
+push(Vec *v, void *p)
+{
+ if (v->size == v->cap)
+ resize((void **)&v->data, &v->cap, sizeof(*v->data), v->cap * 2 + 1, NULL);
+ v->data[v->size++] = p;
+}
+
+static void
+stracat(String *dst, char *src)
+{
+ int new = !dst->cap;
+ size_t len;
+
+ len = (new ? 0 : strlen(dst->str)) + strlen(src) + 1;
+ if (dst->cap < len)
+ resize((void **)&dst->str, &dst->cap, 1, len * 2, NULL);
+ if (new)
+ *dst->str = '\0';
+ strcat(dst->str, src);
+}
+
+static void
+strnacat(String *dst, char *src, size_t n)
+{
+ int new = !dst->cap;
+ size_t len;
+
+ len = strlen(src);
+ len = (new ? 0 : strlen(dst->str)) + MIN(n, len) + 1;
+ if (dst->cap < len)
+ resize((void **)&dst->str, &dst->cap, 1, len * 2, NULL);
+ if (new)
+ *dst->str = '\0';
+ strlcat(dst->str, src, len);
+}
+
+static void
+stracpy(String *dst, char *src)
+{
+ size_t len;
+
+ len = strlen(src) + 1;
+ if (dst->cap < len)
+ resize((void **)&dst->str, &dst->cap, 1, len * 2, NULL);
+ strcpy(dst->str, src);
+}
+
+static void
+leprintf(char *s)
+{
+ if (errno)
+ eprintf("%zu: %s: %s\n", lineno, s, strerror(errno));
+ else
+ eprintf("%zu: %s\n", lineno, s);
+}
+
+/* FIXME: write usage message */
+static void
+usage(void)
+{
+ eprintf("usage: sed [-nrE] script [file ...]\n"
+ " sed [-nrE] -e script [-e script] ... [-f scriptfile] ... [file ...]\n"
+ " sed [-nrE] [-e script] ... -f scriptfile [-f scriptfile] ... [file ...]\n");
+}
+
+/* Differences from POSIX
+ * we allows semicolons and trailing blanks inside {}
+ * we allow spaces after ! (and in between !s)
+ * we allow extended regular expressions (-E)
+ */
+static void
+compile(char *s, int isfile)
+{
+ FILE *f;
+
+ if (isfile) {
+ f = fopen(s, "r");
+ if (!f)
+ eprintf("fopen %s:", s);
+ } else {
+ if (!*s) /* empty string script */
+ return;
+ f = fmemopen(s, strlen(s), "r");
+ if (!f)
+ eprintf("fmemopen:");
+ }
+
+ /* NOTE: get arg functions can't use genbuf */
+ while (read_line(f, &genbuf) != EOF) {
+ s = genbuf.str;
+
+ /* if the first two characters of the script are "#n" default output shall be suppressed */
+ if (++lineno == 1 && *s == '#' && s[1] == 'n') {
+ gflags.n = 1;
+ continue;
+ }
+
+ if (gflags.aci_cont) {
+ aci_append(pc - 1, s);
+ continue;
+ }
+ if (gflags.s_cont)
+ s = (pc - 1)->fninfo->getarg(pc - 1, s);
+
+ while (*s) {
+ s = chompr(s, ';');
+ if (!*s || *s == '#')
+ break;
+
+ if ((size_t)(pc - prog) == pcap)
+ resize((void **)&prog, &pcap, sizeof(*prog), pcap * 2 + 1, (void **)&pc);
+
+ pc->range.beg.type = pc->range.end.type = IGNORE;
+ pc->fninfo = NULL;
+ pc->in_match = 0;
+
+ s = make_range(&pc->range, s);
+ s = chomp(s);
+ pc->negate = *s == '!';
+ s = chompr(s, '!');
+
+ if (!isascii(*s) || !(pc->fninfo = &fns[(unsigned)*s])->fn)
+ leprintf("bad sed function");
+ if (pc->range.naddr > pc->fninfo->naddr)
+ leprintf("wrong number of addresses");
+ s++;
+
+ if (pc->fninfo->getarg)
+ s = pc->fninfo->getarg(pc, s);
+
+ pc++;
+ }
+ }
+
+ fshut(f, s);
+}
+
+/* FIXME: if we decide to honor lack of trailing newline, set/clear a global
+ * flag when reading a line
+ */
+static int
+read_line(FILE *f, String *s)
+{
+ ssize_t len;
+
+ if (!f)
+ return EOF;
+
+ if ((len = getline(&s->str, &s->cap, f)) < 0) {
+ if (ferror(f))
+ eprintf("getline:");
+ return EOF;
+ }
+ if (s->str[--len] == '\n')
+ s->str[len] = '\0';
+ return 0;
+}
+
+/* read first range from s, return pointer to one past end of range */
+static char *
+make_range(Range *range, char *s)
+{
+ s = make_addr(&range->beg, s);
+
+ if (*s == ',')
+ s = make_addr(&range->end, s + 1);
+ else
+ range->end.type = IGNORE;
+
+ if (range->beg.type == EVERY && range->end.type == IGNORE) range->naddr = 0;
+ else if (range->beg.type != IGNORE && range->end.type == IGNORE) range->naddr = 1;
+ else if (range->beg.type != IGNORE && range->end.type != IGNORE) range->naddr = 2;
+ else leprintf("this is impossible...");
+
+ return s;
+}
+
+/* read first addr from s, return pointer to one past end of addr */
+static char *
+make_addr(Addr *addr, char *s)
+{
+ Rune r;
+ char *p = s + strlen(s);
+ size_t rlen = echarntorune(&r, s, p - s);
+
+ if (r == '$') {
+ addr->type = LAST;
+ s += rlen;
+ } else if (isdigitrune(r)) {
+ addr->type = LINE;
+ addr->u.lineno = stol(s, &s);
+ } else if (r == '/' || r == '\\') {
+ Rune delim;
+ if (r == '\\') {
+ s += rlen;
+ rlen = echarntorune(&r, s, p - s);
+ }
+ if (r == '\\')
+ leprintf("bad delimiter '\\'");
+ delim = r;
+ s += rlen;
+ rlen = echarntorune(&r, s, p - s);
+ if (r == delim) {
+ addr->type = LASTRE;
+ s += rlen;
+ } else {
+ addr->type = REGEX;
+ p = find_delim(s, delim, 1);
+ if (!*p)
+ leprintf("unclosed regex");
+ p -= escapes(s, p, delim, 0);
+ *p++ = '\0';
+ addr->u.re = emalloc(sizeof(*addr->u.re));
+ eregcomp(addr->u.re, s, gflags.E ? REG_EXTENDED : 0);
+ s = p;
+ }
+ } else {
+ addr->type = EVERY;
+ }
+
+ return s;
+}
+
+/* return pointer to first delim in s that is not escaped
+ * and if do_brackets is set, not in [] (note possible [::], [..], [==], inside [])
+ * return pointer to trailing nul byte if no delim found
+ *
+ * any escaped character that is not special is just itself (POSIX undefined)
+ * FIXME: pull out into some util thing, will be useful for ed as well
+ */
+static char *
+find_delim(char *s, Rune delim, int do_brackets)
+{
+ enum {
+ OUTSIDE , /* not in brackets */
+ BRACKETS_OPENING, /* last char was first [ or last two were first [^ */
+ BRACKETS_INSIDE , /* inside [] */
+ INSIDE_OPENING , /* inside [] and last char was [ */
+ CLASS_INSIDE , /* inside class [::], or colating element [..] or [==], inside [] */
+ CLASS_CLOSING , /* inside class [::], or colating element [..] or [==], and last character was the respective : . or = */
+ } state = OUTSIDE;
+
+ Rune r, c = 0; /* no c won't be used uninitialized, shutup -Wall */
+ size_t rlen;
+ int escape = 0;
+ char *end = s + strlen(s);
+
+ for (; *s; s += rlen) {
+ rlen = echarntorune(&r, s, end - s);
+
+ if (state == BRACKETS_OPENING && r == '^' ) { continue; }
+ else if (state == BRACKETS_OPENING && r == ']' ) { state = BRACKETS_INSIDE ; continue; }
+ else if (state == BRACKETS_OPENING ) { state = BRACKETS_INSIDE ; }
+
+ if (state == CLASS_CLOSING && r == ']' ) { state = BRACKETS_INSIDE ; }
+ else if (state == CLASS_CLOSING ) { state = CLASS_INSIDE ; }
+ else if (state == CLASS_INSIDE && r == c ) { state = CLASS_CLOSING ; }
+ else if (state == INSIDE_OPENING && (r == ':' ||
+ r == '.' ||
+ r == '=') ) { state = CLASS_INSIDE ; c = r; }
+ else if (state == INSIDE_OPENING && r == ']' ) { state = OUTSIDE ; }
+ else if (state == INSIDE_OPENING ) { state = BRACKETS_INSIDE ; }
+ else if (state == BRACKETS_INSIDE && r == '[' ) { state = INSIDE_OPENING ; }
+ else if (state == BRACKETS_INSIDE && r == ']' ) { state = OUTSIDE ; }
+ else if (state == OUTSIDE && escape ) { escape = 0 ; }
+ else if (state == OUTSIDE && r == '\\' ) { escape = 1 ; }
+ else if (state == OUTSIDE && r == delim) return s;
+ else if (state == OUTSIDE && do_brackets && r == '[' ) { state = BRACKETS_OPENING; }
+ }
+ return s;
+}
+
+static char *
+chomp(char *s)
+{
+ return chompr(s, 0);
+}
+
+/* eat all leading whitespace and occurrences of rune */
+static char *
+chompr(char *s, Rune rune)
+{
+ Rune r;
+ size_t rlen;
+ char *end = s + strlen(s);
+
+ while (*s && (rlen = echarntorune(&r, s, end - s)) && (isspacerune(r) || r == rune))
+ s += rlen;
+ return s;
+}
+
+/* convert first nrunes Runes from UTF-8 string s in allocated Rune*
+ * NOTE: sequence must be valid UTF-8, check first */
+static Rune *
+strtorunes(char *s, size_t nrunes)
+{
+ Rune *rs, *rp;
+
+ rp = rs = ereallocarray(NULL, nrunes + 1, sizeof(*rs));
+
+ while (nrunes--)
+ s += chartorune(rp++, s);
+
+ *rp = '\0';
+ return rs;
+}
+
+static long
+stol(char *s, char **endp)
+{
+ long n;
+ errno = 0;
+ n = strtol(s, endp, 10);
+
+ if (errno)
+ leprintf("strtol:");
+ if (*endp == s)
+ leprintf("strtol: invalid number");
+
+ return n;
+}
+
+/* from beg to end replace "\\d" with "d" and "\\n" with "\n" (where d is delim)
+ * if delim is 'n' and n_newline is 0 then "\\n" is replaced with "n" (normal)
+ * if delim is 'n' and n_newline is 1 then "\\n" is replaced with "\n" (y command)
+ * if delim is 0 all escaped characters represent themselves (aci text)
+ * memmove rest of string (beyond end) into place
+ * return the number of converted escapes (backslashes removed)
+ * FIXME: this has had too many corner cases slapped on and is ugly. rewrite better
+ */
+static size_t
+escapes(char *beg, char *end, Rune delim, int n_newline)
+{
+ size_t num = 0;
+ char *src = beg, *dst = beg;
+
+ while (src < end) {
+ /* handle escaped backslash specially so we don't think the second
+ * backslash is escaping something */
+ if (*src == '\\' && src[1] == '\\') {
+ *dst++ = *src++;
+ if (delim)
+ *dst++ = *src++;
+ else
+ src++;
+ } else if (*src == '\\' && !delim) {
+ src++;
+ } else if (*src == '\\' && src[1]) {
+ Rune r;
+ size_t rlen;
+ num++;
+ src++;
+ rlen = echarntorune(&r, src, end - src);
+
+ if (r == 'n' && delim == 'n') {
+ *src = n_newline ? '\n' : 'n'; /* src so we can still memmove() */
+ } else if (r == 'n') {
+ *src = '\n';
+ } else if (r != delim) {
+ *dst++ = '\\';
+ num--;
+ }
+
+ memmove(dst, src, rlen);
+ dst += rlen;
+ src += rlen;
+ } else {
+ *dst++ = *src++;
+ }
+ }
+ memmove(dst, src, strlen(src) + 1);
+ return num;
+}
+
+static size_t
+echarntorune(Rune *r, char *s, size_t n)
+{
+ size_t rlen = charntorune(r, s, n);
+ if (!rlen || *r == Runeerror)
+ leprintf("invalid UTF-8");
+ return rlen;
+}
+
+static void
+insert_labels(void)
+{
+ size_t i;
+ Cmd *from, *to;
+
+ while (branches.size) {
+ from = prog + (ptrdiff_t)pop(&branches);
+
+ if (!from->u.label) {/* no label branch to end of script */
+ from->u.jump = pc - 1;
+ } else {
+ for (i = 0; i < labels.size; i++) {
+ to = prog + (ptrdiff_t)labels.data[i];
+ if (!strcmp(from->u.label, to->u.label)) {
+ from->u.jump = to;
+ break;
+ }
+ }
+ if (i == labels.size)
+ leprintf("bad label");
+ }
+ }
+}
+
+/*
+ * Getargs / Freeargs
+ * Read argument from s, return pointer to one past last character of argument
+ */
+
+/* POSIX compliant
+ * i\
+ * foobar
+ *
+ * also allow the following non POSIX compliant
+ * i # empty line
+ * ifoobar
+ * ifoobar\
+ * baz
+ *
+ * FIXME: GNU and busybox discard leading spaces
+ * i foobar
+ * i foobar
+ * ifoobar
+ * are equivalent in GNU and busybox. We don't. Should we?
+ */
+static char *
+get_aci_arg(Cmd *c, char *s)
+{
+ c->u.acir.print = check_puts;
+ c->u.acir.str = (String){ NULL, 0 };
+
+ gflags.aci_cont = !!*s; /* no continue flag if empty string */
+
+ /* neither empty string nor POSIX compliant */
+ if (*s && !(*s == '\\' && !s[1]))
+ aci_append(c, s);
+
+ return s + strlen(s);
+}
+
+static void
+aci_append(Cmd *c, char *s)
+{
+ char *end = s + strlen(s), *p = end;
+
+ gflags.aci_cont = 0;
+ while (--p >= s && *p == '\\')
+ gflags.aci_cont = !gflags.aci_cont;
+
+ if (gflags.aci_cont)
+ *--end = '\n';
+
+ escapes(s, end, 0, 0);
+ stracat(&c->u.acir.str, s);
+}
+
+static void
+free_acir_arg(Cmd *c)
+{
+ free(c->u.acir.str.str);
+}
+
+/* POSIX dictates that label is rest of line, including semicolons, trailing
+ * whitespace, closing braces, etc. and can be limited to 8 bytes
+ *
+ * I allow a semicolon or closing brace to terminate a label name, it's not
+ * POSIX compliant, but it's useful and every sed version I've tried to date
+ * does the same.
+ *
+ * FIXME: POSIX dictates that leading whitespace is ignored but trailing
+ * whitespace is not. This is annoying and we should probably get rid of it.
+ */
+static char *
+get_bt_arg(Cmd *c, char *s)
+{
+ char *p = semicolon_arg(s = chomp(s));
+
+ if (p != s) {
+ c->u.label = estrndup(s, p - s);
+ } else {
+ c->u.label = NULL;
+ }
+
+ push(&branches, (void *)(c - prog));
+
+ return p;
+}
+
+/* POSIX dictates file name is rest of line including semicolons, trailing
+ * whitespace, closing braces, etc. and file name must be preceded by a space
+ *
+ * I allow a semicolon or closing brace to terminate a file name and don't
+ * enforce leading space.
+ *
+ * FIXME: decide whether trailing whitespace should be included and fix
+ * accordingly
+ */
+static char *
+get_r_arg(Cmd *c, char *s)
+{
+ char *p = semicolon_arg(s = chomp(s));
+
+ if (p == s)
+ leprintf("no file name");
+
+ c->u.acir.str.str = estrndup(s, p - s);
+ c->u.acir.print = write_file;
+
+ return p;
+}
+
+/* we allow "\\n" in replacement text to mean "\n" (undefined in POSIX)
+ *
+ * FIXME: allow other escapes in regex and replacement? if so change escapes()
+ */
+static char *
+get_s_arg(Cmd *c, char *s)
+{
+ Rune delim, r;
+ Cmd buf;
+ char *p;
+ int esc, lastre;
+
+ /* s/Find/Replace/Flags */
+
+ /* Find */
+ if (!gflags.s_cont) { /* NOT continuing from literal newline in replacement text */
+ lastre = 0;
+ c->u.s.repl = (String){ NULL, 0 };
+ c->u.s.occurrence = 1;
+ c->u.s.file = NULL;
+ c->u.s.p = 0;
+
+ if (!*s || *s == '\\')
+ leprintf("bad delimiter");
+
+ p = s + strlen(s);
+ s += echarntorune(&delim, s, p - s);
+ c->u.s.delim = delim;
+
+ echarntorune(&r, s, p - s);
+ if (r == delim) /* empty regex */
+ lastre = 1;
+
+ p = find_delim(s, delim, 1);
+ if (!*p)
+ leprintf("missing second delimiter");
+ p -= escapes(s, p, delim, 0);
+ *p = '\0';
+
+ if (lastre) {
+ c->u.s.re = NULL;
+ } else {
+ c->u.s.re = emalloc(sizeof(*c->u.s.re));
+ /* FIXME: different eregcomp that calls fatal */
+ eregcomp(c->u.s.re, s, gflags.E ? REG_EXTENDED : 0);
+ }
+ s = p + runelen(delim);
+ }
+
+ /* Replace */
+ delim = c->u.s.delim;
+
+ p = find_delim(s, delim, 0);
+ p -= escapes(s, p, delim, 0);
+ if (!*p) { /* no third delimiter */
+ /* FIXME: same backslash counting as aci_append() */
+ if (p[-1] != '\\')
+ leprintf("missing third delimiter or <backslash><newline>");
+ p[-1] = '\n';
+ gflags.s_cont = 1;
+ } else {
+ gflags.s_cont = 0;
+ }
+
+ /* check for bad references in replacement text */
+ *p = '\0';
+ for (esc = 0, p = s; *p; p++) {
+ if (esc) {
+ esc = 0;
+ if (isdigit(*p) && c->u.s.re && (size_t)(*p - '0') > c->u.s.re->re_nsub)
+ leprintf("back reference number greater than number of groups");
+ } else if (*p == '\\') {
+ esc = 1;
+ }
+ }
+ stracat(&c->u.s.repl, s);
+
+ if (gflags.s_cont)
+ return p;
+
+ s = p + runelen(delim);
+
+ /* Flags */
+ p = semicolon_arg(s = chomp(s));
+
+ /* FIXME: currently for simplicity take last of g or occurrence flags and
+ * ignore multiple p flags. need to fix that */
+ for (; s < p; s++) {
+ if (isdigit(*s)) {
+ c->u.s.occurrence = stol(s, &s);
+ s--; /* for loop will advance pointer */
+ } else {
+ switch (*s) {
+ case 'g': c->u.s.occurrence = 0; break;
+ case 'p': c->u.s.p = 1; break;
+ case 'w':
+ /* must be last flag, take everything up to newline/semicolon
+ * s == p after this */
+ s = get_w_arg(&buf, chomp(s+1));
+ c->u.s.file = buf.u.file;
+ break;
+ }
+ }
+ }
+ return p;
+}
+
+static void
+free_s_arg(Cmd *c)
+{
+ if (c->u.s.re)
+ regfree(c->u.s.re);
+ free(c->u.s.re);
+ free(c->u.s.repl.str);
+}
+
+/* see get_r_arg notes */
+static char *
+get_w_arg(Cmd *c, char *s)
+{
+ char *p = semicolon_arg(s = chomp(s));
+ Wfile *w, **wp;
+
+ if (p == s)
+ leprintf("no file name");
+
+ for (wp = (Wfile **)wfiles.data; (size_t)(wp - (Wfile **)wfiles.data) < wfiles.size; wp++) {
+ if (strlen((*wp)->path) == (size_t)(p - s) && !strncmp(s, (*wp)->path, p - s)) {
+ c->u.file = (*wp)->file;
+ return p;
+ }
+ }
+
+ w = emalloc(sizeof(*w));
+ w->path = estrndup(s, p - s);
+
+ if (!(w->file = fopen(w->path, "w")))
+ leprintf("fopen failed");
+
+ c->u.file = w->file;
+
+ push(&wfiles, w);
+ return p;
+}
+
+static char *
+get_y_arg(Cmd *c, char *s)
+{
+ Rune delim;
+ char *p = s + strlen(s);
+ size_t rlen = echarntorune(&delim, s, p - s);
+ size_t nrunes1, nrunes2;
+
+ c->u.y.set1 = c->u.y.set2 = NULL;
+
+ s += rlen;
+ p = find_delim(s, delim, 0);
+ p -= escapes(s, p, delim, 1);
+ nrunes1 = utfnlen(s, p - s);
+ c->u.y.set1 = strtorunes(s, nrunes1);
+
+ s = p + rlen;
+ p = find_delim(s, delim, 0);
+ p -= escapes(s, p, delim, 1);
+ nrunes2 = utfnlen(s, p - s);
+
+ if (nrunes1 != nrunes2)
+ leprintf("different set lengths");
+
+ c->u.y.set2 = strtorunes(s, utfnlen(s, p - s));
+
+ return p + rlen;
+}
+
+static void
+free_y_arg(Cmd *c)
+{
+ free(c->u.y.set1);
+ free(c->u.y.set2);
+}
+
+/* see get_bt_arg notes */
+static char *
+get_colon_arg(Cmd *c, char *s)
+{
+ char *p = semicolon_arg(s = chomp(s));
+
+ if (p == s)
+ leprintf("no label name");
+
+ c->u.label = estrndup(s, p - s);
+ push(&labels, (void *)(c - prog));
+ return p;
+}
+
+static char *
+get_lbrace_arg(Cmd *c, char *s)
+{
+ push(&braces, (void *)(c - prog));
+ return s;
+}
+
+static char *
+get_rbrace_arg(Cmd *c, char *s)
+{
+ Cmd *lbrace;
+
+ if (!braces.size)
+ leprintf("extra }");
+
+ lbrace = prog + (ptrdiff_t)pop(&braces);
+ lbrace->u.offset = c - prog;
+ return s;
+}
+
+/* s points to beginning of an argument that may be semicolon terminated
+ * return pointer to semicolon or nul byte after string
+ * or closing brace as to not force ; before }
+ * FIXME: decide whether or not to eat trailing whitespace for arguments that
+ * we allow semicolon/brace termination that POSIX doesn't
+ * b, r, t, w, :
+ * POSIX says trailing whitespace is part of label name, file name, etc.
+ * we should probably eat it
+ */
+static char *
+semicolon_arg(char *s)
+{
+ char *p = strpbrk(s, ";}");
+ if (!p)
+ p = s + strlen(s);
+ return p;
+}
+
+static void
+run(void)
+{
+ lineno = 0;
+ if (braces.size)
+ leprintf("extra {");
+
+ /* genbuf has already been initialized, patt will be in new_line
+ * (or we'll halt) */
+ stracpy(&hold, "");
+
+ insert_labels();
+ next_file();
+ new_line();
+
+ for (pc = prog; !gflags.halt; pc++)
+ pc->fninfo->fn(pc);
+}
+
+/* return true if we are in range for c, set c->in_match appropriately */
+static int
+in_range(Cmd *c)
+{
+ if (match_addr(&c->range.beg)) {
+ if (c->range.naddr == 2) {
+ if (c->range.end.type == LINE && c->range.end.u.lineno <= lineno)
+ c->in_match = 0;
+ else
+ c->in_match = 1;
+ }
+ return !c->negate;
+ }
+ if (c->in_match && match_addr(&c->range.end)) {
+ c->in_match = 0;
+ return !c->negate;
+ }
+ return c->in_match ^ c->negate;
+}
+
+/* return true if addr matches current line */
+static int
+match_addr(Addr *a)
+{
+ switch (a->type) {
+ default:
+ case IGNORE: return 0;
+ case EVERY: return 1;
+ case LINE: return lineno == a->u.lineno;
+ case LAST:
+ while (is_eof(file) && !next_file())
+ ;
+ return !file;
+ case REGEX:
+ lastre = a->u.re;
+ return !regexec(a->u.re, patt.str, 0, NULL, 0);
+ case LASTRE:
+ if (!lastre)
+ leprintf("no previous regex");
+ return !regexec(lastre, patt.str, 0, NULL, 0);
+ }
+}
+
+/* move to next input file
+ * stdin if first call and no files
+ * return 0 for success and 1 for no more files
+ */
+static int
+next_file(void)
+{
+ static unsigned char first = 1;
+
+ if (file == stdin)
+ clearerr(file);
+ else if (file)
+ fshut(file, "<file>");
+ /* given no files, default to stdin */
+ file = first && !*files ? stdin : NULL;
+ first = 0;
+
+ while (!file && *files) {
+ if (!strcmp(*files, "-")) {
+ file = stdin;
+ } else if (!(file = fopen(*files, "r"))) {
+ /* warn this file didn't open, but move on to next */
+ weprintf("fopen %s:", *files);
+ ret = 1;
+ }
+ files++;
+ }
+
+ return !file;
+}
+
+/* test if stream is at EOF */
+static int
+is_eof(FILE *f)
+{
+ int c;
+
+ if (!f || feof(f))
+ return 1;
+
+ c = fgetc(f);
+ if (c == EOF && ferror(f))
+ eprintf("fgetc:");
+ if (c != EOF && ungetc(c, f) == EOF)
+ eprintf("ungetc EOF\n");
+
+ return c == EOF;
+}
+
+/* perform writes that were scheduled
+ * for aci this is check_puts(string, stdout)
+ * for r this is write_file(path, stdout)
+ */
+static void
+do_writes(void)
+{
+ Cmd *c;
+ size_t i;
+
+ for (i = 0; i < writes.size; i++) {
+ c = writes.data[i];
+ c->u.acir.print(c->u.acir.str.str, stdout);
+ }
+ writes.size = 0;
+}
+
+/* used for r's u.acir.print()
+ * FIXME: something like util's concat() would be better
+ */
+static void
+write_file(char *path, FILE *out)
+{
+ FILE *in = fopen(path, "r");
+ if (!in) /* no file is treated as empty file */
+ return;
+
+ while (read_line(in, &genbuf) != EOF)
+ check_puts(genbuf.str, out);
+
+ fshut(in, path);
+}
+
+static void
+check_puts(char *s, FILE *f)
+{
+ if (s && fputs(s, f) == EOF)
+ eprintf("fputs:");
+ if (fputs("\n", f) == EOF)
+ eprintf("fputs:");
+}
+
+/* iterate from beg to end updating ranges so we don't miss any commands
+ * e.g. sed -n '1d;1,3p' should still print lines 2 and 3
+ */
+static void
+update_ranges(Cmd *beg, Cmd *end)
+{
+ while (beg < end)
+ in_range(beg++);
+}
+
+/*
+ * Sed functions
+ */
+static void
+cmd_a(Cmd *c)
+{
+ if (in_range(c))
+ push(&writes, c);
+}
+
+static void
+cmd_b(Cmd *c)
+{
+ if (!in_range(c))
+ return;
+
+ /* if we jump backwards update to end, otherwise update to destination */
+ update_ranges(c + 1, c->u.jump > c ? c->u.jump : prog + pcap);
+ pc = c->u.jump;
+}
+
+static void
+cmd_c(Cmd *c)
+{
+ if (!in_range(c))
+ return;
+
+ /* write the text on the last line of the match */
+ if (!c->in_match)
+ check_puts(c->u.acir.str.str, stdout);
+ /* otherwise start the next cycle without printing pattern space
+ * effectively deleting the text */
+ new_next();
+}
+
+static void
+cmd_d(Cmd *c)
+{
+ if (!in_range(c))
+ return;
+
+ new_next();
+}
+
+static void
+cmd_D(Cmd *c)
+{
+ char *p;
+
+ if (!in_range(c))
+ return;
+
+ if ((p = strchr(patt.str, '\n'))) {
+ p++;
+ memmove(patt.str, p, strlen(p) + 1);
+ old_next();
+ } else {
+ new_next();
+ }
+}
+
+static void
+cmd_g(Cmd *c)
+{
+ if (in_range(c))
+ stracpy(&patt, hold.str);
+}
+
+static void
+cmd_G(Cmd *c)
+{
+ if (!in_range(c))
+ return;
+
+ stracat(&patt, "\n");
+ stracat(&patt, hold.str);
+}
+
+static void
+cmd_h(Cmd *c)
+{
+ if (in_range(c))
+ stracpy(&hold, patt.str);
+}
+
+static void
+cmd_H(Cmd *c)
+{
+ if (!in_range(c))
+ return;
+
+ stracat(&hold, "\n");
+ stracat(&hold, patt.str);
+}
+
+static void
+cmd_i(Cmd *c)
+{
+ if (in_range(c))
+ check_puts(c->u.acir.str.str, stdout);
+}
+
+/* I think it makes sense to print invalid UTF-8 sequences in octal to satisfy
+ * the "visually unambiguous form" sed(1p)
+ */
+static void
+cmd_l(Cmd *c)
+{
+ Rune r;
+ char *p, *end;
+ size_t rlen;
+
+ char *escapes[] = { /* FIXME: 7 entries and search instead of 127 */
+ ['\\'] = "\\\\", ['\a'] = "\\a", ['\b'] = "\\b",
+ ['\f'] = "\\f" , ['\r'] = "\\r", ['\t'] = "\\t",
+ ['\v'] = "\\v" , [0x7f] = NULL, /* fill out the table */
+ };
+
+ if (!in_range(c))
+ return;
+
+ /* FIXME: line wrapping. sed(1p) says "length at which folding occurs is
+ * unspecified, but should be appropraite for the output device"
+ * just wrap at 80 Runes?
+ */
+ for (p = patt.str, end = p + strlen(p); p < end; p += rlen) {
+ if (isascii(*p) && escapes[(unsigned int)*p]) {
+ fputs(escapes[(unsigned int)*p], stdout);
+ rlen = 1;
+ } else if (!(rlen = charntorune(&r, p, end - p))) {
+ /* ran out of chars, print the bytes of the short sequence */
+ for (; p < end; p++)
+ printf("\\%03hho", (unsigned char)*p);
+ break;
+ } else if (r == Runeerror) {
+ for (; rlen; rlen--, p++)
+ printf("\\%03hho", (unsigned char)*p);
+ } else {
+ while (fwrite(p, rlen, 1, stdout) < 1 && errno == EINTR)
+ ;
+ if (ferror(stdout))
+ eprintf("fwrite:");
+ }
+ }
+ check_puts("$", stdout);
+}
+
+static void
+cmd_n(Cmd *c)
+{
+ if (!in_range(c))
+ return;
+
+ if (!gflags.n)
+ check_puts(patt.str, stdout);
+ do_writes();
+ new_line();
+}
+
+static void
+cmd_N(Cmd *c)
+{
+ if (!in_range(c))
+ return;
+ do_writes();
+ app_line();
+}
+
+static void
+cmd_p(Cmd *c)
+{
+ if (in_range(c))
+ check_puts(patt.str, stdout);
+}
+
+static void
+cmd_P(Cmd *c)
+{
+ char *p;
+
+ if (!in_range(c))
+ return;
+
+ if ((p = strchr(patt.str, '\n')))
+ *p = '\0';
+
+ check_puts(patt.str, stdout);
+
+ if (p)
+ *p = '\n';
+}
+
+static void
+cmd_q(Cmd *c)
+{
+ if (!in_range(c))
+ return;
+
+ if (!gflags.n)
+ check_puts(patt.str, stdout);
+ do_writes();
+ gflags.halt = 1;
+}
+
+static void
+cmd_r(Cmd *c)
+{
+ if (in_range(c))
+ push(&writes, c);
+}
+
+static void
+cmd_s(Cmd *c)
+{
+ String tmp;
+ Rune r;
+ size_t plen, rlen, len;
+ char *p, *s, *end;
+ unsigned int matches = 0, last_empty = 1, qflag = 0, cflags = 0;
+ regex_t *re;
+ regmatch_t *rm, *pmatch = NULL;
+
+ if (!in_range(c))
+ return;
+
+ if (!c->u.s.re && !lastre)
+ leprintf("no previous regex");
+
+ re = c->u.s.re ? c->u.s.re : lastre;
+ lastre = re;
+
+ plen = re->re_nsub + 1;
+ pmatch = ereallocarray(NULL, plen, sizeof(regmatch_t));
+
+ *genbuf.str = '\0';
+ s = patt.str;
+
+ while (!qflag && !regexec(re, s, plen, pmatch, cflags)) {
+ cflags = REG_NOTBOL; /* match against beginning of line first time, but not again */
+ if (!*s) /* match against empty string first time, but not again */
+ qflag = 1;
+
+ /* don't substitute if last match was not empty but this one is.
+ * s_a*_._g
+ * foobar -> .f.o.o.b.r.
+ */
+ if ((last_empty || pmatch[0].rm_eo) &&
+ (++matches == c->u.s.occurrence || !c->u.s.occurrence)) {
+ /* copy over everything before the match */
+ strnacat(&genbuf, s, pmatch[0].rm_so);
+
+ /* copy over replacement text, taking into account &, backreferences, and \ escapes */
+ for (p = c->u.s.repl.str, len = strcspn(p, "\\&"); *p; len = strcspn(++p, "\\&")) {
+ strnacat(&genbuf, p, len);
+ p += len;
+ switch (*p) {
+ default: leprintf("this shouldn't be possible");
+ case '\0':
+ /* we're at the end, back up one so the ++p will put us on
+ * the null byte to break out of the loop */
+ --p;
+ break;
+ case '&':
+ strnacat(&genbuf, s + pmatch[0].rm_so, pmatch[0].rm_eo - pmatch[0].rm_so);
+ break;
+ case '\\':
+ if (isdigit(*++p)) { /* backreference */
+ /* only need to check here if using lastre, otherwise we checked when building */
+ if (!c->u.s.re && (size_t)(*p - '0') > re->re_nsub)
+ leprintf("back reference number greater than number of groups");
+ rm = &pmatch[*p - '0'];
+ strnacat(&genbuf, s + rm->rm_so, rm->rm_eo - rm->rm_so);
+ } else { /* character after backslash taken literally (well one byte, but it works) */
+ strnacat(&genbuf, p, 1);
+ }
+ break;
+ }
+ }
+ } else {
+ /* not replacing, copy over everything up to and including the match */
+ strnacat(&genbuf, s, pmatch[0].rm_eo);
+ }
+
+ if (!pmatch[0].rm_eo) { /* empty match, advance one rune and add it to output */
+ end = s + strlen(s);
+ rlen = charntorune(&r, s, end - s);
+
+ if (!rlen) { /* ran out of bytes, copy short sequence */
+ stracat(&genbuf, s);
+ s = end;
+ } else { /* copy whether or not it's a good rune */
+ strnacat(&genbuf, s, rlen);
+ s += rlen;
+ }
+ }
+ last_empty = !pmatch[0].rm_eo;
+ s += pmatch[0].rm_eo;
+ }
+ free(pmatch);
+
+ if (!(matches && matches >= c->u.s.occurrence)) /* no replacement */
+ return;
+
+ gflags.s = 1;
+
+ stracat(&genbuf, s);
+
+ tmp = patt;
+ patt = genbuf;
+ genbuf = tmp;
+
+ if (c->u.s.p)
+ check_puts(patt.str, stdout);
+ if (c->u.s.file)
+ check_puts(patt.str, c->u.s.file);
+}
+
+static void
+cmd_t(Cmd *c)
+{
+ if (!in_range(c) || !gflags.s)
+ return;
+
+ /* if we jump backwards update to end, otherwise update to destination */
+ update_ranges(c + 1, c->u.jump > c ? c->u.jump : prog + pcap);
+ pc = c->u.jump;
+ gflags.s = 0;
+}
+
+static void
+cmd_w(Cmd *c)
+{
+ if (in_range(c))
+ check_puts(patt.str, c->u.file);
+}
+
+static void
+cmd_x(Cmd *c)
+{
+ String tmp;
+
+ if (!in_range(c))
+ return;
+
+ tmp = patt;
+ patt = hold;
+ hold = tmp;
+}
+
+static void
+cmd_y(Cmd *c)
+{
+ String tmp;
+ Rune r, *rp;
+ size_t n, rlen;
+ char *s, *end, buf[UTFmax];
+
+ if (!in_range(c))
+ return;
+
+ *genbuf.str = '\0';
+ for (s = patt.str, end = s + strlen(s); *s; s += rlen) {
+ if (!(rlen = charntorune(&r, s, end - s))) { /* ran out of chars, copy rest */
+ stracat(&genbuf, s);
+ break;
+ } else if (r == Runeerror) { /* bad UTF-8 sequence, copy bytes */
+ strnacat(&genbuf, s, rlen);
+ } else {
+ for (rp = c->u.y.set1; *rp; rp++)
+ if (*rp == r)
+ break;
+ if (*rp) { /* found r in set1, replace with Rune from set2 */
+ n = runetochar(buf, c->u.y.set2 + (rp - c->u.y.set1));
+ strnacat(&genbuf, buf, n);
+ } else {
+ strnacat(&genbuf, s, rlen);
+ }
+ }
+ }
+ tmp = patt;
+ patt = genbuf;
+ genbuf = tmp;
+}
+
+static void
+cmd_colon(Cmd *c)
+{
+}
+
+static void
+cmd_equal(Cmd *c)
+{
+ if (in_range(c))
+ printf("%zu\n", lineno);
+}
+
+static void
+cmd_lbrace(Cmd *c)
+{
+ Cmd *jump;
+
+ if (in_range(c))
+ return;
+
+ /* update ranges on all commands we skip */
+ jump = prog + c->u.offset;
+ update_ranges(c + 1, jump);
+ pc = jump;
+}
+
+static void
+cmd_rbrace(Cmd *c)
+{
+}
+
+/* not actually a sed function, but acts like one, put in last spot of script */
+static void
+cmd_last(Cmd *c)
+{
+ if (!gflags.n)
+ check_puts(patt.str, stdout);
+ do_writes();
+ new_next();
+}
+
+/*
+ * Actions
+ */
+
+/* read new line, continue current cycle */
+static void
+new_line(void)
+{
+ while (read_line(file, &patt) == EOF) {
+ if (next_file()) {
+ gflags.halt = 1;
+ return;
+ }
+ }
+ gflags.s = 0;
+ lineno++;
+}
+
+/* append new line, continue current cycle
+ * FIXME: used for N, POSIX specifies do not print pattern space when out of
+ * input, but GNU does so busybox does as well. Currently we don't.
+ * Should we?
+ */
+static void
+app_line(void)
+{
+ while (read_line(file, &genbuf) == EOF) {
+ if (next_file()) {
+ gflags.halt = 1;
+ return;
+ }
+ }
+
+ stracat(&patt, "\n");
+ stracat(&patt, genbuf.str);
+ gflags.s = 0;
+ lineno++;
+}
+
+/* read new line, start new cycle */
+static void
+new_next(void)
+{
+ *patt.str = '\0';
+ update_ranges(pc + 1, prog + pcap);
+ new_line();
+ pc = prog - 1;
+}
+
+/* keep old pattern space, start new cycle */
+static void
+old_next(void)
+{
+ update_ranges(pc + 1, prog + pcap);
+ pc = prog - 1;
+}
+
+int
+main(int argc, char *argv[])
+{
+ char *arg;
+ int script = 0;
+
+ ARGBEGIN {
+ case 'n':
+ gflags.n = 1;
+ break;
+ case 'r':
+ case 'E':
+ gflags.E = 1;
+ break;
+ case 'e':
+ arg = EARGF(usage());
+ compile(arg, 0);
+ script = 1;
+ break;
+ case 'f':
+ arg = EARGF(usage());
+ compile(arg, 1);
+ script = 1;
+ break;
+ default : usage();
+ } ARGEND
+
+ /* no script to run */
+ if (!script && !argc)
+ usage();
+
+ /* no script yet, next argument is script */
+ if (!script)
+ compile(*argv++, 0);
+
+ /* shrink/grow memory to fit and add our last instruction */
+ resize((void **)&prog, &pcap, sizeof(*prog), pc - prog + 1, NULL);
+ pc = prog + pcap - 1;
+ pc->fninfo = &(Fninfo){ cmd_last, NULL, NULL, 0 };
+
+ files = argv;
+ run();
+
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/seq.1 b/util/sbase/seq.1
new file mode 100644
index 00000000..1b9def89
--- /dev/null
+++ b/util/sbase/seq.1
@@ -0,0 +1,40 @@
+.Dd October 8, 2015
+.Dt SEQ 1
+.Os sbase
+.Sh NAME
+.Nm seq
+.Nd print a sequence of numbers
+.Sh SYNOPSIS
+.Nm
+.Op Fl w
+.Op Fl f Ar fmt
+.Op Fl s Ar sep
+.Op Ar startnum Op Ar step
+.Ar endnum
+.Sh DESCRIPTION
+.Nm
+writes a sequence of numbers from
+.Ar startnum
+(default: 1) to
+.Ar endnum
+in
+.Ar step
+intervals (default: 1)
+to stdout.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl f Ar fmt
+Use
+.Ar fmt
+as the output line format according to
+.Xr printf 3 .
+.It Fl s Ar sep
+Print
+.Ar sep
+between output lines.
+The default is "\en".
+.It Fl w
+Print out lines in equal width.
+.El
+.Sh SEE ALSO
+.Xr printf 3
diff --git a/util/sbase/seq.c b/util/sbase/seq.c
new file mode 100644
index 00000000..70763d1e
--- /dev/null
+++ b/util/sbase/seq.c
@@ -0,0 +1,147 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "util.h"
+
+static int
+digitsleft(const char *d)
+{
+ int shift;
+ char *exp;
+
+ if (*d == '+')
+ d++;
+ exp = strpbrk(d, "eE");
+ shift = exp ? estrtonum(exp + 1, INT_MIN, INT_MAX) : 0;
+
+ return MAX(0, strspn(d, "-0123456789") + shift);
+}
+
+static int
+digitsright(const char *d)
+{
+ int shift, after;
+ char *exp;
+
+ exp = strpbrk(d, "eE");
+ shift = exp ? estrtonum(&exp[1], INT_MIN, INT_MAX) : 0;
+ after = (d = strchr(d, '.')) ? strspn(&d[1], "0123456789") : 0;
+
+ return MAX(0, after - shift);
+}
+
+static int
+validfmt(const char *fmt)
+{
+ int occur = 0;
+
+literal:
+ while (*fmt)
+ if (*fmt++ == '%')
+ goto format;
+ return occur == 1;
+
+format:
+ if (*fmt == '%') {
+ fmt++;
+ goto literal;
+ }
+ fmt += strspn(fmt, "-+#0 '");
+ fmt += strspn(fmt, "0123456789");
+ if (*fmt == '.') {
+ fmt++;
+ fmt += strspn(fmt, "0123456789");
+ }
+ if (*fmt == 'L')
+ fmt++;
+
+ switch (*fmt) {
+ case 'f': case 'F':
+ case 'g': case 'G':
+ case 'e': case 'E':
+ case 'a': case 'A':
+ occur++;
+ goto literal;
+ default:
+ return 0;
+ }
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-f fmt] [-s sep] [-w] "
+ "[startnum [step]] endnum\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ double start, step, end, out, dir;
+ int wflag = 0, left, right;
+ char *tmp, ftmp[BUFSIZ], *fmt = ftmp;
+ const char *starts = "1", *steps = "1", *ends = "1", *sep = "\n";
+
+ ARGBEGIN {
+ case 'f':
+ if (!validfmt(tmp=EARGF(usage())))
+ eprintf("%s: invalid format\n", tmp);
+ fmt = tmp;
+ break;
+ case 's':
+ sep = EARGF(usage());
+ break;
+ case 'w':
+ wflag = 1;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ switch (argc) {
+ case 3:
+ steps = argv[1];
+ argv[1] = argv[2];
+ /* fallthrough */
+ case 2:
+ starts = argv[0];
+ argv++;
+ /* fallthrough */
+ case 1:
+ ends = argv[0];
+ break;
+ default:
+ usage();
+ }
+ start = estrtod(starts);
+ step = estrtod(steps);
+ end = estrtod(ends);
+
+ dir = (step > 0) ? 1.0 : -1.0;
+ if (step == 0 || start * dir > end * dir)
+ return 1;
+
+ if (fmt == ftmp) {
+ right = MAX(digitsright(starts),
+ MAX(digitsright(ends),
+ digitsright(steps)));
+
+ if (wflag) {
+ left = MAX(digitsleft(starts), digitsleft(ends));
+
+ snprintf(ftmp, sizeof ftmp, "%%0%d.%df",
+ right + left + (right != 0), right);
+ } else
+ snprintf(ftmp, sizeof ftmp, "%%.%df", right);
+ }
+ for (out = start; out * dir <= end * dir; out += step) {
+ if (out != start)
+ fputs(sep, stdout);
+ printf(fmt, out);
+ }
+ putchar('\n');
+
+ return fshut(stdout, "<stdout>");
+}
diff --git a/util/sbase/setsid.1 b/util/sbase/setsid.1
new file mode 100644
index 00000000..5a7b2412
--- /dev/null
+++ b/util/sbase/setsid.1
@@ -0,0 +1,18 @@
+.Dd July 14, 2020
+.Dt SETSID 1
+.Os sbase
+.Sh NAME
+.Nm setsid
+.Nd run a command in a new session
+.Sh SYNOPSIS
+.Nm
+.Op Fl f
+.Ar cmd
+.Op Ar arg ...
+.Sh DESCRIPTION
+.Nm
+runs
+.Ar cmd
+in a new session.
+.Sh SEE ALSO
+.Xr setsid 2
diff --git a/util/sbase/setsid.c b/util/sbase/setsid.c
new file mode 100644
index 00000000..9a154d13
--- /dev/null
+++ b/util/sbase/setsid.c
@@ -0,0 +1,48 @@
+/* See LICENSE file for copyright and license details. */
+#include <errno.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static int fflag = 0;
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-f] cmd [arg ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int savederrno;
+
+ ARGBEGIN {
+ case 'f':
+ fflag = 1;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc)
+ usage();
+
+ if (fflag || getpgrp() == getpid()) {
+ switch (fork()) {
+ case -1:
+ eprintf("fork:");
+ case 0:
+ break;
+ default:
+ return 0;
+ }
+ }
+ if (setsid() < 0)
+ eprintf("setsid:");
+ execvp(argv[0], argv);
+ savederrno = errno;
+ weprintf("execvp %s:", argv[0]);
+
+ _exit(126 + (savederrno == ENOENT));
+}
diff --git a/util/sbase/sha1.h b/util/sbase/sha1.h
new file mode 100644
index 00000000..86317770
--- /dev/null
+++ b/util/sbase/sha1.h
@@ -0,0 +1,18 @@
+/* public domain sha1 implementation based on rfc3174 and libtomcrypt */
+
+struct sha1 {
+ uint64_t len; /* processed message length */
+ uint32_t h[5]; /* hash state */
+ uint8_t buf[64]; /* message block buffer */
+};
+
+enum { SHA1_DIGEST_LENGTH = 20 };
+
+/* reset state */
+void sha1_init(void *ctx);
+/* process message */
+void sha1_update(void *ctx, const void *m, unsigned long len);
+/* get message digest */
+/* state is ruined after sum, keep a copy if multiple sum is needed */
+/* part of the message might be left in s, zero it if secrecy is needed */
+void sha1_sum(void *ctx, uint8_t md[SHA1_DIGEST_LENGTH]);
diff --git a/util/sbase/sha1sum.1 b/util/sbase/sha1sum.1
new file mode 100644
index 00000000..62187135
--- /dev/null
+++ b/util/sbase/sha1sum.1
@@ -0,0 +1,32 @@
+.Dd October 8, 2015
+.Dt SHA1SUM 1
+.Os sbase
+.Sh NAME
+.Nm sha1sum
+.Nd compute or check SHA-1 message digests
+.Sh SYNOPSIS
+.Nm
+.Op Fl c
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+writes SHA-1 (160-bit) checksums of each
+.Ar file
+to stdout.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl c
+Read list of SHA-1 checksums from each
+.Ar file
+and check them.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.El
diff --git a/util/sbase/sha1sum.c b/util/sbase/sha1sum.c
new file mode 100644
index 00000000..cc8dcae9
--- /dev/null
+++ b/util/sbase/sha1sum.c
@@ -0,0 +1,45 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdint.h>
+#include <stdio.h>
+
+#include "crypt.h"
+#include "sha1.h"
+#include "util.h"
+
+static struct sha1 s;
+struct crypt_ops sha1_ops = {
+ sha1_init,
+ sha1_update,
+ sha1_sum,
+ &s,
+};
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-c] [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int ret = 0, (*cryptfunc)(int, char **, struct crypt_ops *, uint8_t *, size_t) = cryptmain;
+ uint8_t md[SHA1_DIGEST_LENGTH];
+
+ ARGBEGIN {
+ case 'b':
+ case 't':
+ /* ignore */
+ break;
+ case 'c':
+ cryptfunc = cryptcheck;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ ret |= cryptfunc(argc, argv, &sha1_ops, md, sizeof(md));
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/sha224.h b/util/sbase/sha224.h
new file mode 100644
index 00000000..d7f40532
--- /dev/null
+++ b/util/sbase/sha224.h
@@ -0,0 +1,16 @@
+/* public domain sha224 implementation based on fips180-3 */
+
+#include "sha256.h"
+
+#define sha224 sha256 /*struct*/
+
+enum { SHA224_DIGEST_LENGTH = 28 };
+
+/* reset state */
+void sha224_init(void *ctx);
+/* process message */
+#define sha224_update sha256_update
+/* get message digest */
+/* state is ruined after sum, keep a copy if multiple sum is needed */
+/* part of the message might be left in s, zero it if secrecy is needed */
+void sha224_sum(void *ctx, uint8_t md[SHA224_DIGEST_LENGTH]);
diff --git a/util/sbase/sha224sum.1 b/util/sbase/sha224sum.1
new file mode 100644
index 00000000..42141a5f
--- /dev/null
+++ b/util/sbase/sha224sum.1
@@ -0,0 +1,32 @@
+.Dd February 24, 2016
+.Dt SHA224SUM 1
+.Os sbase
+.Sh NAME
+.Nm sha224sum
+.Nd compute or check SHA-224 message digests
+.Sh SYNOPSIS
+.Nm
+.Op Fl c
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+writes SHA-224 (224-bit) checksums of each
+.Ar file
+to stdout.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl c
+Read list of SHA-224 checksums from each
+.Ar file
+and check them.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.El
diff --git a/util/sbase/sha224sum.c b/util/sbase/sha224sum.c
new file mode 100644
index 00000000..e9a10cf9
--- /dev/null
+++ b/util/sbase/sha224sum.c
@@ -0,0 +1,45 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdint.h>
+#include <stdio.h>
+
+#include "crypt.h"
+#include "sha224.h"
+#include "util.h"
+
+static struct sha224 s;
+struct crypt_ops sha224_ops = {
+ sha224_init,
+ sha224_update,
+ sha224_sum,
+ &s,
+};
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-c] [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int ret = 0, (*cryptfunc)(int, char **, struct crypt_ops *, uint8_t *, size_t) = cryptmain;
+ uint8_t md[SHA224_DIGEST_LENGTH];
+
+ ARGBEGIN {
+ case 'b':
+ case 't':
+ /* ignore */
+ break;
+ case 'c':
+ cryptfunc = cryptcheck;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ ret |= cryptfunc(argc, argv, &sha224_ops, md, sizeof(md));
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/sha256.h b/util/sbase/sha256.h
new file mode 100644
index 00000000..5968b8e1
--- /dev/null
+++ b/util/sbase/sha256.h
@@ -0,0 +1,18 @@
+/* public domain sha256 implementation based on fips180-3 */
+
+struct sha256 {
+ uint64_t len; /* processed message length */
+ uint32_t h[8]; /* hash state */
+ uint8_t buf[64]; /* message block buffer */
+};
+
+enum { SHA256_DIGEST_LENGTH = 32 };
+
+/* reset state */
+void sha256_init(void *ctx);
+/* process message */
+void sha256_update(void *ctx, const void *m, unsigned long len);
+/* get message digest */
+/* state is ruined after sum, keep a copy if multiple sum is needed */
+/* part of the message might be left in s, zero it if secrecy is needed */
+void sha256_sum(void *ctx, uint8_t md[SHA256_DIGEST_LENGTH]);
diff --git a/util/sbase/sha256sum.1 b/util/sbase/sha256sum.1
new file mode 100644
index 00000000..1a9aeee9
--- /dev/null
+++ b/util/sbase/sha256sum.1
@@ -0,0 +1,32 @@
+.Dd October 8, 2015
+.Dt SHA256SUM 1
+.Os sbase
+.Sh NAME
+.Nm sha256sum
+.Nd compute or check SHA-256 message digests
+.Sh SYNOPSIS
+.Nm
+.Op Fl c
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+writes SHA-256 (256-bit) checksums of each
+.Ar file
+to stdout.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl c
+Read list of SHA-256 checksums from each
+.Ar file
+and check them.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.El
diff --git a/util/sbase/sha256sum.c b/util/sbase/sha256sum.c
new file mode 100644
index 00000000..686c70f0
--- /dev/null
+++ b/util/sbase/sha256sum.c
@@ -0,0 +1,45 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdint.h>
+#include <stdio.h>
+
+#include "crypt.h"
+#include "sha256.h"
+#include "util.h"
+
+static struct sha256 s;
+struct crypt_ops sha256_ops = {
+ sha256_init,
+ sha256_update,
+ sha256_sum,
+ &s,
+};
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-c] [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int ret = 0, (*cryptfunc)(int, char **, struct crypt_ops *, uint8_t *, size_t) = cryptmain;
+ uint8_t md[SHA256_DIGEST_LENGTH];
+
+ ARGBEGIN {
+ case 'b':
+ case 't':
+ /* ignore */
+ break;
+ case 'c':
+ cryptfunc = cryptcheck;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ ret |= cryptfunc(argc, argv, &sha256_ops, md, sizeof(md));
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/sha384.h b/util/sbase/sha384.h
new file mode 100644
index 00000000..2ab9bc49
--- /dev/null
+++ b/util/sbase/sha384.h
@@ -0,0 +1,16 @@
+/* public domain sha512 implementation based on fips180-3 */
+
+#include "sha512.h"
+
+#define sha384 sha512 /*struct*/
+
+enum { SHA384_DIGEST_LENGTH = 48 };
+
+/* reset state */
+void sha384_init(void *ctx);
+/* process message */
+#define sha384_update sha512_update
+/* get message digest */
+/* state is ruined after sum, keep a copy if multiple sum is needed */
+/* part of the message might be left in s, zero it if secrecy is needed */
+void sha384_sum(void *ctx, uint8_t md[SHA384_DIGEST_LENGTH]);
diff --git a/util/sbase/sha384sum.1 b/util/sbase/sha384sum.1
new file mode 100644
index 00000000..a417ca96
--- /dev/null
+++ b/util/sbase/sha384sum.1
@@ -0,0 +1,32 @@
+.Dd February 24, 2016
+.Dt SHA384SUM 1
+.Os sbase
+.Sh NAME
+.Nm sha384sum
+.Nd compute or check SHA-384 message digests
+.Sh SYNOPSIS
+.Nm
+.Op Fl c
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+writes SHA-384 (384-bit) checksums of each
+.Ar file
+to stdout.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl c
+Read list of SHA-384 checksums from each
+.Ar file
+and check them.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.El
diff --git a/util/sbase/sha384sum.c b/util/sbase/sha384sum.c
new file mode 100644
index 00000000..c76947e6
--- /dev/null
+++ b/util/sbase/sha384sum.c
@@ -0,0 +1,45 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdint.h>
+#include <stdio.h>
+
+#include "crypt.h"
+#include "sha384.h"
+#include "util.h"
+
+static struct sha384 s;
+struct crypt_ops sha384_ops = {
+ sha384_init,
+ sha384_update,
+ sha384_sum,
+ &s,
+};
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-c] [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int ret = 0, (*cryptfunc)(int, char **, struct crypt_ops *, uint8_t *, size_t) = cryptmain;
+ uint8_t md[SHA384_DIGEST_LENGTH];
+
+ ARGBEGIN {
+ case 'b':
+ case 't':
+ /* ignore */
+ break;
+ case 'c':
+ cryptfunc = cryptcheck;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ ret |= cryptfunc(argc, argv, &sha384_ops, md, sizeof(md));
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/sha512-224.h b/util/sbase/sha512-224.h
new file mode 100644
index 00000000..8364fc5f
--- /dev/null
+++ b/util/sbase/sha512-224.h
@@ -0,0 +1,16 @@
+/* public domain sha512/224 implementation based on fips180-3 */
+
+#include "sha512.h"
+
+#define sha512_224 sha512 /*struct*/
+
+enum { SHA512_224_DIGEST_LENGTH = 28 };
+
+/* reset state */
+void sha512_224_init(void *ctx);
+/* process message */
+#define sha512_224_update sha512_update
+/* get message digest */
+/* state is ruined after sum, keep a copy if multiple sum is needed */
+/* part of the message might be left in s, zero it if secrecy is needed */
+void sha512_224_sum(void *ctx, uint8_t md[SHA512_224_DIGEST_LENGTH]);
diff --git a/util/sbase/sha512-224sum.1 b/util/sbase/sha512-224sum.1
new file mode 100644
index 00000000..89206950
--- /dev/null
+++ b/util/sbase/sha512-224sum.1
@@ -0,0 +1,32 @@
+.Dd February 24, 2016
+.Dt SHA512-224SUM 1
+.Os sbase
+.Sh NAME
+.Nm sha512-224sum
+.Nd compute or check SHA-512/224 message digests
+.Sh SYNOPSIS
+.Nm
+.Op Fl c
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+writes SHA-512/224 (224-bit) checksums of each
+.Ar file
+to stdout.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl c
+Read list of SHA-512/224 checksums from each
+.Ar file
+and check them.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.El
diff --git a/util/sbase/sha512-224sum.c b/util/sbase/sha512-224sum.c
new file mode 100644
index 00000000..53f2e625
--- /dev/null
+++ b/util/sbase/sha512-224sum.c
@@ -0,0 +1,45 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdint.h>
+#include <stdio.h>
+
+#include "crypt.h"
+#include "sha512-224.h"
+#include "util.h"
+
+static struct sha512_224 s;
+struct crypt_ops sha512_224_ops = {
+ sha512_224_init,
+ sha512_224_update,
+ sha512_224_sum,
+ &s,
+};
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-c] [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int ret = 0, (*cryptfunc)(int, char **, struct crypt_ops *, uint8_t *, size_t) = cryptmain;
+ uint8_t md[SHA512_224_DIGEST_LENGTH];
+
+ ARGBEGIN {
+ case 'b':
+ case 't':
+ /* ignore */
+ break;
+ case 'c':
+ cryptfunc = cryptcheck;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ ret |= cryptfunc(argc, argv, &sha512_224_ops, md, sizeof(md));
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/sha512-256.h b/util/sbase/sha512-256.h
new file mode 100644
index 00000000..eb0b731d
--- /dev/null
+++ b/util/sbase/sha512-256.h
@@ -0,0 +1,16 @@
+/* public domain sha512/256 implementation based on fips180-3 */
+
+#include "sha512.h"
+
+#define sha512_256 sha512 /*struct*/
+
+enum { SHA512_256_DIGEST_LENGTH = 32 };
+
+/* reset state */
+void sha512_256_init(void *ctx);
+/* process message */
+#define sha512_256_update sha512_update
+/* get message digest */
+/* state is ruined after sum, keep a copy if multiple sum is needed */
+/* part of the message might be left in s, zero it if secrecy is needed */
+void sha512_256_sum(void *ctx, uint8_t md[SHA512_256_DIGEST_LENGTH]);
diff --git a/util/sbase/sha512-256sum.1 b/util/sbase/sha512-256sum.1
new file mode 100644
index 00000000..98b8a098
--- /dev/null
+++ b/util/sbase/sha512-256sum.1
@@ -0,0 +1,32 @@
+.Dd February 24, 2016
+.Dt SHA512-256SUM 1
+.Os sbase
+.Sh NAME
+.Nm sha512-256sum
+.Nd compute or check SHA-512/256 message digests
+.Sh SYNOPSIS
+.Nm
+.Op Fl c
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+writes SHA-512/256 (256-bit) checksums of each
+.Ar file
+to stdout.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl c
+Read list of SHA-512/256 checksums from each
+.Ar file
+and check them.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.El
diff --git a/util/sbase/sha512-256sum.c b/util/sbase/sha512-256sum.c
new file mode 100644
index 00000000..ea556b8d
--- /dev/null
+++ b/util/sbase/sha512-256sum.c
@@ -0,0 +1,45 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdint.h>
+#include <stdio.h>
+
+#include "crypt.h"
+#include "sha512-256.h"
+#include "util.h"
+
+static struct sha512_256 s;
+struct crypt_ops sha512_256_ops = {
+ sha512_256_init,
+ sha512_256_update,
+ sha512_256_sum,
+ &s,
+};
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-c] [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int ret = 0, (*cryptfunc)(int, char **, struct crypt_ops *, uint8_t *, size_t) = cryptmain;
+ uint8_t md[SHA512_256_DIGEST_LENGTH];
+
+ ARGBEGIN {
+ case 'b':
+ case 't':
+ /* ignore */
+ break;
+ case 'c':
+ cryptfunc = cryptcheck;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ ret |= cryptfunc(argc, argv, &sha512_256_ops, md, sizeof(md));
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/sha512.h b/util/sbase/sha512.h
new file mode 100644
index 00000000..c761712a
--- /dev/null
+++ b/util/sbase/sha512.h
@@ -0,0 +1,18 @@
+/* public domain sha512 implementation based on fips180-3 */
+
+struct sha512 {
+ uint64_t len; /* processed message length */
+ uint64_t h[8]; /* hash state */
+ uint8_t buf[128]; /* message block buffer */
+};
+
+enum { SHA512_DIGEST_LENGTH = 64 };
+
+/* reset state */
+void sha512_init(void *ctx);
+/* process message */
+void sha512_update(void *ctx, const void *m, unsigned long len);
+/* get message digest */
+/* state is ruined after sum, keep a copy if multiple sum is needed */
+/* part of the message might be left in s, zero it if secrecy is needed */
+void sha512_sum(void *ctx, uint8_t md[SHA512_DIGEST_LENGTH]);
diff --git a/util/sbase/sha512sum.1 b/util/sbase/sha512sum.1
new file mode 100644
index 00000000..14ef105f
--- /dev/null
+++ b/util/sbase/sha512sum.1
@@ -0,0 +1,32 @@
+.Dd October 8, 2015
+.Dt SHA512SUM 1
+.Os sbase
+.Sh NAME
+.Nm sha512sum
+.Nd compute or check SHA-512 message digests
+.Sh SYNOPSIS
+.Nm
+.Op Fl c
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+writes SHA-512 (512-bit) checksums of each
+.Ar file
+to stdout.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl c
+Read list of SHA-512 checksums from each
+.Ar file
+and check them.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.El
diff --git a/util/sbase/sha512sum.c b/util/sbase/sha512sum.c
new file mode 100644
index 00000000..a76e685b
--- /dev/null
+++ b/util/sbase/sha512sum.c
@@ -0,0 +1,45 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdint.h>
+#include <stdio.h>
+
+#include "crypt.h"
+#include "sha512.h"
+#include "util.h"
+
+static struct sha512 s;
+struct crypt_ops sha512_ops = {
+ sha512_init,
+ sha512_update,
+ sha512_sum,
+ &s,
+};
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-c] [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int ret = 0, (*cryptfunc)(int, char **, struct crypt_ops *, uint8_t *, size_t) = cryptmain;
+ uint8_t md[SHA512_DIGEST_LENGTH];
+
+ ARGBEGIN {
+ case 'b':
+ case 't':
+ /* ignore */
+ break;
+ case 'c':
+ cryptfunc = cryptcheck;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ ret |= cryptfunc(argc, argv, &sha512_ops, md, sizeof(md));
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/sleep.1 b/util/sbase/sleep.1
new file mode 100644
index 00000000..3c58fb95
--- /dev/null
+++ b/util/sbase/sleep.1
@@ -0,0 +1,18 @@
+.Dd October 8, 2015
+.Dt SLEEP 1
+.Os sbase
+.Sh NAME
+.Nm sleep
+.Nd wait for a number of seconds
+.Sh SYNOPSIS
+.Nm
+.Ar num
+.Sh DESCRIPTION
+.Nm
+waits for
+.Ar num
+seconds to elapse.
+.Sh SEE ALSO
+.Xr sleep 3
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/sleep.c b/util/sbase/sleep.c
new file mode 100644
index 00000000..00ea2688
--- /dev/null
+++ b/util/sbase/sleep.c
@@ -0,0 +1,30 @@
+/* See LICENSE file for copyright and license details. */
+#include <unistd.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s num\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ unsigned seconds;
+
+ ARGBEGIN {
+ default:
+ usage();
+ } ARGEND
+
+ if (argc != 1)
+ usage();
+
+ seconds = estrtonum(argv[0], 0, UINT_MAX);
+ while ((seconds = sleep(seconds)) > 0)
+ ;
+
+ return 0;
+}
diff --git a/util/sbase/sort.1 b/util/sbase/sort.1
new file mode 100644
index 00000000..f5a2121e
--- /dev/null
+++ b/util/sbase/sort.1
@@ -0,0 +1,98 @@
+.Dd February 17, 2016
+.Dt SORT 1
+.Os sbase
+.Sh NAME
+.Nm sort
+.Nd sort lines
+.Sh SYNOPSIS
+.Nm
+.Op Fl Cbcdfimnru
+.Op Fl o Ar outfile
+.Op Fl t Ar delim
+.Op Fl k Ar key ...
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+writes the sorted concatenation of each
+.Ar file
+to stdout.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl C
+Check that the concatenation of the given
+.Ar files
+is sorted rather than sorting them.
+In this mode, no output is printed to stdout, and the exit status
+indicates the result of the check.
+.It Fl b
+Skip leading whitespace of columns when sorting.
+.It Fl c
+The same as
+.Fl C
+except that when disorder is detected, a message is written to stderr
+indicating the location of the disorder.
+.It Fl d
+Skip non-whitespace and non-alphanumeric characters.
+.It Fl f
+Ignore letter case when sorting.
+.It Fl i
+Skip non-printable characters.
+.It Fl k Ar key
+Specify a key definition of the form
+.Sm off
+.Sy S
+.No [.
+.Sy s
+.No ][
+.Sy f
+.No ][ ,
+.Sy E
+.No [.
+.Sy e
+.No ][
+.Sy f
+.No ]]
+.Sm on
+where
+.Sy S , s , E
+and
+.Sy e
+are the starting column, starting character in that column, ending column and
+the ending character of that column respectively.
+If they are not specified,
+.Sy s
+refers to the first character of the specified starting column,
+.Sy E
+refers to the last column of every line, and
+.Sy e
+refers to the last character of the ending column.
+.Sy f
+can be used to specify options
+.Sy ( n , b )
+that only apply to this key definition.
+.Sy b
+is special in that it only applies to the column that it was specified after.
+.It Fl m
+Assume sorted input, merge only.
+.It Fl n
+Perform a numeric sort.
+.It Fl o Ar outfile
+Write output to
+.Ar outfile
+rather than stdout.
+.It Fl r
+Reverses the sort.
+.It Fl t Ar delim
+Set
+.Ar delim
+as the field delimiter.
+.It Fl u
+Print equal lines only once.
+.El
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/sort.c b/util/sbase/sort.c
new file mode 100644
index 00000000..fbb1abfe
--- /dev/null
+++ b/util/sbase/sort.c
@@ -0,0 +1,437 @@
+/* See LICENSE file for copyright and license details. */
+#include <ctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "queue.h"
+#include "text.h"
+#include "utf.h"
+#include "util.h"
+
+struct keydef {
+ int start_column;
+ int end_column;
+ int start_char;
+ int end_char;
+ int flags;
+ TAILQ_ENTRY(keydef) entry;
+};
+
+struct column {
+ struct line line;
+ size_t cap;
+};
+
+enum {
+ MOD_N = 1 << 0,
+ MOD_STARTB = 1 << 1,
+ MOD_ENDB = 1 << 2,
+ MOD_R = 1 << 3,
+ MOD_D = 1 << 4,
+ MOD_F = 1 << 5,
+ MOD_I = 1 << 6,
+};
+
+static TAILQ_HEAD(kdhead, keydef) kdhead = TAILQ_HEAD_INITIALIZER(kdhead);
+
+static int Cflag = 0, cflag = 0, uflag = 0;
+static char *fieldsep = NULL;
+static size_t fieldseplen = 0;
+static struct column col1, col2;
+
+static void
+skipblank(struct line *a)
+{
+ while (a->len && (*(a->data) == ' ' || *(a->data) == '\t')) {
+ a->data++;
+ a->len--;
+ }
+}
+
+static void
+skipnonblank(struct line *a)
+{
+ while (a->len && (*(a->data) != '\n' && *(a->data) != ' ' &&
+ *(a->data) != '\t')) {
+ a->data++;
+ a->len--;
+ }
+}
+
+static void
+skipcolumn(struct line *a, int skip_to_next_col)
+{
+ char *s;
+
+ if (fieldsep) {
+ if ((s = memmem(a->data, a->len, fieldsep, fieldseplen))) {
+ if (skip_to_next_col)
+ s += fieldseplen;
+ a->len -= s - a->data;
+ a->data = s;
+ } else {
+ a->data += a->len - 1;
+ a->len = 1;
+ }
+ } else {
+ skipblank(a);
+ skipnonblank(a);
+ }
+}
+
+static void
+columns(struct line *line, const struct keydef *kd, struct column *col)
+{
+ Rune r;
+ struct line start, end;
+ size_t utflen, rlen;
+ int i;
+
+ start.data = line->data;
+ start.len = line->len;
+ for (i = 1; i < kd->start_column; i++)
+ skipcolumn(&start, 1);
+ if (kd->flags & MOD_STARTB)
+ skipblank(&start);
+ for (utflen = 0; start.len > 1 && utflen < kd->start_char - 1;) {
+ rlen = chartorune(&r, start.data);
+ start.data += rlen;
+ start.len -= rlen;
+ utflen++;
+ }
+
+ end.data = line->data;
+ end.len = line->len;
+ if (kd->end_column) {
+ for (i = 1; i < kd->end_column; i++)
+ skipcolumn(&end, 1);
+ if (kd->flags & MOD_ENDB)
+ skipblank(&end);
+ if (kd->end_char) {
+ for (utflen = 0; end.len > 1 && utflen < kd->end_char;) {
+ rlen = chartorune(&r, end.data);
+ end.data += rlen;
+ end.len -= rlen;
+ utflen++;
+ }
+ } else {
+ skipcolumn(&end, 0);
+ }
+ } else {
+ end.data += end.len - 1;
+ end.len = 1;
+ }
+ col->line.len = MAX(0, end.data - start.data);
+ if (!(col->line.data) || col->cap < col->line.len + 1) {
+ free(col->line.data);
+ col->line.data = emalloc(col->line.len + 1);
+ }
+ memcpy(col->line.data, start.data, col->line.len);
+ col->line.data[col->line.len] = '\0';
+}
+
+static int
+skipmodcmp(struct line *a, struct line *b, int flags)
+{
+ Rune r1, r2;
+ size_t offa = 0, offb = 0;
+
+ do {
+ offa += chartorune(&r1, a->data + offa);
+ offb += chartorune(&r2, b->data + offb);
+
+ if (flags & MOD_D && flags & MOD_I) {
+ while (offa < a->len && ((!isblankrune(r1) &&
+ !isalnumrune(r1)) || (!isprintrune(r1))))
+ offa += chartorune(&r1, a->data + offa);
+ while (offb < b->len && ((!isblankrune(r2) &&
+ !isalnumrune(r2)) || (!isprintrune(r2))))
+ offb += chartorune(&r2, b->data + offb);
+ }
+ else if (flags & MOD_D) {
+ while (offa < a->len && !isblankrune(r1) &&
+ !isalnumrune(r1))
+ offa += chartorune(&r1, a->data + offa);
+ while (offb < b->len && !isblankrune(r2) &&
+ !isalnumrune(r2))
+ offb += chartorune(&r2, b->data + offb);
+ }
+ else if (flags & MOD_I) {
+ while (offa < a->len && !isprintrune(r1))
+ offa += chartorune(&r1, a->data + offa);
+ while (offb < b->len && !isprintrune(r2))
+ offb += chartorune(&r2, b->data + offb);
+ }
+ if (flags & MOD_F) {
+ r1 = toupperrune(r1);
+ r2 = toupperrune(r2);
+ }
+ } while (r1 && r1 == r2);
+
+ return r1 - r2;
+}
+
+static int
+slinecmp(struct line *a, struct line *b)
+{
+ int res = 0;
+ double x, y;
+ struct keydef *kd;
+
+ TAILQ_FOREACH(kd, &kdhead, entry) {
+ columns(a, kd, &col1);
+ columns(b, kd, &col2);
+
+ /* if -u is given, don't use default key definition
+ * unless it is the only one */
+ if (uflag && kd == TAILQ_LAST(&kdhead, kdhead) &&
+ TAILQ_LAST(&kdhead, kdhead) != TAILQ_FIRST(&kdhead)) {
+ res = 0;
+ } else if (kd->flags & MOD_N) {
+ x = strtod(col1.line.data, NULL);
+ y = strtod(col2.line.data, NULL);
+ res = (x < y) ? -1 : (x > y);
+ } else if (kd->flags & (MOD_D | MOD_F | MOD_I)) {
+ res = skipmodcmp(&col1.line, &col2.line, kd->flags);
+ } else {
+ res = linecmp(&col1.line, &col2.line);
+ }
+
+ if (kd->flags & MOD_R)
+ res = -res;
+ if (res)
+ break;
+ }
+
+ return res;
+}
+
+static int
+check(FILE *fp, const char *fname)
+{
+ static struct line prev, cur, tmp;
+ static size_t prevsize, cursize, tmpsize;
+ ssize_t len;
+
+ if (!prev.data) {
+ if ((len = getline(&prev.data, &prevsize, fp)) < 0)
+ eprintf("getline:");
+ prev.len = len;
+ }
+ while ((len = getline(&cur.data, &cursize, fp)) > 0) {
+ cur.len = len;
+ if (uflag > slinecmp(&cur, &prev)) {
+ if (!Cflag) {
+ weprintf("disorder %s: ", fname);
+ fwrite(cur.data, 1, cur.len, stderr);
+ }
+ return 1;
+ }
+ tmp = cur;
+ tmpsize = cursize;
+ cur = prev;
+ cursize = prevsize;
+ prev = tmp;
+ prevsize = tmpsize;
+ }
+
+ return 0;
+}
+
+static int
+parse_flags(char **s, int *flags, int bflag)
+{
+ while (isalpha((int)**s)) {
+ switch (*((*s)++)) {
+ case 'b':
+ *flags |= bflag;
+ break;
+ case 'd':
+ *flags |= MOD_D;
+ break;
+ case 'f':
+ *flags |= MOD_F;
+ break;
+ case 'i':
+ *flags |= MOD_I;
+ break;
+ case 'n':
+ *flags |= MOD_N;
+ break;
+ case 'r':
+ *flags |= MOD_R;
+ break;
+ default:
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+static void
+addkeydef(char *kdstr, int flags)
+{
+ struct keydef *kd;
+
+ kd = enmalloc(2, sizeof(*kd));
+
+ /* parse key definition kdstr with format
+ * start_column[.start_char][flags][,end_column[.end_char][flags]]
+ */
+ kd->start_column = 1;
+ kd->start_char = 1;
+ kd->end_column = 0; /* 0 means end of line */
+ kd->end_char = 0; /* 0 means end of column */
+ kd->flags = flags;
+
+ if ((kd->start_column = strtol(kdstr, &kdstr, 10)) < 1)
+ enprintf(2, "invalid start column in key definition\n");
+
+ if (*kdstr == '.') {
+ if ((kd->start_char = strtol(kdstr + 1, &kdstr, 10)) < 1)
+ enprintf(2, "invalid start character in key "
+ "definition\n");
+ }
+ if (parse_flags(&kdstr, &kd->flags, MOD_STARTB) < 0)
+ enprintf(2, "invalid start flags in key definition\n");
+
+ if (*kdstr == ',') {
+ if ((kd->end_column = strtol(kdstr + 1, &kdstr, 10)) < 0)
+ enprintf(2, "invalid end column in key definition\n");
+ if (*kdstr == '.') {
+ if ((kd->end_char = strtol(kdstr + 1, &kdstr, 10)) < 0)
+ enprintf(2, "invalid end character in key "
+ "definition\n");
+ }
+ if (parse_flags(&kdstr, &kd->flags, MOD_ENDB) < 0)
+ enprintf(2, "invalid end flags in key definition\n");
+ }
+
+ if (*kdstr != '\0')
+ enprintf(2, "invalid key definition\n");
+
+ TAILQ_INSERT_TAIL(&kdhead, kd, entry);
+}
+
+static void
+usage(void)
+{
+ enprintf(2, "usage: %s [-Cbcdfimnru] [-o outfile] [-t delim] "
+ "[-k def]... [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ FILE *fp, *ofp = stdout;
+ struct linebuf linebuf = EMPTY_LINEBUF;
+ size_t i;
+ int global_flags = 0, ret = 0;
+ char *outfile = NULL;
+
+ ARGBEGIN {
+ case 'C':
+ Cflag = 1;
+ break;
+ case 'b':
+ global_flags |= MOD_STARTB | MOD_ENDB;
+ break;
+ case 'c':
+ cflag = 1;
+ break;
+ case 'd':
+ global_flags |= MOD_D;
+ break;
+ case 'f':
+ global_flags |= MOD_F;
+ break;
+ case 'i':
+ global_flags |= MOD_I;
+ break;
+ case 'k':
+ addkeydef(EARGF(usage()), global_flags);
+ break;
+ case 'm':
+ /* more or less for free, but for performance-reasons,
+ * we should keep this flag in mind and maybe some later
+ * day implement it properly so we don't run out of memory
+ * while merging large sorted files.
+ */
+ break;
+ case 'n':
+ global_flags |= MOD_N;
+ break;
+ case 'o':
+ outfile = EARGF(usage());
+ break;
+ case 'r':
+ global_flags |= MOD_R;
+ break;
+ case 't':
+ fieldsep = EARGF(usage());
+ if (!*fieldsep)
+ eprintf("empty delimiter\n");
+ fieldseplen = unescape(fieldsep);
+ break;
+ case 'u':
+ uflag = 1;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ /* -b shall only apply to custom key definitions */
+ if (TAILQ_EMPTY(&kdhead) && global_flags)
+ addkeydef("1", global_flags & ~(MOD_STARTB | MOD_ENDB));
+ if (TAILQ_EMPTY(&kdhead) || (!Cflag && !cflag))
+ addkeydef("1", global_flags & MOD_R);
+
+ if (!argc) {
+ if (Cflag || cflag) {
+ if (check(stdin, "<stdin>") && !ret)
+ ret = 1;
+ } else {
+ getlines(stdin, &linebuf);
+ }
+ } else for (; *argv; argc--, argv++) {
+ if (!strcmp(*argv, "-")) {
+ *argv = "<stdin>";
+ fp = stdin;
+ } else if (!(fp = fopen(*argv, "r"))) {
+ enprintf(2, "fopen %s:", *argv);
+ continue;
+ }
+ if (Cflag || cflag) {
+ if (check(fp, *argv) && !ret)
+ ret = 1;
+ } else {
+ getlines(fp, &linebuf);
+ }
+ if (fp != stdin && fshut(fp, *argv))
+ ret = 2;
+ }
+
+ if (!Cflag && !cflag) {
+ if (outfile && !(ofp = fopen(outfile, "w")))
+ eprintf("fopen %s:", outfile);
+
+ qsort(linebuf.lines, linebuf.nlines, sizeof(*linebuf.lines),
+ (int (*)(const void *, const void *))slinecmp);
+
+ for (i = 0; i < linebuf.nlines; i++) {
+ if (!uflag || i == 0 ||
+ slinecmp(&linebuf.lines[i], &linebuf.lines[i - 1])) {
+ fwrite(linebuf.lines[i].data, 1,
+ linebuf.lines[i].len, ofp);
+ }
+ }
+ }
+
+ if (fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>") |
+ fshut(stderr, "<stderr>"))
+ ret = 2;
+
+ return ret;
+}
diff --git a/util/sbase/split.1 b/util/sbase/split.1
new file mode 100644
index 00000000..63b3b521
--- /dev/null
+++ b/util/sbase/split.1
@@ -0,0 +1,46 @@
+.Dd October 8, 2015
+.Dt SPLIT 1
+.Os sbase
+.Sh NAME
+.Nm split
+.Nd split up a file
+.Sh SYNOPSIS
+.Nm
+.Op Fl a Ar num
+.Op Fl b Ar num[k|m|g] | Fl l Ar num
+.Op Fl d
+.Op Ar file Op Ar prefix
+.Sh DESCRIPTION
+.Nm
+splits
+.Ar file
+into files with 1000 lines each, named with
+.Ar prefix
+"x" followed by 2-digit alphabetical count suffixes.
+If
+.Nm
+runs out of suffixes, it stops after the last valid filename.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl a Ar num
+Set suffix length to
+.Ar num
+characters.
+The default is 2.
+.It Fl b Ar num[k|m|g] | Fl l Ar num
+Start a new file every
+.Ar num
+bytes | lines.
+The units k, m, and g are case insensitive and powers of 2, not 10.
+The default is 1000 lines.
+.It Fl d
+Use decimal rather than alphabetical suffixes.
+.El
+.Sh SEE ALSO
+.Xr cat 1
+.Sh STANDARDS
+POSIX.1-2013.
+.Pp
+The
+.Op Fl d
+flag and g unit are an extension to that specification.
diff --git a/util/sbase/split.c b/util/sbase/split.c
new file mode 100644
index 00000000..7033a284
--- /dev/null
+++ b/util/sbase/split.c
@@ -0,0 +1,111 @@
+/* See LICENSE file for copyright and license details. */
+#include <ctype.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "util.h"
+
+static int base = 26, start = 'a';
+
+static int
+itostr(char *str, int x, int n)
+{
+ str[n] = '\0';
+ while (n-- > 0) {
+ str[n] = start + (x % base);
+ x /= base;
+ }
+
+ return x ? -1 : 0;
+}
+
+static FILE *
+nextfile(FILE *f, char *buf, int plen, int slen)
+{
+ static int filecount = 0;
+
+ if (f)
+ fshut(f, "<file>");
+ if (itostr(buf + plen, filecount++, slen) < 0)
+ return NULL;
+
+ if (!(f = fopen(buf, "w")))
+ eprintf("'%s':", buf);
+
+ return f;
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-a num] [-b num[k|m|g] | -l num] [-d] "
+ "[file [prefix]]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ FILE *in = stdin, *out = NULL;
+ off_t size = 1000, n;
+ int ret = 0, ch, plen, slen = 2, always = 0;
+ char name[NAME_MAX + 1], *prefix = "x", *file = NULL;
+
+ ARGBEGIN {
+ case 'a':
+ slen = estrtonum(EARGF(usage()), 0, INT_MAX);
+ break;
+ case 'b':
+ always = 1;
+ if ((size = parseoffset(EARGF(usage()))) < 0)
+ return 1;
+ if (!size)
+ eprintf("size needs to be positive\n");
+ break;
+ case 'd':
+ base = 10;
+ start = '0';
+ break;
+ case 'l':
+ always = 0;
+ size = estrtonum(EARGF(usage()), 1, MIN(LLONG_MAX, SSIZE_MAX));
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (*argv)
+ file = *argv++;
+ if (*argv)
+ prefix = *argv++;
+ if (*argv)
+ usage();
+
+ plen = strlen(prefix);
+ if (plen + slen > NAME_MAX)
+ eprintf("names cannot exceed %d bytes\n", NAME_MAX);
+ estrlcpy(name, prefix, sizeof(name));
+
+ if (file && strcmp(file, "-")) {
+ if (!(in = fopen(file, "r")))
+ eprintf("fopen %s:", file);
+ }
+
+ n = 0;
+ while ((ch = getc(in)) != EOF) {
+ if (!out || n >= size) {
+ if (!(out = nextfile(out, name, plen, slen)))
+ eprintf("fopen: %s:", name);
+ n = 0;
+ }
+ n += (always || ch == '\n');
+ putc(ch, out);
+ }
+
+ ret |= (in != stdin) && fshut(in, "<infile>");
+ ret |= out && (out != stdout) && fshut(out, "<outfile>");
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/sponge.1 b/util/sbase/sponge.1
new file mode 100644
index 00000000..9b668b42
--- /dev/null
+++ b/util/sbase/sponge.1
@@ -0,0 +1,19 @@
+.Dd October 8, 2015
+.Dt SPONGE 1
+.Os sbase
+.Sh NAME
+.Nm sponge
+.Nd soak up standard input and write to a file
+.Sh SYNOPSIS
+.Nm
+.Ar file
+.Sh DESCRIPTION
+.Nm
+reads stdin completely, then writes the saved contents to
+.Ar file .
+This makes it possible to easily create pipes which read from and write to
+the same file.
+.Pp
+If
+.Ar file
+is a symbolic link, it writes to its destination instead.
diff --git a/util/sbase/sponge.c b/util/sbase/sponge.c
new file mode 100644
index 00000000..7a0b272b
--- /dev/null
+++ b/util/sbase/sponge.c
@@ -0,0 +1,42 @@
+/* See LICENSE file for copyright and license details. */
+#include <fcntl.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s file\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ char tmp[] = "/tmp/sponge-XXXXXX";
+ int fd, tmpfd;
+
+ ARGBEGIN {
+ default:
+ usage();
+ } ARGEND
+
+ if (argc != 1)
+ usage();
+
+ if ((tmpfd = mkstemp(tmp)) < 0)
+ eprintf("mkstemp:");
+ unlink(tmp);
+ if (concat(0, "<stdin>", tmpfd, "<tmpfile>") < 0)
+ return 1;
+ if (lseek(tmpfd, 0, SEEK_SET) < 0)
+ eprintf("lseek:");
+
+ if ((fd = creat(argv[0], 0666)) < 0)
+ eprintf("creat %s:", argv[0]);
+ if (concat(tmpfd, "<tmpfile>", fd, argv[0]) < 0)
+ return 1;
+
+ return 0;
+}
diff --git a/util/sbase/strings.1 b/util/sbase/strings.1
new file mode 100644
index 00000000..c7bed734
--- /dev/null
+++ b/util/sbase/strings.1
@@ -0,0 +1,50 @@
+.Dd October 8, 2015
+.Dt STRINGS 1
+.Os sbase
+.Sh NAME
+.Nm strings
+.Nd print strings of printable characters in files
+.Sh SYNOPSIS
+.Nm
+.Op Fl a
+.Op Fl n Ar num
+.Op Fl t Ar format
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+writes sequences of at least 4 printable characters in each
+.Ar file
+to stdout.
+If no
+.Ar file
+is given,
+.Nm
+reads from stdin.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl a
+Scan each
+.Ar file
+entirely.
+This is the default.
+.It Fl n Ar num
+Print sequences of at least
+.Ar num
+characters.
+The default is 4.
+.It Fl t Ar format
+Prepend each string with its byte offset, with
+.Ar format
+being one of
+.Sy d , o , x
+for decimal, octal or hexadecimal numbers.
+.El
+.Sh STANDARDS
+POSIX.1-2013.
+.Pp
+The
+.Op Fl t
+output format has been changed from "%F %s" to "%8lF: %s", with
+.Sy F
+being one of
+.Sy d , o , x .
diff --git a/util/sbase/strings.c b/util/sbase/strings.c
new file mode 100644
index 00000000..8f5a1540
--- /dev/null
+++ b/util/sbase/strings.c
@@ -0,0 +1,98 @@
+/* See LICENSE file for copyright and license details. */
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "utf.h"
+#include "util.h"
+
+static char *format = "";
+
+static void
+strings(FILE *fp, const char *fname, size_t min)
+{
+ Rune r, *rbuf;
+ size_t i, bread;
+ off_t off;
+
+ rbuf = ereallocarray(NULL, min, sizeof(*rbuf));
+
+ for (off = 0, i = 0; (bread = efgetrune(&r, fp, fname)); ) {
+ off += bread;
+ if (r == Runeerror)
+ continue;
+ if (!isprintrune(r)) {
+ if (i == min)
+ putchar('\n');
+ i = 0;
+ continue;
+ }
+ if (i == min) {
+ efputrune(&r, stdout, "<stdout>");
+ continue;
+ }
+ rbuf[i++] = r;
+ if (i < min)
+ continue;
+ printf(format, (long)off - i);
+ for (i = 0; i < min; i++)
+ efputrune(rbuf + i, stdout, "<stdout>");
+ }
+ free(rbuf);
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-a] [-n num] [-t format] [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ FILE *fp;
+ size_t min = 4;
+ int ret = 0;
+ char f;
+
+ ARGBEGIN {
+ case 'a':
+ break;
+ case 'n':
+ min = estrtonum(EARGF(usage()), 1, LLONG_MAX);
+ break;
+ case 't':
+ format = estrdup("%8l#: ");
+ f = *EARGF(usage());
+ if (f == 'd' || f == 'o' || f == 'x')
+ format[3] = f;
+ else
+ usage();
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc) {
+ strings(stdin, "<stdin>", min);
+ } else {
+ for (; *argv; argc--, argv++) {
+ if (!strcmp(*argv, "-")) {
+ *argv = "<stdin>";
+ fp = stdin;
+ } else if (!(fp = fopen(*argv, "r"))) {
+ weprintf("fopen %s:", *argv);
+ ret = 1;
+ continue;
+ }
+ strings(fp, *argv, min);
+ if (fp != stdin && fshut(fp, *argv))
+ ret = 1;
+ }
+ }
+
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/sync.1 b/util/sbase/sync.1
new file mode 100644
index 00000000..85f4526e
--- /dev/null
+++ b/util/sbase/sync.1
@@ -0,0 +1,17 @@
+.Dd October 8, 2015
+.Dt SYNC 1
+.Os sbase
+.Sh NAME
+.Nm sync
+.Nd flush disk cache
+.Sh SYNOPSIS
+.Nm
+.Sh DESCRIPTION
+.Nm
+invokes
+.Xr sync 2
+to flush all unwritten changes to disk.
+This is usually done before shutting down, rebooting or halting.
+.Sh SEE ALSO
+.Xr fsync 2 ,
+.Xr sync 2
diff --git a/util/sbase/sync.c b/util/sbase/sync.c
new file mode 100644
index 00000000..f1b98183
--- /dev/null
+++ b/util/sbase/sync.c
@@ -0,0 +1,25 @@
+/* See LICENSE file for copyright and license details. */
+#include <unistd.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ ARGBEGIN {
+ default:
+ usage();
+ } ARGEND
+
+ if (argc)
+ usage();
+ sync();
+
+ return 0;
+}
diff --git a/util/sbase/tail.1 b/util/sbase/tail.1
new file mode 100644
index 00000000..c425aacf
--- /dev/null
+++ b/util/sbase/tail.1
@@ -0,0 +1,51 @@
+.Dd October 8, 2015
+.Dt TAIL 1
+.Os sbase
+.Sh NAME
+.Nm tail
+.Nd display final lines of files
+.Sh SYNOPSIS
+.Nm
+.Op Fl f
+.Op Fl c Ar num | Fl m Ar num | Fl n Ar num | Fl Ns Ar num
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+writes the last 10 lines of each
+.Ar file
+to stdout.
+If no
+.Ar file
+is given,
+.Nm
+reads from stdin.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl c Ar num | Fl m Ar num | Fl n Ar num | Fl Ns Ar num
+Display final
+.Ar num
+bytes | characters | lines | lines.
+If
+.Ar num
+begins with '+'
+it is an offset from the beginning of each
+.Ar file .
+If
+.Ar num
+begins with '-' it is as if no sign was given.
+The default is 10 lines.
+.It Fl f
+If one
+.Ar file
+is specified, append lines to output as
+.Ar file
+grows.
+.El
+.Sh SEE ALSO
+.Xr head 1
+.Sh STANDARDS
+POSIX.1-2013.
+.Pp
+The
+.Op Fl Ns Ar num
+syntax is an extension to that specification.
diff --git a/util/sbase/tail.c b/util/sbase/tail.c
new file mode 100644
index 00000000..bbc5ad58
--- /dev/null
+++ b/util/sbase/tail.c
@@ -0,0 +1,229 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "utf.h"
+#include "util.h"
+
+static char mode = 'n';
+
+static int
+dropinit(int fd, const char *fname, size_t count)
+{
+ Rune r;
+ char buf[BUFSIZ], *p;
+ ssize_t n;
+ int nr;
+
+ if (count < 2)
+ goto copy;
+ count--; /* numbering starts at 1 */
+ while (count && (n = read(fd, buf, sizeof(buf))) > 0) {
+ switch (mode) {
+ case 'n': /* lines */
+ for (p = buf; count && n > 0; p++, n--) {
+ if (*p == '\n')
+ count--;
+ }
+ break;
+ case 'c': /* bytes */
+ if (count > n) {
+ count -= n;
+ } else {
+ p = buf + count;
+ n -= count;
+ count = 0;
+ }
+ break;
+ case 'm': /* runes */
+ for (p = buf; count && n > 0; p += nr, n -= nr, count--) {
+ nr = charntorune(&r, p, n);
+ if (!nr) {
+ /* we don't have a full rune, move
+ * remaining data to beginning and read
+ * again */
+ memmove(buf, p, n);
+ break;
+ }
+ }
+ break;
+ }
+ }
+ if (count) {
+ if (n < 0)
+ weprintf("read %s:", fname);
+ if (n <= 0)
+ return n;
+ }
+
+ /* write the rest of the buffer */
+ if (writeall(1, p, n) < 0)
+ eprintf("write:");
+copy:
+ switch (concat(fd, fname, 1, "<stdout>")) {
+ case -1: /* read error */
+ return -1;
+ case -2: /* write error */
+ exit(1);
+ default:
+ return 0;
+ }
+}
+
+static int
+taketail(int fd, const char *fname, size_t count)
+{
+ static char *buf = NULL;
+ static size_t size = 0;
+ char *p;
+ size_t len = 0, left;
+ ssize_t n;
+
+ if (!count)
+ return 0;
+ for (;;) {
+ if (len + BUFSIZ > size) {
+ /* make sure we have at least BUFSIZ to read */
+ size += 2 * BUFSIZ;
+ buf = erealloc(buf, size);
+ }
+ n = read(fd, buf + len, size - len);
+ if (n < 0) {
+ weprintf("read %s:", fname);
+ return -1;
+ }
+ if (n == 0)
+ break;
+ len += n;
+ switch (mode) {
+ case 'n': /* lines */
+ /* ignore the last character; if it is a newline, it
+ * ends the last line */
+ for (p = buf + len - 2, left = count; p >= buf; p--) {
+ if (*p != '\n')
+ continue;
+ left--;
+ if (!left) {
+ p++;
+ break;
+ }
+ }
+ break;
+ case 'c': /* bytes */
+ p = count < len ? buf + len - count : buf;
+ break;
+ case 'm': /* runes */
+ for (p = buf + len - 1, left = count; p >= buf; p--) {
+ /* skip utf-8 continuation bytes */
+ if (UTF8_POINT(*p))
+ continue;
+ left--;
+ if (!left)
+ break;
+ }
+ break;
+ }
+ if (p > buf) {
+ len -= p - buf;
+ memmove(buf, p, len);
+ }
+ }
+ if (writeall(1, buf, len) < 0)
+ eprintf("write:");
+ return 0;
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-f] [-c num | -m num | -n num | -num] [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct stat st1, st2;
+ int fd;
+ size_t n = 10;
+ int fflag = 0, ret = 0, newline = 0, many = 0;
+ char *numstr;
+ int (*tail)(int, const char *, size_t) = taketail;
+
+ ARGBEGIN {
+ case 'f':
+ fflag = 1;
+ break;
+ case 'c':
+ case 'm':
+ case 'n':
+ mode = ARGC();
+ numstr = EARGF(usage());
+ n = MIN(llabs(estrtonum(numstr, LLONG_MIN + 1,
+ MIN(LLONG_MAX, SIZE_MAX))), SIZE_MAX);
+ if (strchr(numstr, '+'))
+ tail = dropinit;
+ break;
+ ARGNUM:
+ n = ARGNUMF();
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc) {
+ if (tail(0, "<stdin>", n) < 0)
+ ret = 1;
+ } else {
+ if ((many = argc > 1) && fflag)
+ usage();
+ for (newline = 0; *argv; argc--, argv++) {
+ if (!strcmp(*argv, "-")) {
+ *argv = "<stdin>";
+ fd = 0;
+ } else if ((fd = open(*argv, O_RDONLY)) < 0) {
+ weprintf("open %s:", *argv);
+ ret = 1;
+ continue;
+ }
+ if (many)
+ printf("%s==> %s <==\n", newline ? "\n" : "", *argv);
+ if (fstat(fd, &st1) < 0)
+ eprintf("fstat %s:", *argv);
+ if (!(S_ISFIFO(st1.st_mode) || S_ISREG(st1.st_mode)))
+ fflag = 0;
+ newline = 1;
+ if (tail(fd, *argv, n) < 0) {
+ ret = 1;
+ fflag = 0;
+ }
+
+ if (!fflag) {
+ if (fd != 0)
+ close(fd);
+ continue;
+ }
+ for (;;) {
+ if (concat(fd, *argv, 1, "<stdout>") < 0)
+ exit(1);
+ if (fstat(fd, &st2) < 0)
+ eprintf("fstat %s:", *argv);
+ if (st2.st_size < st1.st_size) {
+ fprintf(stderr, "%s: file truncated\n", *argv);
+ if (lseek(fd, SEEK_SET, 0) < 0)
+ eprintf("lseek:");
+ }
+ st1 = st2;
+ sleep(1);
+ }
+ }
+ }
+
+ return ret;
+}
diff --git a/util/sbase/tar.1 b/util/sbase/tar.1
new file mode 100644
index 00000000..bcc07c21
--- /dev/null
+++ b/util/sbase/tar.1
@@ -0,0 +1,76 @@
+.Dd October 8, 2015
+.Dt TAR 1
+.Os sbase
+.Sh NAME
+.Nm tar
+.Nd create, list or extract a tape archive
+.Sh SYNOPSIS
+.Nm
+.Cm x | Cm t | Fl x | Fl t
+.Op Fl C Ar dir
+.Op Fl J | Fl Z | Fl a | Fl j | Fl z
+.Op Fl m
+.Op Fl p
+.Op Fl f Ar file
+.Op Ar file ...
+.Nm
+.Cm c | Fl c Op Fl C Ar dir
+.Op Fl J | Fl Z | Fl a | Fl j | Fl z
+.Op Fl h
+.Ar path ...
+.Op Fl f Ar file
+.Sh DESCRIPTION
+.Nm
+is the standard file archiver.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl c Ar path ...
+Create archive from
+.Ar path .
+.It Fl C Ar dir
+Change directory to
+.Ar dir
+before beginning.
+.It Fl f Ar file
+Set
+.Ar file
+as input | output archive instead of stdin | stdout.
+If '-', stdin | stdout is used.
+.It Fl m
+Do not preserve modification time.
+.It Fl t
+List all files in the archive.
+.It Fl x
+Extract archive.
+.It Fl h
+Always dereference symbolic links while recursively traversing directories.
+.It Fl J | Fl Z | Fl a | Fl j | Fl z
+Use xz | compress | lzma | bzip2 | gzip compression or decompression.
+These utilities must be installed separately.
+Using these flags is discouraged in favour of the flexibility
+and clarity of pipes:
+.Bd -literal -offset indent
+$ bzip2 -cd archive.tar.bz2 | tar -x
+$ gzip -cd archive.tar.gz | tar -x
+.Ed
+.Bd -literal -offset indent
+$ tar -c file ... | bzip2 > archive.tar.bz2
+$ tar -c file ... | gzip2 > archive.tar.gz
+.Ed
+.El
+.Sh SEE ALSO
+.Xr ar 1 ,
+.Xr bzip2 1 ,
+.Xr gzip 1
+.Sh STANDARDS
+The
+.Nm
+utility is compliant with the UStar (Uniform Standard Tape ARchive)
+format defined in the
+.St -p1003.1-88
+specification.
+For long file paths (>99 bytes), the UStar, 'L' and 'x' header formats are
+supported for reading (to a maximum size of PATH_MAX or 255 bytes, depending on
+format), and the 'L' format is supported for writing (with unlimited path
+size).
+Link targets are limited to the UStar maximum of 100 bytes.
diff --git a/util/sbase/tar.c b/util/sbase/tar.c
new file mode 100644
index 00000000..4d44ec06
--- /dev/null
+++ b/util/sbase/tar.c
@@ -0,0 +1,662 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#ifndef major
+#include <sys/sysmacros.h>
+#endif
+
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <grp.h>
+#include <libgen.h>
+#include <pwd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "fs.h"
+#include "util.h"
+
+#define BLKSIZ (sizeof (struct header)) /* must equal 512 bytes */
+
+enum Type {
+ REG = '0',
+ AREG = '\0',
+ HARDLINK = '1',
+ SYMLINK = '2',
+ CHARDEV = '3',
+ BLOCKDEV = '4',
+ DIRECTORY = '5',
+ FIFO = '6',
+ RESERVED = '7'
+};
+
+struct header {
+ char name[100];
+ char mode[8];
+ char uid[8];
+ char gid[8];
+ char size[12];
+ char mtime[12];
+ char chksum[8];
+ char type;
+ char linkname[100];
+ char magic[6];
+ char version[2];
+ char uname[32];
+ char gname[32];
+ char major[8];
+ char minor[8];
+ char prefix[155];
+ char padding[12];
+};
+
+static struct dirtime {
+ char *name;
+ time_t mtime;
+} *dirtimes;
+
+static size_t dirtimeslen;
+
+static int tarfd;
+static ino_t tarinode;
+static dev_t tardev;
+
+static int mflag, vflag;
+static int filtermode;
+static const char *filtertool;
+
+static const char *filtertools[] = {
+ ['J'] = "xz",
+ ['Z'] = "compress",
+ ['a'] = "lzma",
+ ['j'] = "bzip2",
+ ['z'] = "gzip",
+};
+
+static void
+pushdirtime(char *name, time_t mtime)
+{
+ dirtimes = ereallocarray(dirtimes, dirtimeslen + 1, sizeof(*dirtimes));
+ dirtimes[dirtimeslen].name = estrdup(name);
+ dirtimes[dirtimeslen].mtime = mtime;
+ dirtimeslen++;
+}
+
+static struct dirtime *
+popdirtime(void)
+{
+ if (dirtimeslen) {
+ dirtimeslen--;
+ return &dirtimes[dirtimeslen];
+ }
+ return NULL;
+}
+
+static int
+comp(int fd, const char *tool, const char *flags)
+{
+ int fds[2];
+
+ if (pipe(fds) < 0)
+ eprintf("pipe:");
+
+ switch (fork()) {
+ case -1:
+ eprintf("fork:");
+ case 0:
+ dup2(fd, 1);
+ dup2(fds[0], 0);
+ close(fds[0]);
+ close(fds[1]);
+
+ execlp(tool, tool, flags, NULL);
+ weprintf("execlp %s:", tool);
+ _exit(1);
+ }
+ close(fds[0]);
+ return fds[1];
+}
+
+static int
+decomp(int fd, const char *tool, const char *flags)
+{
+ int fds[2];
+
+ if (pipe(fds) < 0)
+ eprintf("pipe:");
+
+ switch (fork()) {
+ case -1:
+ eprintf("fork:");
+ case 0:
+ dup2(fd, 0);
+ dup2(fds[1], 1);
+ close(fds[0]);
+ close(fds[1]);
+
+ execlp(tool, tool, flags, NULL);
+ weprintf("execlp %s:", tool);
+ _exit(1);
+ }
+ close(fds[1]);
+ return fds[0];
+}
+
+static ssize_t
+eread(int fd, void *buf, size_t n)
+{
+ ssize_t r;
+
+again:
+ r = read(fd, buf, n);
+ if (r < 0) {
+ if (errno == EINTR)
+ goto again;
+ eprintf("read:");
+ }
+ return r;
+}
+
+static ssize_t
+ewrite(int fd, const void *buf, size_t n)
+{
+ ssize_t r;
+
+ if ((r = write(fd, buf, n)) != n)
+ eprintf("write:");
+ return r;
+}
+
+static unsigned
+chksum(struct header *h)
+{
+ unsigned sum, i;
+
+ memset(h->chksum, ' ', sizeof(h->chksum));
+ for (i = 0, sum = 0, assert(BLKSIZ == 512); i < BLKSIZ; i++)
+ sum += *((unsigned char *)h + i);
+ return sum;
+}
+
+static void
+putoctal(char *dst, unsigned num, int size)
+{
+ if (snprintf(dst, size, "%.*o", size - 1, num) >= size)
+ eprintf("putoctal: input number '%o' too large\n", num);
+}
+
+static int
+archive(const char *path)
+{
+ static const struct header blank = {
+ "././@LongLink", "0000600", "0000000", "0000000", "00000000000",
+ "00000000000" , " ", AREG , "" , "ustar", "00",
+ };
+ char b[BLKSIZ + BLKSIZ], *p;
+ struct header *h = (struct header *)b;
+ struct group *gr;
+ struct passwd *pw;
+ struct stat st;
+ ssize_t l, n, r;
+ int fd = -1;
+
+ if (lstat(path, &st) < 0) {
+ weprintf("lstat %s:", path);
+ return 0;
+ } else if (st.st_ino == tarinode && st.st_dev == tardev) {
+ weprintf("ignoring %s\n", path);
+ return 0;
+ }
+ pw = getpwuid(st.st_uid);
+ gr = getgrgid(st.st_gid);
+
+ *h = blank;
+ n = strlcpy(h->name, path, sizeof(h->name));
+ if (n >= sizeof(h->name)) {
+ *++h = blank;
+ h->type = 'L';
+ putoctal(h->size, n, sizeof(h->size));
+ putoctal(h->chksum, chksum(h), sizeof(h->chksum));
+ ewrite(tarfd, (char *)h, BLKSIZ);
+
+ for (p = (char *)path; n > 0; n -= BLKSIZ, p += BLKSIZ) {
+ if (n < BLKSIZ) {
+ p = memcpy(h--, p, n);
+ memset(p + n, 0, BLKSIZ - n);
+ }
+ ewrite(tarfd, p, BLKSIZ);
+ }
+ }
+
+ putoctal(h->mode, (unsigned)st.st_mode & 0777, sizeof(h->mode));
+ putoctal(h->uid, (unsigned)st.st_uid, sizeof(h->uid));
+ putoctal(h->gid, (unsigned)st.st_gid, sizeof(h->gid));
+ putoctal(h->mtime, (unsigned)st.st_mtime, sizeof(h->mtime));
+ estrlcpy(h->uname, pw ? pw->pw_name : "", sizeof(h->uname));
+ estrlcpy(h->gname, gr ? gr->gr_name : "", sizeof(h->gname));
+
+ if (S_ISREG(st.st_mode)) {
+ h->type = REG;
+ putoctal(h->size, st.st_size, sizeof(h->size));
+ fd = open(path, O_RDONLY);
+ if (fd < 0)
+ eprintf("open %s:", path);
+ } else if (S_ISDIR(st.st_mode)) {
+ h->type = DIRECTORY;
+ } else if (S_ISLNK(st.st_mode)) {
+ h->type = SYMLINK;
+ if ((r = readlink(path, h->linkname, sizeof(h->linkname) - 1)) < 0)
+ eprintf("readlink %s:", path);
+ h->linkname[r] = '\0';
+ } else if (S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode)) {
+ h->type = S_ISCHR(st.st_mode) ? CHARDEV : BLOCKDEV;
+ putoctal(h->major, (unsigned)major(st.st_dev), sizeof(h->major));
+ putoctal(h->minor, (unsigned)minor(st.st_dev), sizeof(h->minor));
+ } else if (S_ISFIFO(st.st_mode)) {
+ h->type = FIFO;
+ }
+
+ putoctal(h->chksum, chksum(h), sizeof(h->chksum));
+ ewrite(tarfd, b, BLKSIZ);
+
+ if (fd != -1) {
+ while ((l = eread(fd, b, BLKSIZ)) > 0) {
+ if (l < BLKSIZ)
+ memset(b + l, 0, BLKSIZ - l);
+ ewrite(tarfd, b, BLKSIZ);
+ }
+ close(fd);
+ }
+
+ return 0;
+}
+
+static int
+unarchive(char *fname, ssize_t l, char b[BLKSIZ])
+{
+ struct header *h = (struct header *)b;
+ struct timespec times[2];
+ char lname[101], *tmp, *p;
+ long mode, major, minor, type, mtime, uid, gid;
+ int fd = -1, lnk = h->type == SYMLINK;
+
+ if (!mflag && ((mtime = strtol(h->mtime, &p, 8)) < 0 || *p != '\0'))
+ eprintf("strtol %s: invalid mtime\n", h->mtime);
+ if (strcmp(fname, ".") && strcmp(fname, "./") && remove(fname) < 0)
+ if (errno != ENOENT) weprintf("remove %s:", fname);
+
+ tmp = estrdup(fname);
+ mkdirp(dirname(tmp), 0777, 0777);
+ free(tmp);
+
+ switch (h->type) {
+ case REG:
+ case AREG:
+ case RESERVED:
+ if ((mode = strtol(h->mode, &p, 8)) < 0 || *p != '\0')
+ eprintf("strtol %s: invalid mode\n", h->mode);
+ fd = open(fname, O_WRONLY | O_TRUNC | O_CREAT, 0600);
+ if (fd < 0)
+ eprintf("open %s:", fname);
+ break;
+ case HARDLINK:
+ case SYMLINK:
+ snprintf(lname, sizeof(lname), "%.*s", (int)sizeof(h->linkname),
+ h->linkname);
+ if ((lnk ? symlink:link)(lname, fname) < 0)
+ eprintf("%s %s -> %s:", lnk ? "symlink":"link", fname, lname);
+ lnk++;
+ break;
+ case DIRECTORY:
+ if ((mode = strtol(h->mode, &p, 8)) < 0 || *p != '\0')
+ eprintf("strtol %s: invalid mode\n", h->mode);
+ if (mkdir(fname, (mode_t)mode) < 0 && errno != EEXIST)
+ eprintf("mkdir %s:", fname);
+ pushdirtime(fname, mtime);
+ break;
+ case CHARDEV:
+ case BLOCKDEV:
+ if ((mode = strtol(h->mode, &p, 8)) < 0 || *p != '\0')
+ eprintf("strtol %s: invalid mode\n", h->mode);
+ if ((major = strtol(h->major, &p, 8)) < 0 || *p != '\0')
+ eprintf("strtol %s: invalid major device\n", h->major);
+ if ((minor = strtol(h->minor, &p, 8)) < 0 || *p != '\0')
+ eprintf("strtol %s: invalid minor device\n", h->minor);
+ type = (h->type == CHARDEV) ? S_IFCHR : S_IFBLK;
+ if (mknod(fname, type | mode, makedev(major, minor)) < 0)
+ eprintf("mknod %s:", fname);
+ break;
+ case FIFO:
+ if ((mode = strtol(h->mode, &p, 8)) < 0 || *p != '\0')
+ eprintf("strtol %s: invalid mode\n", h->mode);
+ if (mknod(fname, S_IFIFO | mode, 0) < 0)
+ eprintf("mknod %s:", fname);
+ break;
+ default:
+ eprintf("unsupported tar-filetype %c\n", h->type);
+ }
+
+ if ((uid = strtol(h->uid, &p, 8)) < 0 || *p != '\0')
+ eprintf("strtol %s: invalid uid\n", h->uid);
+ if ((gid = strtol(h->gid, &p, 8)) < 0 || *p != '\0')
+ eprintf("strtol %s: invalid gid\n", h->gid);
+
+ if (fd != -1) {
+ for (; l > 0; l -= BLKSIZ)
+ if (eread(tarfd, b, BLKSIZ) > 0)
+ ewrite(fd, b, MIN(l, BLKSIZ));
+ close(fd);
+ }
+
+ if (lnk == 1)
+ return 0;
+
+ times[0].tv_sec = times[1].tv_sec = mtime;
+ times[0].tv_nsec = times[1].tv_nsec = 0;
+ if (!mflag && utimensat(AT_FDCWD, fname, times, AT_SYMLINK_NOFOLLOW) < 0)
+ weprintf("utimensat %s:", fname);
+ if (lnk) {
+ if (!getuid() && lchown(fname, uid, gid))
+ weprintf("lchown %s:", fname);
+ } else {
+ if (!getuid() && chown(fname, uid, gid))
+ weprintf("chown %s:", fname);
+ if (chmod(fname, mode) < 0)
+ eprintf("fchmod %s:", fname);
+ }
+
+ return 0;
+}
+
+static void
+skipblk(ssize_t l)
+{
+ char b[BLKSIZ];
+
+ for (; l > 0; l -= BLKSIZ)
+ if (!eread(tarfd, b, BLKSIZ))
+ break;
+}
+
+static int
+print(char *fname, ssize_t l, char b[BLKSIZ])
+{
+ puts(fname);
+ skipblk(l);
+ return 0;
+}
+
+static void
+c(int dirfd, const char *name, struct stat *st, void *data, struct recursor *r)
+{
+ archive(r->path);
+ if (vflag)
+ puts(r->path);
+
+ if (S_ISDIR(st->st_mode))
+ recurse(dirfd, name, NULL, r);
+}
+
+static void
+sanitize(struct header *h)
+{
+ size_t i, j, l;
+ struct {
+ char *f;
+ size_t l;
+ } fields[] = {
+ { h->mode, sizeof(h->mode) },
+ { h->uid, sizeof(h->uid) },
+ { h->gid, sizeof(h->gid) },
+ { h->size, sizeof(h->size) },
+ { h->mtime, sizeof(h->mtime) },
+ { h->chksum, sizeof(h->chksum) },
+ { h->major, sizeof(h->major) },
+ { h->minor, sizeof(h->minor) }
+ };
+
+ /* Numeric fields can be terminated with spaces instead of
+ * NULs as per the ustar specification. Patch all of them to
+ * use NULs so we can perform string operations on them. */
+ for (i = 0; i < LEN(fields); i++){
+ j = 0, l = fields[i].l - 1;
+ for (; j < l && fields[i].f[j] == ' '; j++);
+ for (; j <= l; j++)
+ if (fields[i].f[j] == ' ')
+ fields[i].f[j] = '\0';
+ if (fields[i].f[l])
+ eprintf("numeric field #%d (%.*s) is not null or space terminated\n",
+ i, l+1, fields[i].f);
+ }
+}
+
+static void
+chktar(struct header *h)
+{
+ const char *reason;
+ char tmp[sizeof h->chksum], *err;
+ long sum, i;
+
+ if (h->prefix[0] == '\0' && h->name[0] == '\0') {
+ reason = "empty filename";
+ goto bad;
+ }
+ if (h->magic[0] && strncmp("ustar", h->magic, 5)) {
+ reason = "not ustar format";
+ goto bad;
+ }
+ memcpy(tmp, h->chksum, sizeof(tmp));
+ for (i = sizeof(tmp)-1; i > 0 && tmp[i] == ' '; i--) {
+ tmp[i] = '\0';
+ }
+ sum = strtol(tmp, &err, 8);
+ if (sum < 0 || sum >= BLKSIZ*256 || *err != '\0') {
+ reason = "invalid checksum";
+ goto bad;
+ }
+ if (sum != chksum(h)) {
+ reason = "incorrect checksum";
+ goto bad;
+ }
+ memcpy(h->chksum, tmp, sizeof(tmp));
+ return;
+bad:
+ eprintf("malformed tar archive: %s\n", reason);
+}
+
+static void
+xt(int argc, char *argv[], int mode)
+{
+ long size, l;
+ char b[BLKSIZ], fname[l = PATH_MAX + 1], *p, *q = NULL;
+ int i, m, n;
+ int (*fn)(char *, ssize_t, char[BLKSIZ]) = (mode == 'x') ? unarchive : print;
+ struct timespec times[2];
+ struct header *h = (struct header *)b;
+ struct dirtime *dirtime;
+
+ while (eread(tarfd, b, BLKSIZ) > 0 && (h->name[0] || h->prefix[0])) {
+ chktar(h);
+ sanitize(h);
+
+ if ((size = strtol(h->size, &p, 8)) < 0 || *p != '\0')
+ eprintf("strtol %s: invalid size\n", h->size);
+
+ /* Long file path is read directly into fname*/
+ if (h->type == 'L' || h->type == 'x' || h->type == 'g') {
+
+ /* Read header only up to size of fname buffer */
+ for (q = fname; q < fname+size; q += BLKSIZ) {
+ if (q + BLKSIZ >= fname + l)
+ eprintf("name exceeds buffer: %.*s\n", q-fname, fname);
+ eread(tarfd, q, BLKSIZ);
+ }
+
+ /* Convert pax x header with 'path=' field into L header */
+ if (h->type == 'x') for (q = fname; q < fname+size-16; q += n) {
+ if ((n = strtol(q, &p, 10)) < 0 || *p != ' ')
+ eprintf("strtol %.*s: invalid number\n", p+1-q, q);
+ if (n && strncmp(p+1, "path=", 5) == 0) {
+ memmove(fname, p+6, size = q+n - p-6 - 1);
+ h->type = 'L';
+ break;
+ }
+ }
+ fname[size] = '\0';
+
+ /* Non L-like header (eg. pax 'g') is skipped by setting q=null */
+ if (h->type != 'L')
+ q = NULL;
+ continue;
+ }
+
+ /* Ustar path is copied into fname if no L header (ie: q is NULL) */
+ if (!q) {
+ m = sizeof h->prefix, n = sizeof h->name;
+ p = "/" + !h->prefix[0];
+ snprintf(fname, l, "%.*s%s%.*s", m, h->prefix, p, n, h->name);
+ }
+ q = NULL;
+
+ /* If argc > 0 then only extract the given files/dirs */
+ if (argc) {
+ for (i = 0; i < argc; i++) {
+ if (strncmp(argv[i], fname, n = strlen(argv[i])) == 0)
+ if (strchr("/", fname[n]) || argv[i][n-1] == '/')
+ break;
+ }
+ if (i == argc) {
+ skipblk(size);
+ continue;
+ }
+ }
+
+ fn(fname, size, b);
+ if (vflag && mode != 't')
+ puts(fname);
+ }
+
+ if (mode == 'x' && !mflag) {
+ while ((dirtime = popdirtime())) {
+ times[0].tv_sec = times[1].tv_sec = dirtime->mtime;
+ times[0].tv_nsec = times[1].tv_nsec = 0;
+ if (utimensat(AT_FDCWD, dirtime->name, times, 0) < 0)
+ eprintf("utimensat %s:", fname);
+ free(dirtime->name);
+ }
+ free(dirtimes);
+ dirtimes = NULL;
+ }
+}
+
+char **args;
+int argn;
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [x | t | -x | -t] [-C dir] [-J | -Z | -a | -j | -z] [-m] [-p] "
+ "[-f file] [file ...]\n"
+ " %s [c | -c] [-C dir] [-J | -Z | -a | -j | -z] [-h] path ... "
+ "[-f file]\n", argv0, argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct recursor r = { .fn = c, .follow = 'P', .flags = DIRFIRST };
+ struct stat st;
+ char *file = NULL, *dir = ".", mode = '\0';
+ int fd;
+
+ argv0 = argv[0];
+ if (argc > 1 && strchr("cxt", mode = *argv[1]))
+ *(argv[1]+1) ? *argv[1] = '-' : (*++argv = argv0, --argc);
+
+ ARGBEGIN {
+ case 'x':
+ case 'c':
+ case 't':
+ mode = ARGC();
+ break;
+ case 'C':
+ dir = EARGF(usage());
+ break;
+ case 'f':
+ file = EARGF(usage());
+ break;
+ case 'm':
+ mflag = 1;
+ break;
+ case 'J':
+ case 'Z':
+ case 'a':
+ case 'j':
+ case 'z':
+ filtermode = ARGC();
+ filtertool = filtertools[filtermode];
+ break;
+ case 'h':
+ r.follow = 'L';
+ break;
+ case 'v':
+ vflag = 1;
+ break;
+ case 'p':
+ break; /* Do nothing as already default behaviour */
+ default:
+ usage();
+ } ARGEND
+
+ switch (mode) {
+ case 'c':
+ if (!argc)
+ usage();
+ tarfd = 1;
+ if (file && *file != '-') {
+ tarfd = open(file, O_WRONLY | O_TRUNC | O_CREAT, 0644);
+ if (tarfd < 0)
+ eprintf("open %s:", file);
+ if (lstat(file, &st) < 0)
+ eprintf("lstat %s:", file);
+ tarinode = st.st_ino;
+ tardev = st.st_dev;
+ }
+
+ if (filtertool)
+ tarfd = comp(tarfd, filtertool, "-cf");
+
+ if (chdir(dir) < 0)
+ eprintf("chdir %s:", dir);
+ for (; *argv; argc--, argv++)
+ recurse(AT_FDCWD, *argv, NULL, &r);
+ break;
+ case 't':
+ case 'x':
+ tarfd = 0;
+ if (file && *file != '-') {
+ tarfd = open(file, O_RDONLY);
+ if (tarfd < 0)
+ eprintf("open %s:", file);
+ }
+
+ if (filtertool) {
+ fd = tarfd;
+ tarfd = decomp(tarfd, filtertool, "-cdf");
+ close(fd);
+ }
+
+ if (chdir(dir) < 0)
+ eprintf("chdir %s:", dir);
+ xt(argc, argv, mode);
+ break;
+ default:
+ usage();
+ }
+
+ return recurse_status;
+}
diff --git a/util/sbase/tee.1 b/util/sbase/tee.1
new file mode 100644
index 00000000..eaf2e202
--- /dev/null
+++ b/util/sbase/tee.1
@@ -0,0 +1,26 @@
+.Dd October 8, 2015
+.Dt TEE 1
+.Os sbase
+.Sh NAME
+.Nm tee
+.Nd multiply stdin
+.Sh SYNOPSIS
+.Nm
+.Op Fl ai
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+reads from stdin and writes to stdout and each
+.Ar file .
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl a
+Append to each
+.Ar file
+instead of overwriting it.
+.It Fl i
+Ignore SIGINT, see
+.Xr signal 7 .
+.El
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/tee.c b/util/sbase/tee.c
new file mode 100644
index 00000000..eac106ca
--- /dev/null
+++ b/util/sbase/tee.c
@@ -0,0 +1,60 @@
+/* See LICENSE file for copyright and license details. */
+#include <fcntl.h>
+#include <signal.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-ai] [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int *fds = NULL;
+ size_t i, nfds;
+ ssize_t n;
+ int ret = 0, aflag = O_TRUNC, iflag = 0;
+ char buf[BUFSIZ];
+
+ ARGBEGIN {
+ case 'a':
+ aflag = O_APPEND;
+ break;
+ case 'i':
+ iflag = 1;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (iflag && signal(SIGINT, SIG_IGN) == SIG_ERR)
+ eprintf("signal:");
+ nfds = argc + 1;
+ fds = ecalloc(nfds, sizeof(*fds));
+
+ for (i = 0; i < argc; i++) {
+ if ((fds[i] = open(argv[i], O_WRONLY|O_CREAT|aflag, 0666)) < 0) {
+ weprintf("open %s:", argv[i]);
+ ret = 1;
+ }
+ }
+ fds[i] = 1;
+
+ while ((n = read(0, buf, sizeof(buf))) > 0) {
+ for (i = 0; i < nfds; i++) {
+ if (fds[i] >= 0 && writeall(fds[i], buf, n) < 0) {
+ weprintf("write %s:", (i != argc) ? argv[i] : "<stdout>");
+ fds[i] = -1;
+ ret = 1;
+ }
+ }
+ }
+ if (n < 0)
+ eprintf("read <stdin>:");
+
+ return ret;
+}
diff --git a/util/sbase/test.1 b/util/sbase/test.1
new file mode 100644
index 00000000..c7d4f50c
--- /dev/null
+++ b/util/sbase/test.1
@@ -0,0 +1,131 @@
+.Dd October 8, 2015
+.Dt TEST 1
+.Os sbase
+.Sh NAME
+.Nm test
+.Nd evaluate expression
+.Sh SYNOPSIS
+.Nm
+.Ar expression
+.Sh DESCRIPTION
+.Nm
+returns the status of the
+.Ar expression .
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It ! Ar expression
+invert
+.Ar expression .
+.It ( Fl e | Fl s ) Ar file
+.Ar file
+exists and has (any size
+.Op Fl e
+| non-zero size
+.Op Fl s ) .
+.It ( Fl f | Fl d | Fl p | Fl hL | Fl S | Fl b | Fl c ) Ar file
+.Ar file
+exists and is a
+(regular file
+.Op Fl f
+| directory
+.Op Fl d
+| named pipe
+.Op Fl p
+| symbolic link
+.Op Fl h | Fl L
+| socket
+.Op Fl S
+| block special
+.Op Fl b
+| character special
+.Op Fl c ) .
+.It ( Fl k | Fl g | Fl u | Fl r | Fl w | Fl x ) Ar file
+.Ar file
+exists and has
+.Xr ( sticky 1
+.Op Fl k
+|
+.Xr setgid 2
+.Op Fl g
+|
+.Xr setuid 4
+.Op Fl u
+|
+.Xr read 4
+.Op Fl r
+|
+.Xr write 2
+.Op Fl w
+|
+.Xr execute 1
+.Op Fl x )
+permissions.
+.It Fl t Ar fd
+.Ar fd
+as a file descriptor is associated with a terminal.
+.It Ar string
+True if
+.Ar string
+is not the null string.
+.It ( Fl z | Fl n ) Ar string
+True if
+.Ar string
+has (zero
+.Op Fl z
+| non-zero
+.Op Fl n )
+length.
+.It Ar s1 Sy ( = | != ) Ar s2
+True if strings
+.Ar s1
+and
+.Ar s2
+are
+(identical
+.Oo Sy = Oc
+| different
+.Oo Sy != Oc ) .
+.It Ar n1 ( Fl eq | Fl ne | Fl gt | Fl ge | Fl le | Fl lt ) Ar n2
+True if integers
+.Ar n1
+and
+.Ar n2
+are (=
+.Op Fl eq
+| !=
+.Op Fl ne
+| >
+.Op Fl gt
+| >=
+.Op Fl ge
+| <=
+.Op Fl le
+| <
+.Op Fl lt ) .
+.It Ar f1 ( Fl ef | Fl ot | Fl nt ) Ar f2
+True if file
+.Ar f1
+(refer to the same inode as
+.Op Fl ef
+| has an older mtime than
+.Op Fl ot
+| has a newer mtime than
+.Op Fl nt )
+file
+.Ar f2 .
+.El
+.Sh EXIT STATUS
+.Bl -tag -width Ds
+.It 0
+.Ar expression
+is true.
+.It 1
+.Ar expression
+is false.
+.It > 1
+An error occurred.
+.El
+.Sh SEE ALSO
+.Xr expr 1
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/test.c b/util/sbase/test.c
new file mode 100644
index 00000000..1e88a221
--- /dev/null
+++ b/util/sbase/test.c
@@ -0,0 +1,247 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <fcntl.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static int
+intcmp(char *a, char *b)
+{
+ char *s;
+ int asign = *a == '-' ? -1 : 1;
+ int bsign = *b == '-' ? -1 : 1;
+
+ if (*a == '-' || *a == '+') a += 1;
+ if (*b == '-' || *b == '+') b += 1;
+
+ if (!*a || !*b)
+ goto noint;
+ for (s = a; *s; s++)
+ if (!isdigit(*s))
+ goto noint;
+ for (s = b; *s; s++)
+ if (!isdigit(*s))
+ goto noint;
+
+ while (*a == '0') a++;
+ while (*b == '0') b++;
+ asign *= !!*a;
+ bsign *= !!*b;
+
+ if (asign != bsign)
+ return asign < bsign ? -1 : 1;
+ else if (strlen(a) != strlen(b))
+ return asign * (strlen(a) < strlen(b) ? -1 : 1);
+ else
+ return asign * strcmp(a, b);
+
+noint:
+ enprintf(2, "expected integer operands\n");
+
+ return 0; /* not reached */
+}
+
+static int
+mtimecmp(struct stat *buf1, struct stat *buf2)
+{
+ if (buf1->st_mtime < buf2->st_mtime) return -1;
+ if (buf1->st_mtime > buf2->st_mtime) return +1;
+#ifdef st_mtime
+ if (buf1->st_mtim.tv_nsec < buf2->st_mtim.tv_nsec) return -1;
+ if (buf1->st_mtim.tv_nsec > buf2->st_mtim.tv_nsec) return +1;
+#endif
+ return 0;
+}
+
+static int unary_b(char *s) { struct stat buf; if ( stat(s, &buf)) return 0; return S_ISBLK (buf.st_mode); }
+static int unary_c(char *s) { struct stat buf; if ( stat(s, &buf)) return 0; return S_ISCHR (buf.st_mode); }
+static int unary_d(char *s) { struct stat buf; if ( stat(s, &buf)) return 0; return S_ISDIR (buf.st_mode); }
+static int unary_f(char *s) { struct stat buf; if ( stat(s, &buf)) return 0; return S_ISREG (buf.st_mode); }
+static int unary_g(char *s) { struct stat buf; if ( stat(s, &buf)) return 0; return S_ISGID & buf.st_mode ; }
+static int unary_h(char *s) { struct stat buf; if (lstat(s, &buf)) return 0; return S_ISLNK (buf.st_mode); }
+static int unary_k(char *s) { struct stat buf; if ( stat(s, &buf)) return 0; return S_ISVTX & buf.st_mode ; }
+static int unary_p(char *s) { struct stat buf; if ( stat(s, &buf)) return 0; return S_ISFIFO (buf.st_mode); }
+static int unary_S(char *s) { struct stat buf; if ( stat(s, &buf)) return 0; return S_ISSOCK (buf.st_mode); }
+static int unary_s(char *s) { struct stat buf; if ( stat(s, &buf)) return 0; return buf.st_size ; }
+static int unary_u(char *s) { struct stat buf; if ( stat(s, &buf)) return 0; return S_ISUID & buf.st_mode ; }
+
+static int unary_n(char *s) { return *s; }
+static int unary_z(char *s) { return !*s; }
+
+static int unary_e(char *s) { return !faccessat(AT_FDCWD, s, F_OK, AT_EACCESS); }
+static int unary_r(char *s) { return !faccessat(AT_FDCWD, s, R_OK, AT_EACCESS); }
+static int unary_w(char *s) { return !faccessat(AT_FDCWD, s, W_OK, AT_EACCESS); }
+static int unary_x(char *s) { return !faccessat(AT_FDCWD, s, X_OK, AT_EACCESS); }
+
+static int unary_t(char *s) { int fd = enstrtonum(2, s, 0, INT_MAX); return isatty(fd); }
+
+static int binary_se(char *s1, char *s2) { return !strcmp(s1, s2); }
+static int binary_sn(char *s1, char *s2) { return strcmp(s1, s2); }
+
+static int binary_eq(char *s1, char *s2) { return intcmp(s1, s2) == 0; }
+static int binary_ne(char *s1, char *s2) { return intcmp(s1, s2) != 0; }
+static int binary_gt(char *s1, char *s2) { return intcmp(s1, s2) > 0; }
+static int binary_ge(char *s1, char *s2) { return intcmp(s1, s2) >= 0; }
+static int binary_lt(char *s1, char *s2) { return intcmp(s1, s2) < 0; }
+static int binary_le(char *s1, char *s2) { return intcmp(s1, s2) <= 0; }
+
+static int
+binary_ef(char *s1, char *s2)
+{
+ struct stat buf1, buf2;
+ if (stat(s1, &buf1) || stat(s2, &buf2)) return 0;
+ return buf1.st_dev == buf2.st_dev && buf1.st_ino == buf2.st_ino;
+}
+
+static int
+binary_ot(char *s1, char *s2)
+{
+ struct stat buf1, buf2;
+ if (stat(s1, &buf1) || stat(s2, &buf2)) return 0;
+ return mtimecmp(&buf1, &buf2) < 0;
+}
+
+static int
+binary_nt(char *s1, char *s2)
+{
+ struct stat buf1, buf2;
+ if (stat(s1, &buf1) || stat(s2, &buf2)) return 0;
+ return mtimecmp(&buf1, &buf2) > 0;
+}
+
+struct test {
+ char *name;
+ union {
+ int (*u)(char *);
+ int (*b)(char *, char *);
+ } func;
+};
+
+static struct test unary[] = {
+ { "-b", { .u = unary_b } },
+ { "-c", { .u = unary_c } },
+ { "-d", { .u = unary_d } },
+ { "-e", { .u = unary_e } },
+ { "-f", { .u = unary_f } },
+ { "-g", { .u = unary_g } },
+ { "-h", { .u = unary_h } },
+ { "-k", { .u = unary_k } },
+ { "-L", { .u = unary_h } },
+ { "-n", { .u = unary_n } },
+ { "-p", { .u = unary_p } },
+ { "-r", { .u = unary_r } },
+ { "-S", { .u = unary_S } },
+ { "-s", { .u = unary_s } },
+ { "-t", { .u = unary_t } },
+ { "-u", { .u = unary_u } },
+ { "-w", { .u = unary_w } },
+ { "-x", { .u = unary_x } },
+ { "-z", { .u = unary_z } },
+
+ { NULL },
+};
+
+static struct test binary[] = {
+ { "=" , { .b = binary_se } },
+ { "!=" , { .b = binary_sn } },
+ { "-eq", { .b = binary_eq } },
+ { "-ne", { .b = binary_ne } },
+ { "-gt", { .b = binary_gt } },
+ { "-ge", { .b = binary_ge } },
+ { "-lt", { .b = binary_lt } },
+ { "-le", { .b = binary_le } },
+ { "-ef", { .b = binary_ef } },
+ { "-ot", { .b = binary_ot } },
+ { "-nt", { .b = binary_nt } },
+
+ { NULL },
+};
+
+static struct test *
+find_test(struct test *tests, char *name)
+{
+ struct test *t;
+
+ for (t = tests; t->name; t++)
+ if (!strcmp(t->name, name))
+ return t;
+
+ return NULL;
+}
+
+static int
+noarg(char *argv[])
+{
+ return 0;
+}
+
+static int
+onearg(char *argv[])
+{
+ return unary_n(argv[0]);
+}
+
+static int
+twoarg(char *argv[])
+{
+ struct test *t;
+
+ if (!strcmp(argv[0], "!"))
+ return !onearg(argv + 1);
+
+ if ((t = find_test(unary, *argv)))
+ return t->func.u(argv[1]);
+
+ enprintf(2, "bad unary test %s\n", argv[0]);
+
+ return 0; /* not reached */
+}
+
+static int
+threearg(char *argv[])
+{
+ struct test *t = find_test(binary, argv[1]);
+
+ if (t)
+ return t->func.b(argv[0], argv[2]);
+
+ if (!strcmp(argv[0], "!"))
+ return !twoarg(argv + 1);
+
+ enprintf(2, "bad binary test %s\n", argv[1]);
+
+ return 0; /* not reached */
+}
+
+static int
+fourarg(char *argv[])
+{
+ if (!strcmp(argv[0], "!"))
+ return !threearg(argv + 1);
+
+ enprintf(2, "too many arguments\n");
+
+ return 0; /* not reached */
+}
+
+int
+main(int argc, char *argv[])
+{
+ int (*narg[])(char *[]) = { noarg, onearg, twoarg, threearg, fourarg };
+ size_t len;
+
+ argv0 = *argv, argv0 ? (argc--, argv++) : (void *)0;
+
+ len = argv0 ? strlen(argv0) : 0;
+ if (len && argv0[--len] == '[' && (!len || argv0[--len] == '/') && strcmp(argv[--argc], "]"))
+ enprintf(2, "no matching ]\n");
+
+ if (argc > 4)
+ enprintf(2, "too many arguments\n");
+
+ return !narg[argc](argv);
+}
diff --git a/util/sbase/text.h b/util/sbase/text.h
new file mode 100644
index 00000000..9858592b
--- /dev/null
+++ b/util/sbase/text.h
@@ -0,0 +1,16 @@
+/* See LICENSE file for copyright and license details. */
+
+struct line {
+ char *data;
+ size_t len;
+};
+
+struct linebuf {
+ struct line *lines;
+ size_t nlines;
+ size_t capacity;
+};
+#define EMPTY_LINEBUF {NULL, 0, 0,}
+void getlines(FILE *, struct linebuf *);
+
+int linecmp(struct line *, struct line *);
diff --git a/util/sbase/tftp.1 b/util/sbase/tftp.1
new file mode 100644
index 00000000..4ad73a23
--- /dev/null
+++ b/util/sbase/tftp.1
@@ -0,0 +1,32 @@
+.Dd October 8, 2015
+.Dt TFTP 1
+.Os sbase
+.Sh NAME
+.Nm tftp
+.Nd trivial file transfer protocol client
+.Sh SYNOPSIS
+.Nm
+.Fl h Ar host
+.Op Fl p Ar port
+.Op Fl x | c
+.Ar file
+.Sh DESCRIPTION
+.Nm
+is a client that implements the trivial file transfer protocol over
+either IPv4 or IPv6 as specified in RFC 1350.
+It can be used to transfer files to and from remote machines.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl h Ar host
+Set the remote hostname.
+.It Fl p Ar port
+Set the remote port.
+It defaults to port 69.
+.It Fl x
+Extract a file from the server.
+This is the default if no flags are specified.
+Output goes to stdout.
+.It Fl c
+Create a file on the server.
+Input comes from stdin.
+.El
diff --git a/util/sbase/tftp.c b/util/sbase/tftp.c
new file mode 100644
index 00000000..0a099ff2
--- /dev/null
+++ b/util/sbase/tftp.c
@@ -0,0 +1,309 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include <netdb.h>
+#include <netinet/in.h>
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "util.h"
+
+#define BLKSIZE 512
+#define HDRSIZE 4
+#define PKTSIZE (BLKSIZE + HDRSIZE)
+
+#define TIMEOUT_SEC 5
+/* transfer will time out after NRETRIES * TIMEOUT_SEC */
+#define NRETRIES 5
+
+#define RRQ 1
+#define WWQ 2
+#define DATA 3
+#define ACK 4
+#define ERR 5
+
+static char *errtext[] = {
+ "Undefined",
+ "File not found",
+ "Access violation",
+ "Disk full or allocation exceeded",
+ "Illegal TFTP operation",
+ "Unknown transfer ID",
+ "File already exists",
+ "No such user"
+};
+
+static struct sockaddr_storage to;
+static socklen_t tolen;
+static int timeout;
+static int state;
+static int s;
+
+static int
+packreq(unsigned char *buf, int op, char *path, char *mode)
+{
+ unsigned char *p = buf;
+
+ *p++ = op >> 8;
+ *p++ = op & 0xff;
+ if (strlen(path) + 1 > 256)
+ eprintf("filename too long\n");
+ memcpy(p, path, strlen(path) + 1);
+ p += strlen(path) + 1;
+ memcpy(p, mode, strlen(mode) + 1);
+ p += strlen(mode) + 1;
+ return p - buf;
+}
+
+static int
+packack(unsigned char *buf, int blkno)
+{
+ buf[0] = ACK >> 8;
+ buf[1] = ACK & 0xff;
+ buf[2] = blkno >> 8;
+ buf[3] = blkno & 0xff;
+ return 4;
+}
+
+static int
+packdata(unsigned char *buf, int blkno)
+{
+ buf[0] = DATA >> 8;
+ buf[1] = DATA & 0xff;
+ buf[2] = blkno >> 8;
+ buf[3] = blkno & 0xff;
+ return 4;
+}
+
+static int
+unpackop(unsigned char *buf)
+{
+ return (buf[0] << 8) | (buf[1] & 0xff);
+}
+
+static int
+unpackblkno(unsigned char *buf)
+{
+ return (buf[2] << 8) | (buf[3] & 0xff);
+}
+
+static int
+unpackerrc(unsigned char *buf)
+{
+ int errc;
+
+ errc = (buf[2] << 8) | (buf[3] & 0xff);
+ if (errc < 0 || errc >= LEN(errtext))
+ eprintf("bad error code: %d\n", errc);
+ return errc;
+}
+
+static int
+writepkt(unsigned char *buf, int len)
+{
+ int n;
+
+ n = sendto(s, buf, len, 0, (struct sockaddr *)&to,
+ tolen);
+ if (n < 0)
+ if (errno != EINTR)
+ eprintf("sendto:");
+ return n;
+}
+
+static int
+readpkt(unsigned char *buf, int len)
+{
+ int n;
+
+ n = recvfrom(s, buf, len, 0, (struct sockaddr *)&to,
+ &tolen);
+ if (n < 0) {
+ if (errno != EINTR && errno != EWOULDBLOCK)
+ eprintf("recvfrom:");
+ timeout++;
+ if (timeout == NRETRIES)
+ eprintf("transfer timed out\n");
+ } else {
+ timeout = 0;
+ }
+ return n;
+}
+
+static void
+getfile(char *file)
+{
+ unsigned char buf[PKTSIZE];
+ int n, op, blkno, nextblkno = 1, done = 0;
+
+ state = RRQ;
+ for (;;) {
+ switch (state) {
+ case RRQ:
+ n = packreq(buf, RRQ, file, "octet");
+ writepkt(buf, n);
+ n = readpkt(buf, sizeof(buf));
+ if (n > 0) {
+ op = unpackop(buf);
+ if (op != DATA && op != ERR)
+ eprintf("bad opcode: %d\n", op);
+ state = op;
+ }
+ break;
+ case DATA:
+ n -= HDRSIZE;
+ if (n < 0)
+ eprintf("truncated packet\n");
+ blkno = unpackblkno(buf);
+ if (blkno == nextblkno) {
+ nextblkno++;
+ write(1, &buf[HDRSIZE], n);
+ }
+ if (n < BLKSIZE)
+ done = 1;
+ state = ACK;
+ break;
+ case ACK:
+ n = packack(buf, blkno);
+ writepkt(buf, n);
+ if (done)
+ return;
+ n = readpkt(buf, sizeof(buf));
+ if (n > 0) {
+ op = unpackop(buf);
+ if (op != DATA && op != ERR)
+ eprintf("bad opcode: %d\n", op);
+ state = op;
+ }
+ break;
+ case ERR:
+ eprintf("error: %s\n", errtext[unpackerrc(buf)]);
+ }
+ }
+}
+
+static void
+putfile(char *file)
+{
+ unsigned char inbuf[PKTSIZE], outbuf[PKTSIZE];
+ int inb, outb, op, blkno, nextblkno = 0, done = 0;
+
+ state = WWQ;
+ for (;;) {
+ switch (state) {
+ case WWQ:
+ outb = packreq(outbuf, WWQ, file, "octet");
+ writepkt(outbuf, outb);
+ inb = readpkt(inbuf, sizeof(inbuf));
+ if (inb > 0) {
+ op = unpackop(inbuf);
+ if (op != ACK && op != ERR)
+ eprintf("bad opcode: %d\n", op);
+ state = op;
+ }
+ break;
+ case DATA:
+ if (blkno == nextblkno) {
+ nextblkno++;
+ packdata(outbuf, nextblkno);
+ outb = read(0, &outbuf[HDRSIZE], BLKSIZE);
+ if (outb < BLKSIZE)
+ done = 1;
+ }
+ writepkt(outbuf, outb + HDRSIZE);
+ inb = readpkt(inbuf, sizeof(inbuf));
+ if (inb > 0) {
+ op = unpackop(inbuf);
+ if (op != ACK && op != ERR)
+ eprintf("bad opcode: %d\n", op);
+ state = op;
+ }
+ break;
+ case ACK:
+ if (inb < HDRSIZE)
+ eprintf("truncated packet\n");
+ blkno = unpackblkno(inbuf);
+ if (blkno == nextblkno)
+ if (done)
+ return;
+ state = DATA;
+ break;
+ case ERR:
+ eprintf("error: %s\n", errtext[unpackerrc(inbuf)]);
+ }
+ }
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s -h host [-p port] [-x | -c] file\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct addrinfo hints, *res, *r;
+ struct timeval tv;
+ char *host = NULL, *port = "tftp";
+ void (*fn)(char *) = getfile;
+ int ret;
+
+ ARGBEGIN {
+ case 'h':
+ host = EARGF(usage());
+ break;
+ case 'p':
+ port = EARGF(usage());
+ break;
+ case 'x':
+ fn = getfile;
+ break;
+ case 'c':
+ fn = putfile;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!host || !argc)
+ usage();
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_DGRAM;
+ hints.ai_protocol = IPPROTO_UDP;
+ ret = getaddrinfo(host, port, &hints, &res);
+ if (ret)
+ eprintf("getaddrinfo: %s\n", gai_strerror(ret));
+
+ for (r = res; r; r = r->ai_next) {
+ if (r->ai_family != AF_INET &&
+ r->ai_family != AF_INET6)
+ continue;
+ s = socket(r->ai_family, r->ai_socktype,
+ r->ai_protocol);
+ if (s < 0)
+ continue;
+ break;
+ }
+ if (!r)
+ eprintf("cannot create socket\n");
+ memcpy(&to, r->ai_addr, r->ai_addrlen);
+ tolen = r->ai_addrlen;
+ freeaddrinfo(res);
+
+ tv.tv_sec = TIMEOUT_SEC;
+ tv.tv_usec = 0;
+ if (setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0)
+ eprintf("setsockopt:");
+
+ fn(argv[0]);
+ return 0;
+}
diff --git a/util/sbase/time.1 b/util/sbase/time.1
new file mode 100644
index 00000000..645da5ef
--- /dev/null
+++ b/util/sbase/time.1
@@ -0,0 +1,45 @@
+.Dd October 8, 2015
+.Dt TIME 1
+.Os sbase
+.Sh NAME
+.Nm time
+.Nd time a command
+.Sh SYNOPSIS
+.Nm
+.Op Fl p
+.Ar cmd
+.Op Ar arg ...
+.Sh DESCRIPTION
+.Nm
+executes
+.Ar cmd
+and writes timing statistics to stderr after it finishes.
+The statistics include the elapsed real time
+between invocation and termination and the user
+and system CPU time (see
+.Xr times 2 ) .
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl p
+Use the format "real %f\enuser %f\ensys %f\en" for printing.
+This is the default.
+.El
+.Sh EXIT STATUS
+.Bl -tag -width Ds
+.It 0
+.Ar cmd
+executed successfully.
+.It 1
+Internal error.
+.It 126
+.Ar cmd
+was found but could not be executed.
+.It 127
+.Ar cmd
+could not be found.
+.El
+.Sh SEE ALSO
+.Xr times 2 ,
+.Xr waitpid 2
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/time.c b/util/sbase/time.c
new file mode 100644
index 00000000..60a8c8df
--- /dev/null
+++ b/util/sbase/time.c
@@ -0,0 +1,73 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/times.h>
+#include <sys/wait.h>
+
+#include <errno.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-p] cmd [arg ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ pid_t pid;
+ struct tms tms; /* user and sys times */
+ clock_t r0, r1; /* real time */
+ long ticks; /* per second */
+ int status, savederrno, ret = 0;
+
+ ARGBEGIN {
+ case 'p':
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc)
+ usage();
+
+ if ((ticks = sysconf(_SC_CLK_TCK)) <= 0)
+ eprintf("sysconf _SC_CLK_TCK:");
+
+ if ((r0 = times(&tms)) == (clock_t)-1)
+ eprintf("times:");
+
+ switch ((pid = fork())) {
+ case -1:
+ eprintf("fork:");
+ case 0:
+ execvp(argv[0], argv);
+ savederrno = errno;
+ weprintf("execvp %s:", argv[0]);
+ _exit(126 + (savederrno == ENOENT));
+ default:
+ break;
+ }
+ waitpid(pid, &status, 0);
+
+ if ((r1 = times(&tms)) == (clock_t)-1)
+ eprintf("times:");
+
+ if (WIFSIGNALED(status)) {
+ fprintf(stderr, "Command terminated by signal %d\n",
+ WTERMSIG(status));
+ ret = 128 + WTERMSIG(status);
+ }
+
+ fprintf(stderr, "real %f\nuser %f\nsys %f\n",
+ (r1 - r0) / (double)ticks,
+ tms.tms_cutime / (double)ticks,
+ tms.tms_cstime / (double)ticks);
+
+ if (WIFEXITED(status))
+ ret = WEXITSTATUS(status);
+
+ return ret;
+}
diff --git a/util/sbase/touch.1 b/util/sbase/touch.1
new file mode 100644
index 00000000..80c6ebb6
--- /dev/null
+++ b/util/sbase/touch.1
@@ -0,0 +1,63 @@
+.Dd October 8, 2015
+.Dt TOUCH 1
+.Os sbase
+.Sh NAME
+.Nm touch
+.Nd set file timestamps
+.Sh SYNOPSIS
+.Nm
+.Op Fl acm
+.Op Fl d Ar time | Fl r Ar ref_file | Fl T Ar time | Fl t Ar time
+.Ar file ...
+.Sh DESCRIPTION
+.Nm
+sets the access and modification time of each
+.Ar file
+to the current time of day.
+If
+.Ar file
+doesn't exist, it is created with default permissions.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl a | Fl m
+Set the access | modification time of
+.Ar file .
+.It Fl c
+Don't create
+.Ar file
+if it doesn't exist, not affecting exit status.
+.It Fl d Ar time
+Set the
+.Ar time
+of the format YYYY-MM-DDThh:mm:SS[Z] used for
+.Op Fl am .
+.It Fl r Ar ref_file
+Set the
+.Ar time
+used for
+.Op Fl am
+to the modification time of
+.Ar ref_file .
+.It Fl T Ar time
+Set the
+.Ar time
+used for
+.Op Fl am
+given as the number of seconds since the
+Unix epoch 1970-01-01T00:00:00Z.
+.It Fl t Ar time
+Set the
+.Ar time
+of the format [[CC]YY]MMDDhhmm[.SS] used for
+.Op Fl am .
+.El
+.Sh SEE ALSO
+.Xr date 1
+.Sh STANDARDS
+POSIX.1-2013.
+Except for fractional seconds with
+.Op Fl d .
+.Pp
+The
+.Op Fl T
+flag is an extension to that specification.
diff --git a/util/sbase/touch.c b/util/sbase/touch.c
new file mode 100644
index 00000000..6e63bf80
--- /dev/null
+++ b/util/sbase/touch.c
@@ -0,0 +1,159 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static int aflag;
+static int cflag;
+static int mflag;
+static struct timespec times[2] = {{.tv_nsec = UTIME_NOW}};
+
+static void
+touch(const char *file)
+{
+ int fd, ret;
+
+ if (utimensat(AT_FDCWD, file, times, 0) == 0)
+ return;
+ if (errno != ENOENT)
+ eprintf("utimensat %s:", file);
+ if (cflag)
+ return;
+ if ((fd = open(file, O_WRONLY | O_CREAT | O_EXCL, 0666)) < 0)
+ eprintf("open %s:", file);
+ ret = futimens(fd, times);
+ close(fd);
+ if (ret < 0)
+ eprintf("futimens %s:", file);
+}
+
+static time_t
+parsetime(char *str)
+{
+ time_t now;
+ struct tm *cur, t = { 0 };
+ int zulu = 0;
+ char *format;
+ size_t len = strlen(str);
+
+ if ((now = time(NULL)) == -1)
+ eprintf("time:");
+ if (!(cur = localtime(&now)))
+ eprintf("localtime:");
+ t.tm_isdst = -1;
+
+ switch (len) {
+ /* -t flag argument */
+ case 8:
+ t.tm_year = cur->tm_year;
+ format = "%m%d%H%M";
+ break;
+ case 10:
+ format = "%y%m%d%H%M";
+ break;
+ case 11:
+ t.tm_year = cur->tm_year;
+ format = "%m%d%H%M.%S";
+ break;
+ case 12:
+ format = "%Y%m%d%H%M";
+ break;
+ case 13:
+ format = "%y%m%d%H%M.%S";
+ break;
+ case 15:
+ format = "%Y%m%d%H%M.%S";
+ break;
+ /* -d flag argument */
+ case 19:
+ format = "%Y-%m-%dT%H:%M:%S";
+ break;
+ case 20:
+ /* only Zulu-timezone supported */
+ if (str[19] != 'Z')
+ eprintf("Invalid time zone\n");
+ str[19] = 0;
+ zulu = 1;
+ format = "%Y-%m-%dT%H:%M:%S";
+ break;
+ default:
+ eprintf("Invalid date format length\n", str);
+ }
+
+ if (!strptime(str, format, &t))
+ eprintf("strptime %s: Invalid date format\n", str);
+ if (zulu) {
+ t.tm_hour += t.tm_gmtoff / 60;
+ t.tm_gmtoff = 0;
+ t.tm_zone = "Z";
+ }
+
+ return mktime(&t);
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-acm] [-d time | -r ref_file | -t time | -T time] "
+ "file ...\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct stat st;
+ char *ref = NULL;
+
+ ARGBEGIN {
+ case 'a':
+ aflag = 1;
+ break;
+ case 'c':
+ cflag = 1;
+ break;
+ case 'd':
+ case 't':
+ times[0].tv_sec = parsetime(EARGF(usage()));
+ times[0].tv_nsec = 0;
+ break;
+ case 'm':
+ mflag = 1;
+ break;
+ case 'r':
+ ref = EARGF(usage());
+ if (stat(ref, &st) < 0)
+ eprintf("stat '%s':", ref);
+ times[0] = st.st_atim;
+ times[1] = st.st_mtim;
+ break;
+ case 'T':
+ times[0].tv_sec = estrtonum(EARGF(usage()), 0, LLONG_MAX);
+ times[0].tv_nsec = 0;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc)
+ usage();
+ if (!aflag && !mflag)
+ aflag = mflag = 1;
+ if (!ref)
+ times[1] = times[0];
+ if (!aflag)
+ times[0].tv_nsec = UTIME_OMIT;
+ if (!mflag)
+ times[1].tv_nsec = UTIME_OMIT;
+
+ for (; *argv; argc--, argv++)
+ touch(*argv);
+
+ return 0;
+}
diff --git a/util/sbase/tr.1 b/util/sbase/tr.1
new file mode 100644
index 00000000..087bd4bf
--- /dev/null
+++ b/util/sbase/tr.1
@@ -0,0 +1,84 @@
+.Dd October 5, 2016
+.Dt TR 1
+.Os sbase
+.Sh NAME
+.Nm tr
+.Nd translate characters
+.Sh SYNOPSIS
+.Nm
+.Op Fl c | Fl C
+.Op Fl sd
+.Ar set1 set2
+.Sh DESCRIPTION
+.Nm
+matches characters from stdin and performs translations to stdout.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl c | Fl C
+Match to
+.Ar set1
+complement.
+.It Fl d
+Delete characters matching
+.Ar set1 .
+.It Fl s
+Squeeze repeated characters matching
+.Ar set1
+or
+.Ar set2
+if
+.Fl d
+is set.
+.El
+.Sh SET
+.Bl -tag -width Ds
+.It Literal Sy c
+.It Escape sequence Sy \ec
+\e\e, \e', \e", \ea, \eb, \ee, \ef, \en, \er, \et, \ev, \exH[H], \eO[OO]
+.It Range Sy c-d
+.It Repeat Sy [c*n]
+Only in
+.Ar set2 .
+If n = 0 or left out, set n to length of
+.Ar set1 .
+.It Character class Sy [:class:]
+See
+.Xr wctype 3 .
+.It Equivalence class Sy [=c=]
+Resolve to
+.Sy c .
+.El
+.Sh TRANSLATION
+If
+.Fl d
+is not set,
+.Nm
+translates from
+.Ar set1
+to
+.Ar set2
+by index or character class.
+.Pp
+If
+.Ar set2
+is shorter than
+.Ar set1
+or
+.Ar set1
+is a character class,
+overflowing characters translate to the last character in
+.Ar set2 .
+.Sh EXIT STATUS
+.Bl -tag -width Ds
+.It 0
+Input processed successfully.
+.It 1
+An error occurred.
+.El
+.Sh SEE ALSO
+.Xr awk 1 ,
+.Xr sed 1 ,
+.Xr utf8 7
+.Sh STANDARDS
+POSIX.1-2013.
+Except from equivalence classes.
diff --git a/util/sbase/tr.c b/util/sbase/tr.c
new file mode 100644
index 00000000..c96dbdd3
--- /dev/null
+++ b/util/sbase/tr.c
@@ -0,0 +1,300 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdlib.h>
+
+#include "utf.h"
+#include "util.h"
+
+static int cflag = 0;
+static int dflag = 0;
+static int sflag = 0;
+
+struct range {
+ Rune start;
+ Rune end;
+ size_t quant;
+};
+
+static struct {
+ char *name;
+ int (*check)(Rune);
+} classes[] = {
+ { "alnum", isalnumrune },
+ { "alpha", isalpharune },
+ { "blank", isblankrune },
+ { "cntrl", iscntrlrune },
+ { "digit", isdigitrune },
+ { "graph", isgraphrune },
+ { "lower", islowerrune },
+ { "print", isprintrune },
+ { "punct", ispunctrune },
+ { "space", isspacerune },
+ { "upper", isupperrune },
+ { "xdigit", isxdigitrune },
+};
+
+static struct range *set1 = NULL;
+static size_t set1ranges = 0;
+static int (*set1check)(Rune) = NULL;
+static struct range *set2 = NULL;
+static size_t set2ranges = 0;
+static int (*set2check)(Rune) = NULL;
+
+static size_t
+rangelen(struct range r)
+{
+ return (r.end - r.start + 1) * r.quant;
+}
+
+static size_t
+setlen(struct range *set, size_t setranges)
+{
+ size_t len = 0, i;
+
+ for (i = 0; i < setranges; i++)
+ len += rangelen(set[i]);
+
+ return len;
+}
+
+static int
+rstrmatch(Rune *r, char *s, size_t n)
+{
+ size_t i;
+
+ for (i = 0; i < n; i++)
+ if (r[i] != s[i])
+ return 0;
+ return 1;
+}
+
+static size_t
+makeset(char *str, struct range **set, int (**check)(Rune))
+{
+ Rune *rstr;
+ size_t len, i, j, m, n;
+ size_t q, setranges = 0;
+ int factor, base;
+
+ /* rstr defines at most len ranges */
+ unescape(str);
+ rstr = ereallocarray(NULL, utflen(str) + 1, sizeof(*rstr));
+ len = utftorunestr(str, rstr);
+ *set = ereallocarray(NULL, len, sizeof(**set));
+
+ for (i = 0; i < len; i++) {
+ if (rstr[i] == '[') {
+ j = i;
+nextbrack:
+ if (j >= len)
+ goto literal;
+ for (m = j; m < len; m++)
+ if (rstr[m] == ']') {
+ j = m;
+ break;
+ }
+ if (j == i)
+ goto literal;
+
+ /* CLASSES [=EQUIV=] (skip) */
+ if (j - i > 3 && rstr[i + 1] == '=' && rstr[m - 1] == '=') {
+ if (j - i != 4)
+ goto literal;
+ (*set)[setranges].start = rstr[i + 2];
+ (*set)[setranges].end = rstr[i + 2];
+ (*set)[setranges].quant = 1;
+ setranges++;
+ i = j;
+ continue;
+ }
+
+ /* CLASSES [:CLASS:] */
+ if (j - i > 3 && rstr[i + 1] == ':' && rstr[m - 1] == ':') {
+ for (n = 0; n < LEN(classes); n++) {
+ if (rstrmatch(rstr + i + 2, classes[n].name, j - i - 3)) {
+ *check = classes[n].check;
+ return 0;
+ }
+ }
+ eprintf("Invalid character class.\n");
+ }
+
+ /* REPEAT [_*n] (only allowed in set2) */
+ if (j - i > 2 && rstr[i + 2] == '*') {
+ /* check if right side of '*' is a number */
+ q = 0;
+ factor = 1;
+ base = (rstr[i + 3] == '0') ? 8 : 10;
+ for (n = j - 1; n > i + 2; n--) {
+ if (rstr[n] < '0' || rstr[n] > '9') {
+ n = 0;
+ break;
+ }
+ q += (rstr[n] - '0') * factor;
+ factor *= base;
+ }
+ if (n == 0) {
+ j = m + 1;
+ goto nextbrack;
+ }
+ (*set)[setranges].start = rstr[i + 1];
+ (*set)[setranges].end = rstr[i + 1];
+ (*set)[setranges].quant = q ? q : setlen(set1, MAX(set1ranges, 1));
+ setranges++;
+ i = j;
+ continue;
+ }
+
+ j = m + 1;
+ goto nextbrack;
+ }
+literal:
+ /* RANGES [_-__-_], _-__-_ */
+ /* LITERALS _______ */
+ (*set)[setranges].start = rstr[i];
+
+ if (i < len - 2 && rstr[i + 1] == '-' && rstr[i + 2] >= rstr[i])
+ i += 2;
+ (*set)[setranges].end = rstr[i];
+ (*set)[setranges].quant = 1;
+ setranges++;
+ }
+
+ free(rstr);
+ return setranges;
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-cCds] set1 [set2]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ Rune r, lastrune = 0;
+ size_t off1, off2, i, m;
+ int ret = 0;
+
+ ARGBEGIN {
+ case 'c':
+ case 'C':
+ cflag = 1;
+ break;
+ case 'd':
+ dflag = 1;
+ break;
+ case 's':
+ sflag = 1;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc || argc > 2 || (argc == 1 && dflag == sflag))
+ usage();
+ set1ranges = makeset(argv[0], &set1, &set1check);
+ if (argc == 2)
+ set2ranges = makeset(argv[1], &set2, &set2check);
+
+ if (!dflag || (argc == 2 && sflag)) {
+ /* sanity checks as we are translating */
+ if (!sflag && !set2ranges && !set2check)
+ eprintf("cannot map to an empty set.\n");
+ if (set2check && set2check != islowerrune &&
+ set2check != isupperrune) {
+ eprintf("can only map to 'lower' and 'upper' class.\n");
+ }
+ }
+read:
+ if (!efgetrune(&r, stdin, "<stdin>")) {
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+ return ret;
+ }
+ if (argc == 1 && sflag)
+ goto write;
+ for (i = 0, off1 = 0; i < set1ranges; off1 += rangelen(set1[i]), i++) {
+ if (set1[i].start <= r && r <= set1[i].end) {
+ if (dflag) {
+ if (cflag)
+ goto write;
+ else
+ goto read;
+ }
+ if (cflag)
+ goto write;
+
+ /* map r to set2 */
+ if (set2check) {
+ if (set2check == islowerrune)
+ r = tolowerrune(r);
+ else
+ r = toupperrune(r);
+ } else {
+ off1 += r - set1[i].start;
+ if (off1 > setlen(set2, set2ranges) - 1) {
+ r = set2[set2ranges - 1].end;
+ goto write;
+ }
+ for (m = 0, off2 = 0; m < set2ranges; m++) {
+ if (off2 + rangelen(set2[m]) > off1) {
+ m++;
+ break;
+ }
+ off2 += rangelen(set2[m]);
+ }
+ m--;
+ r = set2[m].start + (off1 - off2) / set2[m].quant;
+ }
+ goto write;
+ }
+ }
+ if (set1check && set1check(r)) {
+ if (cflag)
+ goto write;
+ if (dflag)
+ goto read;
+ if (set2check) {
+ if (set2check == islowerrune)
+ r = tolowerrune(r);
+ else
+ r = toupperrune(r);
+ } else {
+ r = set2[set2ranges - 1].end;
+ }
+ goto write;
+ }
+ if (!dflag && cflag) {
+ if (set2check) {
+ if (set2check == islowerrune)
+ r = tolowerrune(r);
+ else
+ r = toupperrune(r);
+ } else {
+ r = set2[set2ranges - 1].end;
+ }
+ goto write;
+ }
+ if (dflag && cflag)
+ goto read;
+write:
+ if (argc == 1 && sflag && r == lastrune) {
+ if (set1check && set1check(r))
+ goto read;
+ for (i = 0; i < set1ranges; i++) {
+ if (set1[i].start <= r && r <= set1[i].end)
+ goto read;
+ }
+ }
+ if (argc == 2 && sflag && r == lastrune) {
+ if (set2check && set2check(r))
+ goto read;
+ for (i = 0; i < set2ranges; i++) {
+ if (set2[i].start <= r && r <= set2[i].end)
+ goto read;
+ }
+ }
+ efputrune(&r, stdout, "<stdout>");
+ lastrune = r;
+ goto read;
+}
diff --git a/util/sbase/true.1 b/util/sbase/true.1
new file mode 100644
index 00000000..7c2104a6
--- /dev/null
+++ b/util/sbase/true.1
@@ -0,0 +1,13 @@
+.Dd October 8, 2015
+.Dt TRUE 1
+.Os sbase
+.Sh NAME
+.Nm true
+.Nd return success
+.Sh SYNOPSIS
+.Nm
+.Sh DESCRIPTION
+.Nm
+returns a status code indicating success.
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/true.c b/util/sbase/true.c
new file mode 100644
index 00000000..cb081ec0
--- /dev/null
+++ b/util/sbase/true.c
@@ -0,0 +1,6 @@
+/* See LICENSE file for copyright and license details. */
+int
+main(void)
+{
+ return 0;
+}
diff --git a/util/sbase/tsort.1 b/util/sbase/tsort.1
new file mode 100644
index 00000000..b2e0c6ae
--- /dev/null
+++ b/util/sbase/tsort.1
@@ -0,0 +1,70 @@
+.Dd February 16, 2016
+.Dt TSORT 1
+.Os sbase
+.Sh NAME
+.Nm tsort
+.Nd topological sort
+.Sh SYNOPSIS
+.Nm
+.Op Ar file
+.Sh DESCRIPTION
+.Nm
+topologically sorts a graph.
+The graph is read either from
+.Ar file
+or from standard input.
+The result is not optimized for any particular usage.
+Loops are detected and reported to standard error, but does not stop the
+sort.
+.Pp
+The input is a list of edges (vertex pairs), where
+the edge is directed from the first vertex to the
+second vertex.
+.Sh OPTIONS
+None.
+.Sh EXIT STATUS
+.Bl -tag -width Ds
+.It 0
+The graph as successfully sorted.
+.It 1
+The graph as successfully sorted, but contained loops.
+.It > 1
+An error occurred.
+.El
+.Sh EXAMPLES
+.Bd -literal -offset left
+The input
+
+ a a
+ a b
+ a c
+ a c
+ a d
+ b c
+ c b
+ e f
+
+or equivalently
+
+ a a a b a c a c a d
+ b c c b e f
+
+represents the graph
+
+ ┌─┐
+ ↓ │
+ ┏━━━┓
+ ┌──────┃ a ┃──────┐
+ │ ┗━━━┛ │
+ │ │ │ │
+ ↓ ↓ ↓ ↓
+ ┏━━━┓───→┏━━━┓ ┏━━━┓
+ ┃ b ┃ ┃ c ┃ ┃ d ┃
+ ┗━━━┛←───┗━━━┛ ┗━━━┛
+
+ ┏━━━┓ ┏━━━┓
+ ┃ e ┃───→┃ f ┃
+ ┗━━━┛ ┗━━━┛
+.Ed
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/tsort.c b/util/sbase/tsort.c
new file mode 100644
index 00000000..f147e3b2
--- /dev/null
+++ b/util/sbase/tsort.c
@@ -0,0 +1,209 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <ctype.h>
+
+#include "util.h"
+
+enum { WHITE = 0, GREY, BLACK };
+
+struct vertex;
+
+struct edge {
+ struct vertex *to;
+ struct edge *next;
+};
+
+struct vertex {
+ char *name;
+ struct vertex *next;
+ struct edge edges;
+ size_t in_edges;
+ int colour;
+};
+
+static struct vertex graph;
+
+static void
+find_vertex(const char *name, struct vertex **it, struct vertex **prev)
+{
+ for (*prev = &graph; (*it = (*prev)->next); *prev = *it) {
+ int cmp = strcmp(name, (*it)->name);
+ if (cmp > 0)
+ continue;
+ if (cmp < 0)
+ *it = 0;
+ return;
+ }
+}
+
+static void
+find_edge(struct vertex *from, const char *to, struct edge **it, struct edge **prev)
+{
+ for (*prev = &(from->edges); (*it = (*prev)->next); *prev = *it) {
+ int cmp = strcmp(to, (*it)->to->name);
+ if (cmp > 0)
+ continue;
+ if (cmp < 0)
+ *it = 0;
+ return;
+ }
+}
+
+static struct vertex *
+add_vertex(char *name)
+{
+ struct vertex *vertex;
+ struct vertex *prev;
+
+ find_vertex(name, &vertex, &prev);
+ if (vertex)
+ return vertex;
+
+ vertex = encalloc(2, 1, sizeof(*vertex));
+ vertex->name = name;
+ vertex->next = prev->next;
+ prev->next = vertex;
+
+ return vertex;
+}
+
+static struct edge *
+add_edge(struct vertex *from, struct vertex* to)
+{
+ struct edge *edge;
+ struct edge *prev;
+
+ find_edge(from, to->name, &edge, &prev);
+ if (edge)
+ return edge;
+
+ edge = encalloc(2, 1, sizeof(*edge));
+ edge->to = to;
+ edge->next = prev->next;
+ prev->next = edge;
+ to->in_edges += 1;
+
+ return edge;
+}
+
+static void
+load_graph(FILE *fp)
+{
+#define SKIP(VAR, START, FUNC) for (VAR = START; FUNC(*VAR) && *VAR; VAR++)
+#define TOKEN_END(P) do { if (*P) *P++ = 0; else P = 0; } while (0)
+
+ char *line = 0;
+ size_t size = 0;
+ ssize_t len;
+ char *p;
+ char *name;
+ struct vertex *from = 0;
+
+ while ((len = getline(&line, &size, fp)) != -1) {
+ if (line[len - 1] == '\n')
+ line[--len] = 0;
+ for (p = line; p;) {
+ SKIP(name, p, isspace);
+ if (!*name)
+ break;
+ SKIP(p, name, !isspace);
+ TOKEN_END(p);
+ if (!from) {
+ from = add_vertex(enstrdup(2, name));
+ } else if (strcmp(from->name, name)) {
+ add_edge(from, add_vertex(enstrdup(2, name)));
+ from = 0;
+ } else {
+ from = 0;
+ }
+ }
+ }
+
+ free(line);
+
+ if (from)
+ enprintf(2, "odd number of tokens in input\n");
+}
+
+static int
+sort_graph_visit(struct vertex *u)
+{
+ struct edge *e = &(u->edges);
+ struct vertex *v;
+ int r = 0;
+ u->colour = GREY;
+ printf("%s\n", u->name);
+ while ((e = e->next)) {
+ v = e->to;
+ if (v->colour == WHITE) {
+ v->in_edges -= 1;
+ if (v->in_edges == 0)
+ r |= sort_graph_visit(v);
+ } else if (v->colour == GREY) {
+ r = 1;
+ fprintf(stderr, "%s: loop detected between %s and %s\n",
+ argv0, u->name, v->name);
+ }
+ }
+ u->colour = BLACK;
+ return r;
+}
+
+static int
+sort_graph(void)
+{
+ struct vertex *u, *prev;
+ int r = 0;
+ size_t in_edges;
+ for (in_edges = 0; graph.next; in_edges++) {
+ for (prev = &graph; (u = prev->next); prev = u) {
+ if (u->colour != WHITE)
+ goto unlist;
+ if (u->in_edges > in_edges)
+ continue;
+ r |= sort_graph_visit(u);
+ unlist:
+ prev->next = u->next;
+ u = prev;
+ }
+ }
+ return r;
+}
+
+static void
+usage(void)
+{
+ enprintf(2, "usage: %s [file]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ FILE *fp = stdin;
+ const char *fn = "<stdin>";
+ int ret = 0;
+
+ ARGBEGIN {
+ default:
+ usage();
+ } ARGEND
+
+ if (argc > 1)
+ usage();
+ if (argc && strcmp(*argv, "-"))
+ if (!(fp = fopen(fn = *argv, "r")))
+ enprintf(2, "fopen %s:", *argv);
+
+ memset(&graph, 0, sizeof(graph));
+ load_graph(fp);
+ enfshut(2, fp, fn);
+
+ ret = sort_graph();
+
+ if (fshut(stdout, "<stdout>") | fshut(stderr, "<stderr>"))
+ ret = 2;
+
+ return ret;
+}
diff --git a/util/sbase/tty.1 b/util/sbase/tty.1
new file mode 100644
index 00000000..11580e32
--- /dev/null
+++ b/util/sbase/tty.1
@@ -0,0 +1,24 @@
+.Dd October 8, 2015
+.Dt TTY 1
+.Os sbase
+.Sh NAME
+.Nm tty
+.Nd print terminal name
+.Sh SYNOPSIS
+.Nm
+.Sh DESCRIPTION
+.Nm
+writes the name of the terminal open on stdin to stdout.
+.Sh EXIT STATUS
+.Bl -tag -width Ds
+.It 0
+stdin is a terminal.
+.It 1
+stdin is not a terminal.
+.It > 1
+An error occurred.
+.El
+.Sh SEE ALSO
+.Xr ttyname 3
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/tty.c b/util/sbase/tty.c
new file mode 100644
index 00000000..65151287
--- /dev/null
+++ b/util/sbase/tty.c
@@ -0,0 +1,31 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdio.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ enprintf(2, "usage: %s\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ char *tty;
+
+ ARGBEGIN {
+ default:
+ usage();
+ } ARGEND
+
+ if (argc)
+ usage();
+
+ tty = ttyname(STDIN_FILENO);
+ puts(tty ? tty : "not a tty");
+
+ enfshut(2, stdout, "<stdout>");
+ return !tty;
+}
diff --git a/util/sbase/uname.1 b/util/sbase/uname.1
new file mode 100644
index 00000000..65c3d318
--- /dev/null
+++ b/util/sbase/uname.1
@@ -0,0 +1,35 @@
+.Dd October 8, 2015
+.Dt UNAME 1
+.Os sbase
+.Sh NAME
+.Nm uname
+.Nd print system information
+.Sh SYNOPSIS
+.Nm
+.Op Fl amnrsv
+.Sh DESCRIPTION
+.Nm
+writes system information to stdout.
+If no flags are given,
+.Nm
+implies
+.Fl s .
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl a
+Print all the information below.
+.It Fl m
+Print the machine's architecture.
+.It Fl n
+Print the system's network node hostname.
+.It Fl r
+Print the operating system's release name.
+.It Fl s
+Print the name of the operating system.
+.It Fl v
+Print the operating system's version name.
+.El
+.Sh SEE ALSO
+.Xr uname 2
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/uname.c b/util/sbase/uname.c
new file mode 100644
index 00000000..122c1721
--- /dev/null
+++ b/util/sbase/uname.c
@@ -0,0 +1,62 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/utsname.h>
+
+#include <stdio.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-amnrsv]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct utsname u;
+ int mflag = 0, nflag = 0, rflag = 0, sflag = 0, vflag = 0;
+
+ ARGBEGIN {
+ case 'a':
+ mflag = nflag = rflag = sflag = vflag = 1;
+ break;
+ case 'm':
+ mflag = 1;
+ break;
+ case 'n':
+ nflag = 1;
+ break;
+ case 'r':
+ rflag = 1;
+ break;
+ case 's':
+ sflag = 1;
+ break;
+ case 'v':
+ vflag = 1;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (argc)
+ usage();
+
+ if (uname(&u) < 0)
+ eprintf("uname:");
+
+ if (sflag || !(nflag || rflag || vflag || mflag))
+ putword(stdout, u.sysname);
+ if (nflag)
+ putword(stdout, u.nodename);
+ if (rflag)
+ putword(stdout, u.release);
+ if (vflag)
+ putword(stdout, u.version);
+ if (mflag)
+ putword(stdout, u.machine);
+ putchar('\n');
+
+ return fshut(stdout, "<stdout>");
+}
diff --git a/util/sbase/unexpand.1 b/util/sbase/unexpand.1
new file mode 100644
index 00000000..1637c090
--- /dev/null
+++ b/util/sbase/unexpand.1
@@ -0,0 +1,41 @@
+.Dd October 8, 2015
+.Dt UNEXPAND 1
+.Os sbase
+.Sh NAME
+.Nm unexpand
+.Nd unexpand spaces to tabs
+.Sh SYNOPSIS
+.Nm
+.Op Fl a
+.Op Fl t Ar tablist
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+converts spaces to tabs in each
+.Ar file
+as specified in
+.Ar tablist .
+If no file is given,
+.Nm
+reads from stdin.
+.Pp
+Backspace characters are preserved and decrement the column count
+for tab calculations.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl a
+Convert spaces to tabs everywhere, not just at the start of lines.
+.It Fl t Ar tablist
+Specify tab size or tabstops.
+.Ar tablist
+is a list of one (in the former case) or multiple (in the latter case)
+strictly positive integers separated by ' ' or ','.
+.Pp
+The default
+.Ar tablist
+is "8".
+.El
+.Sh SEE ALSO
+.Xr expand 1
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/unexpand.c b/util/sbase/unexpand.c
new file mode 100644
index 00000000..1818691e
--- /dev/null
+++ b/util/sbase/unexpand.c
@@ -0,0 +1,174 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "utf.h"
+#include "util.h"
+
+static int aflag = 0;
+static size_t *tablist = NULL;
+static size_t tablistlen = 8;
+
+static size_t
+parselist(const char *s)
+{
+ size_t i;
+ char *p, *tmp;
+
+ tmp = estrdup(s);
+ for (i = 0; (p = strsep(&tmp, " ,")); i++) {
+ if (*p == '\0')
+ eprintf("empty field in tablist\n");
+ tablist = ereallocarray(tablist, i + 1, sizeof(*tablist));
+ tablist[i] = estrtonum(p, 1, MIN(LLONG_MAX, SIZE_MAX));
+ if (i > 0 && tablist[i - 1] >= tablist[i])
+ eprintf("tablist must be ascending\n");
+ }
+ tablist = ereallocarray(tablist, i + 1, sizeof(*tablist));
+
+ return i;
+}
+
+static void
+unexpandspan(size_t last, size_t col)
+{
+ size_t off, i, j;
+ Rune r;
+
+ if (tablistlen == 1) {
+ i = 0;
+ off = last % tablist[i];
+
+ if ((col - last) + off >= tablist[i] && last < col)
+ last -= off;
+
+ r = '\t';
+ for (; last + tablist[i] <= col; last += tablist[i])
+ efputrune(&r, stdout, "<stdout>");
+ r = ' ';
+ for (; last < col; last++)
+ efputrune(&r, stdout, "<stdout>");
+ } else {
+ for (i = 0; i < tablistlen; i++)
+ if (col < tablist[i])
+ break;
+ for (j = 0; j < tablistlen; j++)
+ if (last < tablist[j])
+ break;
+ r = '\t';
+ for (; j < i; j++) {
+ efputrune(&r, stdout, "<stdout>");
+ last = tablist[j];
+ }
+ r = ' ';
+ for (; last < col; last++)
+ efputrune(&r, stdout, "<stdout>");
+ }
+}
+
+static void
+unexpand(const char *file, FILE *fp)
+{
+ Rune r;
+ size_t last = 0, col = 0, i;
+ int bol = 1;
+
+ while (efgetrune(&r, fp, file)) {
+ switch (r) {
+ case ' ':
+ if (!bol && !aflag)
+ last++;
+ col++;
+ break;
+ case '\t':
+ if (tablistlen == 1) {
+ if (!bol && !aflag)
+ last += tablist[0] - col % tablist[0];
+ col += tablist[0] - col % tablist[0];
+ } else {
+ for (i = 0; i < tablistlen; i++)
+ if (col < tablist[i])
+ break;
+ if (!bol && !aflag)
+ last = tablist[i];
+ col = tablist[i];
+ }
+ break;
+ case '\b':
+ if (bol || aflag)
+ unexpandspan(last, col);
+ col -= (col > 0);
+ last = col;
+ bol = 0;
+ break;
+ case '\n':
+ if (bol || aflag)
+ unexpandspan(last, col);
+ last = col = 0;
+ bol = 1;
+ break;
+ default:
+ if (bol || aflag)
+ unexpandspan(last, col);
+ last = ++col;
+ bol = 0;
+ break;
+ }
+ if ((r != ' ' && r != '\t') || (!aflag && !bol))
+ efputrune(&r, stdout, "<stdout>");
+ }
+ if (last < col && (bol || aflag))
+ unexpandspan(last, col);
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-a] [-t tablist] [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ FILE *fp;
+ int ret = 0;
+ char *tl = "8";
+
+ ARGBEGIN {
+ case 't':
+ tl = EARGF(usage());
+ if (!*tl)
+ eprintf("tablist cannot be empty\n");
+ /* Fallthrough: -t implies -a */
+ case 'a':
+ aflag = 1;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ tablistlen = parselist(tl);
+
+ if (!argc) {
+ unexpand("<stdin>", stdin);
+ } else {
+ for (; *argv; argc--, argv++) {
+ if (!strcmp(*argv, "-")) {
+ *argv = "<stdin>";
+ fp = stdin;
+ } else if (!(fp = fopen(*argv, "r"))) {
+ weprintf("fopen %s:", *argv);
+ ret = 1;
+ continue;
+ }
+ unexpand(*argv, fp);
+ if (fp != stdin && fshut(fp, *argv))
+ ret = 1;
+ }
+ }
+
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/uniq.1 b/util/sbase/uniq.1
new file mode 100644
index 00000000..8ed1a015
--- /dev/null
+++ b/util/sbase/uniq.1
@@ -0,0 +1,45 @@
+.Dd October 8, 2015
+.Dt UNIQ 1
+.Os sbase
+.Sh NAME
+.Nm uniq
+.Nd report or filter out repeated lines in a file
+.Sh SYNOPSIS
+.Nm
+.Op Fl c
+.Op Fl d | u
+.Op Fl f Ar num
+.Op Fl s Ar num
+.Op Ar input Op Ar output
+.Sh DESCRIPTION
+.Nm
+reads the
+.Ar input
+file and writes one copy of a line from each group of consecutive
+duplicate lines to the
+.Ar output
+file.
+If no
+.Ar input
+file is given
+.Nm
+reads from stdin.
+If no
+.Ar output
+file is given
+.Nm
+writes to stdout.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl c
+Prefix each line with the number of consecutive occurrences in
+.Ar input .
+.It Fl d | Fl u
+Print duplicate | unique lines only.
+.It Fl f Ar num | Fl s Ar num
+Ignore the first
+.Ar num
+fields | characters in each input line when doing comparisons.
+.El
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/uniq.c b/util/sbase/uniq.c
new file mode 100644
index 00000000..f1ad6a7b
--- /dev/null
+++ b/util/sbase/uniq.c
@@ -0,0 +1,144 @@
+/* See LICENSE file for copyright and license details. */
+#include <ctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "text.h"
+#include "util.h"
+
+static const char *countfmt = "";
+static int dflag = 0;
+static int uflag = 0;
+static int fskip = 0;
+static int sskip = 0;
+
+static struct line prevl;
+static ssize_t prevoff = -1;
+static long prevlinecount = 0;
+
+static size_t
+uniqskip(struct line *l)
+{
+ size_t i;
+ int f = fskip, s = sskip;
+
+ for (i = 0; i < l->len && f; --f) {
+ while (isblank(l->data[i]))
+ i++;
+ while (i < l->len && !isblank(l->data[i]))
+ i++;
+ }
+ for (; s && i < l->len && l->data[i] != '\n'; --s, i++)
+ ;
+
+ return i;
+}
+
+static void
+uniqline(FILE *ofp, struct line *l)
+{
+ size_t loff;
+
+ if (l) {
+ loff = uniqskip(l);
+
+ if (prevoff >= 0 && (l->len - loff) == (prevl.len - prevoff) &&
+ !memcmp(l->data + loff, prevl.data + prevoff, l->len - loff)) {
+ ++prevlinecount;
+ return;
+ }
+ }
+
+ if (prevoff >= 0) {
+ if ((prevlinecount == 1 && !dflag) ||
+ (prevlinecount != 1 && !uflag)) {
+ if (*countfmt)
+ fprintf(ofp, countfmt, prevlinecount);
+ fwrite(prevl.data, 1, prevl.len, ofp);
+ }
+ prevoff = -1;
+ }
+
+ if (l) {
+ if (!prevl.data || l->len >= prevl.len) {
+ prevl.data = erealloc(prevl.data, l->len);
+ }
+ prevl.len = l->len;
+ memcpy(prevl.data, l->data, prevl.len);
+ prevoff = loff;
+ }
+ prevlinecount = 1;
+}
+
+static void
+uniq(FILE *fp, FILE *ofp)
+{
+ static struct line line;
+ static size_t size;
+ ssize_t len;
+
+ while ((len = getline(&line.data, &size, fp)) > 0) {
+ line.len = len;
+ uniqline(ofp, &line);
+ }
+}
+
+static void
+uniqfinish(FILE *ofp)
+{
+ uniqline(ofp, NULL);
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-c] [-d | -u] [-f fields] [-s chars]"
+ " [input [output]]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ FILE *fp[2] = { stdin, stdout };
+ int ret = 0, i;
+ char *fname[2] = { "<stdin>", "<stdout>" };
+
+ ARGBEGIN {
+ case 'c':
+ countfmt = "%7ld ";
+ break;
+ case 'd':
+ dflag = 1;
+ break;
+ case 'u':
+ uflag = 1;
+ break;
+ case 'f':
+ fskip = estrtonum(EARGF(usage()), 0, INT_MAX);
+ break;
+ case 's':
+ sskip = estrtonum(EARGF(usage()), 0, INT_MAX);
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (argc > 2)
+ usage();
+
+ for (i = 0; i < argc; i++) {
+ if (strcmp(argv[i], "-")) {
+ fname[i] = argv[i];
+ if (!(fp[i] = fopen(argv[i], (i == 0) ? "r" : "w")))
+ eprintf("fopen %s:", argv[i]);
+ }
+ }
+
+ uniq(fp[0], fp[1]);
+ uniqfinish(fp[1]);
+
+ ret |= fshut(fp[0], fname[0]) | fshut(fp[1], fname[1]);
+
+ return ret;
+}
diff --git a/util/sbase/unlink.1 b/util/sbase/unlink.1
new file mode 100644
index 00000000..777c89da
--- /dev/null
+++ b/util/sbase/unlink.1
@@ -0,0 +1,19 @@
+.Dd October 8, 2015
+.Dt UNLINK 1
+.Os sbase
+.Sh NAME
+.Nm unlink
+.Nd unlink file
+.Sh SYNOPSIS
+.Nm
+.Ar file
+.Sh DESCRIPTION
+.Nm
+calls
+.Xr unlink 2
+on
+.Ar file .
+.Sh SEE ALSO
+.Xr unlink 2
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/unlink.c b/util/sbase/unlink.c
new file mode 100644
index 00000000..c695fa80
--- /dev/null
+++ b/util/sbase/unlink.c
@@ -0,0 +1,27 @@
+/* See LICENSE file for copyright and license details. */
+#include <unistd.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s file\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ ARGBEGIN {
+ default:
+ usage();
+ } ARGEND
+
+ if (argc != 1)
+ usage();
+
+ if (unlink(argv[0]) < 0)
+ eprintf("unlink: '%s':", argv[0]);
+
+ return 0;
+}
diff --git a/util/sbase/utf.h b/util/sbase/utf.h
new file mode 100644
index 00000000..8e0707a7
--- /dev/null
+++ b/util/sbase/utf.h
@@ -0,0 +1,69 @@
+/* MIT/X Consortium Copyright (c) 2012 Connor Lane Smith <cls@lubutu.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+#include <stdio.h>
+
+typedef int Rune;
+
+enum {
+ UTFmax = 6, /* maximum bytes per rune */
+ Runeself = 0x80, /* rune and utf are equal (<) */
+ Runeerror = 0xFFFD, /* decoding error in utf */
+ Runemax = 0x10FFFF /* maximum rune value */
+};
+
+int runetochar(char *, const Rune *);
+int chartorune(Rune *, const char *);
+int charntorune(Rune *, const char *, size_t);
+int runelen(Rune);
+size_t runenlen(const Rune *, size_t);
+int fullrune(const char *, size_t);
+char *utfecpy(char *, char *, const char *);
+size_t utflen(const char *);
+size_t utfnlen(const char *, size_t);
+size_t utfmemlen(const char *, size_t);
+char *utfrune(const char *, Rune);
+char *utfrrune(const char *, Rune);
+char *utfutf(const char *, const char *);
+
+int isalnumrune(Rune);
+int isalpharune(Rune);
+int isblankrune(Rune);
+int iscntrlrune(Rune);
+int isdigitrune(Rune);
+int isgraphrune(Rune);
+int islowerrune(Rune);
+int isprintrune(Rune);
+int ispunctrune(Rune);
+int isspacerune(Rune);
+int istitlerune(Rune);
+int isupperrune(Rune);
+int isxdigitrune(Rune);
+
+Rune tolowerrune(Rune);
+Rune toupperrune(Rune);
+
+size_t utftorunestr(const char *, Rune *);
+size_t utfntorunestr(const char *, size_t, Rune *);
+
+int fgetrune(Rune *, FILE *);
+int efgetrune(Rune *, FILE *, const char *);
+int fputrune(const Rune *, FILE *);
+int efputrune(const Rune *, FILE *, const char *);
diff --git a/util/sbase/util.h b/util/sbase/util.h
new file mode 100644
index 00000000..6b6a084b
--- /dev/null
+++ b/util/sbase/util.h
@@ -0,0 +1,97 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/types.h>
+
+#include <regex.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdarg.h>
+
+#include "arg.h"
+#include "compat.h"
+
+#define UTF8_POINT(c) (((c) & 0xc0) != 0x80)
+
+#undef MIN
+#define MIN(x,y) ((x) < (y) ? (x) : (y))
+#undef MAX
+#define MAX(x,y) ((x) > (y) ? (x) : (y))
+#undef LIMIT
+#define LIMIT(x, a, b) (x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x)
+
+#define LEN(x) (sizeof (x) / sizeof *(x))
+
+extern char *argv0;
+
+void *ecalloc(size_t, size_t);
+void *emalloc(size_t);
+void *erealloc(void *, size_t);
+#undef reallocarray
+void *reallocarray(void *, size_t, size_t);
+void *ereallocarray(void *, size_t, size_t);
+char *estrdup(const char *);
+char *estrndup(const char *, size_t);
+void *encalloc(int, size_t, size_t);
+void *enmalloc(int, size_t);
+void *enrealloc(int, void *, size_t);
+void *enreallocarray(int, void *, size_t, size_t);
+char *enstrdup(int, const char *);
+char *enstrndup(int, const char *, size_t);
+
+void enfshut(int, FILE *, const char *);
+void efshut(FILE *, const char *);
+int fshut(FILE *, const char *);
+
+void enprintf(int, const char *, ...);
+void eprintf(const char *, ...);
+void weprintf(const char *, ...);
+void xvprintf(const char *, va_list);
+
+int confirm(const char*, ...);
+
+double estrtod(const char *);
+
+#undef strcasestr
+#define strcasestr xstrcasestr
+char *strcasestr(const char *, const char *);
+
+#undef strlcat
+#define strlcat xstrlcat
+size_t strlcat(char *, const char *, size_t);
+size_t estrlcat(char *, const char *, size_t);
+#undef strlcpy
+#define strlcpy xstrlcpy
+size_t strlcpy(char *, const char *, size_t);
+size_t estrlcpy(char *, const char *, size_t);
+
+#undef strsep
+#define strsep xstrsep
+char *strsep(char **, const char *);
+
+void strnsubst(char **, const char *, const char *, size_t);
+
+/* regex */
+int enregcomp(int, regex_t *, const char *, int);
+int eregcomp(regex_t *, const char *, int);
+
+/* io */
+ssize_t writeall(int, const void *, size_t);
+int concat(int, const char *, int, const char *);
+
+/* misc */
+void enmasse(int, char **, int (*)(const char *, const char *, int));
+void fnck(const char *, const char *, int (*)(const char *, const char *, int), int);
+mode_t getumask(void);
+char *humansize(off_t);
+mode_t parsemode(const char *, mode_t, mode_t);
+off_t parseoffset(const char *);
+void putword(FILE *, const char *);
+#undef strtonum
+#define strtonum xstrtonum
+long long strtonum(const char *, long long, long long, const char **);
+long long enstrtonum(int, const char *, long long, long long);
+long long estrtonum(const char *, long long, long long);
+size_t unescape(char *);
+int mkdirp(const char *, mode_t, mode_t);
+#undef memmem
+#define memmem xmemmem
+void *memmem(const void *, size_t, const void *, size_t);
diff --git a/util/sbase/uudecode.1 b/util/sbase/uudecode.1
new file mode 100644
index 00000000..d6408659
--- /dev/null
+++ b/util/sbase/uudecode.1
@@ -0,0 +1,46 @@
+.Dd October 8, 2015
+.Dt UUDECODE 1
+.Os sbase
+.Sh NAME
+.Nm uudecode
+.Nd decode a uuencoded file
+.Sh SYNOPSIS
+.Nm
+.Op Fl m
+.Op Fl o Ar output
+.Op Ar file
+.Sh DESCRIPTION
+.Nm
+reads
+.Ar file
+and writes a decoded version to the file specified in the uuencoded header.
+In case the file already exists, it is truncated.
+Otherwise a new file is created.
+The permissions of the created/accessed file are changed to reflect the
+mode in the header.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl m
+Use Base64 for decoding.
+.It Fl o Ar output
+Write to
+.Ar output
+rather than the file specified in the header.
+.El
+.Sh IMPLEMENTATION NOTES
+For safety uudecode operates on regular files and stdout only.
+Trying to uudecode to a link, directory, or special file
+yields an error.
+.Sh SEE ALSO
+.Xr uuencode 1
+.Sh STANDARDS
+POSIX.1-2013.
+.Pp
+The
+.Op Fl m
+flag is an extension to that specification.
diff --git a/util/sbase/uudecode.c b/util/sbase/uudecode.c
new file mode 100644
index 00000000..1d0bf72a
--- /dev/null
+++ b/util/sbase/uudecode.c
@@ -0,0 +1,282 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "util.h"
+
+static int mflag = 0;
+static int oflag = 0;
+
+static FILE *
+parsefile(const char *fname)
+{
+ struct stat st;
+ int ret;
+
+ if (!strcmp(fname, "/dev/stdout") || !strcmp(fname, "-"))
+ return stdout;
+ ret = lstat(fname, &st);
+ /* if it is a new file, try to open it */
+ if (ret < 0 && errno == ENOENT)
+ goto tropen;
+ if (ret < 0) {
+ weprintf("lstat %s:", fname);
+ return NULL;
+ }
+ if (!S_ISREG(st.st_mode)) {
+ weprintf("for safety uudecode operates only on regular files and /dev/stdout\n");
+ return NULL;
+ }
+tropen:
+ return fopen(fname, "w");
+}
+
+static void
+parseheader(FILE *fp, const char *s, char **header, mode_t *mode, char **fname)
+{
+ static char bufs[PATH_MAX + 18]; /* len header + mode + maxname */
+ char *p, *q;
+ size_t n;
+
+ if (!fgets(bufs, sizeof(bufs), fp))
+ if (ferror(fp))
+ eprintf("%s: read error:", s);
+ if (bufs[0] == '\0' || feof(fp))
+ eprintf("empty or nil header string\n");
+ if (!(p = strchr(bufs, '\n')))
+ eprintf("header string too long or non-newline terminated file\n");
+ p = bufs;
+ if (!(q = strchr(p, ' ')))
+ eprintf("malformed mode string in header, expected ' '\n");
+ *header = bufs;
+ *q++ = '\0';
+ p = q;
+ /* now header should be null terminated, q points to mode */
+ if (!(q = strchr(p, ' ')))
+ eprintf("malformed mode string in header, expected ' '\n");
+ *q++ = '\0';
+ /* now mode should be null terminated, q points to fname */
+ *mode = parsemode(p, *mode, 0);
+ n = strlen(q);
+ while (n > 0 && (q[n - 1] == '\n' || q[n - 1] == '\r'))
+ q[--n] = '\0';
+ if (n > 0)
+ *fname = q;
+ else
+ eprintf("header string does not contain output file\n");
+}
+
+static const char b64dt[] = {
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-2,-2,-2,-2,-2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63,
+ 52,53,54,55,56,57,58,59,60,61,-1,-1,-1, 0,-1,-1,-1, 0, 1, 2, 3, 4, 5, 6,
+ 7, 8, 9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,
+ -1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,
+ 49,50,51,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+};
+
+static void
+uudecodeb64(FILE *fp, FILE *outfp)
+{
+ char bufb[60], *pb;
+ char out[45], *po;
+ size_t n;
+ int b = 0, e, t = -1, l = 1;
+ unsigned char b24[3] = {0, 0, 0};
+
+ while ((n = fread(bufb, 1, sizeof(bufb), fp))) {
+ for (pb = bufb, po = out; pb < bufb + n; pb++) {
+ if (*pb == '\n') {
+ l++;
+ continue;
+ } else if (*pb == '=') {
+ switch (b) {
+ case 0:
+ /* expected '=' remaining
+ * including footer */
+ if (--t) {
+ fwrite(out, 1,
+ (po - out),
+ outfp);
+ return;
+ }
+ continue;
+ case 1:
+ eprintf("%d: unexpected \"=\""
+ "appeared\n", l);
+ case 2:
+ *po++ = b24[0];
+ b = 0;
+ t = 5; /* expect 5 '=' */
+ continue;
+ case 3:
+ *po++ = b24[0];
+ *po++ = b24[1];
+ b = 0;
+ t = 6; /* expect 6 '=' */
+ continue;
+ }
+ } else if ((e = b64dt[(int)*pb]) == -1)
+ eprintf("%d: invalid byte \"%c\"\n", l, *pb);
+ else if (e == -2) /* whitespace */
+ continue;
+ else if (t > 0) /* state is parsing pad/footer */
+ eprintf("%d: invalid byte \"%c\""
+ " after padding\n",
+ l, *pb);
+ switch (b) { /* decode next base64 chr based on state */
+ case 0: b24[0] |= e << 2; break;
+ case 1: b24[0] |= (e >> 4) & 0x3;
+ b24[1] |= (e & 0xf) << 4; break;
+ case 2: b24[1] |= (e >> 2) & 0xf;
+ b24[2] |= (e & 0x3) << 6; break;
+ case 3: b24[2] |= e; break;
+ }
+ if (++b == 4) { /* complete decoding an octet */
+ *po++ = b24[0];
+ *po++ = b24[1];
+ *po++ = b24[2];
+ b24[0] = b24[1] = b24[2] = 0;
+ b = 0;
+ }
+ }
+ fwrite(out, 1, (po - out), outfp);
+ }
+ eprintf("%d: invalid uudecode footer \"====\" not found\n", l);
+}
+
+static void
+uudecode(FILE *fp, FILE *outfp)
+{
+ char *bufb = NULL, *p;
+ size_t n = 0;
+ ssize_t len;
+ int ch, i;
+
+#define DEC(c) (((c) - ' ') & 077) /* single character decode */
+#define IS_DEC(c) ( (((c) - ' ') >= 0) && (((c) - ' ') <= 077 + 1) )
+#define OUT_OF_RANGE(c) eprintf("character %c out of range: [%d-%d]\n", (c), 1 + ' ', 077 + ' ' + 1)
+
+ while ((len = getline(&bufb, &n, fp)) > 0) {
+ p = bufb;
+ /* trim newlines */
+ if (!len || bufb[len - 1] != '\n')
+ eprintf("no newline found, aborting\n");
+ bufb[len - 1] = '\0';
+
+ /* check for last line */
+ if ((i = DEC(*p)) <= 0)
+ break;
+ for (++p; i > 0; p += 4, i -= 3) {
+ if (i >= 3) {
+ if (!(IS_DEC(*p) && IS_DEC(*(p + 1)) &&
+ IS_DEC(*(p + 2)) && IS_DEC(*(p + 3))))
+ OUT_OF_RANGE(*p);
+
+ ch = DEC(p[0]) << 2 | DEC(p[1]) >> 4;
+ putc(ch, outfp);
+ ch = DEC(p[1]) << 4 | DEC(p[2]) >> 2;
+ putc(ch, outfp);
+ ch = DEC(p[2]) << 6 | DEC(p[3]);
+ putc(ch, outfp);
+ } else {
+ if (i >= 1) {
+ if (!(IS_DEC(*p) && IS_DEC(*(p + 1))))
+ OUT_OF_RANGE(*p);
+
+ ch = DEC(p[0]) << 2 | DEC(p[1]) >> 4;
+ putc(ch, outfp);
+ }
+ if (i >= 2) {
+ if (!(IS_DEC(*(p + 1)) &&
+ IS_DEC(*(p + 2))))
+ OUT_OF_RANGE(*p);
+
+ ch = DEC(p[1]) << 4 | DEC(p[2]) >> 2;
+ putc(ch, outfp);
+ }
+ }
+ }
+ if (ferror(fp))
+ eprintf("read error:");
+ }
+ /* check for end or fail */
+ if ((len = getline(&bufb, &n, fp)) < 0)
+ eprintf("getline:");
+ if (len < 3 || strncmp(bufb, "end", 3) || bufb[3] != '\n')
+ eprintf("invalid uudecode footer \"end\" not found\n");
+ free(bufb);
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-m] [-o output] [file]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ FILE *fp = NULL, *nfp = NULL;
+ mode_t mode = 0;
+ int ret = 0;
+ char *fname, *header, *ifname, *ofname = NULL;
+ void (*d) (FILE *, FILE *) = NULL;
+
+ ARGBEGIN {
+ case 'm':
+ mflag = 1; /* accepted but unused (autodetect file type) */
+ break;
+ case 'o':
+ oflag = 1;
+ ofname = EARGF(usage());
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (argc > 1)
+ usage();
+
+ if (!argc || !strcmp(argv[0], "-")) {
+ fp = stdin;
+ ifname = "<stdin>";
+ } else {
+ if (!(fp = fopen(argv[0], "r")))
+ eprintf("fopen %s:", argv[0]);
+ ifname = argv[0];
+ }
+
+ parseheader(fp, ifname, &header, &mode, &fname);
+
+ if (!strncmp(header, "begin", sizeof("begin")))
+ d = uudecode;
+ else if (!strncmp(header, "begin-base64", sizeof("begin-base64")))
+ d = uudecodeb64;
+ else
+ eprintf("unknown header %s:", header);
+
+ if (oflag)
+ fname = ofname;
+ if (!(nfp = parsefile(fname)))
+ eprintf("fopen %s:", fname);
+
+ d(fp, nfp);
+
+ if (nfp != stdout && chmod(fname, mode) < 0)
+ eprintf("chmod %s:", fname);
+
+ ret |= fshut(fp, (fp == stdin) ? "<stdin>" : argv[0]);
+ ret |= fshut(nfp, (nfp == stdout) ? "<stdout>" : fname);
+
+ return ret;
+}
diff --git a/util/sbase/uuencode.1 b/util/sbase/uuencode.1
new file mode 100644
index 00000000..859f4a79
--- /dev/null
+++ b/util/sbase/uuencode.1
@@ -0,0 +1,34 @@
+.Dd October 8, 2015
+.Dt UUENCODE 1
+.Os sbase
+.Sh NAME
+.Nm uuencode
+.Nd encode a binary file
+.Sh SYNOPSIS
+.Nm
+.Op Fl m
+.Op Ar file
+.Ar name
+.Sh DESCRIPTION
+.Nm
+reads
+.Ar file
+and writes an encoded version to stdout.
+The encoding uses only printing ASCII characters and
+includes the mode of the file and the operand
+.Ar name
+for use by uudecode.
+If no
+.Ar name
+is given
+.Nm
+reads from stdin.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl m
+Use Base64 for encoding.
+.El
+.Sh SEE ALSO
+.Xr uudecode 1
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/uuencode.c b/util/sbase/uuencode.c
new file mode 100644
index 00000000..b5317205
--- /dev/null
+++ b/util/sbase/uuencode.c
@@ -0,0 +1,129 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+
+#include <stdio.h>
+#include <string.h>
+
+#include "util.h"
+
+static unsigned int
+b64e(unsigned char *b)
+{
+ unsigned int o, p = b[2] | (b[1] << 8) | (b[0] << 16);
+ const char b64et[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+ o = b64et[p & 0x3f]; p >>= 6;
+ o = (o << 8) | b64et[p & 0x3f]; p >>= 6;
+ o = (o << 8) | b64et[p & 0x3f]; p >>= 6;
+ o = (o << 8) | b64et[p & 0x3f];
+
+ return o;
+}
+
+static void
+uuencodeb64(FILE *fp, const char *name, const char *s)
+{
+ struct stat st;
+ ssize_t n, m = 0;
+ unsigned char buf[45], *pb;
+ unsigned int out[sizeof(buf)/3 + 1], *po;
+
+ if (fstat(fileno(fp), &st) < 0)
+ eprintf("fstat %s:", s);
+ printf("begin-base64 %o %s\n", st.st_mode & 0777, name);
+ /* write line by line */
+ while ((n = fread(buf, 1, sizeof(buf), fp))) {
+ /* clear old buffer if converting with non-multiple of 3 */
+ if (n != sizeof(buf) && (m = n % 3) != 0) {
+ buf[n] = '\0'; /* m == 2 */
+ if (m == 1) buf[n+1] = '\0'; /* m == 1 */
+ }
+ for (pb = buf, po = out; pb < buf + n; pb += 3)
+ *po++ = b64e(pb);
+ if (m != 0) {
+ unsigned int mask = 0xffffffff, dest = 0x3d3d3d3d;
+ /* m==2 -> 0x00ffffff; m==1 -> 0x0000ffff */
+ mask >>= ((3-m) << 3);
+ po[-1] = (po[-1] & mask) | (dest & ~mask);
+ }
+ *po++ = '\n';
+ fwrite(out, 1, (po - out) * sizeof(unsigned int) - 3, stdout);
+ }
+ if (ferror(fp))
+ eprintf("'%s' read error:", s);
+ puts("====");
+}
+
+static void
+uuencode(FILE *fp, const char *name, const char *s)
+{
+ struct stat st;
+ unsigned char buf[45], *p;
+ ssize_t n;
+ int ch;
+
+ if (fstat(fileno(fp), &st) < 0)
+ eprintf("fstat %s:", s);
+ printf("begin %o %s\n", st.st_mode & 0777, name);
+ while ((n = fread(buf, 1, sizeof(buf), fp))) {
+ ch = ' ' + (n & 0x3f);
+ putchar(ch == ' ' ? '`' : ch);
+ for (p = buf; n > 0; n -= 3, p += 3) {
+ if (n < 3) {
+ p[2] = '\0';
+ if (n < 2)
+ p[1] = '\0';
+ }
+ ch = ' ' + ((p[0] >> 2) & 0x3f);
+ putchar(ch == ' ' ? '`' : ch);
+ ch = ' ' + (((p[0] << 4) | ((p[1] >> 4) & 0xf)) & 0x3f);
+ putchar(ch == ' ' ? '`' : ch);
+ ch = ' ' + (((p[1] << 2) | ((p[2] >> 6) & 0x3)) & 0x3f);
+ putchar(ch == ' ' ? '`' : ch);
+ ch = ' ' + (p[2] & 0x3f);
+ putchar(ch == ' ' ? '`' : ch);
+ }
+ putchar('\n');
+ }
+ if (ferror(fp))
+ eprintf("'%s' read error:", s);
+ printf("%c\nend\n", '`');
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-m] [file] name\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ FILE *fp = NULL;
+ void (*uuencode_f)(FILE *, const char *, const char *) = uuencode;
+ int ret = 0;
+
+ ARGBEGIN {
+ case 'm':
+ uuencode_f = uuencodeb64;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc || argc > 2)
+ usage();
+
+ if (argc == 1 || !strcmp(argv[0], "-")) {
+ uuencode_f(stdin, argv[0], "<stdin>");
+ } else {
+ if (!(fp = fopen(argv[0], "r")))
+ eprintf("fopen %s:", argv[0]);
+ uuencode_f(fp, argv[1], argv[0]);
+ }
+
+ ret |= fp && fshut(fp, argv[0]);
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/wc.1 b/util/sbase/wc.1
new file mode 100644
index 00000000..91c05018
--- /dev/null
+++ b/util/sbase/wc.1
@@ -0,0 +1,28 @@
+.Dd October 8, 2015
+.Dt WC 1
+.Os sbase
+.Sh NAME
+.Nm wc
+.Nd word count
+.Sh SYNOPSIS
+.Nm
+.Op Fl c | Fl m
+.Op Fl lw
+.Op Ar file ...
+.Sh DESCRIPTION
+.Nm
+writes the number of lines, words and bytes in each
+.Ar file
+to stdout.
+If no
+.Ar file
+is given
+.Nm
+reads from stdin.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl c | Fl l | Fl m | Fl w
+Print the number of bytes | lines | characters | words.
+.El
+.Sh STANDARDS
+POSIX.1-2013.
diff --git a/util/sbase/wc.c b/util/sbase/wc.c
new file mode 100644
index 00000000..c89d19f8
--- /dev/null
+++ b/util/sbase/wc.c
@@ -0,0 +1,122 @@
+/* See LICENSE file for copyright and license details. */
+#include <string.h>
+
+#include "utf.h"
+#include "util.h"
+
+static int lflag = 0;
+static int wflag = 0;
+static char cmode = 0;
+static size_t tc = 0, tl = 0, tw = 0;
+
+static void
+output(const char *str, size_t nc, size_t nl, size_t nw)
+{
+ int first = 1;
+
+ if (lflag) {
+ first = 0;
+ printf("%zu", nl);
+ }
+ if (wflag) {
+ if (!first)
+ putchar(' ');
+ first = 0;
+ printf("%zu", nw);
+ }
+ if (cmode) {
+ if (!first)
+ putchar(' ');
+ printf("%zu", nc);
+ }
+ if (str)
+ printf(" %s", str);
+ putchar('\n');
+}
+
+static void
+wc(FILE *fp, const char *str)
+{
+ int word = 0, rlen;
+ Rune c;
+ size_t nc = 0, nl = 0, nw = 0;
+
+ while ((rlen = efgetrune(&c, fp, str))) {
+ nc += (cmode == 'c') ? rlen : (c != Runeerror);
+ if (c == '\n')
+ nl++;
+ if (!isspacerune(c))
+ word = 1;
+ else if (word) {
+ word = 0;
+ nw++;
+ }
+ }
+ if (word)
+ nw++;
+ tc += nc;
+ tl += nl;
+ tw += nw;
+ output(str, nc, nl, nw);
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-c | -m] [-lw] [file ...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ FILE *fp;
+ int many;
+ int ret = 0;
+
+ ARGBEGIN {
+ case 'c':
+ cmode = 'c';
+ break;
+ case 'm':
+ cmode = 'm';
+ break;
+ case 'l':
+ lflag = 1;
+ break;
+ case 'w':
+ wflag = 1;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!lflag && !wflag && !cmode) {
+ cmode = 'c';
+ lflag = 1;
+ wflag = 1;
+ }
+
+ if (!argc) {
+ wc(stdin, NULL);
+ } else {
+ for (many = (argc > 1); *argv; argc--, argv++) {
+ if (!strcmp(*argv, "-")) {
+ *argv = "<stdin>";
+ fp = stdin;
+ } else if (!(fp = fopen(*argv, "r"))) {
+ weprintf("fopen %s:", *argv);
+ ret = 1;
+ continue;
+ }
+ wc(fp, *argv);
+ if (fp != stdin && fshut(fp, *argv))
+ ret = 1;
+ }
+ if (many)
+ output("total", tc, tl, tw);
+ }
+
+ ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
+
+ return ret;
+}
diff --git a/util/sbase/which.1 b/util/sbase/which.1
new file mode 100644
index 00000000..9ba4b224
--- /dev/null
+++ b/util/sbase/which.1
@@ -0,0 +1,44 @@
+.Dd October 8, 2015
+.Dt WHICH 1
+.Os sbase
+.Sh NAME
+.Nm which
+.Nd locate programs in the path
+.Sh SYNOPSIS
+.Nm
+.Op Fl a
+.Ar name ...
+.Sh DESCRIPTION
+.Nm
+looks for each
+.Ar name
+in the
+.Ev PATH
+directories, stopping at the first match and printing
+the full path to stdout.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl a
+Don't stop at the first match and search all
+.Ev PATH
+directories.
+.El
+.Sh EXIT STATUS
+.Bl -tag -width Ds
+.It 0
+Each
+.Ar name
+was found.
+.It 1
+At least one
+.Ar name
+was not found.
+.It 2
+No
+.Ar name
+was found.
+.It 3
+An error occurred.
+.El
+.Sh SEE ALSO
+.Xr environ 7
diff --git a/util/sbase/which.c b/util/sbase/which.c
new file mode 100644
index 00000000..abd030e7
--- /dev/null
+++ b/util/sbase/which.c
@@ -0,0 +1,101 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "util.h"
+
+static int aflag;
+
+static int
+canexec(int fd, const char *name)
+{
+ struct stat st;
+
+ if (fstatat(fd, name, &st, 0) < 0 || !S_ISREG(st.st_mode))
+ return 0;
+ return faccessat(fd, name, X_OK, AT_EACCESS) == 0;
+}
+
+static int
+which(const char *path, const char *name)
+{
+ char *ptr, *p;
+ size_t i, len;
+ int dirfd, found = 0;
+
+ if (strchr(name, '/')) {
+ found = canexec(AT_FDCWD, name);
+ if (found)
+ puts(name);
+ return found;
+ }
+
+ ptr = p = enstrdup(3, path);
+ len = strlen(p);
+ for (i = 0; i < len + 1; i++) {
+ if (ptr[i] != ':' && ptr[i] != '\0')
+ continue;
+ ptr[i] = '\0';
+ if ((dirfd = open(p, O_RDONLY)) >= 0) {
+ if (canexec(dirfd, name)) {
+ found = 1;
+ fputs(p, stdout);
+ if (i && ptr[i - 1] != '/')
+ fputc('/', stdout);
+ puts(name);
+ }
+ close(dirfd);
+ if (!aflag && found)
+ break;
+ }
+ p = ptr + i + 1;
+ }
+ free(ptr);
+
+ return found;
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-a] name ...\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ char *path;
+ int found = 0, foundall = 1;
+
+ ARGBEGIN {
+ case 'a':
+ aflag = 1;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (!argc)
+ usage();
+
+ if (!(path = getenv("PATH")))
+ enprintf(3, "$PATH is not set\n");
+
+ for (; *argv; argc--, argv++) {
+ if (which(path, *argv)) {
+ found = 1;
+ } else {
+ weprintf("%s: not an external command\n", *argv);
+ foundall = 0;
+ }
+ }
+
+ return found ? foundall ? 0 : 1 : 2;
+}
diff --git a/util/sbase/whoami.1 b/util/sbase/whoami.1
new file mode 100644
index 00000000..d552a327
--- /dev/null
+++ b/util/sbase/whoami.1
@@ -0,0 +1,9 @@
+.Dd December 14, 2015
+.Dt WHOAMI 1
+.Os sbase
+.Sh NAME
+.Nm whoami
+.Nd show effective uid
+.Sh SYNOPSIS
+.Nm
+writes the name of the effective uid to stdout.
diff --git a/util/sbase/whoami.c b/util/sbase/whoami.c
new file mode 100644
index 00000000..c991ac29
--- /dev/null
+++ b/util/sbase/whoami.c
@@ -0,0 +1,37 @@
+/* See LICENSE file for copyright and license details. */
+#include <errno.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <pwd.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ uid_t uid;
+ struct passwd *pw;
+
+ argv0 = *argv, argv0 ? (argc--, argv++) : (void *)0;
+
+ if (argc)
+ usage();
+
+ uid = geteuid();
+ errno = 0;
+ if (!(pw = getpwuid(uid))) {
+ if (errno)
+ eprintf("getpwuid %d:", uid);
+ else
+ eprintf("getpwuid %d: no such user\n", uid);
+ }
+ puts(pw->pw_name);
+
+ return fshut(stdout, "<stdout>");
+}
diff --git a/util/sbase/xargs.1 b/util/sbase/xargs.1
new file mode 100644
index 00000000..e988278d
--- /dev/null
+++ b/util/sbase/xargs.1
@@ -0,0 +1,121 @@
+.Dd July 30, 2025
+.Dt XARGS 1
+.Os sbase
+.Sh NAME
+.Nm xargs
+.Nd construct argument lists and execute command
+.Sh SYNOPSIS
+.Nm
+.Op Fl 0prtx
+.Op Fl E Ar eofstr
+.Op Fl I Ar replstr
+.Op Fl n Ar num
+.Op Fl P Ar maxprocs
+.Op Fl s Ar num
+.Op Ar cmd Op Ar arg ...
+.Sh DESCRIPTION
+.Nm
+reads space, tab, newline and EOF delimited strings from stdin
+and executes the specified
+.Ar cmd
+with the strings as
+.Ar arguments .
+.Pp
+Any arguments specified on the command line are given to the command upon
+each invocation, followed by some number of the arguments read from
+stdin.
+The command is repeatedly executed one or more times until stdin is exhausted.
+.Pp
+Spaces, tabs and newlines may be embedded in arguments using single (`'')
+or double (`"') quotes or backslashes ('\e').
+Single quotes escape all non-single quote characters, excluding newlines, up
+to the matching single quote.
+Double quotes escape all non-double quote characters, excluding newlines, up
+to the matching double quote.
+Any single character, including newlines, may be escaped by a backslash.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl 0
+Change
+.Nm
+to expect NUL ('\e0') characters as separators, instead of spaces
+and newlines.
+The quoting mechanisms described above are not performed.
+.It Fl E Ar eofstr
+Use
+.Ar eofstr
+as a logical EOF marker.
+.It Fl I Ar replstr
+Use
+.Ar replstr
+as the placeholder for the argument.
+Sets the arguments count to 1 per command line.
+It also implies the option x.
+.It Fl n Ar num
+Use at most
+.Ar num
+arguments per command line.
+.It Fl p
+Prompt mode: the user is asked whether to execute
+.Ar cmd
+at each invocation.
+Trace mode (-t) is turned on to write the command instance to be executed,
+followed by a prompt to standard error.
+An affirmative response read from
+.Pa /dev/tty
+executes the command, otherwise it is skipped.
+.It Fl P Ar maxprocs
+Parallel mode: run at most maxprocs invocations of
+.Ar cmd
+at once.
+.It Fl r
+Do not run the command if there are no arguments.
+Normally the command is executed at least once even if there are no arguments.
+.It Fl s Ar num
+Use at most
+.Ar num
+bytes per command line.
+.It Fl t
+Enable trace mode.
+Write the command line to stderr before executing it.
+.It Fl x
+Terminate if the command line exceeds the system limit or the number of bytes
+given with the
+.Op Fl s
+flag.
+.El
+.Sh EXIT STATUS
+.Nm
+exits with one of the following values:
+.Bl -tag -width Ds
+.It 0
+All invocations of
+.Ar cmd
+returned a zero exit status.
+.It 123
+One or more invocations of
+.Ar cmd
+returned a nonzero exit status.
+.It 124
+.Ar cmd
+exited with a 255 exit status.
+.It 125
+.Ar cmd
+was killed or stopped by a signal.
+.It 126
+.Ar cmd
+was found but could not be executed.
+.It 127
+.Ar cmd
+could not be found.
+.It 1
+Some other error occurred.
+.El
+.Sh STANDARDS
+POSIX.1-2013.
+.Pp
+The
+.Op Fl r
+and
+.Op Fl P
+flag is an extension to that specification.
diff --git a/util/sbase/xargs.c b/util/sbase/xargs.c
new file mode 100644
index 00000000..b3b2a81c
--- /dev/null
+++ b/util/sbase/xargs.c
@@ -0,0 +1,362 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/wait.h>
+
+#include <errno.h>
+#include <limits.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "util.h"
+
+#define NARGS 10000
+
+static int inputc(void);
+static void fillargbuf(int);
+static int eatspace(void);
+static int parsequote(int);
+static int parseescape(void);
+static char *poparg(void);
+static void waitchld(int);
+static void spawn(void);
+
+static size_t argbsz;
+static size_t argbpos;
+static size_t maxargs;
+static size_t curprocs, maxprocs = 1;
+static int nerrors;
+static int nulflag, nflag, pflag, rflag, tflag, xflag, Iflag;
+static char *argb;
+static char *cmd[NARGS];
+static char *eofstr;
+
+static int
+inputc(void)
+{
+ int ch;
+
+ ch = getc(stdin);
+ if (ch == EOF && ferror(stdin))
+ eprintf("getc <stdin>:");
+
+ return ch;
+}
+
+static void
+fillargbuf(int ch)
+{
+ if (argbpos >= argbsz) {
+ argbsz = argbpos == 0 ? 1 : argbsz * 2;
+ argb = erealloc(argb, argbsz);
+ }
+ argb[argbpos] = ch;
+}
+
+static int
+eatspace(void)
+{
+ int ch;
+
+ while ((ch = inputc()) != EOF) {
+ if (nulflag || !(ch == ' ' || ch == '\t' || ch == '\n')) {
+ ungetc(ch, stdin);
+ return ch;
+ }
+ }
+ return -1;
+}
+
+static int
+parsequote(int q)
+{
+ int ch;
+
+ while ((ch = inputc()) != EOF) {
+ if (ch == q)
+ return 0;
+ if (ch != '\n') {
+ fillargbuf(ch);
+ argbpos++;
+ }
+ }
+
+ return -1;
+}
+
+static int
+parseescape(void)
+{
+ int ch;
+
+ if ((ch = inputc()) != EOF) {
+ fillargbuf(ch);
+ argbpos++;
+ return ch;
+ }
+
+ return -1;
+}
+
+static char *
+poparg(void)
+{
+ int ch;
+
+ argbpos = 0;
+ if (eatspace() < 0)
+ return NULL;
+ while ((ch = inputc()) != EOF) {
+ /* NUL separator: no escaping */
+ if (nulflag) {
+ if (ch == '\0')
+ goto out;
+ else
+ goto fill;
+ }
+
+ switch (ch) {
+ case ' ':
+ case '\t':
+ if (Iflag)
+ goto fill;
+ case '\n':
+ goto out;
+ case '\'':
+ if (parsequote('\'') < 0)
+ eprintf("unterminated single quote\n");
+ break;
+ case '\"':
+ if (parsequote('\"') < 0)
+ eprintf("unterminated double quote\n");
+ break;
+ case '\\':
+ if (parseescape() < 0)
+ eprintf("backslash at EOF\n");
+ break;
+ default:
+ fill:
+ fillargbuf(ch);
+ argbpos++;
+ break;
+ }
+ }
+out:
+ fillargbuf('\0');
+
+ return (eofstr && !strcmp(argb, eofstr)) ? NULL : argb;
+}
+
+static void
+waitchld(int waitall)
+{
+ pid_t pid;
+ int status;
+
+ while ((pid = waitpid(-1, &status, !waitall && curprocs < maxprocs ?
+ WNOHANG : 0)) > 0) {
+ curprocs--;
+
+ if (WIFEXITED(status)) {
+ if (WEXITSTATUS(status) == 255)
+ exit(124);
+ if (WEXITSTATUS(status) == 127 ||
+ WEXITSTATUS(status) == 126)
+ exit(WEXITSTATUS(status));
+ if (WEXITSTATUS(status))
+ nerrors++;
+ }
+ if (WIFSIGNALED(status))
+ exit(125);
+ }
+ if (pid == -1 && errno != ECHILD)
+ eprintf("waitpid:");
+}
+
+static int
+prompt(void)
+{
+ FILE *fp;
+ int ch, ret;
+
+ if (!(fp = fopen("/dev/tty", "r")))
+ return -1;
+
+ fputs("?...", stderr);
+ fflush(stderr);
+
+ ch = fgetc(fp);
+ ret = (ch == 'y' || ch == 'Y');
+ if (ch != EOF && ch != '\n') {
+ while ((ch = fgetc(fp)) != EOF) {
+ if (ch == '\n')
+ break;
+ }
+ }
+
+ fclose(fp);
+
+ return ret;
+}
+
+static void
+spawn(void)
+{
+ int savederrno;
+ int first = 1;
+ char **p;
+
+ if (pflag || tflag) {
+ for (p = cmd; *p; p++) {
+ if (!first)
+ fputc(' ', stderr);
+ fputs(*p, stderr);
+ first = 0;
+ }
+ if (pflag) {
+ switch (prompt()) {
+ case -1: break; /* error */
+ case 0: return; /* no */
+ case 1: goto dospawn; /* yes */
+ }
+ }
+ fputc('\n', stderr);
+ fflush(stderr);
+ }
+
+dospawn:
+ switch (fork()) {
+ case -1:
+ eprintf("fork:");
+ case 0:
+ execvp(*cmd, cmd);
+ savederrno = errno;
+ weprintf("execvp %s:", *cmd);
+ _exit(126 + (savederrno == ENOENT));
+ }
+ curprocs++;
+ waitchld(0);
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-0prtx] [-E eofstr] [-n num] [-P maxprocs] [-s num] "
+ "[cmd [arg ...]]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int ret = 0, leftover = 0, i, j;
+ size_t argsz, argmaxsz;
+ size_t arglen, a;
+ char *arg = "";
+ char *replstr;
+
+ if ((argmaxsz = sysconf(_SC_ARG_MAX)) == (size_t)-1)
+ argmaxsz = _POSIX_ARG_MAX;
+ /* Leave some room for environment variables */
+ argmaxsz -= 4096;
+
+ ARGBEGIN {
+ case '0':
+ nulflag = 1;
+ break;
+ case 'n':
+ nflag = 1;
+ maxargs = estrtonum(EARGF(usage()), 1, MIN(SIZE_MAX, LLONG_MAX));
+ break;
+ case 'p':
+ pflag = 1;
+ break;
+ case 'r':
+ rflag = 1;
+ break;
+ case 's':
+ argmaxsz = estrtonum(EARGF(usage()), 1, MIN(SIZE_MAX, LLONG_MAX));
+ break;
+ case 't':
+ tflag = 1;
+ break;
+ case 'x':
+ xflag = 1;
+ break;
+ case 'E':
+ eofstr = EARGF(usage());
+ break;
+ case 'I':
+ Iflag = 1;
+ xflag = 1;
+ nflag = 1;
+ maxargs = 1;
+ replstr = EARGF(usage());
+ break;
+ case 'P':
+ maxprocs = estrtonum(EARGF(usage()), 1, MIN(SIZE_MAX, LLONG_MAX));
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ do {
+ argsz = 0; i = 0; a = 0;
+ if (argc) {
+ for (; i < argc; i++) {
+ cmd[i] = estrdup(argv[i]);
+ argsz += strlen(cmd[i]) + 1;
+ }
+ } else {
+ cmd[i] = estrdup("/bin/echo");
+ argsz += strlen("/bin/echo") + 1;
+ i++;
+ }
+ while (leftover || (arg = poparg())) {
+ arglen = strlen(arg);
+ if (argsz + arglen >= argmaxsz || i >= NARGS - 1) {
+ if (xflag || arglen >= argmaxsz || leftover)
+ eprintf("insufficient argument space\n");
+ leftover = 1;
+ break;
+ }
+
+ if (!Iflag) {
+ cmd[i] = estrdup(arg);
+ argsz += arglen + 1;
+ } else {
+ for (j = 1; j < i; j++) {
+ char *p = cmd[j];
+ argsz -= strlen(cmd[j]);
+ strnsubst(&cmd[j], replstr, arg, 255);
+ argsz += strlen(cmd[j]);
+ free(p);
+ }
+ }
+
+ i++;
+ a++;
+ leftover = 0;
+ if (nflag && a >= maxargs)
+ break;
+ }
+ cmd[i] = NULL;
+ if (a >= maxargs && nflag)
+ spawn();
+ else if (!a || (i == 1 && rflag))
+ ;
+ else
+ spawn();
+ for (; i >= 0; i--)
+ free(cmd[i]);
+ } while (arg);
+
+ free(argb);
+
+ waitchld(1);
+
+ if (nerrors || (fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>")))
+ ret = 123;
+
+ return ret;
+}
diff --git a/util/sbase/xinstall.1 b/util/sbase/xinstall.1
new file mode 100644
index 00000000..2d9535c0
--- /dev/null
+++ b/util/sbase/xinstall.1
@@ -0,0 +1,86 @@
+.Dd December 3, 2016
+.Dt INSTALL 1
+.Os sbase
+.Sh NAME
+.Nm install
+.Nd copy files and set attributes
+.Sh SYNOPSIS
+.Nm
+.Op Fl g Ar group
+.Op Fl o Ar owner
+.Op Fl m Ar mode
+.Po
+.Fl d Ar dir ...
+|
+.Op Fl D
+.Po
+.Fl t Ar dest
+.Ar source ...
+|
+.Ar source ...
+.Ar dest
+.Pc
+.Pc
+.Sh DESCRIPTION
+.Nm
+copies
+.Ar source
+to
+.Ar dest .
+If more than one
+.Ar source
+is given
+.Ar dest
+is treated as a directory.
+Otherwise
+.Ar dest
+is treated as a filename.
+.Nm
+can also change the attributes of the copies.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl d
+Create the directories
+.Ar dir .
+.It Fl D
+Create missing parent directories to
+.Ar dest .
+If
+.Ar dest
+is to be treated as a directory, it is created too if missing.
+.It Fl g Ar group
+Change the installed files' group to
+.Ar group .
+This may be a group name or a group identifier.
+.It Fl m Ar mode
+Change the file modes.
+Both numerical and symbolic values are supported.
+See
+.Xr chmod 1
+for the syntex.
+Default mode 0755.
+If a file has the mode 0644 and is copied with
+.It Fl o Ar owner
+Change the installed files' owner to
+.Ar owner .
+This may be a user name or a user identifier.
+.It Fl t Ar dest
+Copy files into the directory
+.Ar dest .
+.Nm install ,
+the copy's mode will be 0755 unless
+.Fl m
+is used to select another mode.
+When the symbolic notation is used, the base mode is 0000.
+.El
+.Sh SEE ALSO
+.Xr chmod 1 ,
+.Xr chown 1 ,
+.Xr cp 1 ,
+.Xr mkdir 1
+.Sh STANDARDS
+The
+.Nm
+utility is not standardized.
+This implementation is a subset of the GNU implementation and a subset
+with extensions to the FreeBSD implementation.
diff --git a/util/sbase/xinstall.c b/util/sbase/xinstall.c
new file mode 100644
index 00000000..fe1a160e
--- /dev/null
+++ b/util/sbase/xinstall.c
@@ -0,0 +1,194 @@
+/* See LICENSE file for copyright and license details. */
+#include <grp.h>
+#include <pwd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <dirent.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include "util.h"
+
+static int Dflag = 0;
+static gid_t group;
+static uid_t owner;
+static mode_t mode = 0755;
+
+static void
+make_dir(char *dir, int was_missing)
+{
+ if (!mkdir(dir, was_missing ? 0755 : mode)) {
+ if (!was_missing && (lchown(dir, owner, group) < 0))
+ eprintf("lchmod %s:", dir);
+ } else if (errno != EEXIST) {
+ eprintf("mkdir %s:", dir);
+ }
+}
+
+static void
+make_dirs(char *dir, int was_missing)
+{
+ char *p;
+ for (p = strchr(dir + (dir[0] == '/'), '/'); p; p = strchr(p + 1, '/')) {
+ *p = '\0';
+ make_dir(dir, was_missing);
+ *p = '/';
+ }
+ make_dir(dir, was_missing);
+}
+
+static int
+install(const char *s1, const char *s2, int depth)
+{
+ int f1, f2;
+
+ if ((f1 = open(s1, O_RDONLY)) < 0)
+ eprintf("open %s:", s1);
+ if ((f2 = creat(s2, 0600)) < 0) {
+ if (unlink(s2) < 0 && errno != ENOENT)
+ eprintf("unlink %s:", s2);
+ if ((f2 = creat(s2, 0600)) < 0)
+ eprintf("creat %s:", s2);
+ }
+ if (concat(f1, s1, f2, s2) < 0)
+ goto fail;
+ if (fchmod(f2, mode) < 0) {
+ weprintf("fchmod %s:", s2);
+ goto fail;
+ }
+ if (fchown(f2, owner, group) < 0) {
+ weprintf("fchown %s:", s2);
+ goto fail;
+ }
+
+ close(f1);
+ close(f2);
+
+ return 0;
+
+fail:
+ unlink(s2);
+ exit(1);
+}
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-g group] [-o owner] [-m mode] (-d dir ... | [-D] (-t dest source ... | source ... dest))\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int dflag = 0;
+ char *gflag = 0;
+ char *oflag = 0;
+ char *mflag = 0;
+ char *tflag = 0;
+ struct group *gr;
+ struct passwd *pw;
+ struct stat st;
+ char *p;
+
+ ARGBEGIN {
+ case 'c':
+ /* no-op for compatibility */
+ break;
+ case 'd':
+ dflag = 1;
+ break;
+ case 'D':
+ Dflag = 1;
+ break;
+ case 's':
+ /* no-op for compatibility */
+ break;
+ case 'g':
+ gflag = EARGF(usage());
+ break;
+ case 'o':
+ oflag = EARGF(usage());
+ break;
+ case 'm':
+ mflag = EARGF(usage());
+ break;
+ case 't':
+ tflag = EARGF(usage());
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (argc < 1 + (!tflag & !dflag) || dflag & (Dflag | !!tflag))
+ usage();
+
+ if (gflag) {
+ errno = 0;
+ gr = getgrnam(gflag);
+ if (gr) {
+ group = gr->gr_gid;
+ } else {
+ if (errno)
+ eprintf("getgrnam %s:", gflag);
+ group = estrtonum(gflag, 0, UINT_MAX);
+ }
+ } else {
+ group = getgid();
+ }
+
+ if (oflag) {
+ errno = 0;
+ pw = getpwnam(oflag);
+ if (pw) {
+ owner = pw->pw_uid;
+ } else {
+ if (errno)
+ eprintf("getpwnam %s:", oflag);
+ owner = estrtonum(oflag, 0, UINT_MAX);
+ }
+ } else {
+ owner = getuid();
+ }
+
+ if (mflag)
+ mode = parsemode(mflag, mode, 0);
+
+ if (dflag) {
+ for (; *argv; argc--, argv++)
+ make_dirs(*argv, 0);
+ return 0;
+ }
+
+ if (tflag) {
+ argv = memmove(argv - 1, argv, argc * sizeof(*argv));
+ argv[argc++] = tflag;
+ }
+ if (tflag || argc > 2) {
+ if (stat(argv[argc - 1], &st) < 0) {
+ if ((errno == ENOENT) && Dflag) {
+ make_dirs(argv[argc - 1], 1);
+ } else {
+ eprintf("stat %s:", argv[argc - 1]);
+ }
+ } else if (!S_ISDIR(st.st_mode)) {
+ eprintf("%s: not a directory\n", argv[argc - 1]);
+ }
+ }
+ if (stat(argv[argc - 1], &st) < 0) {
+ if (errno != ENOENT)
+ eprintf("stat %s:", argv[argc - 1]);
+ if (tflag || Dflag || argc > 2) {
+ if ((p = strrchr(argv[argc - 1], '/')) != NULL) {
+ *p = '\0';
+ make_dirs(argv[argc - 1], 1);
+ *p = '/';
+ }
+ }
+ }
+ enmasse(argc, argv, install);
+
+ return 0;
+}
diff --git a/util/sbase/yes.1 b/util/sbase/yes.1
new file mode 100644
index 00000000..8415857c
--- /dev/null
+++ b/util/sbase/yes.1
@@ -0,0 +1,14 @@
+.Dd October 8, 2015
+.Dt YES 1
+.Os sbase
+.Sh NAME
+.Nm yes
+.Nd output string repeatedly
+.Sh SYNOPSIS
+.Nm
+.Op Ar string
+.Sh DESCRIPTION
+.Nm
+will repeatedly write 'y' or
+.Ar string
+to stdout.
diff --git a/util/sbase/yes.c b/util/sbase/yes.c
new file mode 100644
index 00000000..b5c3c109
--- /dev/null
+++ b/util/sbase/yes.c
@@ -0,0 +1,25 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdio.h>
+
+#include "util.h"
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [string]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ const char *s;
+
+ ARGBEGIN {
+ default:
+ usage();
+ } ARGEND
+
+ s = argc ? argv[0] : "y";
+ for (;;)
+ puts(s);
+}