Bash TUI Menu Builder
Implements production-ready interactive Bash terminal interfaces using the dialog command and the concrete Bash-Dialog example patterns. This skill makes the model design menus that are safe under set -euo pipefail, preserve cancel/ESC semantics, clean up the terminal, and remain scriptable through non-interactive fallbacks.
TL;DR for Code Generation
- Detect
dialogand an attached TTY before rendering; provide a CLI/env fallback when either is missing. - Under
set -e, capture widget output inside anif choice=$(dialog --stdout ...); then status=0; else status=$?; fiblock so Cancel, ESC, Extra, and errors cannot abort before status handling. - Treat exit code
0as OK/Yes,1as Cancel/No,3as Extra, and255as ESC unless the script intentionally remapsDIALOG_*variables. - Build menu/checklist/radiolist options as Bash arrays and pass them as
"${options[@]}"; never concatenate untrusted labels into one command string. - Use
trapforINT,TERM, andEXITcleanup so temp files and alternate-screen artifacts do not survive aborts. - Validate selected tags with a
casestatement or allowlist before running commands; menu text is not authorization. - Keep UI functions thin: collect choices in TUI functions, execute side effects in testable worker functions.
When to Use
Use this skill when:
- Building an interactive Bash script that guides users through setup, diagnostics, deployment, backups, or local administration tasks.
- Converting a numbered
readprompt into a richer terminal UI with--menu,--yesno,--inputbox,--passwordbox,--checklist, or--radiolist. - Adding a
<More Info>or third action button with--extra-buttonand handling exit code3. - Capturing multi-field input from forms and password prompts while keeping passwords out of logs.
- Supporting both humans at a terminal and automation in CI through flags, environment variables, or defaults.
When NOT to Use
Avoid this skill for:
- Scripts that must be strictly POSIX
sh;dialogworkflows here rely on Bash arrays,[[ ]], and Bash file-descriptor patterns. - Non-terminal automation where prompts would hang CI; expose flags or environment variables instead of forcing a TUI.
- Full-screen applications needing long-lived state, mouse-heavy interaction, or custom rendering; use a real TUI framework in Python, Go, Rust, or Node.
- Security-sensitive account creation where root privileges, password hashing, or policy enforcement are not already designed and reviewed.
- Systems where
dialogcannot be installed and no fallback UX is acceptable.
Core Workflow
Prove the Runtime Can Render a TUI — Check that
dialogexists, stdin/stdout connect to a terminal, and the script is not running in CI/non-interactive mode. Checkpoint: If any check fails, the script must use a deterministic fallback or exit with a clear setup message.Model Choices as Data — Store tags, labels, and defaults in arrays before calling
dialog. For checklist/radiolist widgets, include the required status field (on/off). Checkpoint: The command line must pass array elements with"${items[@]}"so spaces and punctuation in labels remain intact.Render One Widget and Capture Both Result Channels — Use
--stdoutfor simple capture, or3>&1 1>&2 2>&3when stdout must remain available for status text. In strict-mode scripts, put the capture assignment in anifcondition and setstatus=0orstatus=$?inside the corresponding branch. Checkpoint: Cancel, ESC, Extra, and error statuses are captured before anyclear,echo, function call, or side effect runs.Route by Exit Code Before Acting on Data — First branch on OK/Cancel/Extra/ESC/error, then branch on the selected tag. Checkpoint: Cancel and ESC must be intentional paths, not accidental fall-through to the default action.
Execute Side Effects Outside the UI Layer — TUI functions return tags or normalized values; worker functions perform installation, file changes, or command execution. Checkpoint: Worker functions can be tested without
dialogby passing arguments directly.Clean Up and Restore the Terminal — Use
mktemp,trap,clear, and local variables. Checkpoint: Temporary files are deleted on normal exit, Cancel, ESC, Ctrl+C, and command failure.
Implementation Patterns
Pattern 1: Dependency Detection, Safe Capture, and Non-Interactive Fallback
Use this pattern for scripts that should be pleasant for humans but still safe in CI, cron, SSH commands without TTY allocation, or automation. It uses --stdout, as shown in Bash-Dialog's advanced example, because that is easier to read than saving output to a fixed file.
#!/usr/bin/env bash
set -euo pipefail
readonly APP_TITLE="Server Maintenance"
has_interactive_tty() {
[[ -t 0 && -t 1 ]]
}
require_dialog_or_fallback() {
if [[ "${NONINTERACTIVE:-0}" == "1" ]]; then
return 1
fi
if ! has_interactive_tty; then
return 1
fi
command -v dialog >/dev/null 2>&1
}
choose_maintenance_action() {
local default_action="${MAINTENANCE_ACTION:-status}"
if ! require_dialog_or_fallback; then
printf '%s\n' "$default_action"
return 0
fi
local options=(
status "Show service status"
logs "Open recent logs"
restart "Restart service"
exit "Exit without changes"
)
local choice
local status
if choice=$(dialog --clear \
--backtitle "$APP_TITLE" \
--title "Main Menu" \
--cancel-label "Exit" \
--stdout \
--menu "Choose a maintenance action:" 0 0 4 \
"${options[@]}"); then
status=0
else
status=$?
fi
case "$status" in
0) printf '%s\n' "$choice" ;;
1|255) printf '%s\n' "exit" ;;
*) printf 'ERROR: dialog failed with exit code %s\n' "$status" >&2; return 1 ;;
esac
}
run_maintenance_action() {
local action="$1"
case "$action" in
status) systemctl status my-app.service --no-pager ;;
logs) journalctl -u my-app.service -n 100 --no-pager ;;
restart) systemctl restart my-app.service ;;
exit) return 0 ;;
*) printf 'ERROR: unsupported action: %s\n' "$action" >&2; return 64 ;;
esac
}
main() {
local action
action=$(choose_maintenance_action)
run_maintenance_action "$action"
}
main "$@"
Why this works: UI capability is parsed at the boundary. Internal logic receives one trusted action string and can fail fast if an impossible tag appears.
Pattern 2: BAD vs GOOD Selection Handling
The Bash-Dialog examples show two capture styles: 2>&1 >/dev/tty and --stdout. Both are valid, but they become unsafe when the script ignores exit codes, writes to fixed temp files, or runs selected text as code.
#!/usr/bin/env bash
set -euo pipefail
# ❌ BAD — fixed temp file, ignores cancel, unquoted read, and fall-through action.
dialog --menu "Choose:" 15 50 4 \
1 "Restart service" \
2 "Delete cache" \
2>/tmp/menu-choice
choice=$(</tmp/menu-choice)
if [[ $choice == 1 ]]; then
systemctl restart my-app.service
else
rm -rf /var/cache/my-app/*
fi
# ✅ GOOD — result and status captured atomically, all branches explicit.
choose_action() {
local choice
local status
if choice=$(dialog --stdout --menu "Choose:" 15 50 4 \
restart "Restart service" \
purge-cache "Delete cache" \
back "Return to previous menu"); then
status=0
else
status=$?
fi
case "$status" in
0) printf '%s\n' "$choice" ;;
1) printf '%s\n' "back" ;;
255) printf '%s\n' "back" ;;
*) printf 'ERROR: dialog failed: %s\n' "$status" >&2; return 1 ;;
esac
}
dispatch_action() {
local choice="$1"
case "$choice" in
restart) systemctl restart my-app.service ;;
purge-cache) find /var/cache/my-app -mindepth 1 -maxdepth 1 -delete ;;
back) return 0 ;;
*) printf 'ERROR: invalid menu choice: %s\n' "$choice" >&2; return 64 ;;
esac
}
dispatch_action "$(choose_action)"
Pattern 3: Prompt, Password, and Confirmation Flow
Use input widgets for data collection, then parse and validate before executing. Bash-Dialog's user-creation example captures --form output with 3>&1 1>&2 2>&3 3>&- and uses --passwordbox --insecure; production code should avoid echoing secrets and should validate every field before continuing.
#!/usr/bin/env bash
set -euo pipefail
prompt_required_text() {
local title="$1"
local prompt="$2"
local value
local status
if value=$(dialog --clear --stdout \
--title "$title" \
--inputbox "$prompt" 10 60 ""); then
status=0
else
status=$?
fi
case "$status" in
0) [[ -n "$value" ]] || { printf 'ERROR: value cannot be empty\n' >&2; return 64; }
printf '%s\n' "$value" ;;
1|255) return 130 ;;
*) printf 'ERROR: input dialog failed: %s\n' "$status" >&2; return 1 ;;
esac
}
prompt_secret() {
local secret
local status
if secret=$(dialog --clear --stdout \
--title "Credentials" \
--passwordbox "Enter the API token:" 10 60); then
status=0
else
status=$?
fi
case "$status" in
0) [[ -n "$secret" ]] || { printf 'ERROR: token cannot be empty\n' >&2; return 64; }
printf '%s\n' "$secret" ;;
1|255) return 130 ;;
*) printf 'ERROR: password dialog failed: %s\n' "$status" >&2; return 1 ;;
esac
}
confirm_write() {
local username="$1"
dialog --clear \
--title "Confirm" \
--yes-label "Write" \
--no-label "Cancel" \
--yesno "Write configuration for ${username}?" 8 60
}
main() {
local username token
username=$(prompt_required_text "Profile" "Enter the username:") || exit $?
token=$(prompt_secret) || exit $?
if confirm_write "$username"; then
install -m 600 /dev/null "$HOME/.my-app-token"
printf '%s\n' "$token" > "$HOME/.my-app-token"
dialog --msgbox "Configuration saved for ${username}." 7 50
else
dialog --msgbox "No files were changed." 7 40
fi
}
main "$@"
Pattern 4: Checklist and Radiolist Builders
dialog --checklist and dialog --radiolist require triples: tag, item text, and on/off status. Keep those triples in arrays. Checklists return selected tags, commonly quoted and separated by spaces, so only use stable tags without spaces or normalize with --separate-output when processing multiple selections.
#!/usr/bin/env bash
set -euo pipefail
select_components() {
local items=(
nginx "Install nginx reverse proxy" on
certbot "Install certbot TLS tooling" off
logrotate "Install log rotation config" on
firewall "Enable firewall profile" off
)
local selected
local status
if selected=$(dialog --clear --stdout \
--separate-output \
--title "Components" \
--checklist "Select components to install:" 18 72 8 \
"${items[@]}"); then
status=0
else
status=$?
fi
case "$status" in
0) printf '%s\n' "$selected" ;;
1|255) return 130 ;;
*) printf 'ERROR: checklist failed: %s\n' "$status" >&2; return 1 ;;
esac
}
select_environment() {
local choices=(
dev "Developer workstation" on
staging "Shared staging server" off
prod "Production server" off
)
local environment
local status
if environment=$(dialog --clear --stdout \
--title "Environment" \
--radiolist "Choose exactly one target:" 15 64 4 \
"${choices[@]}"); then
status=0
else
status=$?
fi
case "$status" in
0) printf '%s\n' "$environment" ;;
1|255) return 130 ;;
*) printf 'ERROR: radiolist failed: %s\n' "$status" >&2; return 1 ;;
esac
}
main() {
local environment
environment=$(select_environment) || exit $?
printf 'Environment: %s\n' "$environment"
local components_output
local components_status
if components_output=$(select_components); then
components_status=0
else
components_status=$?
fi
case "$components_status" in
0)
while IFS= read -r component; do
case "$component" in
nginx|certbot|logrotate|firewall) printf 'Selected component: %s\n' "$component" ;;
"") ;;
*) printf 'ERROR: unknown component: %s\n' "$component" >&2; exit 64 ;;
esac
done <<< "$components_output"
;;
130) exit 130 ;;
*) printf 'ERROR: checklist failed: %s\n' "$components_status" >&2; exit 1 ;;
esac
}
main "$@"
Pattern 5: Extra Button, Help Path, Cleanup, and Testability
Bash-Dialog's Extra Button examples verify that --extra-button --extra-label returns exit code 3. Use that code for contextual help or details; do not overload Cancel or force users to select an item just to learn what it does.
#!/usr/bin/env bash
set -euo pipefail
readonly TMP_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$TMP_DIR"
if [[ -t 1 ]]; then
clear
fi
}
trap cleanup EXIT INT TERM
show_plan_help() {
local selected="${1:-}"
case "$selected" in
quick) dialog --msgbox "Quick runs only cheap checks." 7 50 ;;
full) dialog --msgbox "Full runs every diagnostic and may take several minutes." 8 60 ;;
*) dialog --msgbox "Highlight a plan, then choose More Info." 7 55 ;;
esac
}
choose_plan() {
local choice status
while true; do
if choice=$(dialog --clear \
--backtitle "Diagnostics" \
--title "Run Plan" \
--extra-button --extra-label "More Info" \
--cancel-label "Exit" \
--stdout \
--menu "Choose a diagnostic plan:" 14 68 4 \
quick "Fast status checks" \
full "Complete diagnostics" \
exit "Leave without running checks"); then
status=0
else
status=$?
fi
case "$status" in
0) printf '%s\n' "$choice"; return 0 ;;
3) show_plan_help "$choice" ;;
1|255) printf '%s\n' "exit"; return 0 ;;
*) printf 'ERROR: dialog failed: %s\n' "$status" >&2; return 1 ;;
esac
done
}
run_plan() {
local plan="$1"
case "$plan" in
quick) printf 'Running quick diagnostics\n' ;;
full) printf 'Running full diagnostics\n' ;;
exit) return 0 ;;
*) printf 'ERROR: invalid plan: %s\n' "$plan" >&2; return 64 ;;
esac
}
run_plan "$(choose_plan)"
Constraints
MUST DO
- Use
#!/usr/bin/env bashandset -euo pipefailunless integrating into an existing script with a different established shell policy. - Check
command -v dialogbefore rendering and provide install guidance or a non-interactive fallback. - Capture dialog output and status separately; branch on status first, selected value second.
- Quote every variable expansion and pass menu data through arrays.
- Use stable machine tags (
restart,logs,prod) rather than user-facing labels as the values executed bycasestatements. - Handle
Cancel,ESC, andExtraintentionally; every exit code path must be explicit. - Use
mktempfor any file capture and remove temp files withtrap. - Keep secrets out of
echo, debug output, process arguments, and dialog summary boxes.
MUST NOT DO
- Do not source a remote Bash-Dialog file or assume Bash-Dialog provides a library API; the verified repository is a guide and examples for the
dialogbinary. - Do not save selections to fixed paths such as
/tmp/drinkChoicein production scripts. - Do not run menu labels, user input, or selected text with
eval, unquoted command substitution, or shell interpolation. - Do not ignore the exit status of
dialog; Cancel must never trigger the first or default destructive action. - Do not force interactive dialogs in CI, cron, systemd units, or scripts called with redirected stdin/stdout.
- Do not put more than roughly 7-10 choices in one menu without grouping or adding search/filter behavior.
- Do not use recursive menu calls for long-running loops; prefer
while truewith clear exit paths.
Output Template
When generating or reviewing a Bash TUI menu script, return:
- Runtime assumptions — Bash version needs,
dialogdependency, TTY/non-interactive behavior. - Menu map — Each widget, its stable tags, and the action for OK/Cancel/ESC/Extra.
- Implementation — Bash code with strict mode, dependency detection, arrays, traps, and worker functions.
- Fallback behavior — Flags or environment variables that bypass the TUI for automation.
- Verification steps — Commands to run with and without a TTY, plus a ShellCheck command when available.
Live References
Repository-standard links checked against the local
SKILL_FORMAT_SPEC.md; these links preserve the Bash-Dialog research trail without treating Bash-Dialog as a sourceable library.
- Bash-Dialog repository
- Bash-Dialog README
- All Menus example
- Advanced menu example
- Advanced Extra Button example
- Dynamic Extra Button example
- User Creation form example
Reference Notes
Verified from https://github.com/RileyMeta/Bash-Dialog
- The repository is a tutorial/example collection for Bash scripts using the
dialogcommand; the cloned tree containsREADME.mdand example scripts underExamples/, not a sourceable Bash library module. - The main README documents installation commands for Arch (
pacman), Debian/Ubuntu (apt), and Fedora (dnf), then demonstrates the minimal widgetdialog --msgbox "Hello World" 0 0. - Documented common options include
--backtitle,--title,--colors,--clear,--cr-wrap,--msgbox,--infobox,--menu,--programbox,--pause,--yesno,--inputbox,--passwordbox,--help-button, and--extra-button. - The README describes advanced styling sequences such as
\Zb/\ZBfor bold and\Z0-\Z7for ANSI colors when--colorsis enabled. - The README records default dialog-related exit-code variables: OK
0, Cancel1, Help2, Extra3, ESC255, and Error-1throughDIALOG_*environment variables. Examples/All_Menus/All_Menus.shdemonstrates--menu,--msgbox,--yesno,--inputbox,--passwordbox,--infobox,--textbox,--checklist,--radiolist,--gauge,--calendar,--timebox,--fselect,--dselect, and--form.Examples/Advanced_Examples/Advanced_Example.shdemonstrates awhile truemain menu,--stdout,--cancel-label,trap 'aborted' INT, and routing selected tags through nestedcasestatements.Examples/Extra_Button/Advanced_Extra_Button.shandExamples/Extra_Button/Dynamic_Extra.shdemonstrate--extra-button --extra-labeland route exit code3separately from OK and Cancel.Examples/Advanced_Examples/User_Creation.shdemonstrates--formcapture with file descriptor swapping and--passwordboxfor hidden input.
Generic terminal-dialog patterns applied here
- Prefer
--stdoutfor simple capture because it is readable and avoids fixed temporary files. - Prefer
--separate-outputfor checklists so each selected tag can be processed line-by-line. - Prefer dependency and TTY detection at startup because
dialogblocks or fails in automation contexts. - Prefer allowlisted tags and worker functions because terminal menus are user input, not trusted execution plans.
Related Skills
| Skill | Purpose |
|---|---|
shell-parameter-expansion |
Use safe defaults and fail-fast required variables in Bash scripts that feed menu labels and fallback values. |
shell-command-chaining |
Build reliable validation gates, fallback chains, and explicit error paths around dialog-driven actions. |
shell-process-management |
Add traps, cleanup, background process handling, and signal-aware termination to long-running TUI scripts. |
output-formatting |
Shape generated script output and user-facing summaries consistently after interactive choices are made. |