gum and whiptail: Building TUI Menus for Bash Scripts
AI generated
$_
#!/
Bash · gum · whiptail · TUI
gum and whiptail: Building TUI Menus for Bash Scripts
from numbered prompts to real terminal dialogs

gum and whiptail turn a Bash script into an application with real borders, forms and confirmation dialogs, without needing a graphical interface. This article shows how to use both tools in your own scripts, where they differ, and when the effort pays off compared to plain Bash built-ins.

19 min read gum · whiptail · forms · styling Bash 4.x · 5.x · Linux · macOS

1. Why TUI menus are more than eye candy

A TUI menu built with gum or whiptail differs from a plain echo menu not only visually. Borders, colored highlights and clearly delimited dialog windows give the user immediate visual feedback about which part of the screen belongs to the current prompt. In more complex maintenance scripts with several consecutive prompts, that noticeably reduces operating mistakes because unclear states occur less often.

The second advantage is the built-in validation of form fields and confirmation dialogs. Instead of writing your own read loops with manual input checking, gum and whiptail provide ready-made building blocks for yes/no prompts, text inputs, selection lists and checkboxes. For admin scripts used by several people on a team, a consistent TUI menu layout makes the difference between a tool people trust and one they avoid.

2. whiptail: dialogs straight from the system package

whiptail is preinstalled on most Debian and Ubuntu systems because it is used internally by the installer. That makes it the most pragmatic choice for scripts that must work without extra installation. Its syntax follows the older dialog program: every dialog type is selected via a parameter such as --yesno, --inputbox or --menu, followed by title, text and size in rows and columns.

The result of a whiptail dialog is not returned via stdout but via stderr, which surprises many newcomers. The exit code signals whether the user confirmed (0) or cancelled (1), while the actual input value has to be redirected from stderr into a variable with 3>&1 1>&2 2>&3. This redirection pattern is the foundation of every whiptail integration in Bash and should be understood once rather than looked up again for every new script.


#!/usr/bin/env bash
set -euo pipefail

if ! command -v whiptail &>/dev/null; then
  echo "[ERROR] whiptail not found" >&2
  exit 1
fi

# yesno dialog — exit code signals the choice, no stdout redirection needed
if whiptail --title "Confirm deployment" \
    --yesno "Deploy to production?" 10 50; then
  echo "Confirmed, starting deployment..."
else
  echo "Aborted." >&2
  exit 1
fi

# inputbox dialog — result comes from stderr, redirected into a variable
target=$(whiptail --title "Target directory" \
  --inputbox "Where should this be deployed?" 10 50 "/var/www/html" \
  3>&1 1>&2 2>&3)

echo "Target directory: $target"

3. whiptail forms: multiple inputs in a single dialog

A single --inputbox is enough for simple prompts, but many maintenance scripts need several values at once, for example hostname, port and username for a database connection. whiptail offers --menu for selection lists and --checklist for multi-selection with checkboxes. Both return the chosen tags separated by spaces, which should be split with read -ra into an array afterwards, rather than processing them as a single string.

A --checklist dialog is excellent for scripts offering optional steps, for example which backup components a run should include. The tags in square brackets after each list entry control whether an option is enabled by default. This pattern replaces several consecutive read -p "yes/no" prompts with a single clear dialog that the user fills out in one pass.


#!/usr/bin/env bash
set -euo pipefail

# checklist: multiple optional steps in a single dialog
choices=$(whiptail --title "Backup components" \
  --checklist "What should be backed up?" 15 60 5 \
  "db" "Database" ON \
  "media" "Media files" ON \
  "config" "Configuration files" OFF \
  "logs" "Log files" OFF \
  3>&1 1>&2 2>&3)

# whiptail returns quoted tags separated by spaces — parse into an array
read -ra selected_components <<< "${choices//\"/}"

echo "Backing up: ${selected_components[*]}"
for component in "${selected_components[@]}"; do
  echo "  -> Processing $component"
done

4. gum: modern styling without dialog syntax

