Files
Developer 1626b53de3 Improve: Better script vs. source context detection in menu-functions.sh
IMPROVEMENTS:
- Line 20-27: Replace 'return || exit' pattern with explicit context check
- Uses BASH_SOURCE check to determine if running as script or sourced
- Clearer intent: exit for scripts, return for sourced libraries

Rationale: 'return 2>/dev/null || exit' works but is confusing.
Explicit 'if' with BASH_SOURCE check is clearer and more maintainable.

RESULTS:
- Library behavior more explicit and easier to understand
- Better error handling for version mismatches
2026-03-20 01:36:03 -04:00

1267 lines
35 KiB
Bash

#!/bin/bash
################################################################################
# Menu Functions Library
################################################################################
# Provides standardized menu display and input handling for all toolkit scripts
# No text colors - uses plain text for maximum compatibility
# Handles proper back/cancel navigation and input validation
#
# REQUIREMENTS:
# - Bash 4.1 or later (uses ${var,,} lowercase expansion and negative array indexing)
# - set -eo pipefail is REQUIRED for error handling
#
# WARNING: This library sets global variables MENU_STACK, MENU_HISTORY, and
# INTERACTIVE_MODE. Do not modify these directly - use provided functions.
################################################################################
set -eo pipefail
# Bash version check
if [ "${BASH_VERSINFO[0]}" -lt 4 ] || ([ "${BASH_VERSINFO[0]}" -eq 4 ] && [ "${BASH_VERSINFO[1]}" -lt 1 ]); then
echo "[ERROR] menu-functions.sh requires Bash 4.1 or later (detected: ${BASH_VERSION})" >&2
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
exit 1
else
return 1
fi
fi
################################################################################
# MENU DISPLAY FUNCTIONS
################################################################################
# Display menu header with separator line
# Usage: menu_header "Menu Title"
menu_header() {
local title="$1"
echo ""
echo "=========================================="
echo " $title"
echo "=========================================="
echo ""
}
# Display a single menu option with number and description
# Usage: menu_option 1 "First Option" "Optional description"
# Note: Handles long labels and descriptions with wrapping
menu_option() {
local num="$1"
local label="$2"
local desc="$3"
if [ -z "$desc" ]; then
# Just label
printf " %d) %s\n" "$num" "$label"
else
# Label with description on same line
printf " %d) %-30s - %s\n" "$num" "$label" "$desc"
fi
}
# Display option with long/wrapped description
# Usage: menu_option_wrapped 1 "Option" "This is a very long description that might wrap to multiple lines"
menu_option_wrapped() {
local num="$1"
local label="$2"
local desc="$3"
local width="${4:-70}"
printf " %d) %s\n" "$num" "$label"
# Wrap description text
if [ -n "$desc" ]; then
# Indent wrapped text
echo "$desc" | fold -w "$((width - 6))" -s | sed 's/^/ /'
fi
}
# Display divider line
# Usage: menu_divider
menu_divider() {
echo ""
echo "=========================================="
}
# Display back/exit option
# Usage: menu_back "Main Menu" (shows "0) Back to Main Menu")
# Usage: menu_back (shows "0) Back")
menu_back() {
local target="${1:-}"
if [ -z "$target" ]; then
printf " %d) %s\n" 0 "Back"
else
printf " %d) %s %s\n" 0 "Back to" "$target"
fi
}
# Display exit option (for main menu only)
# Usage: menu_exit
menu_exit() {
printf " %d) %s\n" 0 "Exit"
}
################################################################################
# INPUT HANDLING FUNCTIONS
################################################################################
# Read menu choice from user with validation
# Usage: read_menu_choice "option" 0 5
# Returns: choice in $MENU_CHOICE variable
# Validates: input is numeric and in range [min, max]
read_menu_choice() {
local prompt="${1:-Select option}"
local min="${2:-0}"
local max="${3:-}"
# Check if interactive (has terminal)
if [ ! -t 0 ] && [ -z "$INTERACTIVE_MODE" ]; then
print_error "Menu requires interactive terminal (TTY)"
return 1
fi
# If max not specified, only validate it's a number
if [ -z "$max" ]; then
while true; do
printf "%s: " "$prompt"
read -r MENU_CHOICE || return 1
# Check if numeric
if [[ "$MENU_CHOICE" =~ ^[0-9]+$ ]]; then
return 0
fi
echo "Invalid input. Enter a number."
done
else
# Validate range
while true; do
printf "%s (%d-%d): " "$prompt" "$min" "$max"
read -r MENU_CHOICE || return 1
# Check if numeric
if ! [[ "$MENU_CHOICE" =~ ^[0-9]+$ ]]; then
echo "Invalid input. Enter a number between $min and $max."
continue
fi
# Check if in range
if [ "$MENU_CHOICE" -ge "$min" ] && [ "$MENU_CHOICE" -le "$max" ]; then
return 0
fi
echo "Invalid option. Enter a number between $min and $max."
done
fi
}
# Validate menu choice is in valid range
# Usage: validate_menu_choice 5 0 10
# Returns: 0 if valid, 1 if invalid
validate_menu_choice() {
local choice="$1"
local min="${2:-0}"
local max="$3"
# Must be numeric
if ! [[ "$choice" =~ ^[0-9]+$ ]]; then
return 1
fi
# Check range
if [ "$choice" -ge "$min" ] && [ "$choice" -le "$max" ]; then
return 0
fi
return 1
}
################################################################################
# ERROR HANDLING FUNCTIONS
################################################################################
# Display invalid option message
# Usage: menu_invalid_choice
menu_invalid_choice() {
echo "Invalid option. Please try again."
sleep 1
}
# Display error for invalid input
# Usage: menu_input_error "Please enter a number"
menu_input_error() {
local msg="${1:-Invalid input}"
print_error "$msg"
sleep 1
}
################################################################################
# COMPLETE MENU WRAPPER
################################################################################
# All-in-one menu display and input handler
# Usage: show_menu "Menu Title" "0" "Back to Main Menu" "option1" "option2" ...
# Returns choice in $MENU_CHOICE
#
# Example:
# show_menu "Security Menu" "3" "Main Menu" \
# "Bot Analyzer" \
# "Malware Scanner" \
# "IP Reputation Manager"
# case "$MENU_CHOICE" in
# 1) run_bot_analyzer ;;
# 2) run_malware_scanner ;;
# 3) run_ip_reputation ;;
# 0) return ;;
# esac
show_menu() {
local title="$1"
local max_option="$2"
local back_target="$3"
shift 3
local options=("$@")
# Display header
menu_header "$title"
# Display options
for i in "${!options[@]}"; do
menu_option $((i+1)) "${options[$i]}"
done
echo ""
menu_back "$back_target"
menu_divider
# Read input
read_menu_choice "Select option" 0 "$max_option"
}
################################################################################
# INTERACTIVE MODE DETECTION
################################################################################
# Detect if running in interactive mode
# Returns: 0 if interactive, 1 if not
# Sets: INTERACTIVE_MODE variable
detect_interactive_mode() {
if [ -t 0 ]; then
# Has TTY
INTERACTIVE_MODE=1
return 0
fi
# Check bash $- variable for 'i' flag
if [[ "$-" == *i* ]]; then
INTERACTIVE_MODE=1
return 0
fi
# Not interactive
INTERACTIVE_MODE=0
return 1
}
################################################################################
# MENU LOOP HELPERS
################################################################################
# Standard menu loop skeleton
# Shows menu, reads input, validates range, returns choice
# Usage: (in your case statement)
# while true; do
# menu_loop "Menu Title" "0" "Back" option1 option2 option3
# case "$MENU_CHOICE" in
# 1) action1 ;;
# 2) action2 ;;
# 3) action3 ;;
# 0) return ;;
# *) menu_invalid_choice ;;
# esac
# done
menu_loop() {
show_menu "$@"
}
# Validate and display invalid choice error
# Usage: if ! validate_menu_choice "$MENU_CHOICE" 0 3; then menu_invalid_choice; fi
show_invalid_choice() {
menu_invalid_choice
}
################################################################################
# CONFIRMATION DIALOGS
################################################################################
# Yes/No confirmation menu
# Usage: confirm_action "Delete all logs?"
# Returns: 0 for yes, 1 for no
confirm_action() {
local question="$1"
local default="${2:-n}" # y or n
if [ ! -t 0 ]; then
print_error "Confirmation requires interactive terminal"
return 1
fi
echo ""
printf "%s (y/n) [%s]: " "$question" "$default"
# Read with error handling - return error if read fails (EOF/closed stdin)
if ! read -r response; then
echo ""
print_error "Confirmation canceled (EOF)"
return 1
fi
case "$response" in
[yY]) return 0 ;;
[nN]) return 1 ;;
"")
# Use default
[ "$default" = "y" ] && return 0 || return 1
;;
*)
echo "Invalid response. Please enter 'y' or 'n'."
return 1
;;
esac
}
################################################################################
# OPTION DISPLAY HELPERS
################################################################################
# Display option with indentation (for nested options)
# Usage: menu_option_nested 1 "Nested Option"
menu_option_nested() {
local num="$1"
local label="$2"
printf " %d) %s\n" "$num" "$label"
}
# Display section header within menu (not main header)
# Usage: menu_section "Analysis Options"
menu_section() {
local section="$1"
echo ""
echo " $section:"
}
# Display option with status indicator
# Usage: menu_option_status 1 "Feature Name" "enabled"
menu_option_status() {
local num="$1"
local label="$2"
local status="$3"
printf " %d) %-30s [%s]\n" "$num" "$label" "$status"
}
# Display disabled option (grayed out / not selectable)
# Usage: menu_option_disabled 5 "Advanced Options" "(requires admin)"
menu_option_disabled() {
local num="$1"
local label="$2"
local reason="$3"
if [ -z "$reason" ]; then
printf " %d) %s (disabled)\n" "$num" "$label"
else
printf " %d) %s %s\n" "$num" "$label" "$reason"
fi
}
# Automatically number and display multiple options
# Usage: menu_options "Option 1" "Option 2" "Option 3"
menu_options() {
local counter=1
for option in "$@"; do
menu_option "$counter" "$option"
((counter++))
done
}
# Display option with default indicator
# Usage: menu_option_default 1 "Use this" true
menu_option_default() {
local num="$1"
local label="$2"
local is_default="${3:-false}"
if [ "$is_default" = "true" ] || [ "$is_default" = "1" ]; then
printf " %d) %s [DEFAULT]\n" "$num" "$label"
else
printf " %d) %s\n" "$num" "$label"
fi
}
# Display multiple options with default
# Usage: menu_options_with_default "1" "Opt1" "Opt2" "Opt3"
menu_options_with_default() {
local default_num="$1"
shift
local counter=1
for option in "$@"; do
if [ "$counter" = "$default_num" ]; then
menu_option_default "$counter" "$option" true
else
menu_option_default "$counter" "$option" false
fi
((counter++))
done
}
# Display group of related options
# Usage: menu_group "Database Options" 1 "MySQL" "PostgreSQL" "MariaDB"
menu_group() {
local group_name="$1"
shift
local start_num="$1"
shift
local options=("$@")
menu_section "$group_name"
local counter="$start_num"
for option in "${options[@]}"; do
menu_option_nested "$counter" "$option"
((counter++))
done
}
################################################################################
# UTILITY FUNCTIONS
################################################################################
# Clear screen and show banner (common pattern)
# Usage: clear_and_banner
clear_and_banner() {
clear
show_banner
}
# Wait for user before continuing (already in common-functions.sh)
# This ensures consistency if called from here
menu_press_enter() {
press_enter
}
# Display loading indicator
# Usage: menu_loading "Scanning logs"
menu_loading() {
local msg="$1"
printf "%s" "$msg"
for i in {1..3}; do
printf "."
sleep 0.5
done
echo ""
}
################################################################################
# MENU STATE TRACKING
################################################################################
# Push menu onto stack
# Usage: menu_push "Security Menu"
# Maximum depth: 50 levels (prevents accidental memory issues in pathological nesting)
menu_push() {
local menu_name="$1"
local max_depth=50
if [ ${#MENU_STACK[@]} -ge "$max_depth" ]; then
print_error "Menu nesting too deep (max $max_depth levels)"
return 1
fi
MENU_STACK+=("$menu_name")
}
# Pop menu from stack
# Usage: menu_pop
menu_pop() {
if [ ${#MENU_STACK[@]} -gt 0 ]; then
unset 'MENU_STACK[-1]'
fi
}
# Get current menu name
# Usage: current_menu=$(menu_current)
menu_current() {
local stack_len=${#MENU_STACK[@]}
if [ "$stack_len" -gt 0 ]; then
echo "${MENU_STACK[$((stack_len - 1))]}"
else
echo "Main Menu"
fi
}
# Get parent menu name
# Usage: parent_menu=$(menu_parent)
menu_parent() {
local stack_len=${#MENU_STACK[@]}
if [ "$stack_len" -gt 1 ]; then
echo "${MENU_STACK[$((stack_len - 2))]}"
else
echo "Main Menu"
fi
}
################################################################################
# MENU HISTORY & LOGGING
################################################################################
# Log menu selection to history
# Usage: menu_log_selection "Bot Analyzer" 1
menu_log_selection() {
local menu_name="$1"
local choice="$2"
local timestamp=$(date +%s)
MENU_HISTORY+=("$timestamp|$menu_name|$choice")
}
# Get menu history
# Usage: menu_get_history
menu_get_history() {
printf '%s\n' "${MENU_HISTORY[@]}"
}
# Clear menu history
# Usage: menu_clear_history
menu_clear_history() {
MENU_HISTORY=()
}
# Get last menu choice
# Usage: last_choice=$(menu_get_last_choice)
menu_get_last_choice() {
local hist_len=${#MENU_HISTORY[@]}
if [ "$hist_len" -gt 0 ]; then
local last="${MENU_HISTORY[$((hist_len - 1))]}"
echo "${last##*|}"
fi
}
################################################################################
# NUMERIC INPUT VALIDATION
################################################################################
# Robust numeric validation
# Usage: is_numeric 123
is_numeric() {
local input="$1"
[[ "$input" =~ ^[0-9]+$ ]]
}
# Get integer value with bounds checking
# Usage: get_int_choice "$MENU_CHOICE" 0 10
# Returns: 0 if valid, 1 if invalid
get_int_choice() {
local choice="$1"
local min="$2"
local max="$3"
# Must be numeric
if ! is_numeric "$choice"; then
return 1
fi
# Check bounds
if [ "$choice" -ge "$min" ] && [ "$choice" -le "$max" ]; then
return 0
fi
return 1
}
# Sanitize input (remove special characters)
# Usage: sanitized=$(sanitize_input "$user_input")
sanitize_input() {
local input="$1"
# Remove all non-alphanumeric characters
echo "$input" | tr -cd '[:alnum:]'
}
# Validate menu handler function exists
# Usage: if validate_handler "run_bot_analyzer"; then run_bot_analyzer; fi
validate_handler() {
local handler="$1"
declare -f "$handler" > /dev/null
}
################################################################################
# BATCH MODE SUPPORT
################################################################################
# Check if in batch mode (non-interactive execution)
# Usage: if is_batch_mode; then ... fi
is_batch_mode() {
[ "$BATCH_MODE" = "1" ] || [ "$BATCH_MODE" = "true" ]
}
# Set batch mode (skip all menus, use defaults or args)
# Usage: set_batch_mode on/off
set_batch_mode() {
case "$1" in
on|true|1) BATCH_MODE=1 ;;
off|false|0) BATCH_MODE=0 ;;
esac
export BATCH_MODE
}
# Menu shortcut for batch mode
# Returns immediately with choice if in batch mode
# Usage: menu_or_batch "1" "Select option" 0 3
menu_or_batch() {
local default_choice="$1"
shift
if is_batch_mode; then
MENU_CHOICE="$default_choice"
return 0
fi
# Interactive mode
read_menu_choice "$@"
}
################################################################################
# BREADCRUMB / NAVIGATION PATH
################################################################################
# Display menu breadcrumb/path
# Usage: menu_breadcrumb
menu_breadcrumb() {
local breadcrumb="Main"
for item in "${MENU_STACK[@]}"; do
breadcrumb="$breadcrumb > $item"
done
echo "Location: $breadcrumb"
}
# Display menu breadcrumb with option to jump back
# Usage: menu_breadcrumb_interactive
menu_breadcrumb_interactive() {
local breadcrumb="[Main]"
local counter=1
for item in "${MENU_STACK[@]}"; do
breadcrumb="$breadcrumb [$counter] $item"
((counter++))
done
echo "$breadcrumb"
}
################################################################################
# MENU VALIDATION
################################################################################
# Validate menu options array is not empty
# Usage: validate_menu_options "option1" "option2" ...
validate_menu_options() {
if [ $# -eq 0 ]; then
print_error "Menu has no options defined"
return 1
fi
return 0
}
# Check if option number is disabled
# Usage: is_option_disabled 5 "disabled_options"
is_option_disabled() {
local option_num="$1"
local disabled_str="${2:-}"
# disabled_str format: "3,5,7" (comma-separated numbers)
if [[ ",$disabled_str," == *",$option_num,"* ]]; then
return 0
fi
return 1
}
################################################################################
# MENU TIMING / TIMEOUT
################################################################################
# Read menu choice with timeout
# Usage: read_menu_choice_timeout 30 "Select option" 0 5
# Will use default (0) if timeout expires
read_menu_choice_timeout() {
local timeout="$1"
local prompt="$2"
local min="$3"
local max="$4"
local remaining="$timeout"
printf "%s (auto-cancel in %d seconds): " "$prompt" "$timeout"
# Use read with timeout
if read -t "$timeout" -r MENU_CHOICE; then
# Got input before timeout
if validate_menu_choice "$MENU_CHOICE" "$min" "$max"; then
return 0
else
echo ""
echo "Invalid option."
return 1
fi
else
# Timeout expired, use default (0 = back/cancel)
echo ""
echo "No input received. Returning..."
MENU_CHOICE=0
return 0
fi
}
################################################################################
# PAGINATION FOR LONG MENUS
################################################################################
# Display menu items with pagination
# Usage: menu_paginate 5 "Option 1" "Option 2" "Option 3" ... (show 5 per page)
menu_paginate() {
local items_per_page="$1"
shift
local items=("$@")
local total=${#items[@]}
local pages=$((($total + $items_per_page - 1) / $items_per_page))
if [ "$pages" -le 1 ]; then
# Just display all
menu_options "${items[@]}"
return 0
fi
# Multi-page display
local page=1
local start=0
while true; do
local end=$((start + items_per_page))
if [ "$end" -gt "$total" ]; then
end=$total
fi
echo ""
echo "Page $page of $pages:"
for ((i=start; i<end; i++)); do
printf " %d) %s\n" "$((i+1))" "${items[$i]}"
done
if [ "$page" -ge "$pages" ]; then
break
fi
echo ""
printf "Press Enter for next page..."
read -r || break
((page++))
((start += items_per_page))
done
}
# Search/filter menu options
# Usage: menu_search "options array" "search term"
menu_search() {
local search_term="$1"
shift
local options=("$@")
local results=()
# Convert search term to lowercase for case-insensitive search
search_term="${search_term,,}"
for option in "${options[@]}"; do
if [[ "${option,,}" == *"$search_term"* ]]; then
results+=("$option")
fi
done
if [ ${#results[@]} -eq 0 ]; then
echo "No matches found for: $search_term"
return 1
fi
menu_options "${results[@]}"
return 0
}
################################################################################
# MENU STATE & CACHING
################################################################################
# Save menu state to file for recovery
# Usage: menu_save_state "/tmp/menu_state"
# WARNING: State files can be restored with menu_restore_state, which sources the file.
# Only use with trusted file paths.
menu_save_state() {
local state_file="$1"
if [ -z "$state_file" ]; then
print_error "menu_save_state: file path required"
return 1
fi
{
echo "# Menu state saved $(date)"
echo "MENU_STACK=("
printf ' "%s"\n' "${MENU_STACK[@]}"
echo ")"
echo "MENU_HISTORY=("
printf ' "%s"\n' "${MENU_HISTORY[@]}"
echo ")"
echo "MENU_USE_COLORS='$MENU_USE_COLORS'"
echo "BATCH_MODE='$BATCH_MODE'"
} > "$state_file"
}
# Restore menu state from file
# Usage: menu_restore_state "/tmp/menu_state"
# SECURITY WARNING: Only restore state files from TRUSTED sources.
# State files are sourced as bash code - never restore from untrusted paths.
menu_restore_state() {
local state_file="$1"
if [ -z "$state_file" ]; then
print_error "menu_restore_state: file path required"
return 1
fi
if [ ! -f "$state_file" ]; then
print_error "menu_restore_state: file not found: $state_file"
return 1
fi
# Security check: verify file is readable and not a symlink to untrusted location
if [ -L "$state_file" ]; then
print_error "menu_restore_state: refusing to restore state from symlink"
return 1
fi
# Source the state file - ONLY from trusted paths!
# shellcheck disable=SC1090
source "$state_file" || {
print_error "menu_restore_state: failed to restore state from $state_file"
return 1
}
}
################################################################################
# MENU TEMPLATE GENERATOR
################################################################################
# Generate menu template skeleton
# Usage: menu_template "Security Menu" "Option 1" "Option 2" "Option 3"
# WARNING: This is a code generator. Input should be safe strings only.
menu_template() {
local title="$1"
shift
local options=("$@")
# Basic validation - check that title is not empty
if [ -z "$title" ]; then
print_error "menu_template: title required"
return 1
fi
# Validate that we have options
if [ $# -eq 0 ]; then
print_error "menu_template: at least one option required"
return 1
fi
# Escape title for use in bash (replace single quotes)
title="${title//\'/\'\\\'\'}"
cat << 'EOF'
#!/bin/bash
# Source required libraries
source "${SCRIPT_DIR}/lib/menu-functions.sh"
source "${SCRIPT_DIR}/lib/common-functions.sh"
# Main menu loop
show_${title,,}_menu() {
while true; do
show_menu "$title" "$((${#options[@]}))" "Main Menu" \\
EOF
for option in "${options[@]}"; do
echo " \"$option\" \\"
done
cat << 'EOF'
case "$MENU_CHOICE" in
EOF
for i in "${!options[@]}"; do
echo " $((i+1))) run_option_$((i+1)) ;;"
done
cat << 'EOF'
0) return ;;
*) menu_invalid_choice ;;
esac
done
}
# Option handlers
run_option_1() {
echo "Option 1 selected"
menu_press_enter
}
run_option_2() {
echo "Option 2 selected"
menu_press_enter
}
# Main execution
show_${title,,}_menu
EOF
}
################################################################################
# MENU DEPTH & NAVIGATION
################################################################################
# Get menu nesting depth (how deep in menu hierarchy)
# Usage: depth=$(menu_depth)
menu_depth() {
echo "${#MENU_STACK[@]}"
}
# Display menu depth indicator
# Usage: menu_show_depth
menu_show_depth() {
local depth=$(menu_depth)
if [ "$depth" -gt 0 ]; then
printf "[Level %d] " "$((depth + 1))"
fi
}
# Display menu hint/help (optional info about available shortcuts)
# Usage: menu_hint "Press 'h' for help, 'q' to quit"
menu_hint() {
local hint="$1"
echo ""
echo "Hint: $hint"
}
# Display keyboard shortcuts available
# Usage: menu_shortcuts "h) Help" "q) Quit" "r) Refresh"
menu_shortcuts() {
echo ""
echo "Shortcuts:"
for shortcut in "$@"; do
echo " $shortcut"
done
}
################################################################################
# KEYBOARD SHORTCUTS & SPECIAL COMMANDS
################################################################################
# Handle special keyboard shortcuts in menu
# Usage: handle_menu_shortcut "$MENU_CHOICE" "show_help_text"
handle_menu_shortcut() {
local input="$1"
local help_text="${2:-}"
case "$input" in
h|H|help|HELP)
if [ -n "$help_text" ]; then
echo "$help_text"
else
menu_help
fi
menu_press_enter
return 1 # Indicate shortcut was handled, redraw menu
;;
q|Q|quit|QUIT|exit|EXIT)
return 2 # Indicate user wants to exit
;;
*)
return 0 # Not a shortcut, continue normally
;;
esac
}
# Check for keyboard interrupt (Ctrl+C)
# Usage: trap 'menu_interrupt_handler' INT
menu_interrupt_handler() {
echo ""
echo "Interrupted by user."
menu_pop
return 1
}
################################################################################
# ERROR RECOVERY
################################################################################
# Wrap menu handler with error recovery
# Usage: menu_safe_execute "handler_function" "arg1" "arg2"
menu_safe_execute() {
local handler="$1"
shift
if ! validate_handler "$handler"; then
print_error "Handler not found: $handler"
return 1
fi
# Try to execute handler with error recovery
if "$handler" "$@"; then
return 0
else
local exit_code=$?
print_error "Handler failed with code: $exit_code"
return "$exit_code"
fi
}
# Menu execution with timeout and recovery
# Usage: menu_execute_with_timeout 300 "handler_function"
menu_execute_with_timeout() {
local timeout="$1"
local handler="$2"
shift 2
local args=("$@")
if ! timeout "$timeout" "$handler" "${args[@]}" 2>/dev/null; then
local exit_code=$?
if [ "$exit_code" = "124" ]; then
print_error "Operation timed out after ${timeout}s"
else
print_error "Operation failed with code: $exit_code"
fi
return "$exit_code"
fi
}
################################################################################
# HELP/DOCUMENTATION
################################################################################
# Display menu functions help
menu_help() {
cat << 'EOF'
Menu Functions Library - Available Functions
=== DISPLAY FUNCTIONS ===
menu_header "Title" - Display menu header with separator
menu_option 1 "Label" "Desc" - Display numbered option
menu_option_wrapped 1 "L" "Desc" - Display option with text wrapping
menu_option_status 1 "L" "stat" - Option with status [enabled]
menu_option_default 1 "L" true - Option marked as [DEFAULT]
menu_option_disabled 1 "L" - Show disabled option
menu_options "Opt1" "Opt2" - Auto-number multiple options
menu_options_with_default "1" "O1" "O2" - Auto-number with default
menu_divider - Display separator line
menu_back "Main Menu" - Display back option
menu_exit - Display exit option (main menu)
menu_section "Group" - Display section header
menu_group "Name" 1 "O1" "O2" - Display grouped options
menu_option_nested 1 "Label" - Display nested/indented option
=== INPUT FUNCTIONS ===
read_menu_choice "Prompt" 0 5 - Read and validate user input
validate_menu_choice 5 0 10 - Check if choice in range
read_menu_choice_timeout 30 "P" 0 5 - Read with timeout (default 0)
is_numeric 123 - Check if input is numeric
get_int_choice "$CHOICE" 0 10 - Validate integer with bounds
=== ERROR FUNCTIONS ===
menu_invalid_choice - Show "invalid option" message
menu_input_error "msg" - Show error with context
=== COMPLETE MENU ===
show_menu "Title" "3" "Back" opt1 opt2 opt3 - All-in-one menu
menu_loop "Title" "3" "Back" opt1 opt2 opt3 - Menu with loop
=== CONFIRMATION ===
confirm_action "Delete?" - Yes/No dialog, returns 0/1
=== HIERARCHY TRACKING ===
menu_push "Menu Name" - Push menu onto stack
menu_pop - Pop menu from stack
menu_current - Get current menu name
menu_parent - Get parent menu name
menu_breadcrumb - Show "Main > Menu1 > Menu2"
menu_breadcrumb_interactive - Show breadcrumb with numbers
menu_depth - Get nesting depth
menu_show_depth - Display "[Level N]"
=== BATCH MODE ===
set_batch_mode on/off - Enable/disable batch mode
is_batch_mode - Check if batch mode active
menu_or_batch "1" "Prompt" 0 5 - Menu or use batch default
=== MENU HISTORY ===
menu_log_selection "Menu" 1 - Log user's choice
menu_get_history - Get all logged selections
menu_get_last_choice - Get last choice number
menu_clear_history - Clear history
=== HINTS & HELP ===
menu_hint "text" - Display hint message
menu_shortcuts "h) Help" "q) Quit" - Display shortcuts
validate_menu_options "O1" "O2" - Check menu has options
is_option_disabled 5 "3,5,7" - Check if option disabled
=== UTILITIES ===
detect_interactive_mode - Check if TTY available
menu_press_enter - Pause for user
menu_loading "Scanning..." - Show loading indicator
=== BASIC EXAMPLE ===
source lib/menu-functions.sh
while true; do
show_menu "Security Menu" "3" "Main Menu" \
"Bot Analyzer" \
"Malware Scanner" \
"IP Reputation Manager"
case "$MENU_CHOICE" in
1) run_bot_analyzer ;;
2) run_malware_scanner ;;
3) run_ip_reputation ;;
0) return ;;
*) menu_invalid_choice ;;
esac
done
=== ADVANCED EXAMPLE ===
source lib/menu-functions.sh
menu_push "Security Menu"
while true; do
menu_show_depth
menu_header "Threat Analysis"
menu_option 1 "Bot Analyzer"
menu_option_status 2 "Malware Scanner" "enabled"
menu_option_disabled 3 "Advanced" "(admin only)"
echo ""
menu_back "$(menu_parent)"
menu_divider
menu_hint "Type number and press Enter"
read_menu_choice "Select" 0 3
case "$MENU_CHOICE" in
1) menu_log_selection "Threat" 1; run_bot_analyzer ;;
2) menu_log_selection "Threat" 2; run_malware_scanner ;;
0) menu_pop; return ;;
*) menu_invalid_choice ;;
esac
done
EOF
}
################################################################################
# COLOR SUPPORT (OPTIONAL - DISABLED BY DEFAULT FOR COMPATIBILITY)
################################################################################
# Enable colors if terminal supports it (optional)
# Usage: enable_menu_colors
enable_menu_colors() {
MENU_USE_COLORS=1
export MENU_USE_COLORS
}
# Disable colors (default)
# Usage: disable_menu_colors
disable_menu_colors() {
MENU_USE_COLORS=0
export MENU_USE_COLORS
}
# Conditional color output (if enabled)
# Usage: menu_color_text "31" "ERROR" (31 = red)
menu_color_text() {
local color_code="$1"
local text="$2"
if [ "${MENU_USE_COLORS:-0}" = "1" ]; then
printf "\033[%sm%s\033[0m\n" "$color_code" "$text"
else
echo "$text"
fi
}
################################################################################
# ERROR MESSAGE FALLBACKS
################################################################################
# Fallback if common-functions.sh not sourced
if ! declare -F print_error > /dev/null; then
print_error() {
echo "[ERROR] $1" >&2
}
fi
if ! declare -F print_warning > /dev/null; then
print_warning() {
echo "[WARNING] $1"
}
fi
if ! declare -F show_banner > /dev/null; then
show_banner() {
echo ""
echo "=================================================="
}
fi
if ! declare -F press_enter > /dev/null; then
press_enter() {
echo ""
printf "Press Enter to continue..."
read -r || true
}
fi
################################################################################
# INITIALIZATION
################################################################################
# Declare global menu state variables
declare -g MENU_STACK=()
declare -g MENU_HISTORY=()
declare -g INTERACTIVE_MODE=0
# Detect interactive mode on library load (ignore return code)
# This sets INTERACTIVE_MODE=1 if interactive, 0 if non-interactive
detect_interactive_mode || true
# Export all functions for sourcing
export -f menu_header menu_option menu_option_wrapped menu_divider menu_back menu_exit
export -f read_menu_choice validate_menu_choice read_menu_choice_timeout
export -f menu_invalid_choice menu_input_error
export -f show_menu detect_interactive_mode
export -f confirm_action menu_option_nested menu_section menu_option_status
export -f menu_option_disabled menu_option_default menu_options menu_options_with_default menu_group
export -f clear_and_banner menu_press_enter menu_loading
export -f menu_push menu_pop menu_current menu_parent menu_breadcrumb menu_breadcrumb_interactive
export -f menu_help menu_loop show_invalid_choice
export -f print_error print_warning show_banner press_enter
export -f is_batch_mode set_batch_mode menu_or_batch
export -f validate_menu_options is_option_disabled
export -f menu_log_selection menu_get_history menu_clear_history menu_get_last_choice
export -f is_numeric get_int_choice
export -f menu_depth menu_show_depth menu_hint menu_shortcuts
export -f enable_menu_colors disable_menu_colors menu_color_text
export -f menu_save_state menu_restore_state menu_template
export -f sanitize_input validate_handler
export -f menu_paginate menu_search
export -f handle_menu_shortcut menu_interrupt_handler
export -f menu_safe_execute menu_execute_with_timeout