Posted on
Artificial Intelligence

Prompt Testing

Author
  • User
    linuxbash
    Posts by this author
    Posts by this author

Prompt Testing: Make Your Bash Prompt Fast, Correct, and Testable

If your prompt hesitates, you hesitate. A 200–300 ms delay every time you press Enter adds up to minutes lost daily. Worse, a broken or miscounted prompt can shift your cursor, corrupt line editing, or hide critical context like the current branch or exit status. Prompt testing is how you treat your PS1 like production code: you verify it’s correct, fast, and resilient.

In this guide you’ll learn why prompt testing matters, how to refactor your prompt for testability, how to write unit tests for it, and how to benchmark and optimize its performance on Linux. You’ll also see a drop-in alternative (Starship) if you want a fast prompt without building everything yourself.

Why prompt testing is worth it

  • Reliability: A misbalanced color escape can break editing and wrapping. Tests prevent regressions.

  • Speed: Subshells inside PS1 (like $(git ...) or $(kubectl ...)) can add 100–500 ms on every prompt render. Profiling and caching bring that near zero.

  • Portability: Guarding interactive-only code and using standard tools ensures your scripts and non-interactive shells don’t pay a penalty.

  • Team productivity: Shared prompts become shared tooling. Tests keep everyone fast and consistent.

Tools you’ll use (install on your distro)

  • bats: Test framework for Bash

  • shellcheck: Lint your prompt functions

  • hyperfine: Benchmark prompt latency

  • strace: See what syscalls your prompt triggers

  • starship (optional): A fast, cross-shell prompt with smart caching

Install with your package manager:

  • apt (Debian/Ubuntu and derivatives):

    sudo apt update
    sudo apt install -y bats shellcheck hyperfine strace starship
    
  • dnf (Fedora/RHEL-based):

    sudo dnf install -y bats ShellCheck hyperfine strace starship
    
  • zypper (openSUSE):

    sudo zypper refresh
    sudo zypper install -y bats ShellCheck hyperfine strace starship
    

If starship or hyperfine aren’t available in your repo:

  • Starship official installer:

    curl -fsSL https://starship.rs/install.sh | sh
    
  • With Rust toolchain:

    cargo install starship
    cargo install hyperfine
    

Step 1 — Refactor your prompt into testable functions

Move logic out of PS1 into functions. Keep color codes wrapped in non-printing markers \[ and \] so readline counts columns correctly.

Create ~/.bash_prompt:

# Colors (non-printing for readline)
RED='\[\e[31m\]'
GRN='\[\e[32m\]'
YEL='\[\e[33m\]'
BLU='\[\e[34m\]'
RST='\[\e[0m\]'

__git_branch() {
  git rev-parse --is-inside-work-tree >/dev/null 2>&1 || return
  git symbolic-ref --short HEAD 2>/dev/null \
    || git describe --tags --exact-match 2>/dev/null \
    || git rev-parse --short HEAD 2>/dev/null
}

__exit_code_segment() {
  local ec=${1:-$?}
  [[ $ec -eq 0 ]] && return 0
  printf '%s✗%s ' "$RED" "$RST"
}

# Optional: cache expensive bits before prompt renders
__prompt_precmd() {
  __PROMPT_GIT_BRANCH="$(__git_branch)"
}
# Bash 5+: PROMPT_COMMAND can be an array; works as a string too
PROMPT_COMMAND=__prompt_precmd

__my_prompt() {
  local ec=$?
  local seg_ec seg_git
  seg_ec="$(__exit_code_segment "$ec")"
  [[ -n $__PROMPT_GIT_BRANCH ]] && seg_git="${BLU}${__PROMPT_GIT_BRANCH}${RST} "
  # \w will be expanded by Bash after this returns
  printf '%s%s%s@%s %s\\w %s$ ' \
    "$seg_ec" "$GRN" "$USER" "$HOSTNAME" "$YEL" "$RST$seg_git"
}

# Set PS1 to call your function
PS1='$(__my_prompt)'

Source it from ~/.bashrc:

# Skip heavy prompt during benchmark if PROMPT_TEST_MINIMAL is set
if [[ -z $PROMPT_TEST_MINIMAL ]]; then
  # shellcheck source=/dev/null
  [[ -f ~/.bash_prompt ]] && source ~/.bash_prompt
fi

Why this helps:

  • You can call __my_prompt directly in tests.

  • Heavy work moves to PROMPT_COMMAND (once) instead of inside PS1 (multiple times).

  • Colors are safe for readline.

Step 2 — Unit-test your prompt behavior with bats

Create test/prompt.bats:

#!/usr/bin/env bats