gum by Charmbracelet takes a different approach than whiptail: instead of a single program with many parameters, gum provides several subcommands such as gum choose, gum input, gum confirm and gum spin, which can be used in pipes like standalone Unix tools. The result lands directly on stdout, without the redirection gymnastics that whiptail requires.

The second big difference is in appearance: gum uses rounded borders, gradients and modern typography by default, closer to today's CLI tools than to the classic dialog look of whiptail. For teams that want to visually upgrade their internal tools, gum is often the preferred choice, though it must be installed separately since it is not part of standard distributions.


#!/usr/bin/env bash
set -euo pipefail

if ! command -v gum &>/dev/null; then
  echo "[ERROR] gum not found, see https://github.com/charmbracelet/gum" >&2
  exit 1
fi

# gum choose — result goes straight to stdout, no redirection needed
environment=$(gum choose "staging" "production" "canary" --header "Select environment")

# gum confirm — clean boolean via exit code
if gum confirm "Really deploy to $environment?"; then
  name=$(gum input --placeholder "Your name" --prompt "Approved by: ")
  echo "Deployment to $environment approved by $name"
else
  echo "Aborted." >&2
  exit 1
fi

5. gum in a complete deployment workflow

The real value of gum shows when several subcommands are combined into a coherent workflow. A typical example: the user first selects the target environment with gum choose, then confirms with gum confirm, after which the actual deployment command runs wrapped in gum spin, which shows a loading indicator during execution and disappears automatically afterward.

This combination of selection, confirmation and visual feedback during execution makes a script noticeably more polished without writing any custom code for spinners or progress indicators. gum spin handles exactly this task and terminates automatically once the given command finishes, including forwarding the exit code to the calling script.


#!/usr/bin/env bash
set -euo pipefail

env=$(gum choose "staging" "production" --header "Target environment")

gum confirm "Deploy to $env?" || { echo "Aborted."; exit 1; }

# gum spin wraps a long-running command with a loading indicator
gum spin --spinner dot --title "Deploying to $env..." -- \
  bash -c "sleep 3 && echo Deployment complete"

echo "Done."

6. Custom styling: colors, borders and widths

Both gum and whiptail allow custom styling, but in different ways. With whiptail customization is limited: colors can only be influenced via environment variables such as NEWT_COLORS, which is poorly documented and correspondingly error prone. gum, on the other hand, offers dedicated flags for almost every subcommand, such as --border rounded, --border-foreground or --width, set directly on the command line.

For scripts used repeatedly as an internal company tool, a small wrapper function that defines the styling parameters centrally is worth the effort. That way the visual appearance stays consistent across all scripts, and changes to the corporate look only need to be maintained in one place instead of being repeated in every single script.

7. Error handling and exit codes on cancel

Both tools signal a user cancellation through a non-zero exit code, but with different numeric values depending on the dialog type. With whiptail, exit code 1 usually stands for "Cancel" or "No", while exit code 255 signals a cancel via Esc, which should be distinguished explicitly for more precise error handling. gum simplifies this: both a cancel with Esc and an explicit rejection return a non-zero code, so a simple || exit 1 is sufficient in most cases.

It is important that set -e has the same effect with both tools as with fzf: a cancelled dialog terminates the script immediately if no explicit handling occurs. For scripts with several consecutive dialogs, a central function that prints a consistent message on every cancel and terminates the script in a controlled way is recommended, rather than reacting differently at various points.

8. Portability: checking installation and building a fallback

Neither gum nor whiptail are guaranteed to be present on every system. whiptail is typically missing on minimal Alpine images or pure server distributions without the installer substrate, and gum practically always has to be installed explicitly via a package manager or Go install. A production ready script checks both dependencies at the start and gives a clear error message with an installation hint instead of failing mid-flow with "command not found".

For maximum portability a script can additionally implement a cascade of several tools: try gum first, fall back to whiptail, and finally to plain read. This three-tier fallback chain is more work to maintain but guarantees that a script stays runnable in any environment, regardless of which additional tools are installed.


#!/usr/bin/env bash
set -euo pipefail

