Chapter 4. Interacting with Your Environment

Basics of Setting the Prompt

The prompt displayed by your shell is contained in a shell variable (Section 35.9) called prompt in C-type shells and PS1 in Bourne-type shells. As such, it can be set like any other shell variable.

There are two or three ways to set a prompt. One is a static prompt (Section 4.2) that doesn’t change during your login session (as you change directories, as the time of day changes, etc.). Some shells let you set a dynamic prompt (Section 4.3) string that is interpreted by the shell before each prompt is printed. Even on shells that don’t interpret prompt strings dynamically, you can simulate a dynamic prompt (Section 4.4) by changing the prompt string automatically.[1]

Depending on your shell’s capabilties, you can use or combine those three techniques — and those found in the rest of this chapter — to do a lot. But, of course, you don’t want to type that prompt-setting command every time you log in. So after you’ve perfected your prompt on the command line, store it in the correct shell setup file (Section 3.3): use the file that’s read by interactive shells or add an interactive shell test to your setup file. (Setting the prompt in noninteractive shells is pointless — and it can even cause problems (Section 4.5).)

—JP, TOR, and SJC

Static Prompts

As Section 4.1 explains, the simplest prompts — which I call static prompts — are prompts whose value are set once. The prompt doesn’t change (until you reset the prompt variable, of course).

The default bash prompt is a good example of a static prompt. It’s "bash$ " (with a space at the end, to make the command you type stand out from the rest of the prompt). You could set that prompt with the simple command:

PS1='bash$ '.

Notice the single quotes (Section 11.3) around the value; this is a good idea unless you want special characters in the prompt value to be interpreted before it’s set. You can try it now: type that command on a command line, just as you would to set any other shell variable. Experiment a bit. The same prompt works on ksh and sh .

If you use csh or tcsh, try one of these, then experiment:

set prompt='csh% '
set prompt='tcsh> '

(zsh users: you can use any of the previous styles, but omit the set from the set prompt style.) Those prompts are fairly useless, right? If you log in to more than one machine, on more than one account, it’s nice to have your hostname and username in the prompt. So try one of the following prompts. (From here on, I won’t show a separate tcsh version with a > instead of a %. You can do that yourself, though, if you like.) If your system doesn’t have uname, try hostname instead:

PS1="$USER@`uname -n`$ "
set prompt="$user@`uname -n`% "

Notice that I’ve used double quotes (Section 12.3) around the values, which lets the shell expand the values inside the prompt string before the prompt is stored. The shell interprets the variable $USER or $user — and it runs the command substitution (Section 28.14) that gives the hostname — once, before the prompt is set. Using double quotes is more efficient if your prompt won’t change as you move around the system.

—JP and SJC

Dynamic Prompts

Many shells can interpret the stored prompt string as each prompt is printed. As Section 4.1 explains, I call these dynamic prompts.

Special character sequences in the prompt let you include the current directory, date and time, username, hostname, and much more. Your shell’s manual page should list these at the PS1 or prompt variable. (If you use the Korn shell or the original C shell, you don’t have these special sequences. Section 4.4 has a technique that should work for you.)

