1626b53de3
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
1267 lines
35 KiB
Bash
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
|
|
|