Interactive Prompts

This commit is contained in:
CanbiZ (MickLesk) 2026-01-27 13:26:31 +01:00
parent 3b9ad58ce3
commit 310d0e54a6
5 changed files with 773 additions and 73 deletions

View File

@ -634,4 +634,224 @@ silent()
- `SILENT_LOGFILE`: Silent execution log file path
- `SPINNER_PID`: Spinner process ID
- `SPINNER_MSG`: Spinner message text
- `MSG_INFO_SHOWN`: Tracks shown info messages
- `MSG_INFO_SHOWN`: Tracks shown info messages- `PHS_SILENT`: Unattended mode flag (1 = silent)
- `var_unattended`: Unattended mode variable (yes/no)
- `UNATTENDED`: Alternative unattended mode variable
## Unattended/Interactive Prompt Functions
These functions provide a unified way to handle user prompts in both interactive and unattended modes. They automatically detect the execution context and either prompt the user (with timeout) or use default values silently.
### `is_unattended()`
**Purpose**: Detect if script is running in unattended/silent mode
**Parameters**: None
**Returns**:
- `0` (true): Running unattended
- `1` (false): Running interactively
**Side Effects**: None
**Dependencies**: None
**Environment Variables Used**: `PHS_SILENT`, `var_unattended`, `UNATTENDED`
**Usage Example**:
```bash
if is_unattended; then
echo "Running in unattended mode"
else
echo "Running interactively"
fi
```
### `prompt_confirm()`
**Purpose**: Prompt user for yes/no confirmation with timeout and unattended support
**Parameters**:
- `$1` - Prompt message (required)
- `$2` - Default value: "y" or "n" (optional, default: "n")
- `$3` - Timeout in seconds (optional, default: 60)
**Returns**:
- `0`: User confirmed (yes)
- `1`: User declined (no) or timeout with default "n"
**Side Effects**: Displays prompt to terminal
**Dependencies**: `is_unattended()`
**Environment Variables Used**: Color variables (`YW`, `CL`)
**Behavior**:
- **Unattended mode**: Immediately returns default value
- **Non-TTY**: Immediately returns default value
- **Interactive**: Displays prompt with timeout countdown
- **Timeout**: Auto-applies default value after specified seconds
**Usage Examples**:
```bash
# Basic confirmation (default: no)
if prompt_confirm "Proceed with installation?"; then
install_package
fi
# Default to yes, 30 second timeout
if prompt_confirm "Continue?" "y" 30; then
continue_operation
fi
# In unattended mode (will use default immediately)
var_unattended=yes
prompt_confirm "Delete files?" "n" && echo "Deleting" || echo "Skipped"
```
### `prompt_input()`
**Purpose**: Prompt user for text input with timeout and unattended support
**Parameters**:
- `$1` - Prompt message (required)
- `$2` - Default value (optional, default: "")
- `$3` - Timeout in seconds (optional, default: 60)
**Output**: Prints the user input or default value to stdout
**Returns**: `0` always
**Side Effects**: Displays prompt to stderr (keeps stdout clean for value)
**Dependencies**: `is_unattended()`
**Environment Variables Used**: Color variables (`YW`, `CL`)
**Behavior**:
- **Unattended mode**: Returns default value immediately
- **Non-TTY**: Returns default value immediately
- **Interactive**: Waits for user input with timeout
- **Empty input**: Returns default value
- **Timeout**: Returns default value
**Usage Examples**:
```bash
# Get username with default
username=$(prompt_input "Enter username:" "admin" 30)
echo "Using username: $username"
# With validation loop
while true; do
port=$(prompt_input "Enter port:" "8080" 30)
[[ "$port" =~ ^[0-9]+$ ]] && break
echo "Invalid port number"
done
# Capture value in unattended mode
var_unattended=yes
db_name=$(prompt_input "Database name:" "myapp_db")
```
### `prompt_select()`
**Purpose**: Prompt user to select from a list of options with timeout support
**Parameters**:
- `$1` - Prompt message (required)
- `$2` - Default option number, 1-based (optional, default: 1)
- `$3` - Timeout in seconds (optional, default: 60)
- `$4+` - Options to display (required, at least 1)
**Output**: Prints the selected option value to stdout
**Returns**:
- `0`: Success
- `1`: No options provided
**Side Effects**: Displays numbered menu to stderr
**Dependencies**: `is_unattended()`
**Environment Variables Used**: Color variables (`YW`, `GN`, `CL`)
**Behavior**:
- **Unattended mode**: Returns default selection immediately
- **Non-TTY**: Returns default selection immediately
- **Interactive**: Displays numbered menu with timeout
- **Invalid selection**: Uses default
- **Timeout**: Auto-selects default
**Usage Examples**:
```bash
# Simple selection
choice=$(prompt_select "Select database:" 1 30 "PostgreSQL" "MySQL" "SQLite")
echo "Selected: $choice"
# Using array
options=("Production" "Staging" "Development")
env=$(prompt_select "Select environment:" 2 60 "${options[@]}")
# In automation scripts
var_unattended=yes
db=$(prompt_select "Database:" 1 30 "postgres" "mysql" "sqlite")
# Returns "postgres" immediately without menu
```
### `prompt_password()`
**Purpose**: Prompt user for password input with hidden characters and auto-generation
**Parameters**:
- `$1` - Prompt message (required)
- `$2` - Default value or "generate" for auto-generation (optional)
- `$3` - Timeout in seconds (optional, default: 60)
- `$4` - Minimum length for validation (optional, default: 0)
**Output**: Prints the password to stdout
**Returns**: `0` always
**Side Effects**: Displays prompt to stderr with hidden input
**Dependencies**: `is_unattended()`, `openssl` (for generation)
**Environment Variables Used**: Color variables (`YW`, `CL`)
**Behavior**:
- **"generate" default**: Creates random 16-character password
- **Unattended mode**: Returns default/generated password immediately
- **Non-TTY**: Returns default/generated password immediately
- **Interactive**: Hidden input with timeout
- **Min length validation**: Falls back to default if too short
- **Timeout**: Returns default/generated password
**Usage Examples**:
```bash
# Auto-generate password if user doesn't provide one
password=$(prompt_password "Enter password:" "generate" 30)
echo "Password has been set"
# Require minimum length
db_pass=$(prompt_password "Database password:" "" 60 12)
# With default password
admin_pass=$(prompt_password "Admin password:" "changeme123" 30)
# In unattended mode with auto-generation
var_unattended=yes
password=$(prompt_password "Password:" "generate")
# Returns randomly generated password
```
## Prompt Function Decision Flow
```
prompt_confirm() / prompt_input() / prompt_select() / prompt_password()
├── is_unattended()? ─────────────────────┐
│ └── PHS_SILENT=1? │
│ └── var_unattended=yes? ├── YES → Return default immediately
│ └── UNATTENDED=yes? │
│ │
├── TTY available? ─────────────── NO ────┘
└── Interactive Mode
├── Display prompt with timeout hint
├── read -t $timeout
│ ├── User input received → Validate and return
│ ├── Empty input → Return default
│ └── Timeout → Return default with message
└── Return value
```
## Migration Guide: Converting read Commands
To make existing scripts unattended-compatible, replace `read` commands with the appropriate prompt function:
### Before (blocking):
```bash
read -rp "Continue? [y/N]: " confirm
[[ "$confirm" =~ ^[Yy]$ ]] && do_something
read -p "Enter port: " port
port="${port:-8080}"
read -p "Select (1-3): " choice
```
### After (unattended-safe):
```bash
prompt_confirm "Continue?" "n" && do_something
port=$(prompt_input "Enter port:" "8080")
choice=$(prompt_select "Select option:" 1 60 "Option 1" "Option 2" "Option 3")
```