It’s simplest to put single quotes around the prompt string to prevent interpretation (Section 27.1) as the prompt is stored. For example, the following prompt shows the date and time, separated by spaces. It also has a special sequence at the end (\$ in bash, %# in tcsh and zsh) that’s printed as a hash mark (#) if you’re the superuser, or the usual prompt character for that shell otherwise. The first command in the following code listing works only in bash; the second only in tcsh:

PS1='\d \t \$ '                   ...bash
set prompt='%w %D %Y %P %# '      ...tcsh
PS1='%W %* %# '                   ...zsh

Having the history number in your prompt, as Section 4.14 shows, makes it easy to use history (Section 30.8) to repeat or modify a previous command. You can glance up your screen to the prompt where you ran the command, spot the history number (for example, 27), and type !27 to repeat it, !27:$ to grab the filename off the end of the line, and much more. In csh, tcsh, and bash prompts, use \! to get the history number. In zsh, use %! instead.

—JP, TOR, and SJC

Simulating Dynamic Prompts

Some shells don’t have the special “dynamic” prompt-setting sequences shown in Section 4.3. If you still want a dynamic prompt, you probably can simulate one. Both ksh and bash will expand variables (like $PWD), do command substitution (to run a command like 'date'), and do arithmetic as they print the prompt. So, for example, you can put single quotes around the prompt string to prevent interpretation of these items as the prompt is stored. When the prompt string is interpreted, the current values will be put into each prompt. (zsh gives control over whether this happens as a prompt is printed. If you want it to happen, put the command setopt prompt_subst (Section 28.14) in your .zshrc file (Section 3.3).)

The following prompt stores the $PWD parameter to give the current directory, followed by a backquoted date command. The argument to date is a format string; because the format string is inside single quotes, I’ve used nested double quotes around it. Because it’s in single quotes, it’s stored verbatim — and the shell gets the latest values from date and $PWD each time a prompt is printed. Try this prompt, then cd around the filesystem a bit:

PS1='`date "+%D %T"` $PWD $ '

That prompt prints a lot of text. If you want all of it, think about a multiline prompt (Section 4.7). Or you could write a simple shell function (Section 29.11) named, say, do_prompt:

# for bash
function do_prompt {
   date=`date '+%D %T'`
   dir=`echo $PWD | sed "s@$HOME@~@"`
   echo "$date $dir"
   unset date dir
}

# for ksh
do_prompt( ) {
   date=`date '+%D %T'`
   dir=`echo $PWD | sed "s@$HOME@~@"`
   echo "$date $dir"
   unset date dir
}

and use its output in your prompt:

PS1='`do_prompt` $ '    ...for sh-type shells

The original C shell does almost no interpretation inside its prompt variable. You can work around this by writing a shell alias (Section 29.2) named something like setprompt ( Section 4.14) that resets the prompt variable after you do something like changing your current directory. Then, every time csh needs to print a prompt, it uses the latest value of prompt, as stored by the most recent run of setprompt. (Original Bourne shell users, see Section 4.15 for a similar trick.)

—JP, TOR, and SJC

C-Shell Prompt Causes Problems in vi, rsh, etc.

Stray prompts can cause trouble for many commands that start a noninteractive shell. This problem may have (and probably has) been fixed in your C shell, but some of the following tricks will speed up your .cshrc, so keep reading.

If you set prompt in your .cshrc file without carefully checking first whether prompt was already set (Section 4.1), many older versions of the C shell will cheerfully print prompts into the pipe vi uses to expand glob characters, such as filename wildcards (*, ?, [ ]) (Section 1.13) and the tilde (~) (Section 31.11).

When you type :r abc*, vi opens a pipe to the C shell, writes the command echo abc* down the pipe, then reads the response. If the response contains spaces or newlines, vi gets confused. If you set your prompt to ( n ) in your .cshrc [i.e., if you show the history number in parentheses as the prompt — TOR], vi tends to get:

(1) abc.file (2)

back from the C shell, instead of just abc.file.

The solution is to kludge your .cshrc like this:

if ($?prompt) then
    # things to do for an interactive shell, like:
    set prompt='(\!) '
endif

This works because a noninteractive shell has no initial prompt, while an interactive shell has it set to % .

If you have a large .cshrc, this can speed things up quite a bit when programs run other programs with csh -c ' command ', if you put all of it inside that test.

— CT

Faster Prompt Setting with Built-ins

To set your prompt, you execute a command (on most shells, that command sets a shell variable). Before setting the prompt, you may run other commands to get information for it: the current directory name, for example. A shell can run two kinds of commands: built-in and external (Section 1.9). Built-in commands usually run faster than external commands. On a slow computer, the difference may be important — waiting a few seconds for your prompt to reset can get irritating (though the computer would have to be quite slow nowadays for it to matter that much). Creative use of your shell’s built-in commands might pay off there, and they are still quite useful for those trying to squeeze the most performance out of their system. Let’s look at some examples:

  • Section 4.3 has examples of some shells’ special characters, such as %D to give the current date. Check your shell’s manual page; if it has these features, using them won’t slow you down the way an external command in backquotes (Section 28.14), like 'date', might.

  • If you’re putting your current directory in your prompt, you may only want the tail of the pathname (the name past the last slash). How can you edit a pathname? You might think of using basename (Section 36.13) or sed (Section 34.1) with the current directory from $cwd — as in the first command in the following code listing, and probably in an alias like setprompt (Section 4.7) to make sure the prompt is updated whenever you change directories. The faster way is with the second command, using the C shell’s built-in “tail” operator, :t:

    set prompt="`basename $cwd`% "

    {} Section 35.9

    set prompt="${cwd:t}% "

    If your current directory is /usr/users/hanna/projects, either of those prompts would look like "projects% " (with a space after the percent sign).

    The C shell has several of these built-in string operators ( Section 28.5) like :t; the Korn Shell, zsh, and bash have more-powerful string operators (Section 28.5).

  • If your prompt gets complex, you can use a shell function (Section 29.11) to access other built-in commands and edit the prompt. This can be faster than using an external shell or Perl script because functions run within the shell instead of in an external process. Here’s an example from my .zshrc file:

    ${+} Section 36.7, $(...) Section 28.14

    # Change "script" prompt automatically so I remember I'm in one.
    alias script='SCRIPT=yes /usr/bin/script'
    
    #
    # Functions:
    #
    setprompt( ) {
        case "${TTY##*/}" in
        tty[1-9]) xpi=':tty%l' ;;  # Virtual console
        *) xpi= ;;
        esac
    
        PS1="
    $USER@%m$xpi $(dirs)
    %* \$(folder -list)
    ${SCRIPT+SCRIPT-}%!%# "
    }

    Before the function, I set an alias that temporarily sets an environment variable named SCRIPT while the script (Section 37.7) program is running. The setprompt function, running in the child shell under script, sees that this environment variable has been set and adds the string SCRIPT- before the prompt. This reminds me that I’m logging to a script file. (If this is hard to visualize, Section 24.3 and Section 35.3 have some background.)

    The setprompt function itself has two parts. The first is a case statement (Section 35.11) that tests $TTY, the name of the tty (Section 2.7) I’m currently using. If the name ends in tty1, tty2, etc., it’s one of my Linux virtual consoles (Section 23.12). In that case, I want to add the console name (tty1, etc.) to my prompt — so I store the name in the xpi (extra prompt info) shell variable. This variable’s value — if it’s been set — is expanded when the prompt is printed. The second part sets the prompt variable PS1. The whole prompt looks like this:

    jpeek@kludge:tty1 ~/pt/art
    15:38:30 inbox pt
    501%

    The first line shows my username, hostname, the virtual console name (if any), and the current directory (in this example, there was nothing else on the directory stack (Section 31.7)). The second line has the time — and my email folder stack, from the MH folder -list command, which is the only nonbuilt-in command used in the prompt. And here’s a subtle point that’s worth perusing. The whole prompt string is inside double quotes (Section 27.12) so that variable and command substitution will happen whenever setprompt is run. But, the way my prompt is set, the MH folder stack may change between the times that setprompt resets the prompt. So I escape the $ in \$(folder -list). This stores the command substitution without executing folder! So, when every prompt is about to be printed, the shell will evaulate the prompt string and expand the $(...) operators into the current folder stack. The third line sets the end of the prompt string: the zsh prompt substitution at %m, %*, %! and %#.

    On a slow machine, I’d try hard to find a way to eliminate the external folder -list command. But my Linux box is fast enough so that I don’t notice any delay before a prompt. To make this work, I needed a good understanding of what’s evaluated when. It’s this sort of subtlety that makes prompt setting a challenge — and a pleasure, too, when you get it working just right.

