May 5, 2008

Korn -- an extended shell

Author: Mark Alexander Bain

Everyone knows what a Linux shell is -- you open up a Linux terminal window (such as Konsole or xterm), type in some commands, and there you are, using your Linux shell. Write your commands to a file, make it executable, run it, and you're a shell programmer. But did you know that there are different shells that you can use, and that each shell operates in a slightly different way? My personal favorite is the Korn shell; by the end of this article, it may be your favorite as well.

By default, Linux generally uses bash, the Bourne again shell. Before moving from that to the Korn shell, consider why you might want to change shells. The Korn binary is smaller than the bash binary, and many functions (such as echo and getopts) are built into the shell rather than separate executables, which means that Korn uses less memory to run and can be quicker as well. In addition, Korn is completely backward compatible with bash. That means that even if you never use any of the Korn functionality, you can still use all of the commands that you're already used to. In addition, Korn offers useful features like built-in arithmetic expressions, compound variables, discipline functions, and coprocesses, all of which we'll cover in a moment.

With Korn's advantages, you may wonder why Linux uses bash by default. Quite simply, the reason is historical. The default shell for Unix was the Bourne shell (or sh), so it was logical that the default shell for Linux should be the Bourne again shell. Of course, when talking about the default, we're actually just talking about your entry in /etc/passwd. If your entry reads something like:

bainm:x:1000:100:Mark Alexander Bain:/home/bainm:/bin/bash

then your default shell is the Bourne again shell, but if it reads something like:

bainm:x:1000:100:Mark Alexander Bain:/home/bainm:/bin/ksh

then your default shell is the Korn shell. You can also check which you're using by querying your system's environment:

> env | grep SHELL
SHELL=/bin/bash

Before you try changing your shell to Korn you may want to make sure that it is actually installed on your system. The easiest way to do that is to look for the executable:

> ls -l /bin/bash /bin/ksh
ls: /bin/ksh: No such file or directory
-rwxr-xr-x 1 root root 677184 2006-12-11 21:20 /bin/bash

If the Korn executable is not there, you can use your distro's installation mechanism to get it; for example, use Yast on SUSE or apt-get install ksh on Debian. Then, to change to the Korn shell, just type /bin/ksh. To stop, type exit at the command line and you're back to bash. You can make Korn your default by changing your information in /etc/passwd, but it's probably safer to do this through your distro's user management software. However, you don't need to change to the shell in order to use it; instead, you can create a script and tell it to use Korn by adding one line at the start of the file:

#!/bin/ksh

Let's now see what additional functionality you get when you start using Korn, starting with Korn's built-in arithmetic expressions.

Built-in arithmetic expressions

If you've tried to carry out any maths using bash you'll have had to have done it indirectly, with an expression like answer=$( echo 1.5 + 1.7 | bc ) or answer=$( echo 1.5 1.7 | awk '{print $1 + $2 }' ). If you use Korn, you'll be able to do that directly by typing answer=$(( 1.5 + 1.7 )) or (( answer = 1.5 + 1.7 )).

You can use this ability in conjunction with some other mathematical functions, such as abs, acos, asin, atan, cos, cosh, exp, int, log, sin, sinh, sqrt, tan, and tanh. So, for example, you could create variable containing a very accurate value for pi with the expression (( pi = 4.0 * ( 4.0 * atan(1.0/5.0) - atan(1.0/239.0) ) )). This would calulate pi to be 3.14159265358979324.

Compound variables

We've just seen how to use a variable to store the value of pi. Let's examine the variables that we might want to use to define a circle using the Korn shell and our pi variable:

> radius=30
> (( circumference = 2 * pi * radius ))
> (( area = pi * radius * radius ))

As you see, you do not need to use the $ sign when using variables; you can if you want to, but you don't need to. In this expression, the variable circumference would now contain 188.495559215387594 and area would contain 2827.43338823081392.

Suppose you want to group these variables together, maybe prefixing each variable with the letters "circle_". With Korn you can use a single variable to do the job -- a compound variable:

