Files
bin/jinja_template_render.sh
Sebastian Krüger a71eef9006 Add utility scripts collection with auto-generated documentation
This commit introduces a comprehensive collection of utility scripts for shell
automation, color manipulation, and documentation generation:

Core Scripts:
- artifact_github_download.sh: Download GitHub Action artifacts via CLI
- css_color_filter.sh: Generate CSS filter values using SPSA algorithm
- css_color_palette.sh: Generate comprehensive color palettes (monochromatic, triadic, etc.)
- css_json_convert.sh: Convert CSS variables to JSON/YAML formats
- doc_bash_generate.sh: Auto-generate README.md with animated GIF demos
- doc_rust_generate.sh: Generate Rust project documentation
- jinja_template_render.sh: Render Jinja2 templates from CLI
- mime_mp4_gif.sh: Convert MP4 videos to GIF format

Documentation Features:
- Comprehensive README.md with table of contents
- 8 animated GIF demos showing real command examples
- Sandboxed demo execution in temporary directories
- 15-second timeout protection for intensive computations
- Automatic example extraction from --help output

Technical Implementation:
- Pure bash color utilities using only bc for arithmetic
- tput-based color codes for portability
- IFS-safe string parsing using parameter expansion
- Stdout/stderr isolation to prevent contamination
- Base64 encoding for multi-line text preservation

All scripts include detailed --help documentation with usage examples.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 03:10:19 +01:00

750 lines
22 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 "$@"