mksh FAQ

Sponsored by
HostEurope Logo

mksh FAQ

MirBSD Korn Shell — frequently asked questions

This list is an (incomplete; feedback and patches welcome) attempt to answer questions that people often have about mksh, the MirBSD Korn Shell.

Table of contents

How do you spell mksh? How do you pronounce it?

This shell is spelt either “mksh” (with, even at the beginning of a sentence, an initial lowercase letter; this is important) or “MirBSD Korn Shell”, possibly with “the”.

I usually pronounce it as “em-ka-es-ha”, that is, the letters individually in my native German, emphasis on the first syllable, or say “MirBSD Korn Shell”, although it is manageable, mostly for Slavic speakers, to actually say “mksh” as if it were a word ☺

Oh… I’ve run into this one, didn’t I? “MirBSD” is pronounced “Mir-Be-Es-De” germanically, for anglophones “Mir-beas’tie” is fine.

This translates well into other languages, such as eme-ka-ese-ache in Spanish, although English speakers may still find “Mir-beastie korn shell” more palatable.


I’m a $OS (Android, OS/2, …) user, so what’s mksh?

mksh is a so-called (Unix) “shell” or “command interpreter”, similar to COMMAND.COM, CMD.EXE or PowerShell on other operating systems you might know. Basically, it runs in a terminal (“console” or “DOS box”) window, taking user input and running that as commands. It’s also used to write so-called (shell) “script”s, short programs made by putting several of those commands into a “batch file”.

On Android, mksh is used as the system shell — basically, the one running commands at system startup, in the background, and on user behalf (but never of its own). Any privilege pop-ups you might be encountering are therefore not caused by mksh but by some other code invoking mksh to do something on its behalf.


I’m an OS/2 user, what else do I need to know?

Unlike the native command prompt, the current working directory is, for security reasons common on Unix systems which the shell is designed for, not in the search path at all; if you really need this, run the command PATH=.$PATHSEP$PATH or add that to a suitable initialisation file (~/.mkshrc).

There are two different newline modes for mksh-os2: standard (Unix) mode, in which only LF (0A hex) is supported as line separator, and “textmode”, which also accepts ASCII newlines (CR+LF), like most other tools on OS/2, but creating an incompatibility with standard mksh. If you compiled mksh from source, you will get the standard Unix mode unless -T is added during compilation; however, you will most likely have gotten this shell through komh’s port on Hobbes, or from his OS/2 Factory on eComStation Korea, which uses “textmode”, though. Most OS/2 users will want to use “textmode” unless they need absolute compatibility with Unix mksh and other Unix shells and tools.


How does this relate to ksh or the Korn Shell?

The Korn Shell (AT&T ksh) was authored by David Korn; two major flavours exist (ksh88 and ksh93), the latter having been maintained until 2012 (last formal release) and 2014 (last beta snapshot, buggy). A ksh86 did exist.

There’s now ksh2020, a project having restarted development around November 2017 forking the last ksh93 v- (beta) snapshot and continuing to develop it, presented at FOSDEM.

AT&T ksh88 is “the (original) Korn Shell”. Other implementations, of varying quality (MKS Toolkit’s MKS ksh being named as an example of the lower end, MirBSD’s mksh at the upper end). They are all not “Korn Shell” or “ksh”. However, mksh got blessed by David Korn, as long as it cannot be confused with the original Korn Shell.

The POSIX shell standard, while lacking most Korn Shell features, was largely based on AT&T ksh88, with some from the Bourne shell.

mksh is the currently active development of what started as the Public Domain Bourne Shell in the mid-1980s with ksh88-compatibl-ish extensions having been added later, making the Public Domain Korn Shell (pdksh), which, while never officially blessed, was the only way for most to get a Korn Shell-like command interpreter for AT&T’s was proprietary, closed-source code for a very long time. pdksh’s development ended in 1999, with some projects like Debian and NetBSD® creating small bug fixes (which often introduced new bugs) as part of maintenance. Around 2003, OpenBSD started cleaning up their shipped version of pdksh, removing old and compatibility code and modernising it. In 2002, development of what is now mksh started as the system shell of MirBSD, which took over almost all of OpenBSD’s cleanup, adding compatibility to other operating systems back on top of it, and after 2004, independent, massive development of bugfixes including a complete reorganisation of the way the parser works, and of new features both independent and compatible with other shells (ksh93, GNU bash, zsh, BSD csh) started and was followed by working with the group behind POSIX to fix issues both in the standard and in mksh. mksh became the system shell in several other operating systems and Linux distributions and Android and thus is likely the Korn shell, if not Unix shell, flavour with the largest user base. It has replaced pdksh in all contemporary systems except QNX, NetBSD® and OpenBSD (who continue to maintain their variant on “low flame”).