View File

@ -12,19 +12,19 @@ setting_up_container
network_check
update_os
if [[ -z "$var_forgejo_instance" ]]; then
read -rp "Forgejo Instance URL (e.g. https://code.forgejo.org): " var_forgejo_instance
fi
# Get required configuration with sensible fallbacks for unattended mode
# These will show a warning if defaults are used
var_forgejo_instance=$(prompt_input_required \
"Forgejo Instance URL:" \
"${var_forgejo_instance:-https://codeberg.org}" \
120 \
"var_forgejo_instance")
if [[ -z "$var_forgejo_runner_token" ]]; then
read -rp "Forgejo Runner Registration Token: " var_forgejo_runner_token
echo
fi
if [[ -z "$var_forgejo_instance" || -z "$var_forgejo_runner_token" ]]; then
echo "❌ Forgejo instance URL and runner token are required."
exit 1
fi
var_forgejo_runner_token=$(prompt_input_required \
"Forgejo Runner Registration Token:" \
"${var_forgejo_runner_token:-REPLACE_WITH_YOUR_TOKEN}" \
120 \
"var_forgejo_runner_token")
export FORGEJO_INSTANCE="$var_forgejo_instance"
export FORGEJO_RUNNER_TOKEN="$var_forgejo_runner_token"
@ -78,6 +78,9 @@ EOF
systemctl enable -q --now forgejo-runner
msg_ok "Created Services"
# Show warning if any required values used fallbacks
show_missing_values_warning
motd_ssh
customize
cleanup_lxc