As another example, Section 4.14 shows more about using dirs in a shell prompt.

—JP and SJC

Multiline Shell Prompts

Lots of people like lots of information in their prompts: hostname, directory name, history number, and maybe username. Lots of people have spent lots of time trying to make their prompts short enough to fit across the screen and still leave room for typing a command longer than ls:

<elaineq@applefarm> [/usr/elaineq/projects/april/week4] 23 % ls

Even with fairly short prompts, if you look back at a screen after running a few commands, telling the data from the prompts can be a little tough (real terminals don’t show user input in boldface, so I won’t do it here either):

+<elaineq@applefarm> [~] 56% cd beta
<elaineq@applefarm> [~/beta] 57% which prog
/usr/tst/applefarm/bin/beta/prog
<elaineq@applefarm> [~/beta] 58% prog
61,102 units inventoried; 3142 to do
<elaineq@applefarm> [~/beta] 59%

Go to http://examples.oreilly.com/upt3 for more information on: mlprompt.cshmlprompt.sh

One nice answer is to make a prompt that has more than one line. Here’s part of a .cshrc file that sets a three-line prompt: one blank line, one line with the hostname and current directory, and a third line with the history number and a percent sign. (If this were a tcsh, I could have gotten the hostname with %m.) The C shell quoting (Section 27.13) is ugly — doubly ugly because the prompt is set inside an alias — but otherwise it’s straightforward:

uname -n Section 2.5, {..} Section 35.9

set hostname=`uname -n`
alias setprompt 'set prompt="\\
${hostname}:${cwd}\\
\! % "'
alias cd 'chdir \!* && setprompt'
alias pushd 'pushd \!* && setprompt'
alias popd 'popd \!* && setprompt'
setprompt           # to set the initial prompt

(There’s a version on the Web for Bourne-type shells.) The prompts look like this:

applefarm:/usr/elaineq/projects/april/week4
23 % prog | tee /dev/tty | mail -s "prog results" bigboss@corpoffice
61,102 units inventoried; 3142 to do

applefarm:/usr/elaineq/projects/april/week4
24 % cd ~/beta

applefarm:/usr/elaineq/beta
25 % prog | mail joanne

The blank lines separate each command — though you may want to save space by omitting them. For example, Mike Sierra of O’Reilly & Associates has used a row of asterisks:

***** 23 *** <mike@mymac> *** ~/calendar *****
% cd Sep*
***** 24 *** <mike@mymac> *** ~/calendar/September *****
%

Other shells have different syntax, but the idea is the same: embed newlines to get multiline prompts. In Bourne-type shells you’ll need zero or one backslash before each newline; Section 27.12 explains. In bash, put a \n (which stands for a newline character) anywhere you want the prompt to break to a new line.

What I like best about multiline prompts is that you get a lot of information but have the whole screen width for typing. Of course, you can put different information in the prompt than I’ve shown here. The important idea is that if you want more information and need room to type, try a multiline prompt.

—JP and SJC

Session Info in Window Title or Status Line

Some people don’t like to put the current directory, hostname, etc. into their prompts because it makes the screen look cluttered to them. Here’s another idea. If your terminal or window system has a status line or titlebar, you might be able to put the information there. That’s nice because you can see the information while you run programs. The down side is that the information can get out of date if you use a command that takes you to another host or directory without updating the status line. The latest bash and zsh shells do this by default when you’re using an xterm window. For the rest of you, here’s how to do it yourself. Because neither csh or tcsh do this by default, I’ll show C-shell-type syntax. But you can do the same thing in Bourne-type shells with a shell function and case (Section 35.10) statement; there’s a ready-to-use version on the web site.

When you use cd, pushd, or popd, an alias uses the echo command to write special escape sequences to the terminal or window.

Here are cd aliases and other commands for your .cshrc or .tcshrc file. If I were logged in to www.jpeek.com in the directory /home/jpeek, this alias would put:

www:/home/jpeek

in the status area or window title, depending on which terminal type I’m using. Of course, you can change the format of the status line. Change the following command string, ${host:h}:${cwd}, to do what you need; see your shell’s manual page for a list of variables, or create your own custom information.

:h Section 28.5, && Section 35.14

Go to http://examples.oreilly.com/upt3 for more information on: stattitle.cshstattitle.sh

set e=`echo x | tr x '\033'`   # Make an ESCape character

