Xterm Titles With Bash

After my last article on Bash prompts, I was challenged to implement a feature under zsh of showing the current command in the xterm window’s title bar. In this article I show you how I did it.

The Challenge

Shortly after posting a link to my previous article on bash prompts I recieved the following email from Ralph Aichinger:

Hello David!

I just saw your bash prompt tutorial linked from your blog. As you are obviously some kind of bash-prompt-expert ; ) one question:

Is it possible to get the name of the currently running command into the xterm title in bash?

I am currently using zsh and the following snippet to do this (found that somewhere IIRC).

if [ "$SHELL" = '/usr/bin/zsh' ]
then
    case $TERM in
         rxvt|*term)
         precmd() { print -Pn "\e]0;%m:%~\a" }
         preexec () { print -Pn "\e]0;$1\a" }
         ;;
    esac
fi

I would love to go back to bash for various reasons, but this feature is really nice, and I did not find a way to emulate it in bash yet, or only half (zsh updates the name while the command is running).

Thanks, /ralph

Never one to ignore a challenge, I started digging through the documentation

$BASH_COMMAND

While reading through some of the documentation for writing the previous article I had seen references to $PROMPT_COMMAND, which gets run just before every prompt is displayed. I thought I would start by reading the man page to see if there was something that was run just before each command was run. In the list of special variables that bahs produces, I discovered $BASH_COMMAND, which according to the manpage is set to:

The command currently being executed or about to be executed, unless the shell is executing a command as the result of a trap, in which case it is the command executing at the time of the trap.

which sounded exactly like what I needed for finding out the running command. A little experimenting resulted in not finding anything particularly more useful than a rather short quine:

beebo david% echo $BASH_COMMAND
echo $BASH_COMMAND
beebo david% true && echo $BASH_COMMAND
echo $BASH_COMMAND

Argh, I can’t even use it in a compound statement. This was getting me nowhere. There was nothing for it; I was going to have to carry on my search for a way to run a command before each command.

Running a command before each command

So, what were my options. I could run a command after command, but by then it was too late to display in the title bar. Hmm what about $PS4. That is displayed before each command as it’s being run. Perhaps I could use that to set the titlebar and turn on set -x. Argh no, that won’t work because after printing out PS4 it displays the command too. That would cause too much output.

Lets read that bit about BASH_PROMPT again. “The command currently being executed or about to be executed, unless the shell is executing a command as the result of a trap, in which case it is the command executing at the time of the trap.” Hold on. Say that last bit again. ” unless the shell is executing a command as the result of a trap, in which case it is the command executing at the time of the trap.”. A ha. Maybe there is a trap we could use. I knew that there were some pseudo-signals from my article on robust shell scripts. If there was one for the program exiting, maybe there was one when a command was about to be executed. So back to the manpage I went and read up on the trap builtin and found:

If a sigspec is DEBUG, the command arg is executed before every simple command, for command, case command, select command, every arithmetic for command, and before the first command executes in a shell function (see SHELL GRAMMAR above).

Excellent, I had it. Two minutes later I had:

 trap 'echo -e "\e]0;"; echo -n $BASH_COMMAND; echo -ne "\007"' DEBUG

It worked. I was setting the xterm titlebar. Challenge over. Well almost.

Little niggles

Two little problem left to fix. The first was that DEBUG trap doesn’t doesn’t get executed in subshells. Searching the manpage for DEBUG found me the set -T (or the more readable version set -o functrace) which sounded perfect:

If set, any traps on DEBUG and RETURN are inherited by shell functions, command substitutions, and commands executed in a subshell environment. The DEBUG and RETURN traps are normally not inherited in such cases.

The second problem was that the title didn’t change after the command had finished. Fortunately this was easily fixed. All I had to do was to reset it to something in my $PS1. As it happens my default prompt already set the titlebar so I could just reuse that. Also it was printing out a newline every time, so needed to add a -n to the echo to stop that.

My final solution, while far from pretty, was:

if [ "$SHELL" = '/bin/bash' ]
then
    case $TERM in
         rxvt|*term)
            set -o functrace
            trap 'echo -ne "\e]0;"; echo -n $BASH_COMMAND; echo -ne "\007"' DEBUG
            export PS1="\e]0;$TERM\007$PS1"
         ;;
    esac
fi

A challenge well completed. I can relax and bask in the glory.

Note

This hack will only work in bash 3.1 or greater due to an interaction between$BASH_COMMAND and DEBUG traps in earlier versions.