View File

@ -110,8 +110,7 @@ msg_ok "Installed garmin-grafana"
msg_info "Setting up garmin-grafana"
# Check if using Chinese garmin servers
read -rp "Are you using Garmin in mainland China? (y/N): " prompt
if [[ "${prompt,,}" =~ ^(y|yes|Y)$ ]]; then
if prompt_confirm "Are you using Garmin in mainland China?" "n" 60; then
GARMIN_CN="True"
else
GARMIN_CN="False"
@ -131,9 +130,9 @@ EOF
# garmin-grafana usually prompts the user for email and password (and MFA) on first run,
# then stores a refreshable token. We try to avoid storing user credentials in the env vars
if [ -z "$(ls -A /opt/garmin-grafana/.garminconnect)" ]; then
read -r -p "Please enter your Garmin Connect Email: " GARMIN_EMAIL
read -r -p "Please enter your Garmin Connect Password (this is used to generate a token and NOT stored): " GARMIN_PASSWORD
read -r -p "Please enter your MFA Code (if applicable, leave blank if not): " GARMIN_MFA
GARMIN_EMAIL=$(prompt_input "Please enter your Garmin Connect Email:" "" 120)
GARMIN_PASSWORD=$(prompt_password "Please enter your Garmin Connect Password (used to generate token, NOT stored):" "" 120)
GARMIN_MFA=$(prompt_input "Please enter your MFA Code (leave blank if not applicable):" "" 60)
# Run the script once to prompt for credential
msg_info "Creating Garmin credentials, this will timeout in 60 seconds"
timeout 60s uv run --env-file /opt/garmin-grafana/.env --project /opt/garmin-grafana/ /opt/garmin-grafana/src/garmin_grafana/garmin_fetch.py <<EOF

View File