dtksh is the “Desktop Korn Shell”, a build of AT&T ksh93 with some additional built-in utilities for graphics programming (windows, menu bars, dialogue boxes, etc.) utilising Motif bindings.

MKS ksh is a proprietary reimplemention aiming for, but not quite getting close to, ksh88 compatibility.

SKsh is an AmigaOS-specific Korn Shell-lookalike by Steve Koren.

The Homepage of the #ksh channel on Freenode IRC contains more information about the Korn Shell in general and its flavours.


How should I package mksh? (common cases)

Export a few environment variables, namely CC (the C compiler), CPPFLAGS (all C præprocessor definitions), CFLAGS (only compiler flags, no -Dfoo or anything!), LDFLAGS (for anything to pass to the C compiler while linking) and LIBS (appended to the linking command line after everything else. You might wish to export LDSTATIC=-static for a static build as well.

When cross-compiling, CC is the cross compiler (mksh currently does not require a compiler targetting the build system), but you must also export TARGET_OS to whatever system you are compiling for, e.g. “Linux”. For most operating systems, that’s just the uname(1) output. Some very rare systems also need TARGET_OSREV; consult the source code of for details.

Create two subdirectories, say build-mksh and build-lksh. In each of them, start a compilation by issuing sh ../ -r followed by running the testsuite¹ via ./ For lksh(1) add -DMKSH_BINSHPOSIX to CPPFLAGS and use sh ../ -r -L to compile.

See below if the testsuite fails.

Install build-mksh/mksh as /bin/mksh (or similar), build-lksh/lksh as /bin/lksh with a symlink(7) to it from /bin/sh (if desred), and mksh.1 and lksh.1 as manpages (mdoc macropackage required). Install dot.mkshrc either as /etc/skel/.mkshrc (meaning your users will have to manually resynchronise their home directories’ copies after every package upgrade) or as /etc/mkshrc, in which case you install a redirection script like Debian’s into /etc/skel/.mkshrc. You may need a summary of the licence information.

At runtime, the presence of /bin/ed as default history editor is recommended, as well as a manpage formatter; you can also install preformatted manpages from build-*ksh/*ksh.cat1 if nroff(1) (or $NROFF) is available at build time by removing the -r flag from either invocation.

Some shell features require the ability to create temporary files and FIFOS (cf. mkfifo(2))/pipes at runtime. Set TMPDIR to a suitable location if /tmp isn’t it; if this is known ahead of time, you can add -DMKSH_DEFAULT_TMPDIR=\"/path/to/tmp\" to CPPFLAGS. We currently are unable to determine one on Android because its bionic libc does not expose any method suitable to do so in the generic case.

① To run the testsuite, ed(1) must be available as /bin/ed, and perl(1) is needed. When cross-compiling, the version of the first ed binary on the PATH must be the same as that in the target system on which the tests are to be run, in order to be able to detect which flavour of ed to adjust the tests for. Busybox ed is broken beyond repair, and all three ed-related tests will always fail with it.


How does mksh load configuration files?

The shell loads first /etc/profile then ~/.profile if called as login shell or with the -l flag, then loads the file $ENV points to (defaulting to ~/.mkshrc) for interactive shells (that includes login shells).

Distributors should take care to either install the dot.mkshrc example provided into /etc/skel/.mkshrc (so that it’s available for newly created user accounts) and ensure it can propagate to existing accounts or, if upgrading these is difficult, install the shipped file as, for example, /etc/mkshrc and install a skeleton file, such as the one in Debian, that sources the file in /etc.

It’s vital that users can change the configuration, so do not force a root-provided config file onto them; the shipped file, after all, is just an example.

If you need central user and configuration management and cannot use something that installs skeleton files upon home directory creation (like pam_mkhomedir), you can export ENV in /etc/profile to a file (say /etc/shellrc) that sources the per-shell file. Users can, this way, still override it by setting a different $ENV in their ~/.profile.


The testsuite fails!

The mksh testsuite has uncovered numerous bugs in operating systems (kernels, libraries), compilers and toolchains. It is likely that you just ran into one. If you’re using LTO (the option -c lto) try to disable it first — especially GCC is a repeat offender breaking LTO and its antecessor -fwhole-program --combine and tends to do wrong code generation quite a bit. Otherwise, try lowering the optimisation levels, bisecting, etc.


I forbid stat(2) in my SELinux policy, and some things do not work!

Don’t break Unix. Read up on the GIGO principle. Duh.


Why doesn’t this use a Makefile to build?

Not all supported target operating environments have a make utility available, and shell was required for “mirtoconf” (like autoconf) already anyway, so it was chosen to run the make part as well.

You can, however, add the -M flag to your invocations to let it produce a file tailored for this specific build which you can then include in a Makefile, such as with the BSD make(1) “.include” command or GNU make equivalent. It even contains, for the user to start out with, a commented-out example of how to do that in the most basic manner.


Why do other BSDs and QNX still use pdksh instead of mksh?

Some systems are resistant to change, mostly due to bikeshedding (some people would, for example, rather see all shells banned to ports/pkgsrc®) and hysterial raisins (historical reasons ☻). Most BSDs have mksh packages available, and it works on all of them and QNX just fine.

In fact, on all of these systems, you can replace their 1999-era /bin/ksh (which is a pdksh) with mksh. On at least NetBSD® 1.6 and up (not 1.5) and OpenBSD, even /bin/sh is fair game.

MidnightBSD notably has adopted mksh as system shell (thanks laffer1).


Why is there no mksh in OpenBSD’s ports tree?

OpenBSD don’t like people who fork off their project at all; heck, they don’t even like the people they themselves forked off (NetBSD®). Several people tried over the years to get one committed, but nobody dared so as to not lose their commit bit. If you try, succeed, and survive Theo, however, kudos to you! See also the “other BSDs” FAQ entry.


I’d like an introduction.

Unfortunately, nobody has written a book about mksh yet, although other shells have received (sometimes decent) attention from authors and publishers. This FAQ lists a subset of things packagers and generic people ask, and the mksh(1) manpage is more of a reference, so you are probably best off starting with a shell-agnostic, POSIX or ksh88 reference such as the first edition (the second one deals with ksh93 which differs far more from mksh than ksh88, as ancient as it is, does) of the O’Reilly book (⚠ disclaimer: only an example, not a recommendation) and going forward by reading scripts (the “shellsnippets” repository referenced in the #ksh channel homepage (see the top of this document) has many examples) and trying to understand them and the mksh specifics from the manpage.


My prompt from <some other shell> does not work!

Contact us on the mailing list or on IRC, we’ll convert it for you. Also have a look at the PS1 section in the mksh(1) manpage (search for “otherwise unused char”, e.g. with / in less(1), to spot it quickly).

My prompt is weird!

There are several reasons why your PS1 might be not what you’d expect:

  • $PS1 is exported. Do not export PS1! (This was agreed upon as suggestion in a discussion between bash, zsh and Korn shell developers.) The feature set of different shells vastly differs and each shell should use its default PS1 or from its startup files.
  • $ENV is set and/or exported.
  • Your prompt is just “# ”: you’re entering a root shell, and $PS1 does not contain the ‘#’ character, in which case the shell forces this prompt, making extra privileges obvious.
  • Your prompt is just “$ ”: perhaps your system administrator did not install the shipped dot.mkshrc file, or you did not copy /etc/skel/.mkshrc into your home directory (perhaps it was created before mksh was installed?). Without another idea for a fix, get this file and store it as ~/.mkshrc then run mksh; this will at the very least install our sample (“user@host:path $ ”) prompt.
  • Your prompt contains things like “\u” or “\w”: it is for another shell and needs converting.
  • Your prompt contains colours, and when the command line is long the cursor position or screen contents, especially using the history, is off: terminal escapes must be escaped from the shell; check the PS1 section in the manpage: search for “otherwise unused char” (see above).
  • If the prompt doesn’t leave enough space on the right, the shell inserts a line break after it when rendering.

On startup files and $ENV across and detecting various shells

Interactive shells look at ~/.mkshrc (or /system/etc/mkshrc on Android and /etc/mkshrc on FreeWRT and OpenWrt) by default. This location can, however, be overridden by setting the ENV environment variable. (FreeBSD is rumoured to set it in their system profile.) It’s better to not set $ENV if possible and let every shell user their native startup files; otherwise, you must ensure that it runs under all shells. Check $BASH_VERSION (GNU bash), $KSH_VERSION (contains “LEGACY KSH” or “MIRBSD KSH” for mksh, “PD KSH” for ancient mirbsdksh/oksh/pdksh, “Version” for ksh93); $NETBSD_SHELL (NetBSD ash); POSH_VERSION (posh, a pdksh derivative); $SH_VERSION (“PD KSH” as sh), $YASH_VERSION (yash), $ZSH_VERSION (or if $VERSION begins with “zsh”); a list of more approaches exists.


Multiline command editing

mksh is very independent of the terminal and external libraries and databases, such as termcap, and therefore is conservative in which ANSI control codes are sent to the terminal.

For this reason, mksh’s input line editing uses a “windowed one-line” concept: the line the cursor is on is a “window” into the whole input, horizontally scrolled. Some other shells (that are much larger and have more dependencies on external tooling) use a “multi-line” editing mode, and users occasionally wish for this. It is on the long-term TODO, but (due to the aforementioned implications) this is not trivial.

One way to achieve multi-line editing is to disable input line editing: set +o emacs +o vi
This will, however, lose you all editing features: tab completion, cursor keys, history, etc.

Another way, if you don’t need it all the time, is to use a function that spawns your editor on the input line: press ^Xe in the default emacs mode or Esc + v in vi mode. Once you exit the editor, whatever was written there is run; this includes the original command line if you quit without saving, so request the editor to exit nōn-zero (e.g. using jupp’s “abendjoe” command) to prevent execution. This is really useful to write ad-hōc scripts as well.


^L (Ctrl-L) does not clear the screen

Use ^[^L (Escape+Ctrl-L) or rebind it:
bind '^L=clear-screen'


^U (Ctrl-U) clears the entire line

If it should only delete the line up to the cursor, use:
bind -m ^U='^[0^K'


Cursor Up behaves differently from zsh

Some shells make Cursor Up search in the history only for commands starting with what was already entered. mksh separates the shortcuts: Cursor Up goes up one command and PgUp searches the history as described above. You can, of course, rebind:
bind '^XA=search-history-up'


Can mksh set the title of the window according to the command running?

There’s no such thing as “the command currently running”; consider pipelines and delays (cmd1 | (cmd2; sleep 3; cmd3) | cmd4). There is, however, a way to make the shell display the command line during the time it is executed; for testing, you will need to download this script and source it. For merging into your ~/.mkshrc you should first understand how it works: lines 4–18 set a PS1 (prompt) equivalent to lines 84–96 of the stock dot.mkshrc, with one change: line 15 (print >/dev/tty …) is new, inserted just before the return command of the function substitution in the default prompt; this is what you’ll need to merge into your own, custom, prompt (if you have one; otherwise pull this adaption to the default one). Line 19 is the only other thing in this script rebinding the Ctrl-M key (which is normally produced by the Enter/Return key) to code that… does something crazy. This trick however does funny things with multiline commands, so if you type something out in multiple lines, for example here documents or loops press Ctrl-J instead of Enter/Return after each line including the first (at PS1) and final (at PS2) one.


How do I start mksh on a specific terminal?

Normally: mksh -T/dev/tty2

However, if you want for it to return (e.g. for an embedded system rescue shell), use this on your real console device instead: mksh -T!/dev/ttyACM0

mksh can also daemonise (send to the background): mksh -T- -c 'exec cdio lock'


What about programmable tab completion?

The shell itself provides static deterministic tab completion. However, you can use hooks like reprogramming the Tab key to a command line editor macro, and using the evaluate-region editor command (modulo a bugfix) together with quote-region and shell functions to implement a programmable completion engine. Multiple people have been considering doing so in our IRC channel; we’ll hyperlink to these engines when they are available.


How POSIX compliant is mksh? Also, UTF-8 vs. locales?

You’ll need to use the lksh binary, unless your C long type is 32 bits wide, for POSIX-compliant arithmetic in the shell. This is because mksh provides consistent, wraparound-defined, 32-bit arithmetics on all platforms normally. You’ll also need to enable POSIX mode (set -o posix) explicitly, which also disables brace expansion upon being enabled (use set -o braceexpand to reenable if needed).

For the purpose of POSIX, mksh supports only the C locale. mksh’s utf8-mode (which only supports the BMP (Basic Multilingual Plane) of UCS and maps raw octets into the U+EF80‥U+EFFF wide character range; see Arithmetic expressions in mksh(1) for details) must stay disabled in POSIX mode (it is disabled upon enabling POSIX mode in R56+ but don’t depend on this to stay once locale tracking will be implemented).

The following POSIX sh-compatible code toggles the utf8-mode option dependent on the current POSIX locale, for mksh to allow using the UTF-8 mode, within the constraints outlined above, in code portable across various shell implementations:

	case ${KSH_VERSION:-} in
		case ${LC_ALL:-${LC_CTYPE:-${LANG:-}}} in
		*[Uu][Tt][Ff]8*|*[Uu][Tt][Ff]-8*) set -U ;;
		*) set +U ;;
		esac ;;

In near future, (UTF-8) locale tracking will be implemented, though.

The shell is pretty close to POSIX, when run as lksh -o posix under the "C" locale it is intended to match. It does not do everything like other POSIX-compatible or ‑compliant shells, though.


What differences in function-local scopes are there?

mksh has a different scope model from AT&T ksh, which leads to subtle differences in semantics for identical builtins. This can cause issues with a nameref to suddenly point to a local variable by accident. (Other common shells share mksh’s scoping model.)

GNU bash allows unsetting local variables; in mksh, doing so in a function allows back access to the global variable (actually the one in the next scope up) with the same name. The following code, when run before function definitions, changes the behaviour of unset to behave like other shells (the alias can be removed after the definitions):

	case ${KSH_VERSION:-} in
		function unset_compat {
			\\builtin typeset unset_compat_x

			for unset_compat_x in "$@"; do
				eval "\\\\builtin unset $unset_compat_x[*]"
		\\builtin alias unset=unset_compat

When a local variable is created (e.g. using local, typeset, integer or \\builtin typeset) it does not, like in other shells, inherit the value from the global (next scope up) variable with the same name; it is rather created without any value (unset but defined).


I get an error in this regex comparison

Use extglobs instead of regexes:
[[ foo =~ (foo|bar).*baz ]]
… becomes…
[[ foo = *@(foo|bar)*baz* ]]


${@?}: bad substitution

In mksh, you cannot assign to or trim a vector (yet). For most cases it is possible to write the affected code in a way avoiding this extension; for example, trimming ${@#foo} could be applied to $1 only and ${@?} can be replaced with a test whether $# -eq 0.


Are there any extensions to avoid?

GNU bash supports “&>” (and “|&”) to redirect both stdout and stderr in one go, but this breaks POSIX and Korn Shell syntax; use POSIX redirections instead:

GNU bash foo |& bar |& baz &>log
POSIX foo 2>&1 | bar 2>&1 | baz >log 2>&1

Something is going wrong with my loop

Most likely, you’ve encountered the problem in which the shell runs all parts of a pipeline as subshell. The inner loop will be executed in a subshell and variable changes cannot be propagated if run in a pipeline:

	bar | baz | while read foo; do ...; done

Note that exit in the inner loop will also only exit the subshell and not the original shell. Likewise, if the code is inside a function, return in the inner loop will only exit the subshell and won’t terminate the function.

Use co-processes instead:

	bar | baz |&
	while read -p foo; do ...; done
	exec 3>&p; exec 3>&-

If read is run in a way such as while read foo; do ...; done then leading whitespace will be removed (IFS) and backslashes processed. You might want to use while IFS= read -r foo; do ...; done for pristine I/O.

Similarly, when using the -a option, use of the -r option might be prudent (read -raN-1 arr <file); the same applies for NUL-terminated lines:

	find . -type f -print0 |& \
	    while IFS= read -d '' -pr filename; do
		print -r -- "found <${filename#./}>"

“command” doesn’t expand aliases as in ksh93

This is because AT&T ksh93 ships a predefined alias enabling this:
alias command='command '
put this into your ~/.mkshrc (note the space before the closing single quote)


Didn’t there used to be a cat(1) builtin?

Up to and including mksh R59c, we indeed shipped a built-in cat(1) inside mksh; this was added originally because Android did not have one at all (but they have since imported a BSD cat). While it could speed up some sh scripts correct signal handling is hard to get right, so (with regret) it was removed in 2021. 🙀


“rename” doesn’t work as expected!

There’s a rename built-in utility in mksh, which is a very thin wrapper around the rename(2) syscall. It receives two pathnames, source and destination where the first is then atomically renamed to the latter. It does not move, i.e. fails for different filesystems.

The GNU package util-linux has a different rename command. If you wish to invoke an external utility (in favour over a builtin), you can use dot.mkshrc’s function enable or put the following into your ~/.mkshrc:

alias rename="$(whence -p rename)"

Didn’t there used to be a sleep(1) builtin?

Up to and including mksh R59c, we indeed shipped a subsecond-capable select(2)-based built-in sleep(1). This got originally added because too many platforms do not support sub-second sleep, which nowadays is of less concern. It also led to users complaining about lack for system *ahem* GNU extensions, but the cause of its demise is that getting signal handling right, in a portable way and without too many syscalls (there’s a threshold over which fork+exec is cheaper!), isn’t feasible if even at all possible.

The MirOS Project now ships a portable sleep which similarily is select(2)-based and capable of subsecond sleep but in addition supports all GNU extensions related to specifying the amount of time to sleep. It will work on at least all platforms on which mksh had a builtin before. Please install this if your operating system lacks a good enough sleep(1) utility.

Note that, if your OS lacks select(2), you’ll lose out either way. In that case, GNU coreutils’ sleep, which is built on older syscalls, may work if the copyleft licence isn’t a showstopper for you.


“+=” behaves differently from other shells

In POSIX shell, “=” in code like var=content is a string assignment, always. You can use var=$((content)) for an arithmetic assignment that mostly uses C language rules.

It stands to consider that the common shell extension “+=” as in var+=content would always do string concatenation; it does in mksh, but not in some other shells, in which, when var has been declared integer, addition is done instead.

You can make the code portable by using “((…))” (a.k.a. let) instead: (( var += content )) does arithmetic addition in all shells involved.


I use “set -e” and my code unexpectedly errors out

I personally recommend people to not use “set -e”, as it makes error handling more difficult. However, some insist. There have been bugfixes (relative to e.g. oksh/loksh and posh) in this aspect, and the user has to make sure $? is always 0 ASAP even after a command that doesn’t check it.

istwo() {
        for i in "$@"; do
                test x"$i" = x"2" && echo two
set -e
istwo 1
echo END

This can be fixed by either adding an explicit “:” (or “true”) after the comparison, or even…

test x"$i" = x"2" && echo two || :

… or right after the done inside the function, but…

test x"$i" != x"2" || echo two

… negating the condition and using “||” is preferable.

Remember that Korn shell-style functions (with function keyword and without parenthesēs) in AT&T ksh93 and mksh R51 and up have their own shell option scope, but while…

function istwo {
        set +e

… might help in error handling, the return status of a function is still the last errorlevel inside, so an explicit true (“:”) or, more explicitly, “return 0” at its end is still needed if the caller runs under set -e.


I use “set -eo pipefail” and my code unexpectedly errors out

Related to the above FAQ entry, using set -o pipefail makes the following construct error out:

	set -e
	for x in 1 2; do
		false && echo $x
	done | cat

This is because, while the && ensures that the inner command’s failure is not taken, it sets the entire fordone loop’s errorlevel, which is passed on by -o pipefail.

Invert the inner command:
true || echo $x


My question is not answered here!

Do read the mksh(1) manual page. You might also wish to read the homepage of the #ksh IRC channel on Freenode which lists several resources for Korn or POSIX-compatible shells in general. Or, contact us (developer and users), for example via IRC.


How do I contact you (to say thanks)?

You can say hi in the #!/bin/mksh channel on Freenode IRC, although a donation wouldn’t be amiss ☺ The mailing list can also be used.

MirBSD Logo