750 lines
22 KiB
Bash
750 lines
22 KiB
Bash
|
|
#!/usr/bin/env bash
|
|||
|
|
|
|||
|
|
##############################################################
|
|||
|
|
# Jinja2 Template Renderer - Ninja Edition
|
|||
|
|
# A sophisticated template rendering engine with style
|
|||
|
|
##############################################################
|
|||
|
|
|
|||
|
|
set -uo pipefail
|
|||
|
|
# Note: Using -u and pipefail, but not -e to allow graceful error handling
|
|||
|
|
|
|||
|
|
# Terminal colors using tput
|
|||
|
|
BLACK="" RED="" GREEN="" YELLOW="" BLUE="" MAGENTA="" CYAN="" WHITE="" GRAY=""
|
|||
|
|
BOLD="" DIM="" ITALIC="" UNDERLINE="" RESET=""
|
|||
|
|
|
|||
|
|
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then
|
|||
|
|
COLORS=$(tput colors 2>/dev/null || echo 0)
|
|||
|
|
if [[ ${COLORS:-0} -ge 8 ]]; then
|
|||
|
|
BLACK=$(tput setaf 0 2>/dev/null || echo "")
|
|||
|
|
RED=$(tput setaf 1 2>/dev/null || echo "")
|
|||
|
|
GREEN=$(tput setaf 2 2>/dev/null || echo "")
|
|||
|
|
YELLOW=$(tput setaf 3 2>/dev/null || echo "")
|
|||
|
|
BLUE=$(tput setaf 4 2>/dev/null || echo "")
|
|||
|
|
MAGENTA=$(tput setaf 5 2>/dev/null || echo "")
|
|||
|
|
CYAN=$(tput setaf 6 2>/dev/null || echo "")
|
|||
|
|
WHITE=$(tput setaf 7 2>/dev/null || echo "")
|
|||
|
|
GRAY=$(tput setaf 8 2>/dev/null || echo "")
|
|||
|
|
BOLD=$(tput bold 2>/dev/null || echo "")
|
|||
|
|
DIM=$(tput dim 2>/dev/null || echo "")
|
|||
|
|
ITALIC=$(tput sitm 2>/dev/null || echo "")
|
|||
|
|
UNDERLINE=$(tput smul 2>/dev/null || echo "")
|
|||
|
|
RESET=$(tput sgr0 2>/dev/null || echo "")
|
|||
|
|
fi
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# Ninja-themed icons (ASCII compatible)
|
|||
|
|
readonly NINJA="[NINJA]"
|
|||
|
|
readonly SHURIKEN="*"
|
|||
|
|
readonly KATANA=">>"
|
|||
|
|
readonly SCROLL="[SCROLL]"
|
|||
|
|
readonly FIRE="[FIRE]"
|
|||
|
|
readonly STAR="*"
|
|||
|
|
readonly CHECK="[OK]"
|
|||
|
|
readonly CROSS="[X]"
|
|||
|
|
readonly ARROW="-->"
|
|||
|
|
readonly LIGHTNING="[!]"
|
|||
|
|
readonly GEAR="[GEAR]"
|
|||
|
|
readonly TARGET="[TARGET]"
|
|||
|
|
|
|||
|
|
# Script configuration
|
|||
|
|
OUTPUT_DIR="${PWD}/output"
|
|||
|
|
TEMPLATES=()
|
|||
|
|
VAR_FILES=()
|
|||
|
|
CLI_VARS=()
|
|||
|
|
VERBOSE=false
|
|||
|
|
PREVIEW=false
|
|||
|
|
WATCH=false
|
|||
|
|
STRICT=false
|
|||
|
|
DRY_RUN=false
|
|||
|
|
|
|||
|
|
# Performance tracking
|
|||
|
|
START_TIME=0
|
|||
|
|
TEMPLATE_COUNT=0
|
|||
|
|
VAR_COUNT=0
|
|||
|
|
|
|||
|
|
##############################################################
|
|||
|
|
# Ninja UI Functions
|
|||
|
|
##############################################################
|
|||
|
|
|
|||
|
|
print_ninja_banner() {
|
|||
|
|
echo "${MAGENTA}${BOLD}"
|
|||
|
|
cat << 'EOF'
|
|||
|
|
TPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPW
|
|||
|
|
Q Q
|
|||
|
|
Q <20> JINJA2 TEMPLATE RENDERER - NINJA EDITION Q
|
|||
|
|
Q Q
|
|||
|
|
Q >w Fast " Powerful " Stealthy Template Magic <20> Q
|
|||
|
|
Q Q
|
|||
|
|
ZPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP]
|
|||
|
|
EOF
|
|||
|
|
echo "${RESET}"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print_ninja_art() {
|
|||
|
|
echo "${CYAN}${DIM}"
|
|||
|
|
cat << 'EOF'
|
|||
|
|
___
|
|||
|
|
__/_ `. .-"""-.
|
|||
|
|
\_,` | \-' / )`-')
|
|||
|
|
"") `"` \ ((`"`
|
|||
|
|
___Y , .'7 /|
|
|||
|
|
(_,___/...-` (_/_/
|
|||
|
|
|
|||
|
|
Silently rendering your templates...
|
|||
|
|
EOF
|
|||
|
|
echo "${RESET}"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Animated ninja message
|
|||
|
|
ninja_say() {
|
|||
|
|
local message="$1"
|
|||
|
|
echo "${CYAN}${NINJA}${RESET} ${BOLD}${message}${RESET}"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ninja_success() {
|
|||
|
|
echo "${GREEN}${CHECK} ${NINJA}${RESET} ${GREEN}$*${RESET}"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ninja_error() {
|
|||
|
|
echo "${RED}${CROSS} ${NINJA}${RESET} ${RED}$*${RESET}" >&2
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ninja_warn() {
|
|||
|
|
echo "${YELLOW}${LIGHTNING}${RESET} ${YELLOW}$*${RESET}"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ninja_info() {
|
|||
|
|
echo "${BLUE}${SHURIKEN}${RESET} ${BLUE}$*${RESET}"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ninja_verbose() {
|
|||
|
|
[[ "$VERBOSE" == true ]] && echo "${DIM} ${ARROW} $*${RESET}"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ninja_progress() {
|
|||
|
|
local current=$1
|
|||
|
|
local total=$2
|
|||
|
|
local item=$3
|
|||
|
|
local percent=$((current * 100 / total))
|
|||
|
|
local bar_length=30
|
|||
|
|
local filled=$((bar_length * current / total))
|
|||
|
|
local bar=""
|
|||
|
|
|
|||
|
|
for ((i=0; i<bar_length; i++)); do
|
|||
|
|
if ((i < filled)); then
|
|||
|
|
bar+="${FIRE}"
|
|||
|
|
else
|
|||
|
|
bar+="<22>"
|
|||
|
|
fi
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
echo -ne "\r${CYAN}${GEAR}${RESET} [${bar}] ${percent}% - ${ITALIC}${item}${RESET}"
|
|||
|
|
|
|||
|
|
if ((current == total)); then
|
|||
|
|
echo "" # New line when complete
|
|||
|
|
fi
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Typing animation effect
|
|||
|
|
ninja_type() {
|
|||
|
|
local text="$1"
|
|||
|
|
local delay="${2:-0.03}"
|
|||
|
|
|
|||
|
|
for ((i=0; i<${#text}; i++)); do
|
|||
|
|
echo -n "${text:$i:1}"
|
|||
|
|
sleep "$delay"
|
|||
|
|
done
|
|||
|
|
echo ""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
##############################################################
|
|||
|
|
# Help and Usage
|
|||
|
|
##############################################################
|
|||
|
|
|
|||
|
|
show_help() {
|
|||
|
|
cat << EOF
|
|||
|
|
${BOLD}${MAGENTA}JINJA2 TEMPLATE RENDERER - NINJA EDITION${RESET}
|
|||
|
|
|
|||
|
|
${BOLD}USAGE:${RESET}
|
|||
|
|
$(basename "$0") [OPTIONS] <template> [template...]
|
|||
|
|
|
|||
|
|
${BOLD}DESCRIPTION:${RESET}
|
|||
|
|
A sophisticated Jinja2 template rendering engine with support for
|
|||
|
|
multiple variable sources, glob patterns, and ninja-style operations.
|
|||
|
|
|
|||
|
|
${BOLD}ARGUMENTS:${RESET}
|
|||
|
|
<template> Template file(s) to render (glob patterns supported)
|
|||
|
|
Examples: template.j2, templates/*.j2, **/*.jinja2
|
|||
|
|
|
|||
|
|
${BOLD}OPTIONS:${RESET}
|
|||
|
|
-o, --output DIR Output directory (default: ./output)
|
|||
|
|
-v, --var KEY=VALUE Define template variable (can be used multiple times)
|
|||
|
|
-f, --file FILE Load variables from JSON/YAML file (repeatable)
|
|||
|
|
-V, --verbose Enable verbose ninja commentary
|
|||
|
|
-p, --preview Preview rendered output without saving
|
|||
|
|
-s, --strict Enable strict mode (fail on undefined variables)
|
|||
|
|
-d, --dry-run Perform dry run without writing files
|
|||
|
|
-w, --watch Watch templates and re-render on changes (experimental)
|
|||
|
|
-h, --help Show this legendary scroll of knowledge
|
|||
|
|
|
|||
|
|
${BOLD}VARIABLE SOURCES:${RESET}
|
|||
|
|
Variables are merged in this order (later sources override earlier):
|
|||
|
|
1. YAML files (loaded via yq)
|
|||
|
|
2. JSON files (loaded via jq)
|
|||
|
|
3. CLI variables (-v KEY=VALUE)
|
|||
|
|
|
|||
|
|
${BOLD}EXAMPLES:${RESET}
|
|||
|
|
${DIM}# Render single template with CLI variables${RESET}
|
|||
|
|
$(basename "$0") template.j2 -v name=Ninja -v level=Master
|
|||
|
|
|
|||
|
|
${DIM}# Render multiple templates with YAML config${RESET}
|
|||
|
|
$(basename "$0") templates/*.j2 -f config.yaml -o dist/
|
|||
|
|
|
|||
|
|
${DIM}# Render with multiple variable sources${RESET}
|
|||
|
|
$(basename "$0") app.j2 -f base.yaml -f env.json -v debug=true
|
|||
|
|
|
|||
|
|
${DIM}# Preview without saving${RESET}
|
|||
|
|
$(basename "$0") template.j2 -f vars.yaml --preview --verbose
|
|||
|
|
|
|||
|
|
${DIM}# Strict mode with dry run${RESET}
|
|||
|
|
$(basename "$0") *.j2 -f vars.json --strict --dry-run
|
|||
|
|
|
|||
|
|
${BOLD}TEMPLATE SYNTAX:${RESET}
|
|||
|
|
Jinja2 template example:
|
|||
|
|
${DIM}
|
|||
|
|
Hello {{ name }}!
|
|||
|
|
{% for item in items %}
|
|||
|
|
- {{ item }}
|
|||
|
|
{% endfor %}
|
|||
|
|
{% if debug %}Debug mode enabled{% endif %}
|
|||
|
|
${RESET}
|
|||
|
|
|
|||
|
|
${BOLD}REQUIREMENTS:${RESET}
|
|||
|
|
- Python 3 with jinja2 package
|
|||
|
|
- jq (for JSON parsing)
|
|||
|
|
- yq (for YAML parsing)
|
|||
|
|
|
|||
|
|
${MAGENTA}${NINJA} May your templates be swift and your renders be flawless! ${KATANA}${RESET}
|
|||
|
|
|
|||
|
|
EOF
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
##############################################################
|
|||
|
|
# Variable Collection Functions
|
|||
|
|
##############################################################
|
|||
|
|
|
|||
|
|
# Parse CLI variable in KEY=VALUE format
|
|||
|
|
parse_cli_var() {
|
|||
|
|
local var="$1"
|
|||
|
|
|
|||
|
|
if [[ ! "$var" =~ ^[a-zA-Z_][a-zA-Z0-9_]*=.* ]]; then
|
|||
|
|
ninja_error "Invalid variable format: $var (expected KEY=VALUE)"
|
|||
|
|
return 1
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
CLI_VARS+=("$var")
|
|||
|
|
ninja_verbose "Added CLI variable: ${CYAN}$var${RESET}"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Load variables from YAML file
|
|||
|
|
load_yaml_vars() {
|
|||
|
|
local file="$1"
|
|||
|
|
|
|||
|
|
if [[ ! -f "$file" ]]; then
|
|||
|
|
ninja_error "YAML file not found: $file"
|
|||
|
|
return 1
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
if ! command -v yq &> /dev/null; then
|
|||
|
|
ninja_error "yq not found. Install with: pip install yq"
|
|||
|
|
return 1
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
ninja_verbose "Loading YAML variables from: ${MAGENTA}$file${RESET}"
|
|||
|
|
|
|||
|
|
# Validate YAML syntax
|
|||
|
|
if ! yq -e '.' "$file" &> /dev/null; then
|
|||
|
|
ninja_error "Invalid YAML syntax in: $file"
|
|||
|
|
return 1
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
VAR_FILES+=("yaml:$file")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Load variables from JSON file
|
|||
|
|
load_json_vars() {
|
|||
|
|
local file="$1"
|
|||
|
|
|
|||
|
|
if [[ ! -f "$file" ]]; then
|
|||
|
|
ninja_error "JSON file not found: $file"
|
|||
|
|
return 1
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
if ! command -v jq &> /dev/null; then
|
|||
|
|
ninja_error "jq not found. Install with: apt-get install jq"
|
|||
|
|
return 1
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
ninja_verbose "Loading JSON variables from: ${MAGENTA}$file${RESET}"
|
|||
|
|
|
|||
|
|
# Validate JSON syntax
|
|||
|
|
if ! jq empty "$file" &> /dev/null; then
|
|||
|
|
ninja_error "Invalid JSON syntax in: $file"
|
|||
|
|
return 1
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
VAR_FILES+=("json:$file")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Build Python dictionary from all variable sources
|
|||
|
|
build_context_dict() {
|
|||
|
|
local python_dict="{"
|
|||
|
|
local first=true
|
|||
|
|
|
|||
|
|
# Process variable files (YAML and JSON)
|
|||
|
|
for var_file in "${VAR_FILES[@]}"; do
|
|||
|
|
local type="${var_file%%:*}"
|
|||
|
|
local file="${var_file#*:}"
|
|||
|
|
|
|||
|
|
if [[ "$type" == "yaml" ]]; then
|
|||
|
|
# Convert YAML to JSON using yq
|
|||
|
|
local json_content
|
|||
|
|
json_content=$(yq -o json '.' "$file" 2>/dev/null) || {
|
|||
|
|
ninja_error "Failed to parse YAML: $file"
|
|||
|
|
return 1
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Extract key-value pairs and fix Python boolean casing
|
|||
|
|
while IFS= read -r line; do
|
|||
|
|
if [[ -n "$line" ]]; then
|
|||
|
|
# Fix boolean values: true -> True, false -> False
|
|||
|
|
line="${line//: true/: True}"
|
|||
|
|
line="${line//: false/: False}"
|
|||
|
|
# Convert hyphens to underscores in key names for Jinja2 compatibility
|
|||
|
|
# Extract key and value, replace hyphens in key only
|
|||
|
|
if [[ "$line" =~ ^\"([^\"]+)\":[[:space:]]*(.+)$ ]]; then
|
|||
|
|
local key="${BASH_REMATCH[1]}"
|
|||
|
|
local value="${BASH_REMATCH[2]}"
|
|||
|
|
key="${key//-/_}" # Replace all hyphens with underscores
|
|||
|
|
line="\"$key\": $value"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
if [[ "$first" == true ]]; then
|
|||
|
|
first=false
|
|||
|
|
else
|
|||
|
|
python_dict+=", "
|
|||
|
|
fi
|
|||
|
|
python_dict+="$line"
|
|||
|
|
fi
|
|||
|
|
done < <(echo "$json_content" | jq -r 'to_entries | .[] | "\"\(.key)\": \(.value | tojson)"')
|
|||
|
|
|
|||
|
|
elif [[ "$type" == "json" ]]; then
|
|||
|
|
# Process JSON file and fix Python boolean casing
|
|||
|
|
while IFS= read -r line; do
|
|||
|
|
if [[ -n "$line" ]]; then
|
|||
|
|
# Fix boolean values: true -> True, false -> False
|
|||
|
|
line="${line//: true/: True}"
|
|||
|
|
line="${line//: false/: False}"
|
|||
|
|
# Convert hyphens to underscores in key names for Jinja2 compatibility
|
|||
|
|
if [[ "$line" =~ ^\"([^\"]+)\":[[:space:]]*(.+)$ ]]; then
|
|||
|
|
local key="${BASH_REMATCH[1]}"
|
|||
|
|
local value="${BASH_REMATCH[2]}"
|
|||
|
|
key="${key//-/_}" # Replace all hyphens with underscores
|
|||
|
|
line="\"$key\": $value"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
if [[ "$first" == true ]]; then
|
|||
|
|
first=false
|
|||
|
|
else
|
|||
|
|
python_dict+=", "
|
|||
|
|
fi
|
|||
|
|
python_dict+="$line"
|
|||
|
|
fi
|
|||
|
|
done < <(jq -r 'to_entries | .[] | "\"\(.key)\": \(.value | tojson)"' "$file")
|
|||
|
|
fi
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
# Process CLI variables (these override file variables)
|
|||
|
|
for var in "${CLI_VARS[@]}"; do
|
|||
|
|
local key="${var%%=*}"
|
|||
|
|
local value="${var#*=}"
|
|||
|
|
|
|||
|
|
if [[ "$first" == true ]]; then
|
|||
|
|
first=false
|
|||
|
|
else
|
|||
|
|
python_dict+=", "
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# Auto-detect value type and format for Python
|
|||
|
|
if [[ "$value" =~ ^[0-9]+$ ]]; then
|
|||
|
|
# Integer
|
|||
|
|
python_dict+="\"$key\": $value"
|
|||
|
|
elif [[ "$value" =~ ^[0-9]+\.[0-9]+$ ]]; then
|
|||
|
|
# Float
|
|||
|
|
python_dict+="\"$key\": $value"
|
|||
|
|
elif [[ "$value" == "true" ]] || [[ "$value" == "false" ]]; then
|
|||
|
|
# Boolean
|
|||
|
|
python_dict+="\"$key\": ${value^}"
|
|||
|
|
elif [[ "$value" == "null" ]]; then
|
|||
|
|
# Null
|
|||
|
|
python_dict+="\"$key\": None"
|
|||
|
|
elif [[ "$value" =~ ^\[.*\]$ ]] || [[ "$value" =~ ^\{.*\}$ ]]; then
|
|||
|
|
# JSON array or object
|
|||
|
|
python_dict+="\"$key\": $value"
|
|||
|
|
else
|
|||
|
|
# String (escape quotes)
|
|||
|
|
value="${value//\"/\\\"}"
|
|||
|
|
python_dict+="\"$key\": \"$value\""
|
|||
|
|
fi
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
python_dict+="}"
|
|||
|
|
echo "$python_dict"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
##############################################################
|
|||
|
|
# Template Rendering Engine
|
|||
|
|
##############################################################
|
|||
|
|
|
|||
|
|
render_template() {
|
|||
|
|
local template_file="$1"
|
|||
|
|
local output_file="$2"
|
|||
|
|
local context="$3"
|
|||
|
|
|
|||
|
|
ninja_verbose "Rendering: ${CYAN}$(basename "$template_file")${RESET}"
|
|||
|
|
|
|||
|
|
# Create Python rendering script
|
|||
|
|
local python_script=$(cat <<'PYTHON_EOF'
|
|||
|
|
import sys
|
|||
|
|
import json
|
|||
|
|
import os
|
|||
|
|
from jinja2 import Environment, FileSystemLoader, StrictUndefined, TemplateError
|
|||
|
|
|
|||
|
|
def render_template(template_path, context_dict, strict=False):
|
|||
|
|
"""Render a Jinja2 template with given context."""
|
|||
|
|
try:
|
|||
|
|
# Setup Jinja2 environment
|
|||
|
|
template_dir = os.path.dirname(os.path.abspath(template_path))
|
|||
|
|
template_name = os.path.basename(template_path)
|
|||
|
|
|
|||
|
|
env_kwargs = {
|
|||
|
|
'loader': FileSystemLoader(template_dir),
|
|||
|
|
'trim_blocks': True,
|
|||
|
|
'lstrip_blocks': True,
|
|||
|
|
'keep_trailing_newline': True,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if strict:
|
|||
|
|
env_kwargs['undefined'] = StrictUndefined
|
|||
|
|
|
|||
|
|
env = Environment(**env_kwargs)
|
|||
|
|
|
|||
|
|
# Load and render template
|
|||
|
|
template = env.get_template(template_name)
|
|||
|
|
rendered = template.render(**context_dict)
|
|||
|
|
|
|||
|
|
return rendered, None
|
|||
|
|
|
|||
|
|
except TemplateError as e:
|
|||
|
|
return None, f"Template Error: {e}"
|
|||
|
|
except Exception as e:
|
|||
|
|
return None, f"Unexpected Error: {e}"
|
|||
|
|
|
|||
|
|
if __name__ == '__main__':
|
|||
|
|
if len(sys.argv) != 4:
|
|||
|
|
print("Usage: script.py <template> <context_json> <strict>", file=sys.stderr)
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
template_path = sys.argv[1]
|
|||
|
|
context_json = sys.argv[2]
|
|||
|
|
strict = sys.argv[3].lower() == 'true'
|
|||
|
|
|
|||
|
|
# Parse context
|
|||
|
|
try:
|
|||
|
|
context = eval(context_json) # Safe in this controlled context
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"Failed to parse context: {e}", file=sys.stderr)
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
# Render template
|
|||
|
|
result, error = render_template(template_path, context, strict)
|
|||
|
|
|
|||
|
|
if error:
|
|||
|
|
print(error, file=sys.stderr)
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
print(result, end='')
|
|||
|
|
PYTHON_EOF
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Check if Python and Jinja2 are available
|
|||
|
|
if ! python3 -c "import jinja2" 2>/dev/null; then
|
|||
|
|
ninja_error "Python jinja2 module not found. Install with: pip install jinja2"
|
|||
|
|
return 1
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# Render template
|
|||
|
|
local rendered
|
|||
|
|
local strict_flag="false"
|
|||
|
|
[[ "$STRICT" == true ]] && strict_flag="true"
|
|||
|
|
|
|||
|
|
if rendered=$(python3 -c "$python_script" "$template_file" "$context" "$strict_flag" 2>&1); then
|
|||
|
|
if [[ "$PREVIEW" == true ]]; then
|
|||
|
|
# Preview mode - just display
|
|||
|
|
echo ""
|
|||
|
|
echo "${CYAN}${BOLD}PPP Preview: $(basename "$template_file") PPP${RESET}"
|
|||
|
|
echo "$rendered"
|
|||
|
|
echo "${CYAN}${BOLD}PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP${RESET}"
|
|||
|
|
echo ""
|
|||
|
|
elif [[ "$DRY_RUN" == true ]]; then
|
|||
|
|
ninja_verbose "Would write to: ${GREEN}$output_file${RESET}"
|
|||
|
|
else
|
|||
|
|
# Write to output file
|
|||
|
|
echo "$rendered" > "$output_file"
|
|||
|
|
ninja_verbose "Wrote: ${GREEN}$output_file${RESET}"
|
|||
|
|
fi
|
|||
|
|
return 0
|
|||
|
|
else
|
|||
|
|
ninja_error "Failed to render $(basename "$template_file")"
|
|||
|
|
echo "${RED}$rendered${RESET}" >&2
|
|||
|
|
return 1
|
|||
|
|
fi
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
##############################################################
|
|||
|
|
# Template Discovery and Processing
|
|||
|
|
##############################################################
|
|||
|
|
|
|||
|
|
discover_templates() {
|
|||
|
|
local pattern="$1"
|
|||
|
|
local found_templates=()
|
|||
|
|
|
|||
|
|
# Check if pattern contains glob characters
|
|||
|
|
if [[ "$pattern" == *"*"* ]] || [[ "$pattern" == *"?"* ]]; then
|
|||
|
|
# Glob expansion
|
|||
|
|
shopt -s nullglob globstar
|
|||
|
|
for file in $pattern; do
|
|||
|
|
if [[ -f "$file" ]]; then
|
|||
|
|
found_templates+=("$file")
|
|||
|
|
fi
|
|||
|
|
done
|
|||
|
|
shopt -u nullglob globstar
|
|||
|
|
else
|
|||
|
|
# Direct file
|
|||
|
|
if [[ -f "$pattern" ]]; then
|
|||
|
|
found_templates+=("$pattern")
|
|||
|
|
else
|
|||
|
|
ninja_error "Template not found: $pattern"
|
|||
|
|
return 1
|
|||
|
|
fi
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
if [[ ${#found_templates[@]} -eq 0 ]]; then
|
|||
|
|
ninja_error "No templates found matching: $pattern"
|
|||
|
|
return 1
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
printf '%s\n' "${found_templates[@]}"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
process_templates() {
|
|||
|
|
local context="$1"
|
|||
|
|
local all_templates=()
|
|||
|
|
|
|||
|
|
# Discover all templates from patterns
|
|||
|
|
ninja_info "Discovering templates with ninja precision..."
|
|||
|
|
|
|||
|
|
for pattern in "${TEMPLATES[@]}"; do
|
|||
|
|
while IFS= read -r template; do
|
|||
|
|
all_templates+=("$template")
|
|||
|
|
done < <(discover_templates "$pattern")
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
TEMPLATE_COUNT=${#all_templates[@]}
|
|||
|
|
|
|||
|
|
if [[ $TEMPLATE_COUNT -eq 0 ]]; then
|
|||
|
|
ninja_error "No templates to render!"
|
|||
|
|
return 1
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
ninja_success "Found ${BOLD}$TEMPLATE_COUNT${RESET}${GREEN} template(s)${RESET}"
|
|||
|
|
|
|||
|
|
# Create output directory
|
|||
|
|
if [[ "$PREVIEW" == false ]] && [[ "$DRY_RUN" == false ]]; then
|
|||
|
|
mkdir -p "$OUTPUT_DIR"
|
|||
|
|
ninja_verbose "Output directory: ${GREEN}$OUTPUT_DIR${RESET}"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# Render each template
|
|||
|
|
echo ""
|
|||
|
|
ninja_say "Initiating stealth rendering sequence..."
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
local success_count=0
|
|||
|
|
local fail_count=0
|
|||
|
|
|
|||
|
|
for i in "${!all_templates[@]}"; do
|
|||
|
|
local template="${all_templates[$i]}"
|
|||
|
|
local template_name
|
|||
|
|
template_name=$(basename "$template")
|
|||
|
|
local output_name="${template_name%.j2}"
|
|||
|
|
output_name="${output_name%.jinja2}"
|
|||
|
|
output_name="${output_name%.jinja}"
|
|||
|
|
local output_file="$OUTPUT_DIR/$output_name"
|
|||
|
|
|
|||
|
|
# Show progress
|
|||
|
|
ninja_progress $((i + 1)) "$TEMPLATE_COUNT" "$template_name"
|
|||
|
|
|
|||
|
|
# Render template
|
|||
|
|
if render_template "$template" "$output_file" "$context"; then
|
|||
|
|
((success_count++))
|
|||
|
|
else
|
|||
|
|
((fail_count++))
|
|||
|
|
fi
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
# Summary
|
|||
|
|
if [[ $fail_count -eq 0 ]]; then
|
|||
|
|
ninja_success "All $TEMPLATE_COUNT templates rendered successfully! ${STAR}"
|
|||
|
|
else
|
|||
|
|
ninja_warn "Rendered: $success_count success, $fail_count failed"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
##############################################################
|
|||
|
|
# Main Function
|
|||
|
|
##############################################################
|
|||
|
|
|
|||
|
|
main() {
|
|||
|
|
START_TIME=$(date +%s)
|
|||
|
|
|
|||
|
|
# Parse arguments
|
|||
|
|
while [[ $# -gt 0 ]]; do
|
|||
|
|
case $1 in
|
|||
|
|
-o|--output)
|
|||
|
|
OUTPUT_DIR="$2"
|
|||
|
|
shift 2
|
|||
|
|
;;
|
|||
|
|
-v|--var)
|
|||
|
|
parse_cli_var "$2"
|
|||
|
|
shift 2
|
|||
|
|
;;
|
|||
|
|
-f|--file)
|
|||
|
|
VAR_FILE_ARG="$2"
|
|||
|
|
VAR_FILE_EXT="${VAR_FILE_ARG##*.}"
|
|||
|
|
|
|||
|
|
case "$VAR_FILE_EXT" in
|
|||
|
|
yaml|yml)
|
|||
|
|
load_yaml_vars "$VAR_FILE_ARG"
|
|||
|
|
;;
|
|||
|
|
json)
|
|||
|
|
load_json_vars "$VAR_FILE_ARG"
|
|||
|
|
;;
|
|||
|
|
*)
|
|||
|
|
ninja_error "Unsupported file format: .$VAR_FILE_EXT (use .yaml, .yml, or .json)"
|
|||
|
|
exit 1
|
|||
|
|
;;
|
|||
|
|
esac
|
|||
|
|
shift 2
|
|||
|
|
;;
|
|||
|
|
-V|--verbose)
|
|||
|
|
VERBOSE=true
|
|||
|
|
shift
|
|||
|
|
;;
|
|||
|
|
-p|--preview)
|
|||
|
|
PREVIEW=true
|
|||
|
|
shift
|
|||
|
|
;;
|
|||
|
|
-s|--strict)
|
|||
|
|
STRICT=true
|
|||
|
|
shift
|
|||
|
|
;;
|
|||
|
|
-d|--dry-run)
|
|||
|
|
DRY_RUN=true
|
|||
|
|
shift
|
|||
|
|
;;
|
|||
|
|
-w|--watch)
|
|||
|
|
WATCH=true
|
|||
|
|
shift
|
|||
|
|
;;
|
|||
|
|
-h|--help)
|
|||
|
|
show_help
|
|||
|
|
exit 0
|
|||
|
|
;;
|
|||
|
|
-*)
|
|||
|
|
ninja_error "Unknown option: $1"
|
|||
|
|
echo "Use -h or --help for usage information"
|
|||
|
|
exit 1
|
|||
|
|
;;
|
|||
|
|
*)
|
|||
|
|
TEMPLATES+=("$1")
|
|||
|
|
shift
|
|||
|
|
;;
|
|||
|
|
esac
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
# Show ninja banner
|
|||
|
|
print_ninja_banner
|
|||
|
|
|
|||
|
|
if [[ "$VERBOSE" == true ]]; then
|
|||
|
|
print_ninja_art
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# Validate inputs
|
|||
|
|
if [[ ${#TEMPLATES[@]} -eq 0 ]]; then
|
|||
|
|
ninja_error "No templates specified!"
|
|||
|
|
echo "Use -h or --help for usage information"
|
|||
|
|
exit 1
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# Build context dictionary
|
|||
|
|
ninja_info "Gathering ninja scrolls and ancient wisdom..."
|
|||
|
|
|
|||
|
|
local context
|
|||
|
|
if ! context=$(build_context_dict); then
|
|||
|
|
exit 1
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
VAR_COUNT=$(echo "$context" | grep -o '"[^"]*":' | wc -l)
|
|||
|
|
|
|||
|
|
if [[ "$VERBOSE" == true ]]; then
|
|||
|
|
ninja_verbose "Context dictionary built with ${CYAN}$VAR_COUNT${RESET} variables"
|
|||
|
|
echo "${DIM}$context${RESET}"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
ninja_success "Loaded ${BOLD}$VAR_COUNT${RESET}${GREEN} variable(s)${RESET}"
|
|||
|
|
|
|||
|
|
# Show mode indicators
|
|||
|
|
[[ "$STRICT" == true ]] && ninja_warn "Strict mode: ${BOLD}ENABLED${RESET}"
|
|||
|
|
[[ "$DRY_RUN" == true ]] && ninja_warn "Dry run mode: ${BOLD}ENABLED${RESET}"
|
|||
|
|
[[ "$PREVIEW" == true ]] && ninja_info "Preview mode: ${BOLD}ENABLED${RESET}"
|
|||
|
|
|
|||
|
|
# Process templates
|
|||
|
|
if ! process_templates "$context"; then
|
|||
|
|
exit 1
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# Calculate execution time
|
|||
|
|
local end_time
|
|||
|
|
end_time=$(date +%s)
|
|||
|
|
local duration=$((end_time - START_TIME))
|
|||
|
|
|
|||
|
|
echo ""
|
|||
|
|
ninja_success "Mission complete in ${BOLD}${duration}s${RESET} ${TARGET}"
|
|||
|
|
|
|||
|
|
if [[ "$PREVIEW" == false ]] && [[ "$DRY_RUN" == false ]]; then
|
|||
|
|
ninja_info "Output directory: ${CYAN}${BOLD}$OUTPUT_DIR${RESET}"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
echo ""
|
|||
|
|
echo "${MAGENTA}${NINJA} The ninja vanishes into the shadows... ${KATANA}${RESET}"
|
|||
|
|
echo ""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Execute main function
|
|||
|
|
main "$@"
|