@ -3685,12 +3685,8 @@ EOF
selected_gpu="${available_gpus[0]}"
msg_ok "Automatically configuring ${selected_gpu} GPU passthrough"
else
# Multiple GPUs - ask user
echo -e "\n${INFO} Multiple GPU types detected:"
for gpu in "${available_gpus[@]}"; do
echo " - $gpu"
done
read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu
# Multiple GPUs - ask user (use first as default in unattended mode)
selected_gpu=$(prompt_select "Which GPU type to passthrough?" 1 60 "${available_gpus[@]}")
selected_gpu="${selected_gpu^^}"
# Validate selection
@ -4113,29 +4109,16 @@ destroy_lxc() {
# Abort on Ctrl-C / Ctrl-D / ESC
trap 'echo; msg_error "Aborted by user (SIGINT/SIGQUIT)"; return 130' INT QUIT
local prompt
if ! read -rp "Remove this Container? <y/N> " prompt; then
# read returns non-zero on Ctrl-D/ESC
msg_error "Aborted input (Ctrl-D/ESC)"
return 130
fi
case "${prompt,,}" in
y | yes)
if prompt_confirm "Remove this Container?" "n" 60; then
if pct stop "$CT_ID" &>/dev/null && pct destroy "$CT_ID" &>/dev/null; then
msg_ok "Removed Container $CT_ID"
else
msg_error "Failed to remove Container $CT_ID"
return 1
fi
;;
"" | n | no)
else
msg_custom "" "${BL}" "Container was not removed."
;;
*)
msg_warn "Invalid response. Container was not removed."
;;
esac
fi
}
# ------------------------------------------------------------------------------
@ -4452,9 +4435,7 @@ create_lxc_container() {
echo " pve-container: installed=${_pvec_i:-n/a} candidate=${_pvec_c:-n/a}"
echo " lxc-pve : installed=${_lxcp_i:-n/a} candidate=${_lxcp_c:-n/a}"
echo
read -rp "Do you want to upgrade now? [y/N] " _ans
case "${_ans,,}" in
y | yes)
if prompt_confirm "Do you want to upgrade now?" "n" 60; then
msg_info "Upgrading Proxmox LXC stack (pve-container, lxc-pve)"
if $STD apt-get update && $STD apt-get install -y --only-upgrade pve-container lxc-pve; then
msg_ok "LXC stack upgraded."
@ -4473,9 +4454,9 @@ create_lxc_container() {
msg_error "Upgrade failed. Please check APT output."
return 3
fi
;;
*) return 2 ;;
esac
else
return 2
fi
}
# ------------------------------------------------------------------------------
@ -4650,16 +4631,12 @@ create_lxc_container() {
)
if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then
echo ""
echo "${BL}Available ${PCT_OSTYPE} versions:${CL}"
for i in "${!AVAILABLE_VERSIONS[@]}"; do
echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}"
done
echo ""
read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or press Enter to cancel: " choice
# Use prompt_select for version selection (supports unattended mode)
local selected_version
selected_version=$(prompt_select "Select ${PCT_OSTYPE} version:" 1 60 "${AVAILABLE_VERSIONS[@]}")
if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then
PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}"
if [[ -n "$selected_version" ]]; then
PCT_OSVERSION="$selected_version"
TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}"
ONLINE_TEMPLATES=()
@ -4713,16 +4690,12 @@ create_lxc_container() {
)
if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then
echo -e "\n${BL}Available versions:${CL}"
for i in "${!AVAILABLE_VERSIONS[@]}"; do
echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}"
done
# Use prompt_select for version selection (supports unattended mode)
local selected_version
selected_version=$(prompt_select "Select ${PCT_OSTYPE} version:" 1 60 "${AVAILABLE_VERSIONS[@]}")
echo ""
read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or Enter to exit: " choice
if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then
export var_version="${AVAILABLE_VERSIONS[$((choice - 1))]}"
if [[ -n "$selected_version" ]]; then
export var_version="$selected_version"
export PCT_OSVERSION="$var_version"
msg_ok "Switched to ${PCT_OSTYPE} ${var_version}"
@ -4767,10 +4740,6 @@ create_lxc_container() {
msg_error "Template still not found after version change"
exit 220
}
else
msg_custom "🚫" "${YW}" "Installation cancelled"
exit 1
fi
else
msg_error "No ${PCT_OSTYPE} templates available"
exit 220

View File

