tmpltool 1.5.0

A fast and simple command-line template rendering tool using MiniJinja templates with environment variables
Documentation
# Command Execution Examples
# ==========================================
# SECURITY WARNING: This template requires --trust mode
# Run with: tmpltool --trust examples/exec-functions.md.tmpltool

## Two Functions Available

# exec(command)      - Simple: returns stdout, throws error on failure
# exec_raw(command)  - Advanced: returns object with exit_code, stdout, stderr

## Simple exec() - For Common Cases

### Basic usage - output goes directly to template
Hostname: {{ exec(command="hostname") }}

### Use in variable
{% set git_hash = exec(command="git rev-parse --short HEAD 2>/dev/null || echo 'unknown'") %}
Git commit: {{ git_hash | trim }}

### Multiple simple commands
System: {{ exec(command="uname -s") | trim }}
Kernel: {{ exec(command="uname -r") | trim }}

## Advanced exec_raw() - For Full Control

### Check exit code and handle different cases
{% set result = exec_raw(command="grep -q 'root' /etc/passwd") %}
{% if result.exit_code == 0 %}
✓ Root user found in /etc/passwd
{% elif result.exit_code == 1 %}
✗ Root user not found
{% else %}
⚠ Error checking /etc/passwd: {{ result.stderr }}
{% endif %}

### Handle commands that might fail
{% set result = exec_raw(command="which docker") %}
{% if result.success %}
Docker installed at: {{ result.stdout | trim }}
{% else %}
Docker not found (exit {{ result.exit_code }})
{% endif %}

### Access stderr for debugging
{% set result = exec_raw(command="ls /nonexistent_path 2>&1") %}
Exit code: {{ result.exit_code }}
Stdout: {{ result.stdout }}
Stderr: {{ result.stderr }}
Success: {{ result.success }}

## Comparison: exec() vs exec_raw()

