Big bang
This commit is contained in:
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Generated files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# IDEs
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
39
HELP
Normal file
39
HELP
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
generate-env.sh — Interactively generate a .env file from a .env.example template.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
./generate-env.sh [options] [input_file]
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
input_file Path to the .env.example file (default: .env.example)
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-o <file> Output file path (default: .env)
|
||||||
|
-h Show this help message
|
||||||
|
|
||||||
|
Annotation syntax in .env.example:
|
||||||
|
# Description text for the variable
|
||||||
|
# @required
|
||||||
|
# @type <type>
|
||||||
|
VAR_NAME=default_value
|
||||||
|
|
||||||
|
Supported types:
|
||||||
|
string Plain text (default when @type is absent)
|
||||||
|
number Numeric value; validated on entry
|
||||||
|
bool Boolean; prompts y/n, stored as true/false
|
||||||
|
secret Hidden input with confirmation; stored as-is
|
||||||
|
url Text; warns if value doesn't start with http(s)://
|
||||||
|
email Text; validated as user@domain.tld
|
||||||
|
enum:a,b,c Presents a numbered menu of choices
|
||||||
|
|
||||||
|
Using as a git submodule:
|
||||||
|
Add to your project:
|
||||||
|
git submodule add https://code.garrettmills.dev/garrettmills/interactive-env
|
||||||
|
|
||||||
|
Run from your project root:
|
||||||
|
./interactive-env/generate-env.sh
|
||||||
|
|
||||||
|
Update to the latest version:
|
||||||
|
git submodule update --remote interactive-env
|
||||||
|
|
||||||
|
Clone a project that uses this submodule:
|
||||||
|
git clone --recurse-submodules <your-repo-url>
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Garrett Mills
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
9
README.md
Normal file
9
README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# `interactive-env`
|
||||||
|
|
||||||
|
Interactively generate a `.env` file from a `.env.example` template.
|
||||||
|
|
||||||
|
It is implemented as a portable bash script. Variables are annotated with structured comments (`@type`, `@required`) to drive prompts, validation, and type-specific input (booleans, enums, secrets, etc.).
|
||||||
|
|
||||||
|
See [`HELP`](./HELP) for full usage and annotation syntax.
|
||||||
|
|
||||||
|
This project is licensed under the [MIT License](./LICENSE).
|
||||||
30
example.env.example
Normal file
30
example.env.example
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# The name of the application
|
||||||
|
# @required
|
||||||
|
APP_NAME=MyApp
|
||||||
|
|
||||||
|
# The environment to run in
|
||||||
|
# @type enum:development,staging,production
|
||||||
|
# @required
|
||||||
|
APP_ENV=development
|
||||||
|
|
||||||
|
# Port number the server listens on
|
||||||
|
# @type number
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
# Enable debug logging
|
||||||
|
# @type bool
|
||||||
|
DEBUG=false
|
||||||
|
|
||||||
|
# Secret key for signing JWTs
|
||||||
|
# @type secret
|
||||||
|
# @required
|
||||||
|
JWT_SECRET=
|
||||||
|
|
||||||
|
# Database connection URL
|
||||||
|
# @type url
|
||||||
|
# @required
|
||||||
|
DATABASE_URL=postgres://localhost:5432/myapp
|
||||||
|
|
||||||
|
# Optional email address for admin notifications
|
||||||
|
# @type email
|
||||||
|
ADMIN_EMAIL=
|
||||||
77
generate-env.sh
Executable file
77
generate-env.sh
Executable file
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# generate-env.sh — Interactively generate a .env file from a .env.example template.
|
||||||
|
# Run with -h for usage information.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# shellcheck source=lib/colors.sh
|
||||||
|
source "${SCRIPT_DIR}/lib/colors.sh"
|
||||||
|
# shellcheck source=lib/parse.sh
|
||||||
|
source "${SCRIPT_DIR}/lib/parse.sh"
|
||||||
|
# shellcheck source=lib/validate.sh
|
||||||
|
source "${SCRIPT_DIR}/lib/validate.sh"
|
||||||
|
# shellcheck source=lib/prompt.sh
|
||||||
|
source "${SCRIPT_DIR}/lib/prompt.sh"
|
||||||
|
# shellcheck source=lib/output.sh
|
||||||
|
source "${SCRIPT_DIR}/lib/output.sh"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Defaults
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
INPUT_FILE=".env.example"
|
||||||
|
OUTPUT_FILE=".env"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Argument parsing
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
usage() {
|
||||||
|
cat "${SCRIPT_DIR}/HELP" >&2
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
while getopts ":o:h" opt; do
|
||||||
|
case "$opt" in
|
||||||
|
o) OUTPUT_FILE="$OPTARG" ;;
|
||||||
|
h) usage ;;
|
||||||
|
:) printf "${RED}Error: option -%s requires an argument.${NC}\n" "$OPTARG" >&2; exit 1 ;;
|
||||||
|
\?) printf "${RED}Error: unknown option -%s${NC}\n" "$OPTARG" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
shift $((OPTIND - 1))
|
||||||
|
|
||||||
|
if [[ $# -gt 0 ]]; then
|
||||||
|
INPUT_FILE="$1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Guard: don't silently overwrite an existing .env without confirmation
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if [[ -f "$OUTPUT_FILE" ]]; then
|
||||||
|
printf "${YELLOW}Warning: %s already exists.${NC}\n" "$OUTPUT_FILE" >&2
|
||||||
|
read -r -p "Overwrite? [y/N]: " confirm >&2
|
||||||
|
case "${confirm,,}" in
|
||||||
|
y|yes) ;;
|
||||||
|
*) printf "Aborted.\n" >&2; exit 0 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
printf "${BOLD}generate-env${NC} %s → %s\n" "$INPUT_FILE" "$OUTPUT_FILE" >&2
|
||||||
|
printf "${DIM}Fill in each variable. Press Enter to accept the default value.${NC}\n" >&2
|
||||||
|
|
||||||
|
parse_env_example "$INPUT_FILE"
|
||||||
|
|
||||||
|
if [[ ${#ENV_VARS[@]} -eq 0 ]]; then
|
||||||
|
printf "${YELLOW}No variables found in %s.${NC}\n" "$INPUT_FILE" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "${DIM}Found %d variable(s).${NC}\n" "${#ENV_VARS[@]}" >&2
|
||||||
|
|
||||||
|
prompt_all_variables
|
||||||
|
|
||||||
|
write_env_file "$OUTPUT_FILE"
|
||||||
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
|
||||||
|
}
|
||||||
21
tests/fixtures/all_types.env.example
vendored
Normal file
21
tests/fixtures/all_types.env.example
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# @type string
|
||||||
|
STR_VAR=hello
|
||||||
|
|
||||||
|
# @type number
|
||||||
|
NUM_VAR=42
|
||||||
|
|
||||||
|
# @type bool
|
||||||
|
BOOL_VAR=false
|
||||||
|
|
||||||
|
# @type secret
|
||||||
|
# @required
|
||||||
|
SECRET_VAR=
|
||||||
|
|
||||||
|
# @type url
|
||||||
|
URL_VAR=https://example.com
|
||||||
|
|
||||||
|
# @type email
|
||||||
|
EMAIL_VAR=user@example.com
|
||||||
|
|
||||||
|
# @type enum:a,b,c
|
||||||
|
ENUM_VAR=a
|
||||||
3
tests/fixtures/basic.env.example
vendored
Normal file
3
tests/fixtures/basic.env.example
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Description
|
||||||
|
# @required
|
||||||
|
BASIC_VAR=default
|
||||||
0
tests/fixtures/empty.env.example
vendored
Normal file
0
tests/fixtures/empty.env.example
vendored
Normal file
3
tests/fixtures/multiline_desc.env.example
vendored
Normal file
3
tests/fixtures/multiline_desc.env.example
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# First line
|
||||||
|
# Second line
|
||||||
|
MULTI_VAR=value
|
||||||
2
tests/fixtures/no_annotations.env.example
vendored
Normal file
2
tests/fixtures/no_annotations.env.example
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
PLAIN_VAR=value
|
||||||
|
ANOTHER_VAR=
|
||||||
4
tests/fixtures/orphaned_comments.env.example
vendored
Normal file
4
tests/fixtures/orphaned_comments.env.example
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# This comment has no variable
|
||||||
|
# It should be ignored
|
||||||
|
|
||||||
|
REAL_VAR=value
|
||||||
3
tests/fixtures/value_with_equals.env.example
vendored
Normal file
3
tests/fixtures/value_with_equals.env.example
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# A URL with query parameters (value contains = signs)
|
||||||
|
# @type url
|
||||||
|
DB_URL=postgres://host:5432/db?sslmode=require&connect_timeout=10
|
||||||
163
tests/framework.sh
Normal file
163
tests/framework.sh
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Minimal test framework. Source this file; do not execute it directly.
|
||||||
|
#
|
||||||
|
# Globals maintained per-process:
|
||||||
|
# _PASS — count of passing assertions
|
||||||
|
# _FAIL — count of failing assertions
|
||||||
|
|
||||||
|
_PASS=0
|
||||||
|
_FAIL=0
|
||||||
|
|
||||||
|
# Color codes (self-contained; do not rely on colors.sh)
|
||||||
|
_R='\033[0;31m' # red
|
||||||
|
_G='\033[0;32m' # green
|
||||||
|
_Y='\033[1;33m' # yellow
|
||||||
|
_B='\033[1m' # bold
|
||||||
|
_D='\033[2m' # dim
|
||||||
|
_N='\033[0m' # reset
|
||||||
|
|
||||||
|
# suite <name>
|
||||||
|
# Print a section header for a group of related tests.
|
||||||
|
suite() {
|
||||||
|
printf "\n${_B}%s${_N}\n" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# pass / fail — low-level counters; prefer assert_* helpers instead
|
||||||
|
pass() {
|
||||||
|
_PASS=$((_PASS + 1))
|
||||||
|
printf " ${_G}✓${_N} %s\n" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
_FAIL=$((_FAIL + 1))
|
||||||
|
printf " ${_R}✗${_N} %s\n" "$1"
|
||||||
|
[[ -n "${2:-}" ]] && printf " expected: %s\n" "$2"
|
||||||
|
[[ -n "${3:-}" ]] && printf " got: %s\n" "${3:-<empty>}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Assertion helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
assert_eq() {
|
||||||
|
local expected="$1" actual="$2" msg="${3:-assert_eq}"
|
||||||
|
if [[ "$expected" == "$actual" ]]; then
|
||||||
|
pass "$msg"
|
||||||
|
else
|
||||||
|
fail "$msg" "$expected" "$actual"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_not_eq() {
|
||||||
|
local unexpected="$1" actual="$2" msg="${3:-assert_not_eq}"
|
||||||
|
if [[ "$unexpected" != "$actual" ]]; then
|
||||||
|
pass "$msg"
|
||||||
|
else
|
||||||
|
fail "$msg (expected values to differ)" "$unexpected" "$actual"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_empty() {
|
||||||
|
local actual="$1" msg="${2:-assert_empty}"
|
||||||
|
if [[ -z "$actual" ]]; then
|
||||||
|
pass "$msg"
|
||||||
|
else
|
||||||
|
fail "$msg (expected empty)" "" "$actual"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_not_empty() {
|
||||||
|
local actual="$1" msg="${2:-assert_not_empty}"
|
||||||
|
if [[ -n "$actual" ]]; then
|
||||||
|
pass "$msg"
|
||||||
|
else
|
||||||
|
fail "$msg (expected non-empty)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_contains() {
|
||||||
|
local needle="$1" haystack="$2" msg="${3:-assert_contains}"
|
||||||
|
if [[ "$haystack" == *"$needle"* ]]; then
|
||||||
|
pass "$msg"
|
||||||
|
else
|
||||||
|
fail "$msg" "*${needle}*" "$haystack"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# assert_success <msg> <cmd> [args...]
|
||||||
|
# Passes when the command exits 0.
|
||||||
|
assert_success() {
|
||||||
|
local msg="$1"; shift
|
||||||
|
local rc=0
|
||||||
|
"$@" 2>/dev/null || rc=$?
|
||||||
|
if [[ $rc -eq 0 ]]; then
|
||||||
|
pass "$msg"
|
||||||
|
else
|
||||||
|
fail "$msg (expected exit 0, got $rc)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# assert_failure <msg> <cmd> [args...]
|
||||||
|
# Passes when the command exits non-zero.
|
||||||
|
assert_failure() {
|
||||||
|
local msg="$1"; shift
|
||||||
|
local rc=0
|
||||||
|
"$@" 2>/dev/null || rc=$?
|
||||||
|
if [[ $rc -ne 0 ]]; then
|
||||||
|
pass "$msg"
|
||||||
|
else
|
||||||
|
fail "$msg (expected non-zero exit)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_file_exists() {
|
||||||
|
local file="$1" msg="${2:-assert_file_exists}"
|
||||||
|
if [[ -f "$file" ]]; then
|
||||||
|
pass "$msg"
|
||||||
|
else
|
||||||
|
fail "$msg" "<file: $file>" "(not found)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_file_contains() {
|
||||||
|
local pattern="$1" file="$2" msg="${3:-assert_file_contains}"
|
||||||
|
if [[ ! -f "$file" ]]; then
|
||||||
|
fail "$msg (file not found: $file)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if grep -qF "$pattern" "$file"; then
|
||||||
|
pass "$msg"
|
||||||
|
else
|
||||||
|
fail "$msg" "$pattern" "(not found in $file)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_file_not_contains() {
|
||||||
|
local pattern="$1" file="$2" msg="${3:-assert_file_not_contains}"
|
||||||
|
if [[ ! -f "$file" ]]; then
|
||||||
|
fail "$msg (file not found: $file)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if ! grep -qF "$pattern" "$file"; then
|
||||||
|
pass "$msg"
|
||||||
|
else
|
||||||
|
fail "$msg" "(should not contain $pattern)" "(found in $file)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Summary
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# print_summary
|
||||||
|
# Print pass/fail totals and exit 1 if any tests failed.
|
||||||
|
print_summary() {
|
||||||
|
local total=$((_PASS + _FAIL))
|
||||||
|
printf "\n"
|
||||||
|
if [[ $_FAIL -eq 0 ]]; then
|
||||||
|
printf "${_G}%d/%d passed${_N}\n" "$_PASS" "$total"
|
||||||
|
else
|
||||||
|
printf "${_R}%d failed${_N}, %d passed, %d total\n" "$_FAIL" "$_PASS" "$total"
|
||||||
|
fi
|
||||||
|
[[ $_FAIL -eq 0 ]]
|
||||||
|
}
|
||||||
75
tests/run_tests.sh
Executable file
75
tests/run_tests.sh
Executable file
@@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Discovers and runs all test_*.sh files, printing a combined summary.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./tests/run_tests.sh # run all suites
|
||||||
|
# ./tests/run_tests.sh [file] # run a specific suite
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# ---- Colors (self-contained) ----
|
||||||
|
_R='\033[0;31m'; _G='\033[0;32m'; _B='\033[1m'; _D='\033[2m'; _N='\033[0m'
|
||||||
|
|
||||||
|
total_pass=0
|
||||||
|
total_fail=0
|
||||||
|
failed_suites=()
|
||||||
|
|
||||||
|
run_suite() {
|
||||||
|
local file="$1"
|
||||||
|
local name
|
||||||
|
name="$(basename "$file")"
|
||||||
|
|
||||||
|
printf "\n${_B}══════ %s ══════${_N}\n" "$name"
|
||||||
|
|
||||||
|
local output exit_code=0
|
||||||
|
output=$(bash "$file" 2>&1) || exit_code=$?
|
||||||
|
|
||||||
|
echo "$output"
|
||||||
|
|
||||||
|
# Count results — strip ANSI codes first so the unicode chars are findable
|
||||||
|
local stripped p f
|
||||||
|
stripped=$(printf '%s' "$output" | sed 's/\x1b\[[0-9;]*[mGKHF]//g')
|
||||||
|
p=$(printf '%s\n' "$stripped" | grep -c '✓' || true)
|
||||||
|
f=$(printf '%s\n' "$stripped" | grep -c '✗' || true)
|
||||||
|
|
||||||
|
total_pass=$((total_pass + p))
|
||||||
|
total_fail=$((total_fail + f))
|
||||||
|
|
||||||
|
if [[ $exit_code -ne 0 ]]; then
|
||||||
|
failed_suites+=("$name")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Collect files to run
|
||||||
|
if [[ $# -gt 0 ]]; then
|
||||||
|
files=("$@")
|
||||||
|
else
|
||||||
|
files=("$TESTS_DIR"/test_*.sh)
|
||||||
|
fi
|
||||||
|
|
||||||
|
for f in "${files[@]}"; do
|
||||||
|
[[ -f "$f" ]] || { printf "${_R}Not found: %s${_N}\n" "$f" >&2; continue; }
|
||||||
|
run_suite "$f"
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---- Aggregate summary ----
|
||||||
|
total=$((total_pass + total_fail))
|
||||||
|
|
||||||
|
printf "\n${_B}══════ Summary ══════${_N}\n"
|
||||||
|
printf "Suites run: %d\n" "${#files[@]}"
|
||||||
|
printf "Tests: %d total, " "$total"
|
||||||
|
|
||||||
|
if [[ $total_fail -eq 0 ]]; then
|
||||||
|
printf "${_G}%d passed${_N}\n" "$total_pass"
|
||||||
|
printf "\n${_G}All tests passed.${_N}\n"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
printf "${_G}%d passed${_N}, ${_R}%d failed${_N}\n" "$total_pass" "$total_fail"
|
||||||
|
printf "\n${_R}Failed suites:${_N}\n"
|
||||||
|
for s in "${failed_suites[@]}"; do
|
||||||
|
printf " - %s\n" "$s"
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
242
tests/test_e2e.sh
Executable file
242
tests/test_e2e.sh
Executable file
@@ -0,0 +1,242 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# End-to-end tests for generate-env.sh.
|
||||||
|
#
|
||||||
|
# Each test pipes a crafted stdin to generate-env.sh and inspects the output
|
||||||
|
# .env file. Input lines are counted to match each variable's prompt exactly.
|
||||||
|
|
||||||
|
TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(dirname "$TESTS_DIR")"
|
||||||
|
SCRIPT="$ROOT_DIR/generate-env.sh"
|
||||||
|
|
||||||
|
source "$TESTS_DIR/framework.sh"
|
||||||
|
|
||||||
|
TMPDIR_SELF="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TMPDIR_SELF"' EXIT
|
||||||
|
|
||||||
|
# run_gen <input_string> <input_file> <output_file>
|
||||||
|
# Runs generate-env.sh, feeding <input_string> as stdin.
|
||||||
|
# Suppresses all terminal output. Removes <output_file> first so the
|
||||||
|
# overwrite guard never triggers.
|
||||||
|
run_gen() {
|
||||||
|
local input="$1"
|
||||||
|
local in_file="$2"
|
||||||
|
local out_file="$3"
|
||||||
|
rm -f "$out_file"
|
||||||
|
# -o must come before the positional input-file argument so getopts sees it
|
||||||
|
printf "%s" "$input" | bash "$SCRIPT" -o "$out_file" "$in_file" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "e2e: accept all defaults"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Fixture: three optional vars with defaults, no required fields
|
||||||
|
IN="$TMPDIR_SELF/defaults.env.example"
|
||||||
|
OUT="$TMPDIR_SELF/defaults.env"
|
||||||
|
cat > "$IN" << 'EOF'
|
||||||
|
# App name
|
||||||
|
APP_NAME=MyApp
|
||||||
|
|
||||||
|
# Port
|
||||||
|
# @type number
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
# Debug mode
|
||||||
|
# @type bool
|
||||||
|
DEBUG=false
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Input: three Enter presses (accept all defaults)
|
||||||
|
run_gen $'\n\n\n' "$IN" "$OUT"
|
||||||
|
|
||||||
|
assert_file_exists "$OUT" "output file created"
|
||||||
|
assert_file_contains "APP_NAME=MyApp" "$OUT" "string default accepted"
|
||||||
|
assert_file_contains "PORT=3000" "$OUT" "number default accepted"
|
||||||
|
assert_file_contains "DEBUG=false" "$OUT" "bool default accepted"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "e2e: override defaults"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
IN="$TMPDIR_SELF/override.env.example"
|
||||||
|
OUT="$TMPDIR_SELF/override.env"
|
||||||
|
cat > "$IN" << 'EOF'
|
||||||
|
APP_NAME=MyApp
|
||||||
|
PORT=3000
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Override both
|
||||||
|
run_gen $'CustomApp\n8080\n' "$IN" "$OUT"
|
||||||
|
|
||||||
|
assert_file_contains "APP_NAME=CustomApp" "$OUT" "string default overridden"
|
||||||
|
assert_file_contains "PORT=8080" "$OUT" "number default overridden"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "e2e: required fields must be filled"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
IN="$TMPDIR_SELF/required.env.example"
|
||||||
|
OUT="$TMPDIR_SELF/required.env"
|
||||||
|
cat > "$IN" << 'EOF'
|
||||||
|
# @required
|
||||||
|
MUST_HAVE=
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# First send empty (should re-prompt), then a value
|
||||||
|
run_gen $'\nfilled\n' "$IN" "$OUT"
|
||||||
|
|
||||||
|
assert_file_contains "MUST_HAVE=filled" "$OUT" "required field filled on retry"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "e2e: bool values"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
IN="$TMPDIR_SELF/bool.env.example"
|
||||||
|
OUT="$TMPDIR_SELF/bool.env"
|
||||||
|
cat > "$IN" << 'EOF'
|
||||||
|
# @type bool
|
||||||
|
FLAG_A=false
|
||||||
|
|
||||||
|
# @type bool
|
||||||
|
FLAG_B=true
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# y for FLAG_A (-> true), Enter for FLAG_B (-> default true)
|
||||||
|
run_gen $'y\n\n' "$IN" "$OUT"
|
||||||
|
|
||||||
|
assert_file_contains "FLAG_A=true" "$OUT" "y -> true"
|
||||||
|
assert_file_contains "FLAG_B=true" "$OUT" "empty accepts true default"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "e2e: enum with numeric choice"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
IN="$TMPDIR_SELF/enum.env.example"
|
||||||
|
OUT="$TMPDIR_SELF/enum.env"
|
||||||
|
cat > "$IN" << 'EOF'
|
||||||
|
# @type enum:development,staging,production
|
||||||
|
# @required
|
||||||
|
APP_ENV=development
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Choose option 3 (production)
|
||||||
|
run_gen $'3\n' "$IN" "$OUT"
|
||||||
|
|
||||||
|
assert_file_contains "APP_ENV=production" "$OUT" "enum numeric choice 3 -> production"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "e2e: enum with string choice"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
OUT="$TMPDIR_SELF/enum_str.env"
|
||||||
|
|
||||||
|
run_gen $'staging\n' "$IN" "$OUT"
|
||||||
|
|
||||||
|
assert_file_contains "APP_ENV=staging" "$OUT" "enum string choice -> staging"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "e2e: enum accepts default"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
OUT="$TMPDIR_SELF/enum_default.env"
|
||||||
|
|
||||||
|
run_gen $'\n' "$IN" "$OUT"
|
||||||
|
|
||||||
|
assert_file_contains "APP_ENV=development" "$OUT" "enum empty input uses default"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "e2e: secret with confirmation"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
IN="$TMPDIR_SELF/secret.env.example"
|
||||||
|
OUT="$TMPDIR_SELF/secret.env"
|
||||||
|
cat > "$IN" << 'EOF'
|
||||||
|
# @type secret
|
||||||
|
# @required
|
||||||
|
JWT_SECRET=
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Enter value twice (entry + confirm)
|
||||||
|
run_gen $'supersecret\nsupersecret\n' "$IN" "$OUT"
|
||||||
|
|
||||||
|
assert_file_contains "JWT_SECRET=supersecret" "$OUT" "secret stored after confirmation"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "e2e: secret uses default on empty input"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
IN="$TMPDIR_SELF/secret_default.env.example"
|
||||||
|
OUT="$TMPDIR_SELF/secret_default.env"
|
||||||
|
cat > "$IN" << 'EOF'
|
||||||
|
# @type secret
|
||||||
|
JWT_SECRET=existing_token
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Empty input -> keep default
|
||||||
|
run_gen $'\n' "$IN" "$OUT"
|
||||||
|
|
||||||
|
assert_file_contains "JWT_SECRET=existing_token" "$OUT" "secret empty input keeps default"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "e2e: -o flag sets output path"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
IN="$TMPDIR_SELF/flag.env.example"
|
||||||
|
CUSTOM_OUT="$TMPDIR_SELF/custom_output.env"
|
||||||
|
cat > "$IN" << 'EOF'
|
||||||
|
KEY=val
|
||||||
|
EOF
|
||||||
|
|
||||||
|
rm -f "$CUSTOM_OUT"
|
||||||
|
printf "\n" | bash "$SCRIPT" -o "$CUSTOM_OUT" "$IN" 2>/dev/null
|
||||||
|
|
||||||
|
assert_file_exists "$CUSTOM_OUT" "custom output path created via -o flag"
|
||||||
|
assert_file_contains "KEY=val" "$CUSTOM_OUT" "content written to custom path"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "e2e: empty optional field written as KEY="
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
IN="$TMPDIR_SELF/optional.env.example"
|
||||||
|
OUT="$TMPDIR_SELF/optional.env"
|
||||||
|
cat > "$IN" << 'EOF'
|
||||||
|
# Optional field with no default
|
||||||
|
# @type email
|
||||||
|
ADMIN_EMAIL=
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Leave empty
|
||||||
|
run_gen $'\n' "$IN" "$OUT"
|
||||||
|
|
||||||
|
assert_file_contains "ADMIN_EMAIL=" "$OUT" "optional empty field written as KEY="
|
||||||
|
assert_file_not_contains "ADMIN_EMAIL=x" "$OUT" "no stray value"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "e2e: full example.env.example"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Variable order and input count:
|
||||||
|
# 1. APP_NAME (string, required, default "MyApp") → "\n" accept default
|
||||||
|
# 2. APP_ENV (enum:…, required, default "development") → "\n" accept default (1)
|
||||||
|
# 3. PORT (number, optional, default "3000") → "\n" accept default
|
||||||
|
# 4. DEBUG (bool, optional, default "false") → "n\n" explicit n
|
||||||
|
# 5. JWT_SECRET(secret, required, no default) → "tok\ntok\n" entry+confirm
|
||||||
|
# 6. DATABASE_URL(url, required, has default) → "\n" accept default
|
||||||
|
# 7. ADMIN_EMAIL (email, optional, no default) → "\n" leave empty
|
||||||
|
|
||||||
|
OUT="$TMPDIR_SELF/full.env"
|
||||||
|
INPUT=$'\n\n\nn\ntok\ntok\n\n\n'
|
||||||
|
|
||||||
|
run_gen "$INPUT" "$ROOT_DIR/example.env.example" "$OUT"
|
||||||
|
|
||||||
|
assert_file_exists "$OUT" "output file created"
|
||||||
|
assert_file_contains "APP_NAME=MyApp" "$OUT" "APP_NAME default"
|
||||||
|
assert_file_contains "APP_ENV=development" "$OUT" "APP_ENV default"
|
||||||
|
assert_file_contains "PORT=3000" "$OUT" "PORT default"
|
||||||
|
assert_file_contains "DEBUG=false" "$OUT" "DEBUG=false (n)"
|
||||||
|
assert_file_contains "JWT_SECRET=tok" "$OUT" "JWT_SECRET value"
|
||||||
|
assert_file_contains "DATABASE_URL=postgres://localhost:5432/myapp" "$OUT" "DATABASE_URL default"
|
||||||
|
assert_file_contains "ADMIN_EMAIL=" "$OUT" "ADMIN_EMAIL empty"
|
||||||
|
assert_file_contains "# Generated by generate-env" "$OUT" "header present"
|
||||||
|
|
||||||
|
print_summary
|
||||||
192
tests/test_output.sh
Executable file
192
tests/test_output.sh
Executable file
@@ -0,0 +1,192 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Unit tests for lib/output.sh
|
||||||
|
|
||||||
|
TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(dirname "$TESTS_DIR")"
|
||||||
|
|
||||||
|
source "$TESTS_DIR/framework.sh"
|
||||||
|
source "$ROOT_DIR/lib/colors.sh"
|
||||||
|
source "$ROOT_DIR/lib/output.sh"
|
||||||
|
|
||||||
|
TMPDIR_SELF="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TMPDIR_SELF"' EXIT
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "output: basic file creation"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ENV_VARS=("FOO" "BAR")
|
||||||
|
ENV_DESC_FOO=""
|
||||||
|
ENV_DESC_BAR=""
|
||||||
|
ENV_VALUE_FOO="hello"
|
||||||
|
ENV_VALUE_BAR="world"
|
||||||
|
|
||||||
|
OUT="$TMPDIR_SELF/basic.env"
|
||||||
|
write_env_file "$OUT" 2>/dev/null
|
||||||
|
|
||||||
|
assert_file_exists "$OUT" "output file created"
|
||||||
|
assert_file_contains "FOO=hello" "$OUT" "FOO written correctly"
|
||||||
|
assert_file_contains "BAR=world" "$OUT" "BAR written correctly"
|
||||||
|
assert_file_contains "# Generated by generate-env" "$OUT" "header present"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "output: variable order matches ENV_VARS"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ENV_VARS=("ALPHA" "BETA" "GAMMA")
|
||||||
|
ENV_VALUE_ALPHA="1"
|
||||||
|
ENV_VALUE_BETA="2"
|
||||||
|
ENV_VALUE_GAMMA="3"
|
||||||
|
|
||||||
|
OUT="$TMPDIR_SELF/order.env"
|
||||||
|
write_env_file "$OUT" 2>/dev/null
|
||||||
|
|
||||||
|
# grep -n returns the line number; verify order
|
||||||
|
alpha_line=$(grep -n "ALPHA=" "$OUT" | cut -d: -f1)
|
||||||
|
beta_line=$(grep -n "BETA=" "$OUT" | cut -d: -f1)
|
||||||
|
gamma_line=$(grep -n "GAMMA=" "$OUT" | cut -d: -f1)
|
||||||
|
|
||||||
|
if (( alpha_line < beta_line && beta_line < gamma_line )); then
|
||||||
|
pass "variable order preserved"
|
||||||
|
else
|
||||||
|
fail "variable order preserved" "ALPHA < BETA < GAMMA" "lines $alpha_line, $beta_line, $gamma_line"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "output: empty values written as KEY="
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ENV_VARS=("WITH_VALUE" "NO_VALUE")
|
||||||
|
ENV_VALUE_WITH_VALUE="set"
|
||||||
|
ENV_VALUE_NO_VALUE=""
|
||||||
|
|
||||||
|
OUT="$TMPDIR_SELF/empty.env"
|
||||||
|
write_env_file "$OUT" 2>/dev/null
|
||||||
|
|
||||||
|
assert_file_contains "WITH_VALUE=set" "$OUT" "non-empty value written"
|
||||||
|
assert_file_contains "NO_VALUE=" "$OUT" "empty value written as KEY="
|
||||||
|
assert_file_not_contains "NO_VALUE=x" "$OUT" "no stray value for empty var"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "output: values containing special characters"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ENV_VARS=("SPECIAL")
|
||||||
|
ENV_VALUE_SPECIAL='postgres://user:p@ss!#$@host:5432/db?ssl=true'
|
||||||
|
|
||||||
|
OUT="$TMPDIR_SELF/special.env"
|
||||||
|
write_env_file "$OUT" 2>/dev/null
|
||||||
|
|
||||||
|
assert_file_contains "SPECIAL=postgres://user:p@ss" "$OUT" "special chars in value preserved"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "output: values containing equals signs"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ENV_VARS=("DB_URL")
|
||||||
|
ENV_VALUE_DB_URL="postgres://host/db?sslmode=require&timeout=10"
|
||||||
|
|
||||||
|
OUT="$TMPDIR_SELF/equals.env"
|
||||||
|
write_env_file "$OUT" 2>/dev/null
|
||||||
|
|
||||||
|
assert_file_contains "DB_URL=postgres://host/db?sslmode=require&timeout=10" \
|
||||||
|
"$OUT" "equals signs in value preserved"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "output: header contains ISO-8601 timestamp"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ENV_VARS=("X")
|
||||||
|
ENV_VALUE_X="1"
|
||||||
|
|
||||||
|
OUT="$TMPDIR_SELF/ts.env"
|
||||||
|
write_env_file "$OUT" 2>/dev/null
|
||||||
|
|
||||||
|
# Header should contain a UTC timestamp in the form YYYY-MM-DDTHH:MM:SSZ
|
||||||
|
if grep -qE '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z' "$OUT"; then
|
||||||
|
pass "header contains ISO-8601 UTC timestamp"
|
||||||
|
else
|
||||||
|
fail "header contains ISO-8601 UTC timestamp" \
|
||||||
|
"YYYY-MM-DDTHH:MM:SSZ" "(not found in header)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "output: single variable"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ENV_VARS=("SOLO")
|
||||||
|
ENV_VALUE_SOLO="only"
|
||||||
|
|
||||||
|
OUT="$TMPDIR_SELF/solo.env"
|
||||||
|
write_env_file "$OUT" 2>/dev/null
|
||||||
|
|
||||||
|
assert_file_exists "$OUT" "file created for single var"
|
||||||
|
assert_file_contains "SOLO=only" "$OUT" "single var written"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "output: description written as comment before KEY=VALUE"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ENV_VARS=("DESCRIBED" "NODESC")
|
||||||
|
ENV_DESC_DESCRIBED="Explains this variable"
|
||||||
|
ENV_DESC_NODESC=""
|
||||||
|
ENV_VALUE_DESCRIBED="val1"
|
||||||
|
ENV_VALUE_NODESC="val2"
|
||||||
|
|
||||||
|
OUT="$TMPDIR_SELF/desc.env"
|
||||||
|
write_env_file "$OUT" 2>/dev/null
|
||||||
|
|
||||||
|
assert_file_contains "# Explains this variable" "$OUT" "description written as comment"
|
||||||
|
assert_file_contains "DESCRIBED=val1" "$OUT" "KEY=VALUE follows comment"
|
||||||
|
|
||||||
|
# The comment line should immediately precede the variable line
|
||||||
|
desc_line=$(grep -n "# Explains this variable" "$OUT" | cut -d: -f1)
|
||||||
|
var_line=$(grep -n "^DESCRIBED=" "$OUT" | cut -d: -f1)
|
||||||
|
if (( var_line == desc_line + 1 )); then
|
||||||
|
pass "comment is on the line directly before KEY=VALUE"
|
||||||
|
else
|
||||||
|
fail "comment is on the line directly before KEY=VALUE" \
|
||||||
|
"$((desc_line + 1))" "$var_line"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Variable with no description should have no comment line immediately before it
|
||||||
|
nodesc_line=$(grep -n "^NODESC=" "$OUT" | cut -d: -f1)
|
||||||
|
prev_line_content=$(sed -n "$((nodesc_line - 1))p" "$OUT")
|
||||||
|
if [[ -z "$prev_line_content" ]]; then
|
||||||
|
pass "variable with no description has blank line before it (no comment)"
|
||||||
|
else
|
||||||
|
fail "variable with no description has blank line before it (no comment)" \
|
||||||
|
"" "$prev_line_content"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "output: blank line separates each variable block"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ENV_VARS=("A" "B" "C")
|
||||||
|
ENV_DESC_A="First"
|
||||||
|
ENV_DESC_B=""
|
||||||
|
ENV_DESC_C="Third"
|
||||||
|
ENV_VALUE_A="1"
|
||||||
|
ENV_VALUE_B="2"
|
||||||
|
ENV_VALUE_C="3"
|
||||||
|
|
||||||
|
OUT="$TMPDIR_SELF/spacing.env"
|
||||||
|
write_env_file "$OUT" 2>/dev/null
|
||||||
|
|
||||||
|
# Each variable block ends with a blank line; verify A and C have their comments
|
||||||
|
assert_file_contains "# First" "$OUT" "first var description present"
|
||||||
|
assert_file_contains "# Third" "$OUT" "third var description present"
|
||||||
|
|
||||||
|
a_line=$(grep -n "^A=" "$OUT" | cut -d: -f1)
|
||||||
|
b_line=$(grep -n "^B=" "$OUT" | cut -d: -f1)
|
||||||
|
|
||||||
|
# There must be a blank line between A= and B=
|
||||||
|
between=$(sed -n "$((a_line + 1)),$((b_line - 1))p" "$OUT")
|
||||||
|
if [[ -z "$between" || "$between" =~ ^[[:space:]]*$ ]]; then
|
||||||
|
pass "blank line between variable blocks"
|
||||||
|
else
|
||||||
|
fail "blank line between variable blocks" "(blank)" "$between"
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_summary
|
||||||
138
tests/test_parse.sh
Executable file
138
tests/test_parse.sh
Executable file
@@ -0,0 +1,138 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Unit tests for lib/parse.sh
|
||||||
|
|
||||||
|
TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(dirname "$TESTS_DIR")"
|
||||||
|
FIXTURES="$TESTS_DIR/fixtures"
|
||||||
|
|
||||||
|
source "$TESTS_DIR/framework.sh"
|
||||||
|
source "$ROOT_DIR/lib/colors.sh"
|
||||||
|
source "$ROOT_DIR/lib/parse.sh"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "parse: basic variable"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
parse_env_example "$FIXTURES/basic.env.example"
|
||||||
|
|
||||||
|
assert_eq "1" "${#ENV_VARS[@]}" "finds exactly one variable"
|
||||||
|
assert_eq "BASIC_VAR" "${ENV_VARS[0]}" "correct variable name"
|
||||||
|
assert_eq "default" "$ENV_DEFAULT_BASIC_VAR" "correct default value"
|
||||||
|
assert_eq "true" "$ENV_REQUIRED_BASIC_VAR" "required flag set"
|
||||||
|
assert_eq "Description" "$ENV_DESC_BASIC_VAR" "description captured"
|
||||||
|
assert_eq "string" "$ENV_TYPE_BASIC_VAR" "type defaults to string"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "parse: all type annotations"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
parse_env_example "$FIXTURES/all_types.env.example"
|
||||||
|
|
||||||
|
assert_eq "string" "$ENV_TYPE_STR_VAR" "explicit string type"
|
||||||
|
assert_eq "number" "$ENV_TYPE_NUM_VAR" "number type"
|
||||||
|
assert_eq "bool" "$ENV_TYPE_BOOL_VAR" "bool type"
|
||||||
|
assert_eq "secret" "$ENV_TYPE_SECRET_VAR" "secret type"
|
||||||
|
assert_eq "url" "$ENV_TYPE_URL_VAR" "url type"
|
||||||
|
assert_eq "email" "$ENV_TYPE_EMAIL_VAR" "email type"
|
||||||
|
assert_eq "enum:a,b,c" "$ENV_TYPE_ENUM_VAR" "enum type with options"
|
||||||
|
|
||||||
|
assert_eq "true" "$ENV_REQUIRED_SECRET_VAR" "required annotation on secret"
|
||||||
|
assert_eq "false" "$ENV_REQUIRED_STR_VAR" "non-required defaults to false"
|
||||||
|
|
||||||
|
assert_eq "42" "$ENV_DEFAULT_NUM_VAR" "numeric default"
|
||||||
|
assert_eq "https://example.com" "$ENV_DEFAULT_URL_VAR" "URL default"
|
||||||
|
assert_empty "$ENV_DEFAULT_SECRET_VAR" "empty default preserved"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "parse: variable order preserved"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
parse_env_example "$FIXTURES/all_types.env.example"
|
||||||
|
|
||||||
|
assert_eq "7" "${#ENV_VARS[@]}" "all seven variables found"
|
||||||
|
assert_eq "STR_VAR" "${ENV_VARS[0]}" "first variable"
|
||||||
|
assert_eq "NUM_VAR" "${ENV_VARS[1]}" "second variable"
|
||||||
|
assert_eq "BOOL_VAR" "${ENV_VARS[2]}" "third variable"
|
||||||
|
assert_eq "SECRET_VAR" "${ENV_VARS[3]}" "fourth variable"
|
||||||
|
assert_eq "URL_VAR" "${ENV_VARS[4]}" "fifth variable"
|
||||||
|
assert_eq "EMAIL_VAR" "${ENV_VARS[5]}" "sixth variable"
|
||||||
|
assert_eq "ENUM_VAR" "${ENV_VARS[6]}" "seventh variable"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "parse: variable with no annotations"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
parse_env_example "$FIXTURES/no_annotations.env.example"
|
||||||
|
|
||||||
|
assert_eq "2" "${#ENV_VARS[@]}" "finds two variables"
|
||||||
|
assert_eq "string" "$ENV_TYPE_PLAIN_VAR" "type defaults to string"
|
||||||
|
assert_eq "false" "$ENV_REQUIRED_PLAIN_VAR" "required defaults to false"
|
||||||
|
assert_empty "$ENV_DESC_PLAIN_VAR" "no description"
|
||||||
|
assert_eq "value" "$ENV_DEFAULT_PLAIN_VAR" "correct default"
|
||||||
|
assert_empty "$ENV_DEFAULT_ANOTHER_VAR" "empty default preserved"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "parse: multi-line description"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
parse_env_example "$FIXTURES/multiline_desc.env.example"
|
||||||
|
|
||||||
|
assert_contains "First line" "$ENV_DESC_MULTI_VAR" "first description line included"
|
||||||
|
assert_contains "Second line" "$ENV_DESC_MULTI_VAR" "second description line appended"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "parse: orphaned comment block (no variable follows)"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
parse_env_example "$FIXTURES/orphaned_comments.env.example"
|
||||||
|
|
||||||
|
assert_eq "1" "${#ENV_VARS[@]}" "only one real variable found"
|
||||||
|
assert_eq "REAL_VAR" "${ENV_VARS[0]}" "correct variable captured"
|
||||||
|
assert_eq "value" "$ENV_DEFAULT_REAL_VAR" "correct default"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "parse: empty file"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
parse_env_example "$FIXTURES/empty.env.example"
|
||||||
|
|
||||||
|
assert_eq "0" "${#ENV_VARS[@]}" "no variables in empty file"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "parse: value containing equals signs"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
parse_env_example "$FIXTURES/value_with_equals.env.example"
|
||||||
|
|
||||||
|
assert_eq "1" "${#ENV_VARS[@]}" "one variable"
|
||||||
|
assert_contains "sslmode=require" "$ENV_DEFAULT_DB_URL" "first query param preserved"
|
||||||
|
assert_contains "connect_timeout=10" "$ENV_DEFAULT_DB_URL" "second query param preserved"
|
||||||
|
assert_eq "url" "$ENV_TYPE_DB_URL" "type annotation on same var"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "parse: missing file returns error"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
assert_failure "non-existent file returns non-zero" \
|
||||||
|
parse_env_example "/nonexistent/path.env.example"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "parse: real-world example.env.example"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
parse_env_example "$ROOT_DIR/example.env.example"
|
||||||
|
|
||||||
|
assert_eq "7" "${#ENV_VARS[@]}" "all 7 variables"
|
||||||
|
assert_eq "APP_NAME" "${ENV_VARS[0]}" "first var is APP_NAME"
|
||||||
|
assert_eq "APP_ENV" "${ENV_VARS[1]}" "second var is APP_ENV"
|
||||||
|
assert_eq "true" "$ENV_REQUIRED_APP_NAME" "APP_NAME required"
|
||||||
|
assert_eq "true" "$ENV_REQUIRED_APP_ENV" "APP_ENV required"
|
||||||
|
assert_eq "false" "$ENV_REQUIRED_PORT" "PORT optional"
|
||||||
|
assert_eq "number" "$ENV_TYPE_PORT" "PORT is number type"
|
||||||
|
assert_eq "bool" "$ENV_TYPE_DEBUG" "DEBUG is bool type"
|
||||||
|
assert_eq "secret" "$ENV_TYPE_JWT_SECRET" "JWT_SECRET is secret type"
|
||||||
|
assert_eq "url" "$ENV_TYPE_DATABASE_URL" "DATABASE_URL is url type"
|
||||||
|
assert_eq "email" "$ENV_TYPE_ADMIN_EMAIL" "ADMIN_EMAIL is email type"
|
||||||
|
assert_eq "enum:development,staging,production" "$ENV_TYPE_APP_ENV" "APP_ENV enum options"
|
||||||
|
|
||||||
|
print_summary
|
||||||
190
tests/test_prompt.sh
Executable file
190
tests/test_prompt.sh
Executable file
@@ -0,0 +1,190 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Unit tests for prompt helper functions in lib/prompt.sh.
|
||||||
|
#
|
||||||
|
# All interactive functions write prompts to stderr and read from stdin.
|
||||||
|
# Tests pipe input via stdin and capture stdout, suppressing stderr.
|
||||||
|
|
||||||
|
TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(dirname "$TESTS_DIR")"
|
||||||
|
|
||||||
|
source "$TESTS_DIR/framework.sh"
|
||||||
|
source "$ROOT_DIR/lib/colors.sh"
|
||||||
|
source "$ROOT_DIR/lib/validate.sh"
|
||||||
|
source "$ROOT_DIR/lib/prompt.sh"
|
||||||
|
|
||||||
|
# Helpers: feed stdin to a function and capture its stdout
|
||||||
|
feed() {
|
||||||
|
# feed <input_string> <function> [args...]
|
||||||
|
local input="$1"; shift
|
||||||
|
printf "%s" "$input" | "$@" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "_prompt_enum: numeric selection"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
assert_eq "a" "$(feed "1"$'\n' _prompt_enum "V" "a,b,c" "a")" "choice 1 -> first option"
|
||||||
|
assert_eq "b" "$(feed "2"$'\n' _prompt_enum "V" "a,b,c" "a")" "choice 2 -> second option"
|
||||||
|
assert_eq "c" "$(feed "3"$'\n' _prompt_enum "V" "a,b,c" "a")" "choice 3 -> third option"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "_prompt_enum: string selection"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
assert_eq "a" "$(feed "a"$'\n' _prompt_enum "V" "a,b,c" "")" "string 'a' selects a"
|
||||||
|
assert_eq "b" "$(feed "b"$'\n' _prompt_enum "V" "a,b,c" "")" "string 'b' selects b"
|
||||||
|
assert_eq "c" "$(feed "c"$'\n' _prompt_enum "V" "a,b,c" "")" "string 'c' selects c"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "_prompt_enum: empty input uses default"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
assert_eq "a" "$(feed $'\n' _prompt_enum "V" "a,b,c" "a")" "empty -> first default"
|
||||||
|
assert_eq "b" "$(feed $'\n' _prompt_enum "V" "a,b,c" "b")" "empty -> middle default"
|
||||||
|
assert_eq "c" "$(feed $'\n' _prompt_enum "V" "a,b,c" "c")" "empty -> last default"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "_prompt_enum: invalid choice then valid"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Send out-of-range number, then a valid number
|
||||||
|
assert_eq "b" "$(printf "99\n2\n" | _prompt_enum "V" "a,b,c" "a" 2>/dev/null)" \
|
||||||
|
"invalid index followed by valid index"
|
||||||
|
|
||||||
|
# Send unknown string, then valid string
|
||||||
|
assert_eq "c" "$(printf "zzz\nc\n" | _prompt_enum "V" "a,b,c" "a" 2>/dev/null)" \
|
||||||
|
"invalid string followed by valid string"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "_prompt_enum: real-world APP_ENV"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
assert_eq "development" \
|
||||||
|
"$(feed $'\n' _prompt_enum "APP_ENV" "development,staging,production" "development")" \
|
||||||
|
"empty -> development (default)"
|
||||||
|
|
||||||
|
assert_eq "production" \
|
||||||
|
"$(feed "3"$'\n' _prompt_enum "APP_ENV" "development,staging,production" "development")" \
|
||||||
|
"choice 3 -> production"
|
||||||
|
|
||||||
|
assert_eq "staging" \
|
||||||
|
"$(feed "staging"$'\n' _prompt_enum "APP_ENV" "development,staging,production" "development")" \
|
||||||
|
"string 'staging' -> staging"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "_prompt_bool: explicit inputs"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
assert_eq "true" "$(feed "y"$'\n' _prompt_bool "")" "y -> true"
|
||||||
|
assert_eq "true" "$(feed "yes"$'\n' _prompt_bool "")" "yes -> true"
|
||||||
|
assert_eq "true" "$(feed "true"$'\n' _prompt_bool "")" "true -> true"
|
||||||
|
assert_eq "true" "$(feed "1"$'\n' _prompt_bool "")" "1 -> true"
|
||||||
|
assert_eq "false" "$(feed "n"$'\n' _prompt_bool "")" "n -> false"
|
||||||
|
assert_eq "false" "$(feed "no"$'\n' _prompt_bool "")" "no -> false"
|
||||||
|
assert_eq "false" "$(feed "false"$'\n' _prompt_bool "")" "false -> false"
|
||||||
|
assert_eq "false" "$(feed "0"$'\n' _prompt_bool "")" "0 -> false"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "_prompt_bool: empty input uses default"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
assert_eq "true" "$(feed $'\n' _prompt_bool "true")" "empty -> true default"
|
||||||
|
assert_eq "true" "$(feed $'\n' _prompt_bool "yes")" "empty -> yes default normalizes to true"
|
||||||
|
assert_eq "false" "$(feed $'\n' _prompt_bool "false")" "empty -> false default"
|
||||||
|
assert_eq "false" "$(feed $'\n' _prompt_bool "no")" "empty -> no default normalizes to false"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "_prompt_bool: invalid then valid"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
assert_eq "true" "$(printf "banana\ny\n" | _prompt_bool "" 2>/dev/null)" \
|
||||||
|
"invalid input followed by y"
|
||||||
|
assert_eq "false" "$(printf "maybe\nn\n" | _prompt_bool "" 2>/dev/null)" \
|
||||||
|
"invalid input followed by n"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "_prompt_secret: matching values accepted"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
assert_eq "mysecret" \
|
||||||
|
"$(printf "mysecret\nmysecret\n" | _prompt_secret "V" "" 2>/dev/null)" \
|
||||||
|
"matching entries return value"
|
||||||
|
|
||||||
|
assert_eq "p@ss!#\$word" \
|
||||||
|
"$(printf 'p@ss!#$word\np@ss!#$word\n' | _prompt_secret "V" "" 2>/dev/null)" \
|
||||||
|
"special characters in secret preserved"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "_prompt_secret: empty input uses default"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
assert_eq "defaultsecret" \
|
||||||
|
"$(printf "\n" | _prompt_secret "V" "defaultsecret" 2>/dev/null)" \
|
||||||
|
"empty input returns default"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "_prompt_secret: mismatch retries"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# First pair mismatches, second pair matches
|
||||||
|
assert_eq "correct" \
|
||||||
|
"$(printf "wrong\nmismatch\ncorrect\ncorrect\n" | _prompt_secret "V" "" 2>/dev/null)" \
|
||||||
|
"mismatch retries until match"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "prompt_variable: string type stores value"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Use process substitution (<(...)) so prompt_variable runs in the current
|
||||||
|
# shell and its variable assignments (ENV_VALUE_*) persist.
|
||||||
|
|
||||||
|
ENV_VARS=("MYVAR")
|
||||||
|
ENV_DESC_MYVAR="A test variable"
|
||||||
|
ENV_TYPE_MYVAR="string"
|
||||||
|
ENV_REQUIRED_MYVAR="false"
|
||||||
|
ENV_DEFAULT_MYVAR="default_val"
|
||||||
|
|
||||||
|
# Accept default
|
||||||
|
prompt_variable "MYVAR" < <(printf "\n") 2>/dev/null
|
||||||
|
assert_eq "default_val" "$ENV_VALUE_MYVAR" "empty input stores default"
|
||||||
|
|
||||||
|
# Provide a value
|
||||||
|
prompt_variable "MYVAR" < <(printf "custom\n") 2>/dev/null
|
||||||
|
assert_eq "custom" "$ENV_VALUE_MYVAR" "typed value stored"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "prompt_variable: number type re-prompts on invalid input"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ENV_TYPE_MYVAR="number"
|
||||||
|
ENV_DEFAULT_MYVAR="42"
|
||||||
|
|
||||||
|
prompt_variable "MYVAR" < <(printf "abc\n99\n") 2>/dev/null
|
||||||
|
assert_eq "99" "$ENV_VALUE_MYVAR" "invalid number re-prompts, then stores valid"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "prompt_variable: bool type"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ENV_TYPE_MYVAR="bool"
|
||||||
|
ENV_DEFAULT_MYVAR="false"
|
||||||
|
|
||||||
|
prompt_variable "MYVAR" < <(printf "y\n") 2>/dev/null
|
||||||
|
assert_eq "true" "$ENV_VALUE_MYVAR" "bool y stored as true"
|
||||||
|
|
||||||
|
prompt_variable "MYVAR" < <(printf "\n") 2>/dev/null
|
||||||
|
assert_eq "false" "$ENV_VALUE_MYVAR" "bool empty uses false default"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "prompt_variable: enum type"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ENV_TYPE_MYVAR="enum:x,y,z"
|
||||||
|
ENV_DEFAULT_MYVAR="x"
|
||||||
|
|
||||||
|
prompt_variable "MYVAR" < <(printf "2\n") 2>/dev/null
|
||||||
|
assert_eq "y" "$ENV_VALUE_MYVAR" "enum choice 2 stores y"
|
||||||
|
|
||||||
|
prompt_variable "MYVAR" < <(printf "\n") 2>/dev/null
|
||||||
|
assert_eq "x" "$ENV_VALUE_MYVAR" "enum empty uses default x"
|
||||||
|
|
||||||
|
print_summary
|
||||||
131
tests/test_validate.sh
Executable file
131
tests/test_validate.sh
Executable file
@@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Unit tests for lib/validate.sh
|
||||||
|
|
||||||
|
TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(dirname "$TESTS_DIR")"
|
||||||
|
|
||||||
|
source "$TESTS_DIR/framework.sh"
|
||||||
|
source "$ROOT_DIR/lib/colors.sh"
|
||||||
|
source "$ROOT_DIR/lib/validate.sh"
|
||||||
|
|
||||||
|
# Shorthand wrappers to reduce repetition
|
||||||
|
ok() { assert_success "$1" validate_value "VAR" "$2" "$3" "$4"; }
|
||||||
|
bad() { assert_failure "$1" validate_value "VAR" "$2" "$3" "$4"; }
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "validate: required field"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
bad "empty value is invalid when required" "" "string" "true"
|
||||||
|
ok "non-empty value is valid when required" "x" "string" "true"
|
||||||
|
ok "empty value is valid when optional" "" "string" "false"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "validate: number"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ok "positive integer" "42" "number" "false"
|
||||||
|
ok "zero" "0" "number" "false"
|
||||||
|
ok "negative integer" "-7" "number" "false"
|
||||||
|
ok "decimal" "3.14" "number" "false"
|
||||||
|
ok "negative decimal" "-0.5" "number" "false"
|
||||||
|
bad "plain string" "abc" "number" "false"
|
||||||
|
bad "mixed alphanumeric" "42abc" "number" "false"
|
||||||
|
bad "leading decimal" ".5" "number" "false"
|
||||||
|
bad "multiple dots" "1.2.3" "number" "false"
|
||||||
|
ok "empty optional number" "" "number" "false"
|
||||||
|
bad "empty required number" "" "number" "true"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "validate: bool"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ok "true" "true" "bool" "false"
|
||||||
|
ok "false" "false" "bool" "false"
|
||||||
|
ok "yes" "yes" "bool" "false"
|
||||||
|
ok "no" "no" "bool" "false"
|
||||||
|
ok "1" "1" "bool" "false"
|
||||||
|
ok "0" "0" "bool" "false"
|
||||||
|
ok "y" "y" "bool" "false"
|
||||||
|
ok "n" "n" "bool" "false"
|
||||||
|
ok "TRUE (uppercase)" "TRUE" "bool" "false"
|
||||||
|
ok "FALSE (uppercase)" "FALSE" "bool" "false"
|
||||||
|
bad "banana" "banana" "bool" "false"
|
||||||
|
bad "yes-no" "yes-no" "bool" "false"
|
||||||
|
ok "empty optional bool" "" "bool" "false"
|
||||||
|
bad "empty required bool" "" "bool" "true"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "validate: url"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# URL validation only warns — it always returns 0
|
||||||
|
ok "http URL passes" "http://example.com" "url" "false"
|
||||||
|
ok "https URL passes" "https://example.com" "url" "false"
|
||||||
|
ok "non-URL still passes (warn only)" "not-a-url" "url" "false"
|
||||||
|
ok "empty optional url" "" "url" "false"
|
||||||
|
bad "empty required url" "" "url" "true"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "validate: email"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ok "simple email" "user@example.com" "email" "false"
|
||||||
|
ok "subdomain email" "user@mail.example.com" "email" "false"
|
||||||
|
ok "plus addressing" "user+tag@example.com" "email" "false"
|
||||||
|
bad "missing @" "userexample.com" "email" "false"
|
||||||
|
bad "missing domain" "user@" "email" "false"
|
||||||
|
bad "missing TLD" "user@example" "email" "false"
|
||||||
|
bad "spaces in email" "user @example.com" "email" "false"
|
||||||
|
ok "empty optional email" "" "email" "false"
|
||||||
|
bad "empty required email" "" "email" "true"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "validate: enum"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ok "first option" "a" "enum:a,b,c" "false"
|
||||||
|
ok "middle option" "b" "enum:a,b,c" "false"
|
||||||
|
ok "last option" "c" "enum:a,b,c" "false"
|
||||||
|
bad "not in list" "d" "enum:a,b,c" "false"
|
||||||
|
bad "partial match" "ab" "enum:a,b,c" "false"
|
||||||
|
bad "case mismatch" "A" "enum:a,b,c" "false"
|
||||||
|
ok "single-option enum" "only" "enum:only" "false"
|
||||||
|
bad "not the single option" "other" "enum:only" "false"
|
||||||
|
ok "empty optional enum" "" "enum:a,b,c" "false"
|
||||||
|
bad "empty required enum" "" "enum:a,b,c" "true"
|
||||||
|
|
||||||
|
# Enum with real-world values
|
||||||
|
ok "development" "development" "enum:development,staging,production" "true"
|
||||||
|
ok "staging" "staging" "enum:development,staging,production" "true"
|
||||||
|
ok "production" "production" "enum:development,staging,production" "true"
|
||||||
|
bad "test env" "test" "enum:development,staging,production" "true"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "validate: string and secret (no structural validation)"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ok "any string value" "anything goes" "string" "false"
|
||||||
|
ok "string with specials" "f@o=bar!#$%" "string" "false"
|
||||||
|
ok "any secret value" "s3cr3t!@#" "secret" "false"
|
||||||
|
ok "empty optional string" "" "string" "false"
|
||||||
|
bad "empty required string" "" "string" "true"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
suite "normalize_bool"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
assert_eq "true" "$(normalize_bool "true")" "true -> true"
|
||||||
|
assert_eq "true" "$(normalize_bool "yes")" "yes -> true"
|
||||||
|
assert_eq "true" "$(normalize_bool "1")" "1 -> true"
|
||||||
|
assert_eq "true" "$(normalize_bool "y")" "y -> true"
|
||||||
|
assert_eq "true" "$(normalize_bool "TRUE")" "TRUE -> true (case insensitive)"
|
||||||
|
assert_eq "true" "$(normalize_bool "YES")" "YES -> true (case insensitive)"
|
||||||
|
assert_eq "false" "$(normalize_bool "false")" "false -> false"
|
||||||
|
assert_eq "false" "$(normalize_bool "no")" "no -> false"
|
||||||
|
assert_eq "false" "$(normalize_bool "0")" "0 -> false"
|
||||||
|
assert_eq "false" "$(normalize_bool "n")" "n -> false"
|
||||||
|
assert_eq "false" "$(normalize_bool "FALSE")" "FALSE -> false (case insensitive)"
|
||||||
|
assert_eq "banana" "$(normalize_bool "banana")" "unknown value passes through"
|
||||||
|
|
||||||
|
print_summary
|
||||||
Reference in New Issue
Block a user