Big bang
This commit is contained in:
20
lib/colors.sh
Normal file
20
lib/colors.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Terminal color and formatting constants.
|
||||
# Source this file; do not execute it directly.
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
NC='\033[0m' # No Color / Reset
|
||||
|
||||
# Print a colored message to stderr
|
||||
# Usage: color_echo <color_var> <message>
|
||||
color_echo() {
|
||||
local color="$1"
|
||||
local msg="$2"
|
||||
printf "${color}%s${NC}\n" "$msg" >&2
|
||||
}
|
||||
28
lib/output.sh
Normal file
28
lib/output.sh
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# Writes collected ENV_VALUE_* variables to a .env file.
|
||||
# Source this file; do not execute it directly.
|
||||
# Requires colors.sh to be sourced first.
|
||||
|
||||
# write_env_file <output_file>
|
||||
# Writes all populated ENV_VALUE_<NAME> entries to <output_file>.
|
||||
write_env_file() {
|
||||
local output_file="$1"
|
||||
|
||||
{
|
||||
printf "# Generated by generate-env on %s\n" "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
printf "\n"
|
||||
|
||||
local var_name
|
||||
for var_name in "${ENV_VARS[@]}"; do
|
||||
local desc_var="ENV_DESC_${var_name}"
|
||||
local desc="${!desc_var}"
|
||||
local value_var="ENV_VALUE_${var_name}"
|
||||
local value="${!value_var}"
|
||||
[[ -n "$desc" ]] && printf "# %s\n" "$desc"
|
||||
printf "%s=%s\n" "$var_name" "$value"
|
||||
printf "\n"
|
||||
done
|
||||
} > "$output_file"
|
||||
|
||||
printf "\n${GREEN}✓ Written to %s${NC}\n" "$output_file" >&2
|
||||
}
|
||||
87
lib/parse.sh
Normal file
87
lib/parse.sh
Normal file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# Parses a .env.example file into global metadata arrays.
|
||||
# Source this file; do not execute it directly.
|
||||
#
|
||||
# After calling parse_env_example, the following globals are populated:
|
||||
# ENV_VARS - ordered array of variable names
|
||||
# ENV_DESC_<NAME> - description string
|
||||
# ENV_TYPE_<NAME> - type annotation (string if absent)
|
||||
# ENV_REQUIRED_<NAME> - "true" or "false"
|
||||
# ENV_DEFAULT_<NAME> - default value (may be empty)
|
||||
|
||||
# parse_env_example <file>
|
||||
# Reads the given .env.example file and populates global metadata.
|
||||
parse_env_example() {
|
||||
local file="$1"
|
||||
|
||||
if [[ ! -f "$file" ]]; then
|
||||
printf "Error: file not found: %s\n" "$file" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
ENV_VARS=()
|
||||
|
||||
local desc=""
|
||||
local type=""
|
||||
local required="false"
|
||||
local in_block=0 # 1 once we've started accumulating comments for a var
|
||||
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
|
||||
# --- Empty line: flush pending comment block ---
|
||||
if [[ -z "$line" ]]; then
|
||||
if [[ $in_block -eq 1 ]]; then
|
||||
desc=""
|
||||
type=""
|
||||
required="false"
|
||||
in_block=0
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
# --- Comment line ---
|
||||
if [[ "$line" =~ ^[[:space:]]*#(.*) ]]; then
|
||||
local comment="${BASH_REMATCH[1]}"
|
||||
# Trim one leading space if present
|
||||
comment="${comment# }"
|
||||
in_block=1
|
||||
|
||||
if [[ "$comment" =~ ^@required([[:space:]]|$) ]]; then
|
||||
required="true"
|
||||
elif [[ "$comment" =~ ^@type[[:space:]]+(.+)$ ]]; then
|
||||
type="${BASH_REMATCH[1]}"
|
||||
# Trim trailing whitespace
|
||||
type="${type%"${type##*[![:space:]]}"}"
|
||||
else
|
||||
# Regular description text — append
|
||||
if [[ -n "$desc" ]]; then
|
||||
desc="${desc} ${comment}"
|
||||
else
|
||||
desc="$comment"
|
||||
fi
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
# --- KEY=VALUE line ---
|
||||
if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
|
||||
local var_name="${BASH_REMATCH[1]}"
|
||||
local default="${BASH_REMATCH[2]}"
|
||||
|
||||
ENV_VARS+=("$var_name")
|
||||
|
||||
printf -v "ENV_DESC_${var_name}" '%s' "$desc"
|
||||
printf -v "ENV_TYPE_${var_name}" '%s' "${type:-string}"
|
||||
printf -v "ENV_REQUIRED_${var_name}" '%s' "$required"
|
||||
printf -v "ENV_DEFAULT_${var_name}" '%s' "$default"
|
||||
|
||||
# Reset for next block
|
||||
desc=""
|
||||
type=""
|
||||
required="false"
|
||||
in_block=0
|
||||
continue
|
||||
fi
|
||||
|
||||
done < "$file"
|
||||
}
|
||||
217
lib/prompt.sh
Normal file
217
lib/prompt.sh
Normal file
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env bash
|
||||
# Interactive per-variable prompting.
|
||||
# Source this file; do not execute it directly.
|
||||
# Requires colors.sh and validate.sh to be sourced first.
|
||||
|
||||
# _prompt_enum <var_name> <options_csv> <default>
|
||||
# Prints a numbered menu and returns the chosen value via stdout.
|
||||
_prompt_enum() {
|
||||
local var_name="$1"
|
||||
local options_csv="$2"
|
||||
local default="$3"
|
||||
|
||||
local -a opts
|
||||
local IFS=','
|
||||
read -ra opts <<< "$options_csv"
|
||||
unset IFS
|
||||
|
||||
local default_idx=0
|
||||
local i
|
||||
for i in "${!opts[@]}"; do
|
||||
printf " %d) %s" $((i + 1)) "${opts[$i]}" >&2
|
||||
if [[ "${opts[$i]}" == "$default" ]]; then
|
||||
printf " ${DIM}(default)${NC}" >&2
|
||||
default_idx=$((i + 1))
|
||||
fi
|
||||
printf "\n" >&2
|
||||
done
|
||||
|
||||
local choice value
|
||||
while true; do
|
||||
if [[ $default_idx -gt 0 ]]; then
|
||||
read -r -p " Choice [1-${#opts[@]}, default ${default_idx}]: " choice >&2
|
||||
else
|
||||
read -r -p " Choice [1-${#opts[@]}]: " choice >&2
|
||||
fi
|
||||
|
||||
# Empty input → use default
|
||||
if [[ -z "$choice" && $default_idx -gt 0 ]]; then
|
||||
echo "${opts[$((default_idx - 1))]}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Numeric selection
|
||||
if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#opts[@]} )); then
|
||||
echo "${opts[$((choice - 1))]}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Direct string match
|
||||
local opt
|
||||
for opt in "${opts[@]}"; do
|
||||
if [[ "$choice" == "$opt" ]]; then
|
||||
echo "$opt"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
printf "${RED} Invalid choice. Enter a number between 1 and %d.${NC}\n" "${#opts[@]}" >&2
|
||||
done
|
||||
}
|
||||
|
||||
# _prompt_bool <default>
|
||||
# Prompts y/n and returns canonical "true" or "false" via stdout.
|
||||
_prompt_bool() {
|
||||
local default="$1"
|
||||
local hint
|
||||
|
||||
case "${default,,}" in
|
||||
true|yes|1|y) hint="Y/n" ;;
|
||||
false|no|0|n) hint="y/N" ;;
|
||||
*) hint="y/n" ;;
|
||||
esac
|
||||
|
||||
local input
|
||||
while true; do
|
||||
read -r -p " Enable? [${hint}]: " input >&2
|
||||
if [[ -z "$input" && -n "$default" ]]; then
|
||||
normalize_bool "$default"
|
||||
return 0
|
||||
fi
|
||||
case "${input,,}" in
|
||||
y|yes|true|1) echo "true"; return 0 ;;
|
||||
n|no|false|0) echo "false"; return 0 ;;
|
||||
*) printf "${RED} Enter y or n.${NC}\n" >&2 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# _prompt_secret <var_name> <default>
|
||||
# Reads a secret (hidden) value, with optional confirmation, via stdout.
|
||||
_prompt_secret() {
|
||||
local var_name="$1"
|
||||
local default="$2"
|
||||
local value confirm
|
||||
|
||||
while true; do
|
||||
if [[ -n "$default" ]]; then
|
||||
read -r -s -p " Value (leave blank to keep default): " value >&2
|
||||
else
|
||||
read -r -s -p " Value: " value >&2
|
||||
fi
|
||||
printf "\n" >&2
|
||||
|
||||
if [[ -z "$value" && -n "$default" ]]; then
|
||||
echo "$default"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -n "$value" ]]; then
|
||||
read -r -s -p " Confirm value: " confirm >&2
|
||||
printf "\n" >&2
|
||||
if [[ "$value" == "$confirm" ]]; then
|
||||
echo "$value"
|
||||
return 0
|
||||
fi
|
||||
printf "${RED} Values do not match. Try again.${NC}\n" >&2
|
||||
else
|
||||
printf "${RED} Value cannot be empty for a secret field with no default.${NC}\n" >&2
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# prompt_variable <var_name>
|
||||
# Interactively prompts the user for a value and stores it in ENV_VALUE_<var_name>.
|
||||
prompt_variable() {
|
||||
local var_name="$1"
|
||||
|
||||
# Dereference metadata
|
||||
local desc_var="ENV_DESC_${var_name}"
|
||||
local type_var="ENV_TYPE_${var_name}"
|
||||
local required_var="ENV_REQUIRED_${var_name}"
|
||||
local default_var="ENV_DEFAULT_${var_name}"
|
||||
|
||||
local desc="${!desc_var}"
|
||||
local type="${!type_var}"
|
||||
local required="${!required_var}"
|
||||
local default="${!default_var}"
|
||||
|
||||
# ---- Header ----
|
||||
printf "\n" >&2
|
||||
printf "${BOLD}${CYAN}%s${NC}" "$var_name" >&2
|
||||
if [[ "$required" == "true" ]]; then
|
||||
printf " ${RED}(required)${NC}" >&2
|
||||
else
|
||||
printf " ${DIM}(optional)${NC}" >&2
|
||||
fi
|
||||
if [[ -n "$type" && "$type" != "string" ]]; then
|
||||
printf " ${DIM}[%s]${NC}" "$type" >&2
|
||||
fi
|
||||
printf "\n" >&2
|
||||
|
||||
if [[ -n "$desc" ]]; then
|
||||
printf " ${DIM}%s${NC}\n" "$desc" >&2
|
||||
fi
|
||||
|
||||
# ---- Type-specific prompt ----
|
||||
local value
|
||||
|
||||
case "$type" in
|
||||
bool)
|
||||
value="$(_prompt_bool "$default")"
|
||||
;;
|
||||
|
||||
secret)
|
||||
value="$(_prompt_secret "$var_name" "$default")"
|
||||
;;
|
||||
|
||||
enum:*)
|
||||
local options="${type#enum:}"
|
||||
printf " ${DIM}Options:${NC}\n" >&2
|
||||
value="$(_prompt_enum "$var_name" "$options" "$default")"
|
||||
;;
|
||||
|
||||
*)
|
||||
# Generic text input (string, number, url, email)
|
||||
local prompt_str
|
||||
if [[ -n "$default" ]]; then
|
||||
prompt_str=" Value (default: ${default}): "
|
||||
else
|
||||
prompt_str=" Value: "
|
||||
fi
|
||||
|
||||
while true; do
|
||||
read -r -p "$prompt_str" value >&2
|
||||
if [[ -z "$value" ]]; then
|
||||
value="$default"
|
||||
fi
|
||||
|
||||
if validate_value "$var_name" "$value" "$type" "$required"; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
;;
|
||||
esac
|
||||
|
||||
# For bool/enum/secret we still run the required check after collecting value
|
||||
if [[ "$type" == "bool" || "$type" == "secret" || "$type" == enum:* ]]; then
|
||||
while ! validate_value "$var_name" "$value" "$type" "$required"; do
|
||||
case "$type" in
|
||||
bool) value="$(_prompt_bool "$default")" ;;
|
||||
secret) value="$(_prompt_secret "$var_name" "$default")" ;;
|
||||
enum:*) value="$(_prompt_enum "$var_name" "${type#enum:}" "$default")" ;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
printf -v "ENV_VALUE_${var_name}" '%s' "$value"
|
||||
}
|
||||
|
||||
# prompt_all_variables
|
||||
# Iterates over ENV_VARS and prompts for each one.
|
||||
prompt_all_variables() {
|
||||
local var_name
|
||||
for var_name in "${ENV_VARS[@]}"; do
|
||||
prompt_variable "$var_name"
|
||||
done
|
||||
}
|
||||
90
lib/validate.sh
Normal file
90
lib/validate.sh
Normal file
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env bash
|
||||
# Value validation helpers, keyed by @type annotation.
|
||||
# Source this file; do not execute it directly.
|
||||
# Requires colors.sh to be sourced first.
|
||||
|
||||
# validate_value <var_name> <value> <type> <required>
|
||||
# Returns 0 if valid, 1 if invalid (prints a message to stderr).
|
||||
validate_value() {
|
||||
local var_name="$1"
|
||||
local value="$2"
|
||||
local type="$3"
|
||||
local required="$4"
|
||||
|
||||
# Required check
|
||||
if [[ "$required" == "true" && -z "$value" ]]; then
|
||||
printf "${RED} Error: %s is required and cannot be empty.${NC}\n" "$var_name" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# No further validation needed for empty optional values
|
||||
if [[ -z "$value" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$type" in
|
||||
number)
|
||||
if ! [[ "$value" =~ ^-?[0-9]+(\.[0-9]+)?$ ]]; then
|
||||
printf "${RED} Error: Must be a number (got: %s).${NC}\n" "$value" >&2
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
|
||||
bool)
|
||||
case "${value,,}" in
|
||||
true|false|yes|no|1|0|y|n) ;;
|
||||
*)
|
||||
printf "${RED} Error: Must be a boolean value (true/false/yes/no).${NC}\n" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
|
||||
url)
|
||||
if ! [[ "$value" =~ ^https?:// ]]; then
|
||||
printf "${YELLOW} Warning: Value doesn't look like a URL (expected http:// or https://).${NC}\n" >&2
|
||||
# Warn only — allow the user to proceed
|
||||
fi
|
||||
;;
|
||||
|
||||
email)
|
||||
if ! [[ "$value" =~ ^[^@[:space:]]+@[^@[:space:]]+\.[^@[:space:]]+$ ]]; then
|
||||
printf "${RED} Error: Must be a valid email address (got: %s).${NC}\n" "$value" >&2
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
|
||||
enum:*)
|
||||
local options="${type#enum:}"
|
||||
local valid=false
|
||||
local IFS=','
|
||||
local opt
|
||||
for opt in $options; do
|
||||
if [[ "$value" == "$opt" ]]; then
|
||||
valid=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ "$valid" == "false" ]]; then
|
||||
printf "${RED} Error: Must be one of: %s (got: %s).${NC}\n" "$options" "$value" >&2
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
|
||||
secret|string|*)
|
||||
# No structural validation for these types
|
||||
;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# normalize_bool <value>
|
||||
# Converts a truthy/falsy string to canonical "true" or "false".
|
||||
normalize_bool() {
|
||||
case "${1,,}" in
|
||||
true|yes|1|y) echo "true" ;;
|
||||
false|no|0|n) echo "false" ;;
|
||||
*) echo "$1" ;; # pass-through for validation to catch
|
||||
esac
|
||||
}
|
||||
Reference in New Issue
Block a user