Article dec2006.tar

Korn Shell Nuances

Ed Schaefer and John Spurgeon

We use the Korn shell almost exclusively. This month we'll investigate an eclectic mix of topics pertaining to the Korn shell and related shells:

  • How can you tell which shell you are using?
  • What happens if the shell isn't specified on the first line of a script?
  • Setting the environment variable PS1.
Which Shell Am I Using?

From the command line, how can users determine which shell they are currently using? Looking at the user's entry in the /etc/passwd file only indicates the user's login shell, not necessarily what the shell is now.

Similarly, the environment variable SHELL or shell may be set but not reset for any spawned child shells. The Solaris 9 man page for ksh states that SHELL is not set at all by the shell, but is set by login on some systems.

Within a script, $0 is set to the name of the script. But at the command line, $0 provides the name of the shell. For example, if you are using the Korn shell, then the command "echo $0" displays "-ksh".

This method also works for the Bourne shell. Spawn a Bourne shell and verify the child "sh" displays:

echo $0 
sh 
            
Unfortunately, this technique doesn't work with the shells csh and tcsh.

Probably the best method of tracing which shells are running is to use the ps command. One technique is to execute ps with no arguments. Echoing $$ obtains the process id of your current shell. By cross-referencing the process id with the output of ps, you can determine that the current shell is the Bourne shell (sh).

When spawning multiple shells, you can quickly lose track of which shell is spawning what. Here is a long listing process command, ps -l:

F S   UID   PID  PPID  C PRI NI     ADDR    SZ    WCHAN TTY     TIME CMD 
8 S  1001 11404 11399  0  51 20 e4f35898   408 e4f35904 pts/0   0:00 ksh 
8 S  1001 16823 16822  0  51 20 e4d92158    62 e4d921c4 pts/0   0:00 sh 
8 S  1001 16822 11404  0  41 20 e3758120   291 e3758344 pts/0   0:00 csh 
8 S  1001 16824 16823  0  51 20 e3755118   408 e3755184 pts/0   0:00 ksh 
In the above example, tracing the parent id, PPID, tells you that parent shell ksh, PID 11404, spawned a csh followed by sh, followed by another ksh.

Executing ps -p $$ lists only data from the current process id. The last line contains the current shell:

   PID TTY      TIME CMD 
 16824 pts/0    0:00 ksh 
A script can use a variety of tools to parse the output of ps and pick out the shell. In the following example, the xargs command spits out all the words on a continuous line, and the awk command grabs the last field:

ps -p $$ | xargs | awk ' { print $NF } ' 
    
If you prefer to eliminate xargs and its plumbing, save the last field of every line read, and print only the last one at the end of the input:

ps -p $$| awk ' { lf=$NF } END { print lf } ' 
Alternatively, if your ps command supports the -o format option, execute ps with the comm format:

ps -o comm -p $$ 
The output is:

COMMAND 
ksh 
The comm format displays only the command being executed -- ksh in this case. Now, by using the formatting option, simply grab the last line to obtain the shell:

ps -o comm -p $$ | tail -1 
Specifying a Shell on the First Line of a Script

One of the first lessons a new shell programmer learns is not to rely on the default environment. It's always a good idea, for example, to invoke the shell of choice on the first line of a script like this:

#!/bin/ksh 
Not only must the shell specification be on the first line, but the "#" character must be in the first column. What happens if these conditions are not met?

A simple test script illustrates the effect. To begin, invoke the Bourne shell from the command line, so the script is interpreted by sh if a shell is not explicitly specified on the first line of the script. Next, place the following commands in a script:

#!/bin/ksh 
echo $(ps -p $$) 
    
Executing the script produces the following output on our system:

PID TTY TIME CMD 16057 pts/3 0:00 test.ss 
Next, try placing a comment on the first line, so the script looks something like this:

# Doh! 
#/bin/ksh 
echo $(ps -p $$) 
Execute the script again, and this error displays:

./test.ss: syntax error at line 3: '(' unexpected 
The problem is that the Bourne shell is interpreting the script instead of the Korn, and the ksh command substitution syntax $() is not supported by sh. The same thing happens if white space precedes the string #/bin/ksh on the first line of the script.

Since many Korn shell scripts appear to work fine when interpreted by the Bourne shell, problems with where the shell specification is located may not always be obvious. We learned this the hard way.

Setting the Primary Prompt String

Using the environment variable PS1, users can create a dynamic prompt string. Here's a common example using Korn shell environmental variables:

export PS1='$LOGNAME@$SYSNAME:$PWD'":> "
    
Since environmental variable PWD is dynamically set by the cd command, each time the directory changes, our PS1 displays the system name, the user name, and the current working directory.

According to Bolsky and Korn, the "ksh performs parameter expansion, arithmetic expansion, and command substitution on the value of PS1 each time before displaying the prompt". However, not all implementations of the Korn shell provide all of these features.

The version of the Korn shell we use most often performs parameter expansion but not command substitution. Under these conditions, suppose we want the prompt string to only display the last two directories of the current working directory's full pathname. For example, if PWD were:

/home/eds/jspurgeo/myentry/src 
we only want to see:

myentry/src 
Here's one way to solve the problem:

export PS1='${PWD##/*/*}${PWD#${PWD%/*/*}} >' 
To simplify the explanation, we break the solution into three pieces:

  • With ${PWD%/*/*} -- The % operator matches the end of the PWD variable for everything between two slashes. This leaves an intermediate string, which is actually what should be deleted.
  • Match the beginning of the PWD directory with the intermediate string using the # operator: ${PWD#${PWD%/*/*}}, which deletes the start of the string leaving only the last two directories.
  • The second step doesn't solve the special case where PWD is less than two directories, that is, /tmp, /. So, we perform a final match using the ## operator:
    ${PWD##/*/*}${PWD#${PWD%/*/*}} 
        
    If the PWD string contains two or less slashes, don't delete anything.

References

Bolsky, Morris I., David G. Korn. 1995. The New Kornshell Command and Programming Language. Upper Saddle River, NJ: Prentice Hall PTR.

John Spurgeon is a software developer and systems administrator for Intel's Factory Information Control Systems, IFICS, in Aloha, Oregon. He is currently preparing to ride a single-speed bicycle in Race Across America in 2007.

Ed Schaefer is a frequent contributor to Sys Admin. He is a software developer and DBA for Intel's Factory Information Control Systems, IFICS, in Aloha, Oregon. Ed also hosts the monthly Shell Corner column on UnixReview.com. He can be reached at: shellcorner@comcast.net.