setup() {
  # shellcheck source=/dev/null
  source "$HOME/.bash_prompt"
}

@test "exit code indicator appears on failure" {
  false  # set $?=1
  run bash -lc '__my_prompt'  # ensure a Bash context
  [[ "$output" == *"✗"* ]]
}

@test "no exit code indicator on success" {
  true
  run bash -lc '__my_prompt'
  [[ "$output" != *"✗"* ]]
}

@test "shows git branch in a repo" {
  tmpdir="$(mktemp -d)"
  pushd "$tmpdir" >/dev/null
  git init -q
  git config user.email test@example.com
  git config user.name test
  touch a && git add a && git commit -q -m init
  branch="$(git rev-parse --abbrev-ref HEAD)"
  run bash -lc '__prompt_precmd; __my_prompt'
  [[ "$output" == *"$branch"* ]]
  popd >/dev/null
  rm -rf "$tmpdir"
}

Run tests:

bats test/prompt.bats

Lint your prompt code:

shellcheck ~/.bash_prompt

Tip: Add one test that asserts color wrappers are non-printing:

@test "color sequences are wrapped for readline" {
  run bash -lc '__my_prompt'
  # Visible length won't count \e[…]m if wrapped in \[ \]
  stripped="$(sed -r $'s/\\x1b\\[[0-9;]*m//g' <<<"$output")"
  [[ ${#stripped} -le ${#output} ]]
}

Step 3 — Benchmark prompt latency and fix hotspots

Measuring interactive prompt time is tricky because prompts render in a TTY. Use a pseudo-terminal with script and feed a newline so Bash draws the prompt, then exits.

Benchmark baseline vs. your prompt:

# Minimal shell (no prompt section)
hyperfine --warmup 3 \
  'printf "\nexit\n" | script -qfec "env PROMPT_TEST_MINIMAL=1 bash -i" /dev/null'

# With your prompt enabled
hyperfine --warmup 3 \
  'printf "\nexit\n" | script -qfec "bash -i" /dev/null'

Compare the mean times. Aim for sub-50 ms on modern machines.

Find syscalls and file hits (e.g., repeated git invocations):

strace -tt -f -o /tmp/trace \
  script -qfec 'bash -i -c exit' /dev/null
grep -E 'git|open|stat' /tmp/trace | head -50

Common fixes:

  • Move $(git …) out of PS1 into PROMPT_COMMAND (as shown) and cache the result.

  • Avoid calling external tools on every prompt; memoize values and update only when $PWD changes.

  • Use faster git queries:

    • git -C "$PWD" rev-parse --is-inside-work-tree is cheaper than subshelling into other dirs.
    • Prefer git symbolic-ref --short HEAD over parsing git branch.

Real-world example: In a large monorepo, replacing $(git status --porcelain) in PS1 with a cached git rev-parse --short HEAD in PROMPT_COMMAND cut prompt time from ~180 ms to ~8 ms.

Step 4 — Make it robust and portable

  • Interactive guard:

    [[ $- != *i* ]] && return  # at top of ~/.bashrc
    
  • Ensure your prompt code is idempotent (safe to source multiple times).

  • Handle missing tools:

    command -v git >/dev/null || __git_branch() { return; }
    
  • Keep non-printing sequences inside \[ and \] to prevent cursor drift.

  • Consider a proven, fast prompt if you don’t want to maintain one:

    # After installing starship:
    echo 'eval "$(starship init bash)"' >> ~/.bashrc
    

Optional: Async and caching patterns

For heavyweight status (e.g., kube context), compute asynchronously and display the last known value:

__KUBE_CTX=""
__kube_update() {
  while :; do
    __KUBE_CTX="$(kubectl config current-context 2>/dev/null)"
    sleep 2
  done
}

# Start in background only once
if [[ -z $__KUBE_ASYNC_PID ]]; then
  __kube_update & __KUBE_ASYNC_PID=$!
fi

# In your prompt function:
[[ -n $__KUBE_CTX ]] && printf '%s%s%s ' "$YEL" "$__KUBE_CTX" "$RST"

This keeps prompt renders instant while a background job refreshes state.

Conclusion and next steps

Treating your prompt as code pays off quickly:

  • You extracted logic into functions with safe color handling.

  • You wrote bats tests to prevent regressions.

  • You benchmarked and eliminated latency hotspots.

  • You made your prompt robust across environments.

Pick one action to do now:

  • Add the refactored __my_prompt to your ~/.bash_prompt.

  • Write two bats tests (exit code and git branch).

  • Run the hyperfine benchmark and record your current prompt latency.

If you’d rather not maintain any of this, install Starship and be done in minutes. Either way, make your prompt work for you—fast, correct, and tested.