I spend a lot of time in the terminal these days, and I've started to accumulate a pretty nice set of tricks for making that time less tedious.

This is the first post of my Terminal Velocity series, where I'll be discussing several interesting features and capabilities of modern terminal emulators that can help you operate more swiftly while using the command line.

What are ANSI escape sequences?

I'll start with a brief primer on ANSI escape sequences, as a lot of what I'll be discussing uses them.

ANSI escape sequences are special sequences of control characters that allow a system to send signals to a terminal. When a terminal receives an ANSI escape sequence, it is interpreted, not displayed. Common usages include setting text color and positioning the cursor (through CSI sequences).

Of particular interest are Operating System Command (OSC) sequences, which allow a system to interact with features of a terminal emulator. OSC sequences enable useful functionality like setting the window title, letting a remote system copy text to the local clipboard, and semantically marking up command output so it's easier to parse.

The format of OSC sequences

All ANSI escape sequences start with \e, followed by a control code. The control code for OSC is ]. The OSC sequence is then comprised of an OSC code, which specifies the command to perform, followed by any arguments for the command separated by semicolons. The sequence is finally terminated with \a1.

For example, you can use OSC 0 to change the window title to meow by running: printf "\e]0;meow\a"

Format of an OSC sequence to change the window title to "meow"

There are many OSC codes that are widely-supported, but a number of modern terminal emulators have extended the OSC command set with proprietary commands as well. For example, iTerm2 even supports inline file transfers through its own OSC 1337.

Shell integration & command marks with OSC 133

One of the most tedious things while working in a terminal can be dealing with commands that repeatedly produce a lot of output. It can very quickly become a chore to navigate through the results of each execution, let alone do things like quickly copy the output.

In 2013, the Final Term emulator pioneered a clever solution for this. By embedding special OSC sequences within shell prompt variables, it was able to differentiate between an entered command and the command's subsequent output. And although Final Term is now defunct, modern terminal emulators like iTerm2 and Windows Terminal now use this same approach to support features like navigating through previous commands, automatic output selection, and showing markers with exit results in the scroll bar.

This feature is now often referred to as shell integration, command marks, or semantic prompts.

Windows Terminal showing scrollbar command marks and previous output selection
Windows Terminal showing scrollbar command marks and previous output selection

How it works

Bash has several special prompt variables that are used in different stages of input and output, including:

  • PS1, the primary prompt string, printed before reading the command
  • PS0, printed after reading the command, but before it starts
  • PROMPT_COMMAND, executed after the command finishes, but before PS1 is displayed

Shell integration works by embedding 4 OSC sequences into these variables (see below for their values). These sequences are often referred to by names that pay tribute to their origin in Final Term:

Sequence name Description
FTCS_PROMPT Start of the prompt
FTCS_COMMAND_START Start of the entered command
FTCS_COMMAND_EXECUTED Start of the command's output
FTCS_COMMAND_FINISHED End of the command's output

When integrated into the prompt variables, their position during the shell's read–eval–print loop looks like:

[FTCS_PROMPT]raccoon@gibson$ [FTCS_COMMAND_START]whoami
[FTCS_COMMAND_EXECUTED]raccoon
[FTCS_COMMAND_FINISHED]

The terminal is then able to use the positioning of the OSC sequences to distinguish between an entered command and its subsequent output.

FTCS_COMMAND_FINISHED is able to take the command's exit code as an argument, further allowing the terminal to interpret if a command succeeded or not.

Integrating the sequences

ℹ️ Note

When using ANSI escape sequences in Bash prompt variables, they must be wrapped in \[ and \]

The following Bash snippet demonstrates the process of wrapping existing prompt variable values with the OSC sequences to enable shell integration features:

FTCS_PROMPT="\e]133;A\a"        # OSC 133;A (start of the prompt)
FTCS_CMD_START="\e]133;B\a"     # OSC 133;B (start of entered command / end of the prompt)
FTCS_CMD_EXECUTED="\e]133;C\a"  # OSC 133;C (start of the command's output)

# PS1: Mark the start of the prompt and the start of the entered command
PS1="\[${FTCS_PROMPT}\]${PS1}\[${FTCS_CMD_START}\]"

# PS0: Mark the start of command output
PS0="${PS0}\[${FTCS_CMD_EXECUTED}\]"

# PROMPT_COMMAND: Mark the end of command output, with exit code as an argument
# Exit code *must* be retrieved first, otherwise it may be overwritten.
print_cmd_finished() {
  local FTCS_CMD_FINISHED="\e]133;D;${?}\a" # OSC 133;D (end of the command's output)
  printf $FTCS_CMD_FINISHED
}
PROMPT_COMMAND="print_cmd_finished; $PROMPT_COMMAND"

The positioning of the sequences within the prompt variables is an important detail. Terminal emulators effectively use these sequences as delimiters, so any extraneous data encountered between marks can result in output not being interpreted correctly.

FTCS_PROMPT must be at the start of PS1, with FTCS_CMD_START at the end, so the position of the prompt and the command can correctly be identified. Similarly, FTCS_CMD_EXECUTED must be at the end of PS0 to correctly mark the start of command output.

The call to print FTCS_CMD_FINISHED must be at the start of PROMPT_COMMAND, both to correctly mark the end of output and to retrieve the command's exit code via $?. If any other commands run before that point, the entered command's exit code will be overwritten.

The above code is written in a generic way that wraps existing prompt variables, so it may be appended to the very end of an existing .bashrc file.

Choice of prompt variables

There are multiple ways to go about printing the sequences at the correct time. For example, Microsoft recently published a related tutorial that dynamically updates PS1 with the FTCS_COMMAND_FINISHED sequence. While this does work, I find it to be unnecessarily complicated, as the same thing can be achieved through simply printing the sequence via PROMPT_COMMAND.

Compatability

This works nicely in iTerm2 and Windows Terminal, but regardless of terminal support, it won't work with programs like tmux or screen (at least not out of the box). Shell integrations are not currently supported by terminal emulators like xfce4-terminal, PuTTY and mintty – but at least according to my testing, the sequences are merely ignored, so they shouldn't interfere with anything.

Trying to use this with cmd.exe (and by extension ConEmu) may result in some funny characters. :)


  1. Although the standard ANSI sequence terminator is \e\\ (ST, hex 0x9C), OSC commands may be (and often are) terminated with \a (BEL, hex 0x07) for historical reasons↩︎