confirm_action() {
  local message="$1"

  if command -v gum &>/dev/null; then
    gum confirm "$message"
  elif command -v whiptail &>/dev/null; then
    whiptail --yesno "$message" 10 50
  else
    read -rp "$message [y/N] " reply
    [[ "$reply" =~ ^[Yy]$ ]]
  fi
}

if confirm_action "Really continue?"; then
  echo "Continuing."
else
  echo "Aborted." >&2
  exit 1
fi

9. gum, whiptail and dialog compared

The choice between gum, whiptail and the classic dialog program depends on the target system and visual requirements. All three solve the same underlying problem but differ noticeably in availability and usability.

Tool Preinstalled stdout output Visual styling
gum No Directly on stdout Modern, configurable
whiptail Usually yes (Debian/Ubuntu) Only via stderr redirection Classic, limited
dialog Sometimes Only via stderr redirection Classic, limited
read -p / select Always (Bash builtin) Direct None

In practice the target environment often decides: for scripts that must run on arbitrary servers, whiptail is the safer choice because of its wide preinstallation. For internal developer tools where the team controls the installation of gum, the more modern look and simpler stdout integration is convincing. Both tools do not exclude each other and, as shown in the fallback example, can be combined in the same codebase.

Mironsoft

Shell automation and CLI tooling for development teams

Internal scripts that feel like real tools?

We build TUI menus with gum or whiptail for your maintenance scripts, with forms, confirmation dialogs and solid fallbacks for any target environment.

TUI development

Building forms, menus and confirmation dialogs with gum or whiptail

Fallback chains

Robust scripts that run on any target environment without extra installation

Styling consistency

A unified appearance across all internal maintenance scripts

10. Summary

gum and whiptail solve the same underlying problem in different ways: a script with plain text output becomes an application with real dialog windows, forms and confirmations. whiptail scores through wide preinstallation and is the pragmatic choice for scripts meant to run on arbitrary servers. gum convinces through modern styling, simple stdout integration and a more intuitive command line syntax, but requires separate installation.

In practice, a pragmatic combination pays off for many teams: gum for internal developer tools in a controlled environment, whiptail for scripts on foreign or minimally equipped servers, and a simple read fallback for the case that neither is available. This three-tier strategy ensures a script stays runnable everywhere while offering the best possible usability where possible.

gum and whiptail — The essentials at a glance

whiptail core pattern

Redirect result via 3>&1 1>&2 2>&3 from stderr into a variable, exit code checks confirmation.

gum core pattern

Subcommands like gum choose, gum confirm return results directly on stdout, no redirection needed.

Forms

--checklist in whiptail for multi-selection, parse result with read -ra into an array.

Portability

Check availability with command -v, use a three-tier fallback chain of gum, whiptail, read.

11. FAQ: gum and whiptail in Bash Scripts

1Difference gum vs. whiptail?
whiptail usually preinstalled, result via stderr. gum installed separately, more modern, result directly on stdout.
2Why stderr with whiptail?
Historical legacy of dialog. With 3>&1 1>&2 2>&3 the value can be redirected cleanly into a variable.
3Multi-selection with whiptail?
--checklist with tag, description, ON/OFF per entry. Parse result with read -ra into an array.
4Is gum preinstalled?
No, always install separately. Check with command -v gum and provide a fallback.
5Loading indicator with gum?
gum spin --title 'Text' -- command. Runs alongside, terminates automatically, forwards exit code.
6Detecting Esc cancel in whiptail?
Usually exit code 255 on Esc, 1 on No/Cancel. Check the exact code for differentiated handling.
7Combining gum and whiptail?
Yes, check gum first with command -v, then whiptail, finally read as a fallback chain.
8Styling colors in whiptail?
Via NEWT_COLORS, limited and poorly documented compared to gum's direct styling flags.
9Better for foreign servers?
whiptail, because of wide preinstallation on Debian/Ubuntu. gum better suited for internal, controlled environments.
10set -e special cases?
A cancelled dialog produces a non-zero exit code, terminating the script instantly without explicit || or if handling.