Devious Fish
Music daemons & more

Why I am a Korn Shell Fan

A lot of Linux fanboys these days love everything Gnu, and like to talk shit about non-Gnu stuff. Perhaps it’s just a modern-day version of the empty Commodore vs. Apple vs. TRS-80 arguments we had back in the day: they each had strengths and weaknesses, so a lot depended on what you wanted to do. And a lot had to do with that you were familiar with.

Nevertheless, there are reasons I prefer Korn shell. If you have different reasons, you may prefer other shells. And that’s just fine too–but if you’re just sticking with something because it’s all the rage, that’s not a good reason.

So let’s start with the small stuff and work our way up.

Floating point math

You don’t need it often, but when you do, you don’t have to screw around using bc or dc to do it.

Korn shell’s built-in sleep accepts floating-point values too.

print command

echo has differing variants on different platforms. Ksh’s print works the same everywhere.

Compound variables

You can create a variable that has multiple fields in it. A simple struct of sorts. See typeset -C, which, by the way, you can do with…

Built-in documentation

Try it out: typeset --?? (or typeset --man) to learn about the myriad of options to typeset. No longer do you need to pore through a crazy long man page to find what you want, it’s right at your finger tips. Almost all the built-ins have them. And the man page isn’t all: -? gives a short synopsis, and --help shows the synopsis and option details.

And if you want to print that:

typeset --nroff 2>&1 | groff -man -T ps | ps2pdf > typeset.pdf

It also does --html. See getopts --man for more.

Self-documenting getopts

Those man pages just mentioned? They come from the getopts string using Ksh’s long format. (The commonplace short format is accepted too.) The long format is described in the getopts built-in man page, but you may also need to cookbook from an example; I’ve included some info after this essay. The format is a little weird, but easier than writing raw man pages. And it’s used to provide the same sort of short synopses, long synopses, and man pages on demand. Lastly, thegetopts long options string is both directive and documentation, ensuring docs and code are in sync.

Good documentation

Bash has decent documentation, comparable to the quality of Korn shell. zsh documentation, however, can be sparse, especially regarding some of the command options. That and the frequently-corrupting shell history (especially if you do a lot of multi-statement commands on the command line, which seem to encourage the behavior, then are lost uninvoked when the corruption happens) drove me away.


You can set a function path. If a function is requested and not known, Korn shell searches and loads it if found. You can have a library of functions, without having to copy-and-paste your code (or the bugs that go with that code).

True local variables

In modern shells, there are two ways to declare functions:

