diff --git a/lib/menu-functions-example.sh b/lib/menu-functions-example.sh new file mode 100755 index 0000000..4e03158 --- /dev/null +++ b/lib/menu-functions-example.sh @@ -0,0 +1,299 @@ +#!/bin/bash + +################################################################################ +# MENU FUNCTIONS LIBRARY - EXAMPLE SCRIPT +################################################################################ +# This script demonstrates how to use lib/menu-functions.sh +# Usage: bash lib/menu-functions-example.sh +################################################################################ + +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source required libraries +source "$SCRIPT_DIR/menu-functions.sh" +source "$SCRIPT_DIR/common-functions.sh" + +################################################################################ +# EXAMPLE 1: SIMPLE MENU WITH 3 OPTIONS +################################################################################ + +show_simple_menu() { + while true; do + show_menu "Simple Menu" "3" "Main Menu" \ + "Option 1" \ + "Option 2" \ + "Option 3" + + case "$MENU_CHOICE" in + 1) echo "You selected Option 1"; sleep 1 ;; + 2) echo "You selected Option 2"; sleep 1 ;; + 3) echo "You selected Option 3"; sleep 1 ;; + 0) return ;; + *) menu_invalid_choice ;; + esac + done +} + +################################################################################ +# EXAMPLE 2: MENU WITH STATUS INDICATORS +################################################################################ + +show_status_menu() { + while true; do + menu_header "Server Status" + menu_option_status 1 "Web Server" "running" + menu_option_status 2 "Database" "enabled" + menu_option_disabled 3 "Backup Manager" "(admin only)" + echo "" + menu_back "Main Menu" + menu_divider + + read_menu_choice "Select option" 0 3 + + case "$MENU_CHOICE" in + 1) echo "Web Server is running"; sleep 1 ;; + 2) echo "Database is enabled"; sleep 1 ;; + 3) echo "Backup Manager requires admin access"; sleep 1 ;; + 0) return ;; + *) menu_invalid_choice ;; + esac + done +} + +################################################################################ +# EXAMPLE 3: HIERARCHICAL MENUS WITH BREADCRUMBS +################################################################################ + +show_security_menu() { + menu_push "Security Menu" + + while true; do + menu_header "Security Menu" + menu_show_depth + menu_option 1 "Threat Analysis" + menu_option 2 "Firewall Rules" + menu_option 3 "User Permissions" + echo "" + menu_back "$(menu_parent)" + menu_divider + menu_breadcrumb + + read_menu_choice "Select option" 0 3 + + case "$MENU_CHOICE" in + 1) show_threat_menu ;; + 2) echo "Firewall Rules selected"; sleep 1 ;; + 3) echo "User Permissions selected"; sleep 1 ;; + 0) menu_pop; return ;; + *) menu_invalid_choice ;; + esac + done +} + +show_threat_menu() { + menu_push "Threat Analysis" + + while true; do + menu_header "Threat Analysis" + menu_show_depth + menu_option 1 "Bot Analyzer" + menu_option 2 "Malware Scanner" + echo "" + menu_back "$(menu_parent)" + menu_divider + menu_breadcrumb + + read_menu_choice "Select option" 0 2 + + case "$MENU_CHOICE" in + 1) echo "Running Bot Analysis..."; sleep 2 ;; + 2) echo "Running Malware Scan..."; sleep 2 ;; + 0) menu_pop; return ;; + *) menu_invalid_choice ;; + esac + + menu_log_selection "Threat Analysis" "$MENU_CHOICE" + done +} + +################################################################################ +# EXAMPLE 4: MENU WITH PAGINATION +################################################################################ + +show_pagination_menu() { + menu_header "Long Options Menu (Paginated)" + + local options=( + "Database Options" + "Backup Management" + "Security Hardening" + "Performance Tuning" + "User Management" + "Log Analysis" + "Network Configuration" + "Monitoring Tools" + "System Update" + "Documentation" + ) + + menu_paginate 5 "${options[@]}" +} + +################################################################################ +# EXAMPLE 5: MENU WITH SEARCH CAPABILITY +################################################################################ + +show_search_menu() { + menu_header "Search in Menu Options" + + echo "Available options:" + local options=( + "Bot Analyzer" + "Bot Blocker" + "Malware Scanner" + "WordPress Manager" + "WordPress Cron Manager" + "IP Reputation Manager" + "Performance Analyzer" + ) + + printf " %s\n" "${options[@]}" + + echo "" + printf "Search for (e.g., 'wordpress', 'bot'): " + read -r search_term + + if [ -z "$search_term" ]; then + return + fi + + echo "" + menu_search "$search_term" "${options[@]}" || echo "No results found" +} + +################################################################################ +# EXAMPLE 6: MENU WITH CONFIRMATION +################################################################################ + +show_confirmation_menu() { + menu_header "Dangerous Operations" + + menu_option 1 "Delete all logs" + menu_option 2 "Reset configuration" + menu_option 3 "Purge cache" + echo "" + menu_back "Main Menu" + menu_divider + + read_menu_choice "Select option" 0 3 + + case "$MENU_CHOICE" in + 1) + if confirm_action "Really delete all logs?"; then + echo "Deleting logs..." + sleep 1 + else + echo "Operation cancelled" + fi + ;; + 2) + if confirm_action "Really reset configuration? This cannot be undone"; then + echo "Resetting configuration..." + sleep 1 + else + echo "Operation cancelled" + fi + ;; + 3) + if confirm_action "Really purge cache?"; then + echo "Purging cache..." + sleep 1 + else + echo "Operation cancelled" + fi + ;; + 0) return ;; + *) menu_invalid_choice ;; + esac +} + +################################################################################ +# EXAMPLE 7: MENU WITH BATCH MODE +################################################################################ + +show_batch_menu() { + menu_header "Batch Mode Example" + + echo "Current mode: $(is_batch_mode && echo "BATCH" || echo "INTERACTIVE")" + echo "" + menu_option 1 "Enable batch mode" + menu_option 2 "Disable batch mode" + menu_option 3 "Run task (auto-default in batch)" + echo "" + menu_back "Main Menu" + menu_divider + + read_menu_choice "Select option" 0 3 + + case "$MENU_CHOICE" in + 1) set_batch_mode on; echo "Batch mode enabled" ;; + 2) set_batch_mode off; echo "Batch mode disabled" ;; + 3) + # This will return "1" immediately in batch mode + menu_or_batch "1" "Execute task" 0 3 + echo "Task executed with choice: $MENU_CHOICE" + ;; + 0) return ;; + *) menu_invalid_choice ;; + esac + + sleep 1 +} + +################################################################################ +# MAIN MENU +################################################################################ + +show_main_menu() { + while true; do + menu_header "Menu Functions Library - Examples" + + menu_option 1 "Simple Menu (3 options)" + menu_option 2 "Menu with Status Indicators" + menu_option 3 "Hierarchical Menus (nested)" + menu_option 4 "Menu Pagination" + menu_option 5 "Menu Search/Filter" + menu_option 6 "Confirmation Dialogs" + menu_option 7 "Batch Mode" + menu_option 8 "View Menu Help" + echo "" + menu_exit + menu_divider + + read_menu_choice "Select example" 0 8 + + case "$MENU_CHOICE" in + 1) show_simple_menu ;; + 2) show_status_menu ;; + 3) show_security_menu ;; + 4) show_pagination_menu ;; + 5) show_search_menu ;; + 6) show_confirmation_menu ;; + 7) show_batch_menu ;; + 8) menu_help ;; + 0) echo "Exiting..."; return ;; + *) menu_invalid_choice ;; + esac + done +} + +################################################################################ +# EXECUTION +################################################################################ + +clear +show_banner +show_main_menu +press_enter diff --git a/lib/menu-functions.sh b/lib/menu-functions.sh new file mode 100644 index 0000000..dc0558c --- /dev/null +++ b/lib/menu-functions.sh @@ -0,0 +1,1262 @@ +#!/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 + return 1 2>/dev/null || exit 1 +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 "$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 +