@ -810,6 +810,517 @@ is_verbose_mode() {
[[ "$verbose" != "no" || ! -t 2 ]]
}
# ------------------------------------------------------------------------------
# is_unattended()
#
# - Detects if script is running in unattended/silent mode
# - Checks PHS_SILENT, var_unattended, and UNATTENDED variables
# - Returns 0 (true) if unattended, 1 (false) otherwise
# - Used by prompt functions to auto-apply defaults
# ------------------------------------------------------------------------------
is_unattended() {
[[ "${PHS_SILENT:-0}" == "1" ]] && return 0
[[ "${var_unattended:-}" =~ ^(yes|true|1)$ ]] && return 0
[[ "${UNATTENDED:-}" =~ ^(yes|true|1)$ ]] && return 0
return 1
}
# ------------------------------------------------------------------------------
# show_missing_values_warning()
#
# - Displays a summary of required values that used fallback defaults
# - Should be called at the end of install scripts
# - Only shows warning if MISSING_REQUIRED_VALUES array has entries
# - Provides clear guidance on what needs manual configuration
#
# Global:
# MISSING_REQUIRED_VALUES - Array of variable names that need configuration
#
# Example:
# # At end of install script:
# show_missing_values_warning
# ------------------------------------------------------------------------------
show_missing_values_warning() {
if [[ ${#MISSING_REQUIRED_VALUES[@]} -gt 0 ]]; then
echo ""
echo -e "${YW}╔════════════════════════════════════════════════════════════╗${CL}"
echo -e "${YW}║ ⚠️ MANUAL CONFIGURATION REQUIRED ║${CL}"
echo -e "${YW}╠════════════════════════════════════════════════════════════╣${CL}"
echo -e "${YW}║ The following values were not provided and need to be ║${CL}"
echo -e "${YW}║ configured manually for the service to work properly: ║${CL}"
echo -e "${YW}╟────────────────────────────────────────────────────────────╢${CL}"
for val in "${MISSING_REQUIRED_VALUES[@]}"; do
printf "${YW}${CL} • %-56s ${YW}${CL}\n" "$val"
done
echo -e "${YW}╟────────────────────────────────────────────────────────────╢${CL}"
echo -e "${YW}║ Check the service configuration files or environment ║${CL}"
echo -e "${YW}║ variables and update the placeholder values. ║${CL}"
echo -e "${YW}╚════════════════════════════════════════════════════════════╝${CL}"
echo ""
return 1
fi
return 0
}
# ------------------------------------------------------------------------------
# prompt_confirm()
#
# - Prompts user for yes/no confirmation with timeout and unattended support
# - In unattended mode: immediately returns default value
# - In interactive mode: waits for user input with configurable timeout
# - After timeout: auto-applies default value
#
# Arguments:
# $1 - Prompt message (required)
# $2 - Default value: "y" or "n" (optional, default: "n")
# $3 - Timeout in seconds (optional, default: 60)
#
# Returns:
# 0 - User confirmed (yes)
# 1 - User declined (no) or timeout with default "n"
#
# Example:
# if prompt_confirm "Proceed with installation?" "y" 30; then
# echo "Installing..."
# fi
#
# # Unattended: prompt_confirm will use default without waiting
# var_unattended=yes
# prompt_confirm "Delete files?" "n" && echo "Deleting" || echo "Skipped"
# ------------------------------------------------------------------------------
prompt_confirm() {
local message="${1:-Confirm?}"
local default="${2:-n}"
local timeout="${3:-60}"
local response
# Normalize default to lowercase
default="${default,,}"
[[ "$default" != "y" ]] && default="n"
# Build prompt hint
local hint
if [[ "$default" == "y" ]]; then
hint="[Y/n]"
else
hint="[y/N]"
fi
# Unattended mode: apply default immediately
if is_unattended; then
if [[ "$default" == "y" ]]; then
return 0
else
return 1
fi
fi
# Check if running in a TTY
if [[ ! -t 0 ]]; then
# Not a TTY, use default
if [[ "$default" == "y" ]]; then
return 0
else
return 1
fi
fi
# Interactive prompt with timeout
echo -en "${YW}${message} ${hint} (auto-${default} in ${timeout}s): ${CL}"
if read -t "$timeout" -r response; then
# User provided input
response="${response,,}" # lowercase
case "$response" in
y|yes)
return 0
;;
n|no)
return 1
;;
"")
# Empty response, use default
if [[ "$default" == "y" ]]; then
return 0
else
return 1
fi
;;
*)
# Invalid input, use default
echo -e "${YW}Invalid response, using default: ${default}${CL}"
if [[ "$default" == "y" ]]; then
return 0
else
return 1
fi
;;
esac
else
# Timeout occurred
echo "" # Newline after timeout
echo -e "${YW}Timeout - auto-selecting: ${default}${CL}"
if [[ "$default" == "y" ]]; then
return 0
else
return 1
fi
fi
}
# ------------------------------------------------------------------------------
# prompt_input()
#
# - Prompts user for text input with timeout and unattended support
# - In unattended mode: immediately returns default value
# - In interactive mode: waits for user input with configurable timeout
# - After timeout: auto-applies default value
#
# Arguments:
# $1 - Prompt message (required)
# $2 - Default value (optional, default: "")
# $3 - Timeout in seconds (optional, default: 60)
#
# Output:
# Prints the user input or default value to stdout
#
# Example:
# username=$(prompt_input "Enter username:" "admin" 30)
# echo "Using username: $username"
#
# # With validation
# while true; do
# port=$(prompt_input "Enter port:" "8080" 30)
# [[ "$port" =~ ^[0-9]+$ ]] && break
# echo "Invalid port number"
# done
# ------------------------------------------------------------------------------
prompt_input() {
local message="${1:-Enter value:}"
local default="${2:-}"
local timeout="${3:-60}"
local response
# Build display default hint
local hint=""
[[ -n "$default" ]] && hint=" (default: ${default})"
# Unattended mode: return default immediately
if is_unattended; then
echo "$default"
return 0
fi
# Check if running in a TTY
if [[ ! -t 0 ]]; then
# Not a TTY, use default
echo "$default"
return 0
fi
# Interactive prompt with timeout
echo -en "${YW}${message}${hint} (auto-default in ${timeout}s): ${CL}" >&2
if read -t "$timeout" -r response; then
# User provided input (or pressed Enter for empty)
if [[ -n "$response" ]]; then
echo "$response"
else
echo "$default"
fi
else
# Timeout occurred
echo "" >&2 # Newline after timeout
echo -e "${YW}Timeout - using default: ${default}${CL}" >&2
echo "$default"
fi
}
# ------------------------------------------------------------------------------
# prompt_input_required()
#
# - Prompts user for REQUIRED text input with fallback support
# - In unattended mode: Uses fallback value if no env var set (with warning)
# - In interactive mode: loops until user provides non-empty input
# - Tracks missing required values for end-of-script summary
#
# Arguments:
# $1 - Prompt message (required)
# $2 - Fallback/example value for unattended mode (optional)
# $3 - Timeout in seconds (optional, default: 120)
# $4 - Environment variable name hint for error messages (optional)
#
# Output:
# Prints the user input or fallback value to stdout
#
# Returns:
# 0 - Success (value provided or fallback used)
# 1 - Failed (interactive timeout without input)
#
# Global:
# MISSING_REQUIRED_VALUES - Array tracking fields that used fallbacks
#
# Example:
# # With fallback - script continues even in unattended mode
# token=$(prompt_input_required "Enter API Token:" "YOUR_TOKEN_HERE" 60 "var_api_token")
#
# # Check at end of script if any values need manual configuration
# if [[ ${#MISSING_REQUIRED_VALUES[@]} -gt 0 ]]; then
# msg_warn "Please configure: ${MISSING_REQUIRED_VALUES[*]}"
# fi
# ------------------------------------------------------------------------------
# Global array to track missing required values
declare -g -a MISSING_REQUIRED_VALUES=()
prompt_input_required() {
local message="${1:-Enter required value:}"
local fallback="${2:-CHANGE_ME}"
local timeout="${3:-120}"
local env_var_hint="${4:-}"
local response=""
# Check if value is already set via environment variable (if hint provided)
if [[ -n "$env_var_hint" ]]; then
local env_value="${!env_var_hint:-}"
if [[ -n "$env_value" ]]; then
echo "$env_value"
return 0
fi
fi
# Unattended mode: use fallback with warning
if is_unattended; then
if [[ -n "$env_var_hint" ]]; then
echo -e "${YW}⚠ Required value '${env_var_hint}' not set - using fallback: ${fallback}${CL}" >&2
MISSING_REQUIRED_VALUES+=("$env_var_hint")
else
echo -e "${YW}⚠ Required value not provided - using fallback: ${fallback}${CL}" >&2
MISSING_REQUIRED_VALUES+=("(unnamed)")
fi
echo "$fallback"
return 0
fi
# Check if running in a TTY
if [[ ! -t 0 ]]; then
echo -e "${YW}⚠ Not interactive - using fallback: ${fallback}${CL}" >&2
MISSING_REQUIRED_VALUES+=("${env_var_hint:-unnamed}")
echo "$fallback"
return 0
fi
# Interactive prompt - loop until non-empty input or use fallback on timeout
local attempts=0
while [[ -z "$response" ]]; do
((attempts++))
if [[ $attempts -gt 3 ]]; then
echo -e "${YW}Too many empty inputs - using fallback: ${fallback}${CL}" >&2
MISSING_REQUIRED_VALUES+=("${env_var_hint:-manual_input}")
echo "$fallback"
return 0
fi
echo -en "${YW}${message} (required, timeout ${timeout}s): ${CL}" >&2
if read -t "$timeout" -r response; then
if [[ -z "$response" ]]; then
echo -e "${YW}This field is required. Please enter a value. (attempt ${attempts}/3)${CL}" >&2
fi
else
# Timeout occurred - use fallback
echo "" >&2
echo -e "${YW}Timeout - using fallback value: ${fallback}${CL}" >&2
MISSING_REQUIRED_VALUES+=("${env_var_hint:-timeout}")
echo "$fallback"
return 0
fi
done
echo "$response"
}
# ------------------------------------------------------------------------------
# prompt_select()
#
# - Prompts user to select from a list of options with timeout support
# - In unattended mode: immediately returns default selection
# - In interactive mode: displays numbered menu and waits for choice
# - After timeout: auto-applies default selection
#
# Arguments:
# $1 - Prompt message (required)
# $2 - Default option number, 1-based (optional, default: 1)
# $3 - Timeout in seconds (optional, default: 60)
# $4+ - Options to display (required, at least 2)
#
# Output:
# Prints the selected option value to stdout
#
# Returns:
# 0 - Success
# 1 - No options provided or invalid state
#
# Example:
# choice=$(prompt_select "Select database:" 1 30 "PostgreSQL" "MySQL" "SQLite")
# echo "Selected: $choice"
#
# # With array
# options=("Option A" "Option B" "Option C")
# selected=$(prompt_select "Choose:" 2 60 "${options[@]}")
# ------------------------------------------------------------------------------
prompt_select() {
local message="${1:-Select option:}"
local default="${2:-1}"
local timeout="${3:-60}"
shift 3
local options=("$@")
local num_options=${#options[@]}
# Validate options
if [[ $num_options -eq 0 ]]; then
echo "" >&2
return 1
fi
# Validate default
if [[ ! "$default" =~ ^[0-9]+$ ]] || [[ "$default" -lt 1 ]] || [[ "$default" -gt "$num_options" ]]; then
default=1
fi
# Unattended mode: return default immediately
if is_unattended; then
echo "${options[$((default - 1))]}"
return 0
fi
# Check if running in a TTY
if [[ ! -t 0 ]]; then
echo "${options[$((default - 1))]}"
return 0
fi
# Display menu
echo -e "${YW}${message}${CL}" >&2
local i
for i in "${!options[@]}"; do
local num=$((i + 1))
if [[ $num -eq $default ]]; then
echo -e " ${GN}${num})${CL} ${options[$i]} ${YW}(default)${CL}" >&2
else
echo -e " ${GN}${num})${CL} ${options[$i]}" >&2
fi
done
# Interactive prompt with timeout
echo -en "${YW}Select [1-${num_options}] (auto-select ${default} in ${timeout}s): ${CL}" >&2
local response
if read -t "$timeout" -r response; then
if [[ -z "$response" ]]; then
# Empty response, use default
echo "${options[$((default - 1))]}"
elif [[ "$response" =~ ^[0-9]+$ ]] && [[ "$response" -ge 1 ]] && [[ "$response" -le "$num_options" ]]; then
# Valid selection
echo "${options[$((response - 1))]}"
else
# Invalid input, use default
echo -e "${YW}Invalid selection, using default: ${options[$((default - 1))]}${CL}" >&2
echo "${options[$((default - 1))]}"
fi
else
# Timeout occurred
echo "" >&2 # Newline after timeout
echo -e "${YW}Timeout - auto-selecting: ${options[$((default - 1))]}${CL}" >&2
echo "${options[$((default - 1))]}"
fi
}
# ------------------------------------------------------------------------------
# prompt_password()
#
# - Prompts user for password input with hidden characters
# - In unattended mode: returns default or generates random password
# - Supports auto-generation of secure passwords
# - After timeout: generates random password if allowed
#
# Arguments:
# $1 - Prompt message (required)
# $2 - Default value or "generate" for auto-generation (optional)
# $3 - Timeout in seconds (optional, default: 60)
# $4 - Minimum length for validation (optional, default: 0 = no minimum)
#
# Output:
# Prints the password to stdout
#
# Example:
# password=$(prompt_password "Enter password:" "generate" 30 8)
# echo "Password set"
#
# # Require user input (no default)
# db_pass=$(prompt_password "Database password:" "" 60 12)
# ------------------------------------------------------------------------------
prompt_password() {
local message="${1:-Enter password:}"
local default="${2:-}"
local timeout="${3:-60}"
local min_length="${4:-0}"
local response
# Generate random password if requested
local generated=""
if [[ "$default" == "generate" ]]; then
generated=$(openssl rand -base64 16 2>/dev/null | tr -dc 'a-zA-Z0-9' | head -c 16)
[[ -z "$generated" ]] && generated=$(head /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 16)
default="$generated"
fi
# Unattended mode: return default immediately
if is_unattended; then
echo "$default"
return 0
fi
# Check if running in a TTY
if [[ ! -t 0 ]]; then
echo "$default"
return 0
fi
# Build hint
local hint=""
if [[ -n "$generated" ]]; then
hint=" (Enter for auto-generated)"
elif [[ -n "$default" ]]; then
hint=" (Enter for default)"
fi
[[ "$min_length" -gt 0 ]] && hint="${hint} [min ${min_length} chars]"
# Interactive prompt with timeout (silent input)
echo -en "${YW}${message}${hint} (timeout ${timeout}s): ${CL}" >&2
if read -t "$timeout" -rs response; then
echo "" >&2 # Newline after hidden input
if [[ -n "$response" ]]; then
# Validate minimum length
if [[ "$min_length" -gt 0 ]] && [[ ${#response} -lt "$min_length" ]]; then
echo -e "${YW}Password too short (min ${min_length}), using default${CL}" >&2
echo "$default"
else
echo "$response"
fi
else
echo "$default"
fi
else
# Timeout occurred
echo "" >&2 # Newline after timeout
echo -e "${YW}Timeout - using generated password${CL}" >&2
echo "$default"
fi
}
# ==============================================================================
# SECTION 6: CLEANUP & MAINTENANCE
# ==============================================================================
@ -898,15 +1409,13 @@ check_or_create_swap() {
msg_error "No active swap detected"
read -p "Do you want to create a swap file? [y/N]: " create_swap
create_swap="${create_swap,,}" # to lowercase
if [[ "$create_swap" != "y" && "$create_swap" != "yes" ]]; then
if ! prompt_confirm "Do you want to create a swap file?" "n" 60; then
msg_info "Skipping swap file creation"
return 1
fi
read -p "Enter swap size in MB (e.g., 2048 for 2GB): " swap_size_mb
local swap_size_mb
swap_size_mb=$(prompt_input "Enter swap size in MB (e.g., 2048 for 2GB):" "2048" 60)
if ! [[ "$swap_size_mb" =~ ^[0-9]+$ ]]; then
msg_error "Invalid size input. Aborting."
return 1