### exec() - Throws error on failure
{# This works fine #}
CPU cores: {{ exec(command="nproc 2>/dev/null || echo '2'") | trim }}

{# This would throw an error if the file doesn't exist #}
{# Content: {{ exec(command="cat /etc/nonexistent") }} #}

### exec_raw() - Never throws, you handle errors
{% set result = exec_raw(command="cat /etc/hosts") %}
{% if result.success %}
Hosts file (first 100 chars):
{{ result.stdout[:100] }}
{% else %}
Failed to read /etc/hosts (exit {{ result.exit_code }})
{% endif %}

## Real-World Use Cases

### 1. Build Info with exec()
```yaml
build:
  commit: {{ exec(command="git rev-parse --short HEAD 2>/dev/null || echo 'dev'") | trim }}
  branch: {{ exec(command="git branch --show-current 2>/dev/null || echo 'unknown'") | trim }}
  date: {{ exec(command="date -u +%Y-%m-%dT%H:%M:%SZ") | trim }}
  user: {{ exec(command="whoami") | trim }}
```

### 2. Conditional Configuration with exec_raw()
{% set docker_check = exec_raw(command="which docker") %}
{% set node_check = exec_raw(command="which node") %}

services:
  docker_enabled: {{ docker_check.success | lower }}
  {% if docker_check.success %}
  docker_path: {{ docker_check.stdout | trim }}
  {% endif %}

  node_enabled: {{ node_check.success | lower }}
  {% if node_check.success %}
  node_version: {{ exec(command="node --version") | trim }}
  {% endif %}

### 3. Dynamic Worker Count
{% set cpu_count = exec(command="nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo '2'") | trim | int %}

workers:
  count: {{ cpu_count * 2 }}
  per_worker_connections: 1000
  total_capacity: {{ cpu_count * 2 * 1000 }}

### 4. SSL Certificate Check with exec_raw()
{% set cert_check = exec_raw(command="openssl x509 -enddate -noout -in /etc/ssl/cert.pem 2>/dev/null") %}
{% if cert_check.success %}
ssl:
  status: active
  expires: {{ cert_check.stdout | trim }}
{% else %}
ssl:
  status: unavailable
  reason: {{ cert_check.stderr | trim if cert_check.stderr else "Certificate file not found" }}
{% endif %}

### 5. Version Detection
{% set node = exec_raw(command="node --version 2>/dev/null") %}
{% set python = exec_raw(command="python3 --version 2>/dev/null") %}
{% set ruby = exec_raw(command="ruby --version 2>/dev/null") %}
{% set go = exec_raw(command="go version 2>/dev/null") %}

runtime_versions:
  node: {% if node.success %}{{ node.stdout | trim }}{% else %}not installed{% endif %}
  python: {% if python.success %}{{ python.stdout | trim }}{% else %}not installed{% endif %}
  ruby: {% if ruby.success %}{{ ruby.stdout | trim }}{% else %}not installed{% endif %}
  go: {% if go.success %}{{ go.stdout | trim }}{% else %}not installed{% endif %}

### 6. Parse Command Output
{% set result = exec_raw(command="ls -1 /etc/*.conf 2>/dev/null | head -n 5") %}
{% if result.success %}
Configuration files:
{{ result.stdout }}
{% endif %}

### 7. Disk Space Warning
{% set disk_result = exec_raw(command="df / | tail -n 1 | awk '{print $5}' | tr -d '%'") %}
{% if disk_result.success %}
{% set disk_usage = disk_result.stdout | trim | int %}
disk:
  usage_percent: {{ disk_usage }}
  {% if disk_usage > 90 %}
  status: CRITICAL
  action: immediate_cleanup_required
  {% elif disk_usage > 75 %}
  status: WARNING
  action: monitor_closely
  {% else %}
  status: OK
  {% endif %}
{% endif %}

### 8. Service Health Check
{% set services = ["sshd", "nginx", "postgresql"] %}

service_health:
{% for service in services %}
  {{ service }}:
    {% set check = exec_raw(command="systemctl is-active " ~ service ~ " 2>/dev/null") %}
    {% if check.success and check.stdout | trim == "active" %}
    status: running
    {% else %}
    status: stopped
    exit_code: {{ check.exit_code }}
    {% endif %}
{% endfor %}

### 9. Network Interface Discovery
{% set iface_result = exec_raw(command="ip -o link show | awk '{print $2}' | tr -d ':' | grep -v '^lo$' | head -n 1") %}
{% if iface_result.success %}
{% set primary_interface = iface_result.stdout | trim %}
network:
  primary_interface: {{ primary_interface }}
  {% set ip_result = exec_raw(command="ip addr show " ~ primary_interface ~ " | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1") %}
  {% if ip_result.success %}
  ip_address: {{ ip_result.stdout | trim }}
  {% endif %}
{% endif %}

### 10. Fallback Pattern with exec()
{# exec() makes fallback chains easy #}
{% set hostname = exec(command="hostname -f 2>/dev/null || hostname 2>/dev/null || echo 'localhost'") | trim %}
{% set ip = exec(command="hostname -I 2>/dev/null | awk '{print $1}' || echo '127.0.0.1'") | trim %}

server:
  name: {{ hostname }}
  address: {{ ip }}

## Error Handling Patterns

### Pattern 1: Simple with fallback in command
Memory: {{ exec(command="free -h 2>/dev/null | grep Mem | awk '{print $2}' || echo 'unknown'") | trim }}

### Pattern 2: Check result with exec_raw()
{% set mem_result = exec_raw(command="free -h | grep Mem | awk '{print $2}'") %}
Memory: {% if mem_result.success %}{{ mem_result.stdout | trim }}{% else %}unknown{% endif %}

### Pattern 3: Try-catch style with exec_raw()
{% set db_check = exec_raw(command="pg_isready -h localhost") %}
{% if db_check.exit_code == 0 %}
  database: ready
{% elif db_check.exit_code == 1 %}
  database: rejecting_connections
{% elif db_check.exit_code == 2 %}
  database: connection_failed
{% else %}
  database: unknown_error
  details: {{ db_check.stderr }}
{% endif %}

## Security Considerations

{# ✓ GOOD: Hardcoded, trusted commands #}
Date: {{ exec(command="date") | trim }}
Hostname: {{ exec(command="hostname") | trim }}

{# ⚠️ WARNING: Be extremely careful with any form of user input
   NEVER do this with untrusted input:
   {% set user_input = get_env(name="USER_INPUT") %}
   {{ exec(command="echo " ~ user_input) }}  ← COMMAND INJECTION!

   Even with quotes, shell metacharacters can break out:
   Input: foo; rm -rf /
   Result: echo foo; rm -rf /  ← VERY BAD!

   ✓ BETTER: If you must use variables, sanitize heavily or use exec_raw()
   and check the result carefully #}

## Performance Notes

{# Commands are executed sequentially, each one blocks
   Keep commands fast:
   ✓ Good: date, hostname, uname
   ⚠ Slow: ping with high count, long-running scripts
   ✗ Bad: sleep 100, infinite loops #}

## Timeout (documented but not yet enforced)

{# The timeout parameter is accepted but not yet enforced in this version: #}
{% set result = exec_raw(command="echo hello", timeout=5) %}
Timeout test: {{ result.stdout | trim }}

{# In a future version, this will kill the command after 5 seconds.
   For now, use quick-running commands. #}