name () { ...


function name { ...

Usually, these behave the same. And because of backward combatability, the former must behave like the old Bourne Shell, which was insanely simple by today’s standards, to the point of being broken by modern expectations. But it’s a long-lived problem in software: we can’t fix anything because of all the things the fixes would break.

One of those simplicities is the lack of local variables. Recursion wasn’t possible in the old world. (Well, maybe with judicious use of eval. Is your hair gray yet?)

And looking back, Bourne shell only had the first form of function invocation. Korn shell introduced the second form with different behaviors to correct the defects.

So in Korn shell, if you create a local variable in a C-style function declaration (with the parenthesis), the shell ignores you and uses a global variable.

When you use function, you do get local variables. Real, true, lexical-scoped function scope locals: the functions you call can’t see them, nor fudge them up. And if they forget to declare their own locals, they pollute the global namespace instead of silently screwing up someone else’s locals.

In Bash, locals are created in the current context. But they are dynamically scoped, thus available to every function you call. Or any function they call in turn. And if one of those functions forgets to declare a local, it may very well unintentionally alter a variable belonging to another function. Pollution of the global namespace isn’t good, but it’s less hazardous than this nonsense.

Pipeline variable retention

When running a pipeline, Korn shell retains any values set at the end of the pipeline. So this construct works:

grep "^#" < source | sed -e ... | some other stuff | read variable

But in Bash, it doesn’t. The variable is read into a subshell which is disbanded when the pipeline completes. Now, the argument from Bash proponents is to just do this:

variable=$(grep "^#" < source | sed -e ... | some other stuff)

And that’s true enough when all you want is one line out of a file. But what if you want to stick a while loop on the end?

float total=0
grep "cleared" < checkbook | awk '{print $NF}' | 
while read value
        let "total += value"
print "The total is $total."

In Bash, this wouldn’t work because it’s using floating point math. But even if it was integer math, it wouldn’t work because it would total up the value, then the pipeline would close and the subshell would exit, taking total with it.

I realize there’s other ways to do this particular example. It wouldn’t be hard to run that through sed and make input you could feed to bc or dc to get a total. Or you could feed in a sentinel value to cause the loop to print its value, and capture that. But this is clearer and simpler. No hurdles.

And even if this is a hokey example, the concept stands. The pattern of read input, process it through a pipeline, and then amalgamate/ aggregate it at the end is not rare.

Note that since Bash 4.2, this is behavior is available via shopt -s lastpipe. However, this behavior only takes effect if job control is disabled, so it won’t work on the command line or in any functions you have loaded.

Interactive Use

For interactive use, the two shells seem very similar to me. Both have emacs and vi edit modes, quoting is identical (or nearly so) between the two, and many of the built-in variables are the same.

Bash has the bind command for keymapping; ksh has a KEYBD trap. I’m not sure which is better.

To the extent that Bash has emulated lots of Korn-shell pioneered behaviors, they’ve done well. Except the pipeline handling.

I have spotted at least one feature–case manipulation in parameter substitution–that’s been added to ksh based on its presence in Bash. So the copy-catting is going both ways.

And there are other unique Bash features, like the hook function for missing commands; ksh has no equivalent. (If I was implementing it, though, I’d make it a trap. It would make more sense than a magically named function.)


I do worry about the state of Korn shell. David Korn has moved on, and though AT&T has open sourced the code, will Korn shell be kept up? Only time will tell.

The code is old, and it looks it; like something written in the 80s. How much edit fatigue does it have? I haven’t delved enough into it to know.

Admittedly, I’m more familiar with Korn shell, and that’s an influence. While I can use Bash just fine interactively, I find it frustrating to try to script with. After ksh, bash seems just broken–and in some cases, it’s probably because I’m not used to it. But in other cases, Bash requires jumping through hoops that aren’t necessary when working in Korn shell.

So, in programming features, ksh seems far ahead to me. Bash does get constant attention, though, and perhaps in the future it’ll catch up.

Using the self-documenting getopts

Older shells might not support the advanced getopts. If you are worried about this, you can detect availability with this construct:

if [[ $(getopts '[-][12:abc]' flag --abc; print -- 0$flag) == "012" ]]
    GETOPTS_STRING="old-style-getopts-string here"

The $'' quoting uses C-style quoting within, to keep you sane.

In your parsing loop, provide the selected string:

while getopts "$GETOPTS_STRING" flag
    case "$flag" in
        ... handle your options flags here

So what do you put in the new-style getopts?

A section title and a paragraph for it in the resulting man page:

[+TITLE?example - how to use the \bksh\b(1) getopts string]

If you need a new paragraph, leave out the title:

[+?This is another paragraph under the last title.]

To insert a definition list instead of a paragraph, wrap the entries in braces:

    [+0?The command succeeded.]
    [+1?The file was not mangled correctly.]
    [+2?The file could not be found.]

To define an option:

[s?Flag with short option only.]
[f:long-flag-name?Description of the flag.]
[x:?This flag has no long-option name]
[12:flag-with-only-long-name?There is no short-form of this flag.]

The letter after the opening bracket is the short option. If a long option is used, it’s converted to the short form when provided to you. If you don’t want a short form, provide a 2 or more digit number to be used instead. Numeric argument specifications are enforced, but note 0xhex is allowed and decimal numbers (0.5) are an error.

For options with arguments, you can include a list of values. These are rendered in documentation, but they are not used for getopts processing. So don’t expect any restrictions they document to be applied by getopts.

[l:flag-with-list?Flag that takes only one of the defined values.]:[argument-name:type:attributes:=default-value]{
    [+one?This is the \afirst\a choice.]
    [+two?This is the \asecond\a choice.]
    [+three?This is the \athird\a choice.]

To include information about the script’s creators:

[-title?Name and contact information]

For example, [-author?Perette Barella] or [-license?This is free software released under the MIT license.]

Parameters go after a blank line following all your other text:

from-file to-file

If you have multiple forms, you can list them on separate lines:

from-file to-file
from-file ... to-directory

Lastly, there’s a handful of useful escape sequences: \b (backspace) toggles strong emphasis (bold). \a (bell) toggles emphasis (italic/underline). \v (vertical tab) toggles fixed-width display.

Putting it all together, here’s a demo program you can tinker with:

while getopts -a getopts_info $'
[+NAME?getopts info - document describing the ksh long-form getopts]
[+TITLE?This is a section and a paragraph in the resulting document.]
[+SOME OTHER SECTION?This is a spare section]
[+?Here is an example of a \adefinition list\a]
        [+SOME] ?Text inside this \vdefinition list\v.]
        [+OTHER]?And text for another \vdefinition list\v entry.]
[+?This is a continuation paragraph]
[s?Flag with short option only.]
[f:long-flag-name?Description of the flag.]
[x:?This flag has no long-option name]
[12:flag-with-only-long-name?There is no short-form of this flag.]
[l:flag-with-list?Flag that takes only one of the defined values.]:[argument-name:type:attributes:=default-value]{
    [+one?This is the \afirst\a choice.]
    [+two?This is the \asecond\a choice.]
    [+three?This is the \athird\a choice.]
[-Author?Perette Barella]
[-Copyright?This document is public domain.]

argument argument2 ...
Other forms of this command
[Note that at this point, this is plain text]' option "$@"
        print -- "Argument: '$option' optarg:'$OPTARG'"