> circle=
> circle.radius=30
> (( circle.circumference = 2 * pi * circle.radius ))
> (( circle.area = pi * circle.radius * circle.radius ))

You can use this technique to do more than just neaten up variables; you can use it to create the structures and records that you see in many programming languages:

> driverx=(
name=""
float wage=145.50
integer travel_radius=50
)

Here we've defined a compound variable as a structure and defined a data type for each field, and given each of them a default value. Next we can use our structure to build a record set:

> eval "driver01=$driverx"
> driver01.name="Bill"
> eval "driver02=$driverx"
> driver02.name="Fred"
> (( driver02.wage = ${driverx.wage} * 1.10 )) # Let's give Fred a pay rise
> print "${driver01.name} \$${driver01.wage} ${driver01.travel_radius} miles"
Bill $145.50 50 miles
> print "${driver02.name} \$${driver02.wage} ${driver02.travel_radius} miles"
Fred $160.05 50 miles

Discipline functions

Not only can you assign multiple values to a single variable, but with Korn you can even associate functions with one -- Korn's discipline functions.

A Korn variable can have three discipline functions -- get, set, and unset -- which are executed when the variable is read, written to, or removed. There's also one big difference between these and other shell functions -- these functions don't output anything directly; you'll need make use of the Korn special variable .sh.value. Changing the value of .sh.value from within your function changes the value of your variable. So, using discipline functions, the method for calculating the circumference of a circle becomes:

> function circle.circumference.get {
(( .sh.value = 2 * pi * circle.radius ))
}

This function can be defined on the command line for use in the current shell, or written into a script file, but once done you only have to change the value for the radius variable to change the circumference variable:

> circle.radius=1
> echo ${circle.circumference}
6.28318530717958648
> circle.radius=20
> echo ${circle.circumference}
125.66370614359173

In a very short time you'd be able to create a set of functions that calculates the area and circumference when you input the radius; the area and radius when you input the circumference; and the circumference and radius when you input the area -- all using just the three variables circle.area, circle.circumference, and circle.radius.

Next, we'll just look at something else that you may not have come across before, but which you may find useful -- the coprocess.

Coprocesses

If you're familiar with bash, you already know that a pipe, represented by |, allows the output of one process to be fed as the input to another. A Korn coprocess is simply a two-way pipe. With it you can make background and foreground processes communicate with each other. All you have to do is to send one process to the background (using |&), then use print -p to send information to the process and read -p to obtain outputs from it.

The first thing to do is to create the script to run in the background. The following script accepts a numerical input and keeps a running total:

#!/bin/ksh
total=0
while [ "" == "" ]
do
read input
(( total = total + input ))
echo $total
done

If you save this as keep_count.ksh, you can start using it in the following way, either from the command line or from within a script:

./keep_count.ksh |&
print -p 2
read -p x # x will contain 2
print -p 10
read -p x # will now contain 12

You can have as many processes running in the background as you want. The only problem is working out how to communicate to each of them -- except that it isn't a problem. The secret is to redefine each pipe's file descriptors:

./ keep_count.ksh |&
exec 4>&p # Redefine the input
exec 5&p
exec 7

A couple of points to finish with

Though we said that Korn had backwards compatibility with bash, there are some differences. For example: history. If you type history 100 in bash then the shell will display the last 100 history entries. If you do the same in Korn, it will display all entries from line 100 of the history file. If you see one of the lines that you want to run again, type r and the line number:

>history
1069 ls -l /lib/ast/ksh
1070 ls -l /bin/ksh93
1071 html2text -style pretty /home/bainm/Articles/Bain_ksh.html | wc -w
1072 grep bainm /etc/passwd
1073 ps -ef | grep firefox
1074 kill -9 3639
>r 1071

One other issue you may find if you start using Korn is that if you press the up-arrow key, then instead of seeing the last command that you typed in, you'll see a set of control characters. The same occurs if you press the left, right, or down arrows. The remedy is simple: edit your /etc/.kshsrc file and add the line set -o emacs. Next time you start the shell the keys will work in the way that you're used to.

Category:

  • Shell & CLI