set g=`echo x | tr x '\07'`    # And a Ctrl-g
set host=`uname -n`
# Puts $host and $cwd in VT102 status line. Escape sequences are:
# ${e}7 = save cursor position, ${e}[25;1f = go to start of status
# line (line 25), ${e}[0K = erase line, ${e}8 = restore cursor
alias setstatline 'echo -n "${e}7${e}[25;1f${e}[0K    ${host:h}:${cwd}${e}8"'
alias settitle 'echo -n "${e}]2;${host:h}:${cwd}${g}"'
switch ($TERM)
case vt10?:
  alias cd 'cd \!* && setstatline'
  alias pushd 'pushd \!* && setstatline'
  alias popd 'popd \!* && setstatline'
  breaksw
case xterm*:
  alias cd 'cd \!* && settitle'
  alias pushd 'pushd \!* && settitle'
  alias popd 'popd \!* && settitle'
  breaksw
endsw

(Section 5.15 has more about how this works in xterms.)

The ESC and CTRL-g characters are stored with a trick that should work on all Unix shells. Most modern echos will let you make a nonprintable character directly, like this: g='echo '\07''.

If you always use a VT102-type terminal (and many people do), the setstatline alias will work fine. If you use a different terminal, try it anyway! Otherwise, read the terminal manual or its termcap/terminfo entry and find the escape sequences that work for it; then add a new case to the switch statement.

Note that you might have some trouble here: if this code is in your .cshrc file but your terminal type is set in your .login file, the terminal type may not be set until after the alias has been read. There are workarounds (Section 3.8).

The status line or titlebar can also get out of sync with reality if you use remote logins ( Section 1.21), subshells (Section 24.4), etc. These might make a new status line or titlebar but not reset the original one when needed. To fix this, just type setstatline or settitle at a shell prompt. Or, if you don’t want to bother to think of the name of the alias, use the following command to change to the current directory (.), which will use the correct alias and reset the status or title:

% cd .

If you’re using tcsh , its special alias cwdcmd will be run every time you change the shell’s current directory. So, in tcsh, you can replace the three aliases for cd, pushd, and popd with something like this:

alias cwdcmd settitle

—JP and SJC

A “Menu Prompt” for Naive Users

Some people don’t want to be faced with a Unix % or $ shell prompt. If you (or, if you’re a sys admin on a multiuser system, your users) usually run only a few particular Unix commands, you can put those command names in the shell prompt. Here’s a simple one-line Bourne-shell prompt for a .profile:

PS1='Type "rn", "mailx", "wp", or "logout": '

Next, a multiline prompt (Section 4.7) for the C shell .cshrc or .tcshrc file:

if ($?prompt) then
set prompt='\\
Type "pine" to read the news,\\
type "mutt" to read and send mail,\\
type "wp" for word processing, or\\
type "logout" to log out.\\
YES, MASTER? '
endif

You get the idea.

—JP and SJC

Highlighting and Color in Shell Prompts

If your prompt has some information that you want to stand out — or if you want your whole prompt to stand out from the rest of the text on the screen — you might be able to make it in enhanced characters or colors. If your terminal has special escape sequences for enhancing the characters (and most do), you can use them to make part or all of your prompt stand out. Newer versions of xterm also have color capability, as does the Mac OS X Terminal program, though Terminal may not properly support the escape sequences we discuss later. (The GNU dircolors (Section 8.6) command sets up a color-capable terminal.)

Go to http://examples.oreilly.com/upt3 for more information on: blinkprompt.cshblinkprompt.sh

Let’s say that you want to make sure people notice that they’re logged in as root (the superuser) by making part of the root prompt flash. Here are lines for the root .cshrc:

# Put ESCape character in $e.  Use to start blinking mode (${e}[5m)
# and go back to normal mode (${e}[0m) on VT100-series terminals:
set e="`echo x | tr x '\033'`"

uname -n Section 2.5

set prompt="${e}[5mroot${e}[0m@`uname -n`# "

That prompt might look like this, with the word root flashing:

root@www.jpeek.com#

Note

Shells with command-line editing need to calculate the width of your prompt string. When you put nonprinting escape sequences in a prompt (as we’re doing here), in zsh and tcsh you have to delimit them with %{ and %}. In bash , bracket nonprinting characters with \[ and \]. In the Korn shell, prefix your prompt with a nonprinting character (such as CTRL-a) followed by a RETURN, and delimit the escape codes with this same nonprinting character. As the pdksh manual page says, “Don’t blame me for this hack; it’s in the original ksh.”

