An Almquist-family POSIX shell for z/OS UNIX System Services that also
builds on Linux and BSD. It has line editing through a bundled port of
NetBSD libedit, bash-compatible
prompts and history, and z/OS support for spawn(), file tagging, MVS
data sets and TSO.
The line-editing library in libedit/ is NetBSD code. Apart from the
port layer, the only change is a small, verified patch for EBCDIC and
xlc; see libedit/README.md.
FreeBSD sh, oksh and BusyBox ash were used as references only. The shell is
new, and nothing was copied from BusyBox (GPLv2).
Early, but usable for scripting on Linux:
- The test suite passes with gcc and clang (
-std=c11 -Wall -Wextra -pedantic), with or without libedit. - Output matches dash on the portable tests.
- In-shell work (loops, arithmetic, expansion,
read) is as fast as dash or faster; see Performance.
The z/OS platform layer has never been compiled on z/OS. It was written
from IBM documentation and checked only against stub headers. Calls still
to be confirmed on a real system are marked VERIFY in the source.
Missing: job control (fg, bg, jobs, ^Z), trap, kill, getopts,
printf, command, alias, ulimit. See Limitations.
Any POSIX make works (tested with GNU make and bmake). There are three
toolchains:
| Command | Platform | Compiler |
|---|---|---|
make (or make linux) |
Linux, BSD | clang |
make zos |
z/OS | IBM Open XL C/C++ (ibm-clang -m64) |
make xlc |
z/OS | IBM z/OS XL C (xlc -q64) |
make # Linux/BSD, clang
make CC=gcc # if clang isn't installed
make EDIT_CPPFLAGS= EDIT_LIBS= EDIT_DEP= # no line editing (any target)
make check # run the tests
make install PREFIX=$HOME/.local # installs bin/zash
make dist # zash.tar for transfer to z/OSLine editing uses the bundled NetBSD libedit in libedit/, which is built
automatically and linked statically. Its one outside dependency is the
termcap functions from curses: ncurses on Linux (ncurses-devel on
Fedora, libncurses-dev on Debian/Ubuntu), and system curses
(-lcurses) on z/OS. Override the library with TERMLIB= on Linux or
TERMLIB_ZOS= on z/OS. To use the system libedit instead:
make EDIT_CPPFLAGS=-DWITH_LIBEDIT EDIT_LIBS=-ledit EDIT_DEP=check and install use the zash already built and never rebuild it.
That way make zos followed by make check tests the z/OS binary.
Changing the compiler or flags deletes the old objects and rebuilds
everything, so the same tree can switch between make zos and make xlc.
Both z/OS builds default to ASCII mode, the zopen convention. Clear the ASCII macro for an EBCDIC build:
make zos # Open XL, ASCII
make zos OXL_ASCII= # Open XL, EBCDIC
make xlc # XL C, ASCII
make xlc XLC_ASCII= # XL C, EBCDIC- Open XL builds with
-m64.ibm-clangdefaults to 31-bit non-XPLINK (-m32n), which rejects-fzos-le-char-mode=ascii. - XL C builds with
-q64 -qxplink -qlanglvl=extc1x;extc1xis xlc's C11 level. xlc has no-Wallor-Wextra. The ASCII flags (-qascii -D_ENHANCED_ASCII_EXT=0xFFFFFFFF) haven't been checked on a real system yet. - Overrides: use
OXL_CC,OXL_CFLAGS,OXL_LDFLAGS,XLC_CC,XLC_CFLAGS,XLC_LDFLAGSorZOS_CPPFLAGS. PlainCFLAGSonly applies to the Linux build.
To copy the source to z/OS, run make dist on Linux, which writes
zash.tar (ustar format). Transfer it in binary mode: sftp does that by
default; with FTP, use binary. On z/OS:
pax -rf zash.tar -ofrom=ISO8859-1,to=IBM-1047 -o setfiletag
cd zash && make zos && make checkThe pax options convert the files to EBCDIC and tag them as IBM-1047
text. That works for both compilers, for /bin/sh and, through automatic
conversion, for an ASCII-mode zash. All shipped files are plain ASCII, so
the conversion is lossless.
Cloning with git on z/OS (zopen or IBM Open Enterprise git) gives the
same result: .gitattributes asks for an IBM-1047 working tree, so
git clone and then make zos work directly with no pax step. Run
make verify in libedit/ on Linux only: it checksums the vendored
sources as ASCII bytes.
Line editing works in both modes. In EBCDIC mode libedit runs on
native IBM-1047 characters, and make check also runs a test comparing
its character table with z/OS's own conversion. Other EBCDIC code pages
aren't supported.
To use zopen's ncurses instead of system curses:
make zos TERMLIB_ZOS='-L$ZOPEN_ROOT/lib -lncurses'Neither mode has been fully built on z/OS yet; libedit/README.md lists what to check.
zash script [arg...]
zash -c 'command' [name [arg...]]
zash -s [arg...] # read commands from stdin
zash -i # force interactiveOptions follow set: -C -e -f -n -u -v -x, plus -o with
noclobber errexit noglob noexec nounset verbose xtrace vi emacs.
When invoked as a login shell (argv[0] starts with -), zash reads
/etc/profile and then ~/.profile. An interactive shell then reads the
file named by $ENV.
Line editing. libedit provides emacs bindings by default, or vi with
set -o vi. It reads ~/.editrc for custom bindings.
Completion. TAB completes the word under the cursor:
- First word of a command: keywords, builtins, functions and PATH
programs. This also applies after
|,&&,;,$(,then,do,sudo,envor aNAME=valueassignment. $prefix: variable names.- Anything else: files. Directories get a trailing
/, and~/expands.
Completions are quoted for where the cursor is: backslashes outside
quotes, the right escapes inside "..." or '...'. With several matches,
TAB fills in the common prefix and then lists the rest in columns.
Prompts. PS1 and PS2 use bash's escapes:
| Escape | Meaning |
|---|---|
\u \h \H |
user, short host, full host |
\w \W |
working directory (with ~), its basename |
\$ |
# for root, otherwise $ |
\t \T \@ \A \d \D{fmt} |
time and date |
\j \! \# |
job count, history number, command number |
\e \nnn \n \\ |
escape, octal byte, newline, backslash |
\[ \] |
wrap non-printing text such as color codes |
After the escapes, the prompt is expanded like a variable, so $VAR and
$(cmd) work. An existing bash prompt should work unchanged:
PS1='\[\e[32m\]\u@\h\[\e[0m\]:\[\e[34m\]\w\[\e[0m\]\$ 'Names produced by \w, \u and \h are quoted before that expansion, so
a directory named $(cmd) is displayed, not run.
History.
- Saved to
HISTFILE(default~/.zash_history) in bash's plain format, so the file can be shared with bash. - At exit, a shell appends only its own new commands. Concurrent shells keep each other's history.
- The file is trimmed by writing a temporary file and renaming it, so an interrupted save can't truncate the file. New files are created mode 0600, since history often contains secrets.
- Closing the terminal (SIGHUP) saves before exiting.
| Variable | Default | Meaning |
|---|---|---|
HISTFILE |
~/.zash_history |
history file; empty disables saving |
HISTSIZE |
500 | entries kept in memory |
HISTFILESIZE |
HISTSIZE |
entries kept in the file |
HISTCONTROL |
unset | ignorespace, ignoredups or ignoreboth |
FCEDIT |
$EDITOR, then ed |
editor for fc |
fc works as POSIX describes: -l, -n, -r, -s old=new and
-e editor. history takes [n], -c (clear), -a (append to the
file), -w (write the file) and -r (read the file).
. : [ break cd chtag continue echo eval exec exit export extattr false fc hash history pwd read readonly return set shift test true tso type umask unset wait
chtag, extattr and tso are z/OS only. Elsewhere they exit with
status 2.
- No fork for external commands. They are started with
spawn(), passing redirections as itsfd_map._BPX_SHAREAS=YES(run the child in the shell's address space) andSTEPLIBtake effect through the environment, with no extra shell code. Children sharing the address space cannot change user identity, and they end if the shell ends. Scripts without#!are run by zash itself, unless_BPX_SPAWN_SCRIPT=YESlets spawn handle them first. - Tagging. Each build follows the conventions of the tools around it.
The ASCII build (the default) follows zopen for files: new files made by
zash take the tag named by
_ENCODE_FILE_NEW(ISO8859-1, the default,IBM-1047orBINARY), and untagged input files are checked for EBCDIC. Pipes are tagged IBM-1047 rather than zopen's ISO8859-1, so ASCII and EBCDIC programs can be mixed freely in a pipeline. The EBCDIC build followsshin the z/OS UNIX Command Reference:_TAG_REDIR_OUTand_TAG_REDIR_ERR(TXTorBIN) give untagged output files a deferred tag, set at the first write. In both builds_TAG_REDIR_INturns conversion of input files on or off, and a one-command form like `_TAG_REDIR_OUT=TXT cmdfile` works.
- MVS data sets.
cmd >"//'HLQ.DATA.SET'"andcmd <"//DD:NAME"go through a small helper process, becauseopen()cannot open data sets. The shell does not wait for that helper before running the next command. - Builtins.
chtag -pruns inside the shell. Otherchtagandextattroptions are passed to/bin/chtagand/bin/extattr.tsorunstsocmd. - ASCII or EBCDIC. Words are stored without in-band marker bytes; the markers in ash-derived shells are letters in IBM-1047. Tables are sorted at startup, and the escape character is chosen at compile time, so either build mode works.
Line editing, completion and colors need a real terminal, such as ssh or
rlogin. They do not work under TSO OMVS, which is 3270 line mode.
External commands are spawned, never forked and then exec'd. The shell forks only when shell code must run in a child: a subshell, or a builtin or compound command in a pipeline. Parse trees and expansion buffers come from a per-command arena, PATH lookups are hashed, and the exported environment is rebuilt only after an exported variable changes.
Linux x86-64, best of 3 runs (bench/run.sh):
| Workload | zash | dash | bash | ksh93 |
|---|---|---|---|---|
| arithmetic loop, 200k iterations | 0.17 s | 0.25 s | 0.59 s | 0.20 s |
| function calls, 100k | 0.16 s | 0.20 s | 0.68 s | 0.25 s |
${p##*/} and similar, 100k |
0.31 s | 0.36 s | 0.96 s | 0.44 s |
while read, 100k lines |
0.19 s | 0.69 s | 0.57 s | 0.18 s |
3000 x /bin/true |
2.89 s | 2.99 s | 3.67 s | 3.19 s |
2000 x $(/bin/echo) |
1.98 s | 2.26 s | 2.98 s | 2.38 s |
2000 x sh -c : |
2.69 s | 2.16 s | 3.86 s | 3.29 s |
Startup is slower than dash, probably because libedit is linked even for
non-interactive use; this hasn't been profiled. zash also lacks traps and
job control, so part of its lead is less bookkeeping. On z/OS the gap
should be larger, since fork() copies the address space there, but this
hasn't been measured.
To compare other shells, such as oksh:
bench/run.sh ./zash dash bash oksh # -n N runs, -v per-run timesmake check runs tests/run.sh ./zash. Each tests/*.sh script is run in
a temporary directory, and its output is compared with the matching .out
file. Use tests/run.sh -v ./zash to see diffs.
Most expected output was generated with dash. stdin.sh uses bash's
output, because dash handles a script read from stdin incorrectly.
history.sh was checked by hand.
make check also runs libedit/charset_test, which checks the EBCDIC
character handling (it does nothing in an ASCII build). On Linux,
cd libedit && make test-ebcdic checks the EBCDIC code paths with gcc's
EBCDIC literals, and make verify confirms the vendored sources are
upstream plus the patch.
make compdbwritescompile_commands.jsonfor clangd and clang-tidy. It covers every zash and libedit source with the flags the build actually uses; rerun it after changing flags..clang-format(clang-format 21 or later) and.clang-tidyset the house style and checks:clang-format -i *.c *.h libedit/port/*.[ch],clang-tidy -p . *.c libedit/port/*.c.- Vendored code is exempt.
libedit/netbsd/has its own.clang-formatand.clang-tidythat switch both tools off, because it is kept as upstream plus one patch. .gitattributeskeeps LF line endings everywhere (the tests, the vendored sources and their patch are compared byte for byte) and, on z/OS, checks the text out as IBM-1047 and tags it.
| File | Role |
|---|---|
shell.h |
shared declarations |
main.c |
startup, options, errors, top-level loop |
input.c |
input sources; keeps stdin offsets correct for scripts on stdin |
zparse.c |
lexer and recursive-descent parser, here-documents |
expand.c |
expansion, field splitting, globbing, arithmetic |
eval.c |
evaluation, pipelines, background jobs, $(...) |
exec.c |
PATH hash, functions, redirections, fd plans |
var.c |
variables and the cached environment |
jobs.c |
background process table |
builtins.c |
POSIX builtins |
lineedit.c complete.c zprompt.c zhistory.c |
interactive features |
sys_posix.c sys_zos.c |
platform layer: spawn, opening redirections, tagging |
zos_builtins.c |
tso, chtag, extattr |
mem.c |
arena allocator and growable buffers |
libedit/ |
bundled NetBSD libedit: upstream files in netbsd/, port layer in port/ |
DESIGN.md explains the design decisions in more detail.
- No job control,
trap,kill,getopts,printf,command,alias,ulimitortimes. - Arithmetic has no assignment operators (
=,+=,++) and no?:. cdalways resolves physical paths (no-L). There is no$LINENO, and error messages have no line numbers.- No programmable completion (
complete,compgen). The bash-completion scripts are written in bash itself and can't be reused. - No
!!history expansion. A multi-line command is stored as several history entries. $(...)containing builtins or compound commands still forks.
zash is BSD 3-Clause; see LICENSE.
libedit/netbsd/ is third-party code under its original licenses:
3-clause BSD (University of California), 2-clause BSD (The NetBSD
Foundation) and ISC. These are reproduced in
libedit/LICENSE. Binary distributions of zash built
with the bundled libedit must include those notices.