Implements robust interactive Bash TUI menus with dialog widgets, safe
Scanned 9/4/2026
Install to Claude Code
npx -y skills add paulpas/agent-skill-router --skill bash-tui-menu --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Bash Tui Menu?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/paulpas-bash-tui-menu)More formats (shields.io, HTML) on the badges page.
---
name: bash-tui-menu
description: Implements robust interactive Bash TUI menus with dialog widgets, safe
selection handling, cancel paths, and non-interactive fallbacks.
license: MIT
compatibility: opencode
metadata:
version: "1.0.0"
domain: coding
role: implementation
scope: implementation
output-format: code
content-types:
- code
- guidance
- do-dont
- examples
triggers: bash tui menu, dialog command, interactive shell script, terminal menu,
checklist radiolist, how do i make bash menus, ncurses dialog
archetypes:
- tactical
- generation
anti_triggers:
- brainstorming
- vague ideation
- code golf
- over-engineering
response_profile:
verbosity: low
directive_strength: high
abstraction_level: operational
related-skills: shell-parameter-expansion, shell-command-chaining, shell-process-management,
output-formatting
---
# 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 `dialog` and an attached TTY before rendering; provide a CLI/env fallback when either is missing.
- [ ] Under `set -e`, capture widget output inside an `if choice=$(dialog --stdout ...); then status=0; else status=$?; fi` block so Cancel, ESC, Extra, and errors cannot abort before status handling.
- [ ] Treat exit code `0` as OK/Yes, `1` as Cancel/No, `3` as Extra, and `255` as ESC unless the script intentionally remaps `DIALOG_*` variables.
- [ ] Build menu/checklist/radiolist options as Bash arrays and pass them as `"${options[@]}"`; never concatenate untrusted labels into one command string.
- [ ] Use `trap` for `INT`, `TERM`, and `EXIT` cleanup so temp files and alternate-screen artifacts do not survive aborts.
- [ ] Validate selected tags with a `case` statement 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 `read` prompt into a richer terminal UI with `--menu`, `--yesno`, `--inputbox`, `--passwordbox`, `--checklist`, or `--radiolist`.
- Adding a `<More Info>` or third action button with `--extra-button` and handling exit code `3`.
- 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`; `dialog` workflows 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 `dialog` cannot be installed and no fallback UX is acceptable.
---
## Core Workflow
1. **Prove the Runtime Can Render a TUI** — Check that `dialog` exists, 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.
2. **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.
3. **Render One Widget and Capture Both Result Channels** — Use `--stdout` for simple capture, or `3>&1 1>&2 2>&3` when stdout must remain available for status text. In strict-mode scripts, put the capture assignment in an `if` condition and set `status=0` or `status=$?` inside the corresponding branch. **Checkpoint:** Cancel, ESC, Extra, and error statuses are captured before any `clear`, `echo`, function call, or side effect runs.
4. **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.
5. **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 `dialog` by passing arguments directly.
6. **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.
```bash
#!/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.
```bash
#!/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.
```bash
#!/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.
```bash
#!/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.
```bash
#!/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 bash` and `set -euo pipefail` unless integrating into an existing script with a different established shell policy.
- Check `command -v dialog` before 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 by `case` statements.
- Handle `Cancel`, `ESC`, and `Extra` intentionally; every exit code path must be explicit.
- Use `mktemp` for any file capture and remove temp files with `trap`.
- 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 `dialog` binary.
- Do not save selections to fixed paths such as `/tmp/drinkChoice` in 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 true` with clear exit paths.
---
## Output Template
When generating or reviewing a Bash TUI menu script, return:
1. **Runtime assumptions** — Bash version needs, `dialog` dependency, TTY/non-interactive behavior.
2. **Menu map** — Each widget, its stable tags, and the action for OK/Cancel/ESC/Extra.
3. **Implementation** — Bash code with strict mode, dependency detection, arrays, traps, and worker functions.
4. **Fallback behavior** — Flags or environment variables that bypass the TUI for automation.
5. **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](https://github.com/RileyMeta/Bash-Dialog)
- [Bash-Dialog README](https://github.com/RileyMeta/Bash-Dialog/blob/main/README.md)
- [All Menus example](https://github.com/RileyMeta/Bash-Dialog/blob/main/Examples/All_Menus/All_Menus.sh)
- [Advanced menu example](https://github.com/RileyMeta/Bash-Dialog/blob/main/Examples/Advanced_Examples/Advanced_Example.sh)
- [Advanced Extra Button example](https://github.com/RileyMeta/Bash-Dialog/blob/main/Examples/Extra_Button/Advanced_Extra_Button.sh)
- [Dynamic Extra Button example](https://github.com/RileyMeta/Bash-Dialog/blob/main/Examples/Extra_Button/Dynamic_Extra.sh)
- [User Creation form example](https://github.com/RileyMeta/Bash-Dialog/blob/main/Examples/Advanced_Examples/User_Creation.sh)
---
## Reference Notes
### Verified from `https://github.com/RileyMeta/Bash-Dialog`
- The repository is a tutorial/example collection for Bash scripts using the `dialog` command; the cloned tree contains `README.md` and example scripts under `Examples/`, 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 widget `dialog --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`/`\ZB` for bold and `\Z0`-`\Z7` for ANSI colors when `--colors` is enabled.
- The README records default dialog-related exit-code variables: OK `0`, Cancel `1`, Help `2`, Extra `3`, ESC `255`, and Error `-1` through `DIALOG_*` environment variables.
- `Examples/All_Menus/All_Menus.sh` demonstrates `--menu`, `--msgbox`, `--yesno`, `--inputbox`, `--passwordbox`, `--infobox`, `--textbox`, `--checklist`, `--radiolist`, `--gauge`, `--calendar`, `--timebox`, `--fselect`, `--dselect`, and `--form`.
- `Examples/Advanced_Examples/Advanced_Example.sh` demonstrates a `while true` main menu, `--stdout`, `--cancel-label`, `trap 'aborted' INT`, and routing selected tags through nested `case` statements.
- `Examples/Extra_Button/Advanced_Extra_Button.sh` and `Examples/Extra_Button/Dynamic_Extra.sh` demonstrate `--extra-button --extra-label` and route exit code `3` separately from OK and Cancel.
- `Examples/Advanced_Examples/User_Creation.sh` demonstrates `--form` capture with file descriptor swapping and `--passwordbox` for hidden input.
### Generic terminal-dialog patterns applied here
- Prefer `--stdout` for simple capture because it is readable and avoids fixed temporary files.
- Prefer `--separate-output` for checklists so each selected tag can be processed line-by-line.
- Prefer dependency and TTY detection at startup because `dialog` blocks 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. |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!