The prompt is set inside double quotes ("), so the uname' -n command is run once, when the PS1 string is first stored. In some shells, like bash and pdksh, you can put single quotes (') around the PS1 string; this stores the backquotes (`) in the string, and the shell will interpret them before it prints each prompt. (In this case, that’s useless because the output of uname -n will always be the same in a particular invocation of a shell. But if you want constantly updated information in your prompt, it’s very handy.) Section 4.6 and Section 27.12 have more info.

Because the same escape sequences won’t work on all terminals, it’s probably a good idea to add an if test that only sets the prompt if the terminal type $TERM is in the Digital Equipment Corporation VT100 series (or one that emulates it). Table 4-1 shows a few escape sequences for VT100 and compatible terminals. (The ESC in each sequence stands for an ESCape character. )

Table 4-1. VT100 escape sequences for highlighting

Sequence

What it does

ESC[1m

Bold, intensify foreground

ESC[4m

Underscore

ESC[5m

Blink

ESC[7m

Reverse video

ESC[0m

All attributes off

Of course, you can use different escape sequences if your terminal needs them. Better, read your terminal’s terminfo or termcap database with a program like tput or tcap to get the correct escape sequences for your terminal. Store the escape sequences in shell variables (Section 35.9).

bash interprets octal character codes (like \033) in the prompt. It also has special-backslashed special-prompt characters — for instance, bash Version 2 has \e, which outputs an ESCape character, and \H, which gives the complete hostname. The string \$ is replaced by a dollar sign ($) on non-root shells and a hash mark (#) if you’re currently root. So, on bash, you can make the previous csh prompt this way:

PS1='\[\e[5m\]root\[\e[0m\]@\H\$ '

(The delimiters for nonprinting characters, \[ and \], might make it look complicated. Try spotting them first, as you look at the prompt string, so you can see what’s left.)

Eight-bit-clean versions of tcsh can put standout, boldface, and underline — and any other terminal escape sequence, too — into your shell prompt. For instance, %S starts standout mode and %s ends it; the tcsh manpage has details for your version. The next example shows how to make the same prompt as earlier with the word root in standout mode. (tcsh puts the hostname into %m.) Because tcsh “knows” the width of its special %S and %s formatting sequences, they don’t need to be delimited with %{ or %}:

set prompt = '%Sroot%s@%m# '

You also can add color to your prompt! For instance, make the previous prompt for bash using bright red (1;31) on a blue background (44):

PS1='\[\e[1;31;44m\]root\[\e[0m\]@\H# '

—JP and SJC

Right-Side Prompts

Both zsh and tcsh have an optional prompt at the right side of the screen. Unlike the normal left-side prompt, the cursor doesn’t sit next to the right-side prompt (though the right prompt disappears if you type a long command line and the cursor passes over it). It’s stored in the zsh RPROMPT variable and in tcsh rprompt.

What can you do with a right-hand prompt? Anything you want to! (You’ll probably want to keep it fairly short, though.) Put the time of day on the right-hand side, for instance; on tcsh, it’s this easy:

[jpeek@ruby ~]% set rprompt='%t'
[jpeek@ruby ~]% users                                         3:44pm
jpeek ollie
[jpeek@ruby ~]%                                               3:45pm

As another idea, you could use sched to remind you of an important meeting by setting the right-hand prompt. Here’s a shell function for zsh that sets the right prompt to “LEAVE NOW” at a particular time. You can give it one argument to set the time to remind you. Or, with no argument, it removes the right-hand prompt:

leave( ) {
    case "$#" in
    0) unset RPROMPT ;;
    1) sched "$1" "RPROMPT='LEAVE NOW'" ;;
    *) echo "Usage: leave [time]" 1>&2 ;;
    esac
}

Here’s an example:

jpeek$ date
Fri May 12 15:48:49 MST 2000
jpeek$ leave 15:55
               ...do some work...
jpeek$ pwd
/u/jpeek/pt
jpeek$ date                                        LEAVE NOW
Fri May 12 15:55:22 MST 2000
jpeek$ lpr report                                  LEAVE NOW
jpeek$ leave                                       LEAVE NOW
jpeek$

—JP and SJC

Show Subshell Level with $SHLVL

If you’re like me, when you start a shell escape (Section 17.21) or any subshell (Section 24.4), you can forget that you aren’t in your login shell. Your shell history (Section 30.1) might get confused, shell variables (Section 35.9) may not be set, and other problems may come up. zsh and bash have a built-in SHLVL environment variable (Section 35.3) that lets you track how many subshells deep your current shell is. tcsh has a shlvl shell variable that’s automatically set from (and sets) SHLVL. So, all three shells cooperate with each other to set the right value, even if you start one shell from another. (For other shells that don’t have SHLVL — ksh and csh — you can set up something similar with a bit of arithmetic in the ENV (Section 35.5) file or the .cshrc file, respectively.)

In your top-level shell, the value of $shlvl is 1 (one). In the first subshell, it’s 2; in a sub-subshell, it’s 3; and so on. You can use this to control your shell startup files — for example, have some commands in your .cshrc that run when you first log in (and $shlvl is 1), but don’t run in subshells. You can also put $shlvl in your prompt (but only during subshells, if you’d like — as a reminder that you aren’t in your top-level shell). You can set your prompt to mike% in top-level shells, (1) mike% in a first-level subshell, (2) mike% in a second-level subshell, and so on. Here’s some sample prompt-setting code for your .tcshrc:

# If this is a subshell, put shell level in prompt:
if ($shlvl == 1) then
    set prompt="${USER}% "
else
    set prompt="($SHLVL) ${USER}% "
endif

bash doesn’t need an if because login shells read your .bash_profile (or .profile) and subshells read your .bashrc. Here are commands to set the prompts I mentioned earlier:

PS1='\u\$ '             ...for the .bash_profile
PS1='($SHLVL) \u\$ '    ...for the .bashrc

Does your account run a windowing system that’s started from your top-level shell startup file (like .login )? If it does, lines like the following examples (these are for .login) will reset SHLVL so that the shell in the window will start at a SHLVL of 1 — and act like a top-level shell. This code assumes that your first login shell starts on a tty named /dev/tty1 through /dev/tty6 (which are the Linux virtual consoles (Section 23.12)) and that the windows that open won’t have a tty with the same name (which is true on Linux). (If you aren’t sure, check who (Section 2.8).) You may need to adapt this. The trick is to make SHLVL 0 (zero) before you start the windowing system. When the windows’ shells start, they’ll raise SHLVL to 1:

# If on a virtual console, bury this shell and start X right away:
if ("`tty`" =~ /dev/tty[1-6]) then
   setenv SHLVL 0
   startx
endif

Getting this to work right in every situation (rsh (Section 1.21), ssh, su, shell escapes (Section 17.21) — both interactive and noninteractive (Section 3.4) — subshells, window systems, at jobs (Section 25.5), and so on) can be a challenge (Section 3.8)! It takes a little planning. Sit down and think about all the ways you start subshells — which subshells are interactive and which aren’t — and whether they’ll get SHLVL passed from their parent process. (If you aren’t sure, test that with an env or printenv command (Section 35.3).) Then plan which kind of shell needs which SHLVL settings. If it gets too complicated, make it work in most cases! If you use many subshells, this system can be too handy to ignore.

—JP and SJC

What Good Is a Blank Shell Prompt?

Note

This tip is also great if you use a mouse to copy and paste command lines in your window.

Some terminals I’ve used (like old Hewlett-Packard and Tektronix terminals) had local editing. You could move your cursor up the screen to a previous command line, maybe make some edits to it, then press a SEND LINE key to resend that line to the host. This didn’t have anything to do with sophisticated command-line editing (Section 30.14) that modern Unix shells have, though. Maybe your terminal can do that, too. Depending on how your emacs editor is configured, shell-mode may work that way, as well.

The problem was that unless I erased the shell prompt (%) on my screen, it would be sent back to the shell and give the error "%: Command not found.” So I set my shell prompt to this:

set prompt='     '

That’s right: four spaces. Most Unix commands start their output at column 1, so my command lines were easy to find because they were indented. The shell didn’t care if I sent four spaces before the command line. So everything was fine until I got my new terminal without a SEND LINE key . . .

If you want some information in your prompt, too, make a multiline prompt (Section 4.7) with four spaces in the last line.

—JP and SJC

dirs in Your Prompt: Better Than $cwd

Many people use the current directory in their prompts. If you use the pushd and popd (Section 30.7) commands, you may not always remember exactly what’s in your directory stack (I don’t, at least). Here’s how: run the dirs command, and use its output in your prompt. A simple csh and tcsh alias looks like this:

alias cd 'chdir \!* && set prompt="`dirs`% "'

and the prompts look like:

/work/project % cd
~ % cd bin
~/bin %

Here’s what to put in .cshrc or .tcshrc to make a multiline prompt (Section 4.7) that shows the directory stack:

uname -n Section 2.5, expr Section 36.21

# PUT hostname.domain.name IN $hostname AND hostname IN $HOST:
set hostname=`uname -n`
setenv HOST `expr $hostname : '\([^.]*\).*'`
alias setprompt 'set prompt="\\

Go to http://examples.oreilly.com/upt3 for more information on: dirs-prompt.cshdirs-prompt.sh

${USER}@${HOST} `dirs`\\
\! % "'
alias cd  'chdir \!* && setprompt'
alias pushd  'pushd \!* && setprompt'
alias popd  'popd  \!* && setprompt'
setprompt   # SET THE INITIAL PROMPT

Because bash can run a command each time it sets its prompt, and because it has built-in prompt operators (Section 4.3) like \u, the bash version of all the previous stuff fits on one line:

$(...) Section 28.14

PS1='\n\u@\h $(dirs)\n\! \$ '

That makes a blank line before each prompt; if you don’t want that, join the first and second lines of the setprompt alias or remove the first \n. Let’s push a couple of directories and watch the prompt:

jerry@ora ~
1 % pushd /work/src/perl
/work/src/perl ~

jerry@ora /work/src/perl ~
2 % cd ../cnews

jerry@ora /work/src/cnews ~
3 % pushd ~/bin
~/bin /work/src/cnews ~

jerry@ora ~/bin /work/src/cnews ~
4 %

Of course, the prompt looks a little redundant here because each pushd command also shows the dirs output. A few commands later, though, having your directory stack in the prompt will be handy. If your directory stack has a lot of entries, the first line of the prompt can get wider than the screen. In that case, store the dirs output in a shell array, and edit it with a command like sed or with the built-in csh string editing (Section 28.5).

For example, to show just the tail of each path in the dirs output, use the following alias; the C shell operator :gt globally edits all words, to the tail of each pathname:

Go to http://examples.oreilly.com/upt3 for more information on: dirstail-prompt.csh

alias setprompt 'set dirs=(`dirs`); set prompt="\\
${USER}@${HOST} $dirs:gt\\
\! % "'

Watch the prompt. If you forget what the names in the prompt mean, just type dirs:

jerry@ora bin cnews jerry
5 % pushd ~/tmp/test
~/tmp/test ~/bin /work/src/cnews ~
   ...
jerry@ora test bin cnews jerry
12 % dirs
~/tmp/test ~/bin /work/src/cnews ~

—JP and SJC

External Commands Send Signals to Set Variables

The Bourne shell’s trap (Section 35.17) will run one or more commands when the shell gets a signal (Section 24.10) (usually, from the kill command). The shell will run any command, including commands that set shell variables. For instance, the shell could reread a configuration file; Section 24.13 shows that. Or it could set a new PS1 prompt variable that’s updated any time an external command (like another shell script or a cron job (Section 25.2)) sends the shell a signal. There are lots of possibilities.

This trick takes over signal 5 (SIGTRAP), which usually isn’t used. When the shell gets signal 5, a trap runs a command to get the date and time, then resets the prompt. A background (Section 23.2) job springs this trap once a minute. So, every minute, after you type any command, your prompt will change.

You can use any command’s output in your prompt (possibly with some editing, probably with sed (Section 34.1) or expr (Section 36.21)): count the number of users, show the load average (Section 26.4), whatever. Newer shells, like bash, can run a command in backquotes (Section 28.14) each time the prompt is displayed — Section 4.10 has an example. But, to have an external command update a shell variable at any random time, this trap trick is still the best.

Go to http://examples.oreilly.com/upt3 for more information on: date- prompt.sh

Now on to the specific example of putting date and time in the old Bourne shell’s prompt. If your system’s date command doesn’t understand date formats (like +%a), get one that does. Put these lines in your .profile file (or just type them in at a Bourne shell prompt):

# Put date and time in prompt; update every 60 seconds:
trap 'PS1=`date "+%a %D %H:%M%n"`\
$\ ' 5
while :

: Section 36.6

do
    sleep 60
    kill -5 $$
done &
promptpid=$!

Now, every minute after you type a command, your prompt will change:

Thu 06/20/02 02:33
$ cc bigprog.c
undefined symbol                first referenced in file
xputc                               bigprog.o
ld fatal: Symbol referencing errors.
Thu 06/20/02 02:34
$ ls
bigprog.c
bigprog.o
Thu 06/20/02 02:35
$

The prompt format is up to you. This example makes a two-line prompt (Section 3.7) with backslashes (\) to protect the newline and space from the trap; a single-line prompt might be easier to design. The manual page for date lists what you can put in the prompt.

This setup starts a while loop (Section 35.15) in the background. The promptpid variable holds the process ID number ( Section 24.3) of the background shell. Before you log out, you should kill (Section 24.12) the loop. You can type the command:

kill $promptpid

at a prompt or put it in a file that’s executed when you log out (Section 4.18).

—JP and SJC

Preprompt, Pre-execution, and Periodic Commands

bash, tcsh, and zsh can run a Unix command, or multiple commands, before printing each prompt. tcsh and zsh also can do something you specify before executing the command you’ve typed at a prompt. Finally, tcsh and zsh can do something periodically (every n seconds) before whatever prompt comes next. (Section 4.15 shows how to execute commands periodically in the original Bourne shell.) These commands don’t have anything to do with setting the prompt itself, though they can. The command could do some system checking, reset shell variables, or almost anything that you could type at a shell prompt. If the commands run slowly, they’ll delay whatever else you’re doing, so keep that in mind.

Let’s start with precmd , the tcsh alias that’s run after your command line is read and before the command is executed. In zsh, the same thing is done by the shell function named preexec. Shell history is available, so you can use history substitution (Section 30.8) inside the alias or function. Here’s a nice example adapted from the tcsh manual page: showing the command line you’re running in your xterm window titlebar. It’s ugly because it has ESC and CTRL-g characters embedded directly in the alias; I’d rather store the escape sequences in shell variables, as shown in the xterm titlebar article (Section 4.8). The if sets the alias only if you’re using an xterm terminal:

# Show each command line in xterm title bar:
if ($TERM == xterm) alias postcmd 'echo -n "^[ ]2;\!#^G"'

Next, let’s look at running a command periodically. You’d like to watch the load average by running uptime (Section 26.4) every minute, before a prompt. Here’s how to do it in zsh: put code like this in your .zshrc file (Section 3.3) (or just type it at a prompt to try it). The PERIOD shell variable is the interval, in seconds, between runs of the periodic function as shown in the following code:

# Run "uptime" every 60 seconds; put blank line before:
periodic( ) {echo "\n==> $(uptime) <==";}
PERIOD=60

Here’s how it looks:

jpeek@ruby$ pwd
/u/jpeek/pt

==>   5:16pm  up  4:07,  6 users,  load average: 0.22, 0.15, 0.08 <==
jpeek@ruby$ lpr xrefs
jpeek@ruby$ mail -s "xrefs list" jan < xrefs

==>   5:17pm  up  4:08,  7 users,  load average: 1.29, 0.55, 0.23 <==
jpeek@ruby$

Finally, here’s how to set preprompt commands. These are run before each shell prompt is printed. In tcsh, define a precmd alias. In zsh, define a precmd function. In bash, store the command(s) in the PROMPT_COMMAND shell variable. Let’s look at bash this time. Here’s a silly example that I used to have in my bash setup file (Section 3.3):

IFS Section 36.23, set Section 35.25, shift $# Section 36.10

PROMPT_COMMAND='
# Save old $IFS; set IFS to tab:
OIFS="$IFS"; IFS="   "
# Put x in $1, face in $2, explanation[s] in $3[, $4, ...]:
set x `smiley`
# Put face into $face and explanation(s) into $explan:
face="$2"; shift 2; explan="$*"
# Restore shell environment:
shift $#; IFS="$OIFS"'

# Prompt I use (includes the latest $face):
PS1='\u@\h $face '

The first part is a series of shell commands that are stored in the PROMPT_COMMAND variable; they’re surrounded by a pair of single quotes ('' '), one on the first line (after the =) and the other after IFS is reset. That series of commands is executed before every prompt. It sets two shell variables, $face and $explan, with new values before each prompt is set. The prompt is set on the last line; it includes the value of $face.

Here’s what my screen looked like with this ridiculous setup. Notice that the prompt keeps changing as the PROMPT_COMMAND resets $face and $explan. If I wanted the explanation of a face I saw as I went along, I could type echo <">$explan<">:

jerry@ruby :-{) echo "$explan"
normal smiling face with a moustache
jerry@ruby +<||-) vi proj.cc
   ...
jerry@ruby :-O echo "$explan"
Mr. Bill
        Wow!
        ohh, big mouth, Mick Jagger
        uh oh
jerry@ruby :-)   < g++ -Wall proj.cc
   ...

(It was even more useless than psychoanalyze-pinhead (Section 19.13), but it was fun while it lasted.) Seriously now, I’ll say again: preprompt commands do not have to be used to set a prompt. You can use them to do anything. If the commands in PROMPT_COMMAND — or any of the other functions or aliases we’ve covered — write to standard output or standard error, you’ll see that text on your screen, before or after the prompt, at the point where the commands are executed.

—JP and SJC

Running Commands When You Log Out

Is there something you want to do every time you log out: run a program that deletes temporary files, asks you a question, or prints a fortune to your screen? If you use the C shell, make a file named .logout (Section 3.3) in your home directory and put the commands there. Before a login C shell exits, it will read that file. A login bash reads .bash_logout, and zsh reads .zlogout. But not all shells are login shells; you might want these shells to read your logout-type file, too. Section 3.18 shows a fix for the Bourne and Korn shells; Section 3.8 and Section 3.4 have background information.

Some ideas for your logout file are:

  • A command like fortune to give you something fun to think about when you log out.

  • A command to list a “reminder” file — for example, work to take home.

  • A script that prompts you for the hours you’ve worked on projects so you can make a timesheet later.

  • The command clear to erase your screen. This keeps the next user from reading what you did.[2] In the Mac OS X Terminal application, command-k will delete the scrollback buffer. It also helps to stop “burn-in” damage to old, monochrome monitors caused by characters left over from your login session (though this is hardly a concern nowadays; most of us have moved on to color screens that are not subject to burn-in). (Some Unixes clear the screen before printing the login: prompt. Of course, this won’t help users who connect with a data switch or port manager because the connection will be broken before the next login prompt.)

If you connect to this host over a network, with a slow modem or on a data switch — and you don’t see all the logout commands run before your connection closes — try putting the command sleep 2 (Section 25.9) at the end of the file. That makes the shell wait two seconds before it exits, which gives output more time to get to your screen.

—JP and SJC

Running Commands at Bourne/Korn Shell Logout

Section 4.17 describes logout files. Commands in those files are run when you log out. The Bourne and Korn shells don’t have a logout file, though. Here’s how to make one:

  1. In your .profile file, add the line:

    trap Section 35.17, . Section 35.29

    trap '. $HOME/.sh_logout; exit' 0

    (Some systems may need $LOGDIR instead of $HOME.)

  2. Make a file in your home directory named .sh_logout. Put in the commands you want to be run when you log out. For example:

    if Section 35.13, [ -f Section 35.26

    clear
    if [ -f $HOME/todo.tomorrow ]
    then
        echo "=========== STUFF TO DO TOMORROW: ============"
        cat $HOME/todo.tomorrow
    fi

The trap will read the .sh_logout file when the shell exits.

—JP and SJC

Stop Accidental Bourne-Shell Logouts

It’s pretty easy to type one too many CTRL-d characters and log out of a Bourne shell without meaning to. The C shell has an ignoreeof shell variable that won’t let you log out with CTRL-d. So do the Korn shell and bash; use set -o ignoreeof.

Here’s a different sort of solution for the Bourne shell. When you end the shell, it asks if you’re sure. If you don’t answer yes, a new shell is started to replace your old one.

First, make a file like the C shell’s .logout that will be read when your Bourne shell exits (Section 4.18). Save your tty (Section 2.7) name in an environment variable (Section 35.3), too — you’ll need it later:

trap Section 35.17

TTY=`tty`; export TTY
trap '. $HOME/.sh_logout; exit' 0

(Your system may need $LOGDIR instead of $HOME.) Put the following lines in your new .sh_logout file:

exec < Section 36.15, case Section 35.11, exec Section 24.2, -sh Section 3.19

exec < $TTY
echo "Do you really want to log out? \c"
read ans
case "$ans" in
[Yy]*) ;;
*)  exec $HOME/bin/-sh ;;
esac

The last line uses some trickery to start a new login shell (Section 3.19). The shell closes your tty (Section 36.15) before reading your .sh_logout file; the exec < $TTY reconnects the shell’s standard input to your terminal.

Note that if your system is very slow, you may not get the reminder message for a couple of seconds — consequently, you might forget that it’s coming and walk away. That hasn’t been a problem where I’ve tested this. If it is for you, though, replace the read ans with a program like grabchars that times out and gives a default answer after a while. There may be some Bourne shells that need other tricks — and others that don’t need these tricks — but this should give you an idea of what to do.

—JP and SJC



[1] I haven’t seen prompts described this way before. I invented the terms static prompt and dynamic prompt to make them easier to talk about.

[2] Some terminals and windows have “scroll back” memory of previous screens. clear usually doesn’t erase all of that. To set scrollback in xterm, use the -sb and -sl options. Most other terminal emulators have similar mechanisms to set the number of lines to keep in the scrollback buffer.

Get Unix Power Tools, 3rd Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.