From a8d97cf1f523f6036d502de08378ff381fc484ac Mon Sep 17 00:00:00 2001 From: Chris Bednarczyk Date: Wed, 31 Dec 2025 00:19:42 +0100 Subject: [PATCH] chore: Add github required files --- .github/ISSUE_TEMPLATE/bug_report.md | 68 + .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/documentation.md | 57 + .github/ISSUE_TEMPLATE/feature_request.md | 74 + .github/SECURITY.md | 169 ++ .github/pull_request_template.md | 158 ++ Cargo.toml | 6 + LICENSE | 21 + README.md | 1974 ++++++------------- README.md.backup | 2165 +++++++++++++++++++++ 10 files changed, 3305 insertions(+), 1395 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/documentation.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/SECURITY.md create mode 100644 .github/pull_request_template.md create mode 100644 LICENSE create mode 100644 README.md.backup diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..70e0ffa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,68 @@ +--- +name: Bug Report +about: Report a bug or unexpected behavior +title: '[BUG] ' +labels: bug +assignees: '' +--- + +## Bug Description + +A clear and concise description of what the bug is. + +## Steps to Reproduce + +1. Create a template with '...' +2. Run command '....' +3. See error + +## Expected Behavior + +A clear and concise description of what you expected to happen. + +## Actual Behavior + +A clear and concise description of what actually happened. + +## Minimal Reproducible Example + +**Template** (`example.tmpl`): +``` +# Paste your template here +``` + +**Command:** +```bash +# Paste the exact command you ran +tmpltool example.tmpl +``` + +**Environment Variables (if applicable):** +```bash +# List any environment variables you set +export VAR_NAME="value" +``` + +**Output/Error:** +``` +# Paste the actual output or error message +``` + +## Environment + +- **tmpltool version:** [e.g., 1.2.0] (run `tmpltool --version`) +- **OS:** [e.g., Ubuntu 22.04, macOS 14.0, Windows 11] +- **Installation method:** [e.g., GitHub releases, Docker, built from source] +- **Docker version (if applicable):** [e.g., 24.0.5] + +## Additional Context + +Add any other context about the problem here. For example: +- Does it work with `--trust` flag? +- Does it happen only with specific file types? +- Screenshots, if applicable +- Related issues or discussions + +## Possible Solution (Optional) + +If you have an idea of how to fix this, please describe it here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..1b16b4a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Question or Discussion + url: https://github.com/bordeux/tmpltool/discussions + about: Ask questions or discuss ideas in GitHub Discussions + - name: Tera Template Engine Docs + url: https://keats.github.io/tera/docs/ + about: Official Tera template engine documentation diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md new file mode 100644 index 0000000..ee4b019 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -0,0 +1,57 @@ +--- +name: Documentation Issue +about: Report missing, unclear, or incorrect documentation +title: '[DOCS] ' +labels: documentation +assignees: '' +--- + +## Documentation Issue + +**What part of the documentation is affected?** +- [ ] README.md +- [ ] Function reference +- [ ] Examples +- [ ] Code comments +- [ ] Contributing guide +- [ ] Other: _____________ + +**Link or section:** +Provide a link or describe which section of the documentation is affected. + +## Problem + +Describe what's missing, unclear, or incorrect in the documentation. + +**Current documentation:** +``` +# Quote or describe the current documentation +``` + +**What's wrong with it:** +- [ ] Missing information +- [ ] Incorrect information +- [ ] Unclear/confusing +- [ ] Out of date +- [ ] Broken link +- [ ] Typo/grammar +- [ ] Other: _____________ + +## Suggested Improvement + +**What should be added/changed:** +``` +# Describe or provide the corrected documentation +``` + +**Why this is important:** +Explain why this documentation improvement matters (e.g., helps new users, clarifies confusion, etc.) + +## Additional Context + +Add any other context, screenshots, or examples here. + +## Willingness to Contribute + +- [ ] I'm willing to submit a PR to fix this documentation +- [ ] I'm just reporting the issue diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..7868710 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,74 @@ +--- +name: Feature Request +about: Suggest a new feature or enhancement +title: '[FEATURE] ' +labels: enhancement +assignees: '' +--- + +## Feature Description + +A clear and concise description of the feature you'd like to see. + +## Problem or Use Case + +**Is your feature request related to a problem?** +Describe the problem or limitation you're facing. For example: +- "I'm always frustrated when..." +- "It's difficult to..." +- "I need to manually..." + +**What are you trying to accomplish?** +Describe your use case and why this feature would be valuable. + +## Proposed Solution + +Describe the solution you'd like. How should this feature work? + +**Example usage:** +```bash +# Show how you'd like to use this feature +tmpltool --new-flag template.tmpl +``` + +**Example template:** +``` +{# Show template syntax if applicable #} +{{ new_function(arg="value") }} +``` + +**Expected output:** +``` +# What output do you expect? +``` + +## Alternatives Considered + +Describe any alternative solutions or features you've considered. + +## Additional Context + +Add any other context, screenshots, or examples about the feature request here. + +**Related to:** +- [ ] New custom function +- [ ] New command-line option +- [ ] Improvement to existing feature +- [ ] Documentation enhancement +- [ ] Performance improvement +- [ ] Other (please describe) + +## Priority + +How important is this feature to you? +- [ ] Critical - Blocking my workflow +- [ ] High - Would significantly improve my workflow +- [ ] Medium - Nice to have +- [ ] Low - Small improvement + +## Willingness to Contribute + +- [ ] I'm willing to submit a PR for this feature +- [ ] I can help with testing +- [ ] I can help with documentation +- [ ] I'm just suggesting the idea diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..49104a5 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,169 @@ +# Security Policy + +## Supported Versions + +We release security updates for the following versions: + +| Version | Supported | +| ------- | ------------------ | +| 1.2.x | :white_check_mark: | +| 1.1.x | :white_check_mark: | +| 1.0.x | :x: | +| < 1.0 | :x: | + +## Reporting a Vulnerability + +**Please do not report security vulnerabilities through public GitHub issues.** + +If you discover a security vulnerability in tmpltool, please report it by emailing the maintainers. You can find the contact information in the repository. + +### What to Include + +Please include the following information in your report: + +- **Description** of the vulnerability +- **Steps to reproduce** the issue +- **Potential impact** of the vulnerability +- **Suggested fix** (if you have one) +- **Your contact information** for follow-up + +### Response Timeline + +- **Initial Response:** We aim to respond to security reports within 48 hours +- **Confirmation:** We will confirm the vulnerability within 5 business days +- **Fix Timeline:** We will work on a fix and aim to release a patch within 14 days for critical vulnerabilities +- **Disclosure:** We will coordinate with you on the disclosure timeline + +## Security Best Practices for tmpltool + +When using tmpltool, please follow these security best practices: + +### 1. Trust Mode (`--trust`) + +**⚠️ WARNING:** Only use `--trust` mode with templates you completely trust. + +```bash +# ✗ DANGEROUS - Don't use --trust with untrusted templates +tmpltool --trust untrusted_template.tmpl + +# ✓ SAFE - Use without --trust for untrusted templates +tmpltool untrusted_template.tmpl +``` + +**Why?** Trust mode disables security restrictions: +- Can read any file on the system (e.g., `/etc/passwd`, SSH keys) +- Can access parent directories (`../`) +- Can read sensitive configuration files + +**Only use `--trust` when:** +- You wrote the template yourself +- You've reviewed and audited the template +- You trust the template source completely +- You need to access system files intentionally + +### 2. Template Security + +**Avoid processing untrusted templates without review:** + +```bash +# ✗ DANGEROUS - Don't process templates from untrusted sources +curl https://untrusted-site.com/template.tmpl | tmpltool + +# ✓ SAFE - Review templates before using them +curl https://trusted-site.com/template.tmpl -o template.tmpl +# Review the template +cat template.tmpl +# Then use it +tmpltool template.tmpl +``` + +**What to check in templates:** +- File system access attempts +- Unexpected environment variable usage +- Suspicious patterns or obfuscation + +### 3. Environment Variables + +**Be careful with sensitive environment variables:** + +```bash +# ✗ DANGEROUS - Don't expose secrets in environment +export DATABASE_PASSWORD="secret123" +tmpltool template.tmpl # Template could leak this + +# ✓ BETTER - Use secure secret management +# Load secrets only when needed and clear them after use +``` + +**Best practices:** +- Don't put secrets in environment variables if possible +- Use dedicated secret management tools +- Clear sensitive env vars after use +- Review templates for `filter_env(pattern="*")` which could expose all env vars + +### 4. Filesystem Access + +**Default security (without `--trust`):** +- ✓ Only relative paths allowed +- ✓ No access to parent directories (`..`) +- ✓ No absolute paths (`/etc/passwd`) +- ✓ Restricted to current working directory + +**With `--trust` mode:** +- ⚠️ Full filesystem access +- ⚠️ Can read any file the user can access +- ⚠️ No restrictions + +### 5. Docker Security + +When using Docker: + +```bash +# ✓ SAFE - Mount only necessary directories +docker run --rm -v $(pwd):/workspace -w /workspace tmpltool template.tmpl + +# ✗ DANGEROUS - Don't mount entire filesystem +docker run --rm -v /:/host tmpltool template.tmpl +``` + +## Known Security Considerations + +### 1. Template Injection + +tmpltool uses the Tera template engine. While Tera is designed to be safe, be aware that: +- Templates have access to environment variables via `get_env()` +- Templates can read files via filesystem functions +- Trust mode disables all security restrictions + +### 2. Filesystem Access + +Filesystem functions enforce security by default: +- `read_file()` - Restricted to relative paths +- `file_exists()` - Restricted to relative paths +- `list_dir()` - Restricted to relative paths +- `glob()` - Restricted to current directory + +Use `--trust` only when necessary and with caution. + +### 3. Environment Variable Exposure + +Functions like `filter_env(pattern="*")` can expose all environment variables. Review templates carefully to ensure they don't leak sensitive information. + +## Security Updates + +Security updates will be: +1. Developed privately +2. Tested thoroughly +3. Released as patch versions (e.g., 1.2.1) +4. Announced in release notes with `[SECURITY]` tag +5. Added to this SECURITY.md file + +## Security Hall of Fame + +We'd like to thank the following people for responsibly disclosing security issues: + + + +--- + +**Remember:** Security is everyone's responsibility. If you see something, say something! diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..ba980ef --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,158 @@ +## Description + + + +## Type of Change + + + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Documentation update +- [ ] Code refactoring +- [ ] Performance improvement +- [ ] Test improvement +- [ ] Build/CI improvement +- [ ] Other (please describe): + +## Related Issues + + + +Closes # +Related to # + +## Changes Made + + + +- +- +- + +## Testing + + + +**Test environment:** +- OS: [e.g., Ubuntu 22.04, macOS 14.0, Windows 11] +- Rust version: [e.g., 1.75.0] + +**How to test:** + +1. +2. +3. + +**Test results:** + +```bash +# Paste relevant test output +cargo test +``` + +## Examples + + + +**Before:** +```bash +# Show the old behavior +``` + +**After:** +```bash +# Show the new behavior +``` + +**Template example (if applicable):** +``` +# Show template usage +{{ new_function(arg="value") }} +``` + +## Checklist + + + +### Code Quality + +- [ ] My code follows the project's style guidelines +- [ ] I have run `cargo fmt` to format my code +- [ ] I have run `cargo clippy` and addressed all warnings +- [ ] I have run `cargo make qa` and all checks pass +- [ ] My changes generate no new warnings or errors + +### Testing + +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] I have tested my changes manually +- [ ] I have tested on multiple platforms (if applicable) + +### Documentation + +- [ ] I have updated the README.md (if applicable) +- [ ] I have added/updated code comments for complex logic +- [ ] I have added/updated examples (if applicable) +- [ ] I have updated TODO.md to mark completed features (if applicable) + +### Commits + +- [ ] My commits follow the [Conventional Commits](https://www.conventionalcommits.org/) format + - `feat:` for new features + - `fix:` for bug fixes + - `docs:` for documentation + - `refactor:` for refactoring + - `test:` for tests + - `chore:` for maintenance +- [ ] Each commit has a clear and descriptive message +- [ ] I have squashed unnecessary commits (if applicable) + +### Breaking Changes + + + +- [ ] I have marked this PR with breaking changes +- [ ] I have documented the breaking changes in the commit message with `BREAKING CHANGE:` +- [ ] I have updated the migration guide (if applicable) + +**Breaking changes description (if applicable):** +``` +# Describe what breaks and how users should migrate +``` + +## Additional Notes + + + +## Screenshots (if applicable) + + + +## Performance Impact + + + +- [ ] No performance impact +- [ ] Performance improved +- [ ] Performance may be affected (explain below) + +**Performance notes:** +``` +# Describe performance impact and any benchmarks +``` + +## Security Considerations + + + +- [ ] No security impact +- [ ] Security improved +- [ ] Security implications (explain below) + +**Security notes:** +``` +# Describe security implications +``` diff --git a/Cargo.toml b/Cargo.toml index 238a461..3387692 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,12 @@ name = "tmpltool" version = "1.2.0" edition = "2024" +license = "MIT" +description = "A fast and simple command-line template rendering tool using Tera templates with environment variables" +repository = "https://github.com/bordeux/tmpltool" +readme = "README.md" +keywords = ["template", "tera", "cli", "environment", "variables"] +categories = ["command-line-utilities", "template-engine"] [dependencies] tera = { version = "1", features = ["builtins"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4cb38d9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 tmpltool contributors + +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. diff --git a/README.md b/README.md index 22b9306..52caa54 100644 --- a/README.md +++ b/README.md @@ -9,20 +9,71 @@ A fast and simple command-line template rendering tool using [Tera](https://keats.github.io/tera/) templates with environment variables. +## Table of Contents + +- [Quick Start](#quick-start) +- [Features](#features) +- [Installation](#installation) +- [CLI Reference](#cli-reference) +- [Basic Usage](#basic-usage) +- [Template Syntax](#template-syntax) +- [Function Reference](#function-reference) + - [Environment Variables](#environment-variables) + - [Hash & Crypto Functions](#hash--crypto-functions) + - [Filesystem Functions](#filesystem-functions) + - [Data Parsing Functions](#data-parsing-functions) + - [Validation Functions](#validation-functions) +- [Advanced Examples](#advanced-examples) +- [Error Handling](#error-handling) +- [Development](#development) +- [CI/CD](#cicd) +- [Contributing](#contributing) +- [License](#license) + +## Quick Start + +Get started in 30 seconds: + +```bash +# Download for your platform (or use Docker) +docker pull ghcr.io/bordeux/tmpltool:latest + +# Create a simple template +cat > greeting.tmpl << 'EOF' +Hello {{ get_env(name="USER", default="World") }}! +EOF + +# Render it +docker run --rm -v $(pwd):/workspace -w /workspace ghcr.io/bordeux/tmpltool:latest greeting.tmpl +# Output: Hello World! + +# Or with your own name +docker run --rm -e USER=Alice -v $(pwd):/workspace -w /workspace ghcr.io/bordeux/tmpltool:latest greeting.tmpl +# Output: Hello Alice! +``` + +**Without Docker:** +```bash +# Install binary from releases +# See Installation section below + +# Create and render template +echo 'Hello {{ get_env(name="USER", default="World") }}!' > greeting.tmpl +tmpltool greeting.tmpl +``` + ## Features -- Render Tera templates with environment variable support via `get_env()` function -- Filter environment variables by pattern with `filter_env()` function -- Cryptographic hash functions: `md5()`, `sha1()`, `sha256()`, `sha512()` -- UUID generation with `uuid()` function -- Random string generation with `random_string()` function -- Filesystem functions: `read_file()`, `file_exists()`, `list_dir()`, `glob()`, `file_size()`, `file_modified()` -- Output to file or stdout (for piping) -- Simple CLI interface -- Single binary executable -- Full Tera template syntax support (variables, conditionals, loops, filters, etc.) -- Built-in filters: slugify, date, urlencode, filesizeformat, and more -- Default values for environment variables (no errors when variables are missing) +- **Environment Variables**: Access env vars with `get_env()` and filter with `filter_env()` +- **Hash & Crypto**: MD5, SHA1, SHA256, SHA512, UUID generation, random strings +- **Filesystem**: Read files, check existence, list directories, glob patterns, file info +- **Data Parsing**: Parse and read JSON, YAML, TOML files +- **Validation**: Validate emails, URLs, IPs, UUIDs, regex matching +- **Security**: Built-in protections with optional `--trust` mode +- **Flexible I/O**: File or stdin input, file or stdout output +- **Full Tera Syntax**: Conditionals, loops, filters, and more +- **Single Binary**: No runtime dependencies +- **Docker Support**: Multi-arch images available ## Installation @@ -51,21 +102,6 @@ Pull from GitHub Container Registry: docker pull ghcr.io/bordeux/tmpltool:latest ``` -Run with Docker: - -```bash -# Using a template file -docker run --rm -v $(pwd):/workspace -w /workspace ghcr.io/bordeux/tmpltool:latest template.tmpl - -# With environment variables -docker run --rm -e NAME=Alice -v $(pwd):/workspace -w /workspace \ - ghcr.io/bordeux/tmpltool:latest greeting.tmpl - -# Output to file -docker run --rm -v $(pwd):/workspace -w /workspace \ - ghcr.io/bordeux/tmpltool:latest template.tmpl -o output.txt -``` - Create a shell alias for convenience: ```bash @@ -85,33 +121,26 @@ cargo build --release # Binary will be at: ./target/release/tmpltool ``` -## Usage +## CLI Reference + +### Syntax ```bash -# Read from file tmpltool [TEMPLATE] [OPTIONS] - -# Read from stdin cat template.txt | tmpltool [OPTIONS] ``` ### Arguments -- `[TEMPLATE]` - Path to the template file (optional) - - If omitted, reads template from stdin +- `[TEMPLATE]` - Path to template file (optional, reads from stdin if omitted) ### Options -- `-o, --output ` - Output file path (optional) - - If not specified, output is printed to stdout -- `--trust` - Trust mode: Allow filesystem functions to access absolute paths and parent directories (optional) - - **WARNING:** This disables security restrictions. Only use with trusted templates. - - Without this flag, filesystem functions are restricted to relative paths within the current working directory - - With this flag, you can access any file on the system (e.g., `/etc/passwd`, `../../secret.txt`) - -### Input/Output Combinations +- `-o, --output ` - Output file path (prints to stdout if not specified) +- `--trust` - Trust mode: Allow filesystem functions to access absolute paths and parent directories + - **WARNING:** Only use with trusted templates. Disables security restrictions. -tmpltool supports all standard Unix I/O patterns: +### Input/Output Patterns | Input | Output | Command | |--------|--------|---------| @@ -122,130 +151,72 @@ tmpltool supports all standard Unix I/O patterns: ### Examples -#### File to stdout - ```bash +# File to stdout tmpltool template.txt -``` -#### File to file - -```bash +# File to file tmpltool template.txt -o output.txt -``` - -#### stdin to stdout (pipe) - -```bash -cat template.txt | tmpltool -echo "Hello {{ get_env(name=\"NAME\", default=\"World\") }}!" | tmpltool -``` - -#### stdin to file -```bash -cat template.txt | tmpltool -o output.txt -``` +# Stdin to stdout (pipe) +echo "Hello {{ get_env(name=\"USER\") }}!" | tmpltool -#### Chaining with other tools +# With environment variables +DB_HOST=postgres tmpltool config.tmpl -o config.txt -```bash -# Generate and validate +# Chaining with other tools tmpltool config.json.tmpl | jq . - -# Generate from stdin and apply cat k8s-deployment.yaml.tmpl | tmpltool | kubectl apply -f - -# Combine multiple templates -cat header.tmpl body.tmpl footer.tmpl | tmpltool > complete.html +# Trust mode for system files +tmpltool --trust system_info.tmpl # Can read /etc/passwd, etc. ``` -#### Using Trust Mode for System Files - -```bash -# Create a template that reads system files -cat > system_info.tmpl << 'EOF' -# System Information - -## Hostname -{{ read_file(path="/etc/hostname") }} - -## Hosts File (first 200 chars) -{{ read_file(path="/etc/hosts") | truncate(length=200) }} - -## Files in /etc (first 10) -{% for file in list_dir(path="/etc") | slice(end=10) %} -- {{ file }} -{% endfor %} -EOF +## Basic Usage -# Without --trust: Security error -tmpltool system_info.tmpl -# Error: Security: Absolute paths and parent directory (..) access are not allowed - -# With --trust: Works! -tmpltool --trust system_info.tmpl -o system_info.md -``` +### Simple Variable Substitution -#### Using Environment Variables - -Create a template file `greeting.tmpl`: +**Template** (`greeting.tmpl`): ``` Hello {{ get_env(name="USER") }}! Your home directory is: {{ get_env(name="HOME") }} -Your shell: {{ get_env(name="SHELL") }} ``` -Render it: +**Render:** ```bash tmpltool greeting.tmpl ``` -Output: -``` -Hello username! -Your home directory is: /home/username -Your shell: /bin/bash -``` - -**Note:** These environment variables (USER, HOME, SHELL) are typically available on Unix systems. +### Using Default Values -#### Setting Custom Environment Variables - -Create a template `config.tmpl`: +**Template** (`config.tmpl`): ``` Database: {{ get_env(name="DB_HOST", default="localhost") }}:{{ get_env(name="DB_PORT", default="5432") }} Environment: {{ get_env(name="APP_ENV", default="development") }} Debug: {{ get_env(name="DEBUG", default="false") }} ``` -Render with custom variables: -```bash -DB_HOST=postgres DB_PORT=5432 APP_ENV=production DEBUG=true tmpltool config.tmpl -``` - -Output: -``` -Database: postgres:5432 -Environment: production -Debug: true -``` - -Or render without any environment variables (using defaults): +**Render with defaults:** ```bash tmpltool config.tmpl +# Output: +# Database: localhost:5432 +# Environment: development +# Debug: false ``` -Output: -``` -Database: localhost:5432 -Environment: development -Debug: false +**Render with custom values:** +```bash +DB_HOST=postgres DB_PORT=5432 APP_ENV=production tmpltool config.tmpl +# Output: +# Database: postgres:5432 +# Environment: production +# Debug: false ``` -#### Using Conditionals +### Conditionals and Loops -Template `status.tmpl`: +**Template** (`status.tmpl`): ``` {% set debug = get_env(name="DEBUG", default="false") %} {% if debug == "true" %} @@ -255,36 +226,7 @@ Log level: verbose Production mode Log level: error {% endif %} -``` - -Render with DEBUG enabled: -```bash -DEBUG=true tmpltool status.tmpl -``` - -Output: -``` -DEBUG MODE ENABLED -Log level: verbose -``` - -Render without DEBUG (uses default): -```bash -tmpltool status.tmpl -``` - -Output: -``` -Production mode -Log level: error -``` - -#### Using Loops - -You can use environment variables with Tera's split filter to create lists: -Template `list.tmpl`: -``` {% set items_str = get_env(name="ITEMS", default="apple,banana,orange") %} {% set items = items_str | split(pat=",") %} Items: @@ -293,75 +235,14 @@ Items: {% endfor %} ``` -Render with custom list: -```bash -ITEMS="apple,banana,orange,grape" tmpltool list.tmpl -``` - -Output: -``` -Items: - - apple - - banana - - orange - - grape -``` - -Or use the default list: -```bash -tmpltool list.tmpl -``` - -Output: -``` -Items: - - apple - - banana - - orange -``` - -#### Using Filters - -Template `formatted.tmpl`: -``` -{% set name = get_env(name="NAME", default="john doe") %} -Uppercase: {{ name | upper }} -Lowercase: {{ name | lower }} -Title Case: {{ name | title }} -Slugified: {{ name | slugify }} -``` - -Render with custom name: -```bash -NAME="Jane Smith" tmpltool formatted.tmpl -``` - -Output: -``` -Uppercase: JANE SMITH -Lowercase: jane smith -Title Case: Jane Smith -Slugified: jane-smith -``` - -Or use default: +**Render:** ```bash -tmpltool formatted.tmpl -``` - -Output: -``` -Uppercase: JOHN DOE -Lowercase: john doe -Title Case: John Doe -Slugified: john-doe +DEBUG=true ITEMS="apple,banana,orange,grape" tmpltool status.tmpl ``` -#### Filtering Environment Variables by Pattern +### Filtering Environment Variables -Use the `filter_env()` function to get all environment variables matching a pattern: - -Template `server-vars.tmpl`: +**Template** (`server-vars.tmpl`): ``` Server Configuration: {% for var in filter_env(pattern="SERVER_*") %} @@ -369,422 +250,53 @@ Server Configuration: {% endfor %} ``` -Set environment variables: -```bash -SERVER_HOST=localhost \ -SERVER_PORT=8080 \ -SERVER_NAME=myapp \ -OTHER_VAR=ignored \ -tmpltool server-vars.tmpl -``` - -Output: -``` -Server Configuration: - SERVER_HOST=localhost - SERVER_NAME=myapp - SERVER_PORT=8080 -``` - -**Pattern Syntax:** -- `*` - matches any characters (e.g., `SERVER_*` matches `SERVER_HOST`, `SERVER_PORT`, etc.) -- `?` - matches exactly one character (e.g., `DB_?` matches `DB_A`, `DB_B`, but not `DB_AB`) -- Patterns can be at the beginning, middle, or end (e.g., `*_PORT`, `APP_*_NAME`) - -The results are returned as an array of objects with `key` and `value` fields, sorted alphabetically by key. - -#### Complex Example - Docker Compose Generator - -Template `docker-compose.tmpl`: -```yaml -version: '3.8' - -services: - {{ get_env(name="SERVICE_NAME", default="app") }}: - image: {{ get_env(name="DOCKER_IMAGE", default="node:18") }} - ports: - - "{{ get_env(name="HOST_PORT", default="3000") }}:{{ get_env(name="CONTAINER_PORT", default="3000") }}" - environment: - - NODE_ENV={{ get_env(name="NODE_ENV", default="development") }} - {% set db_url = get_env(name="DATABASE_URL", default="") %} - {% if db_url %} - - DATABASE_URL={{ db_url }} - {% endif %} - {% set enable_volumes = get_env(name="ENABLE_VOLUMES", default="false") %} - {% if enable_volumes == "true" %} - volumes: - - ./app:/app - {% endif %} -``` - -Render with all custom values: -```bash -SERVICE_NAME=web \ -DOCKER_IMAGE=node:20 \ -HOST_PORT=8080 \ -CONTAINER_PORT=3000 \ -NODE_ENV=production \ -DATABASE_URL=postgres://db:5432/mydb \ -ENABLE_VOLUMES=true \ -tmpltool docker-compose.tmpl -o docker-compose.yml -``` - -Or use all defaults (works out of the box!): +**Render:** ```bash -tmpltool docker-compose.tmpl -o docker-compose.yml +SERVER_HOST=localhost SERVER_PORT=8080 SERVER_NAME=myapp tmpltool server-vars.tmpl +# Output: +# Server Configuration: +# SERVER_HOST=localhost +# SERVER_NAME=myapp +# SERVER_PORT=8080 ``` -This generates a working docker-compose.yml with sensible defaults. +### Working with Files -#### Pipeline Usage - -Generate and validate JSON config: -```bash -tmpltool config.json.tmpl | jq . +**Template** (`build-report.tmpl`): ``` +# Build Report -Generate and apply Kubernetes config: -```bash -tmpltool k8s-deployment.yaml.tmpl | kubectl apply -f - -``` +{% if file_exists(path="README.md") %} +✓ README.md found ({{ file_size(path="README.md") | filesizeformat }}) +{% else %} +✗ README.md missing +{% endif %} -Generate nginx config and test it: -```bash -tmpltool nginx.conf.tmpl | nginx -t -c /dev/stdin +Source files: +{% for file in glob(pattern="src/**/*.rs") %} + - {{ file }} ({{ file_size(path=file) | filesizeformat }}) +{% endfor %} ``` -#### Comprehensive Example - All Features - -This example demonstrates all tmpltool features in a single template: - -Template `comprehensive-app-config.tmpl`: -```yaml -# Application Configuration -# Generated: {{ now() }} -# Instance ID: {{ uuid() }} - -{# ============================================ - Service Configuration - ============================================ #} -service: - name: {{ get_env(name="APP_NAME", default="myapp") | upper }} - version: {{ get_env(name="APP_VERSION", default="1.0.0") }} - environment: {{ get_env(name="ENV", default="development") | upper }} - - # Unique identifiers - instance_id: {{ uuid() }} - deployment_id: {{ uuid() }} - -{# ============================================ - Security & Authentication - ============================================ #} -security: - # Hash functions for integrity checks - config_checksum: {{ md5(string="v1.0-config") }} - license_hash: {{ sha256(string=get_env(name="LICENSE_KEY", default="trial-license")) }} - - # Generated secrets - api_key: {{ random_string(length=32, charset="hex") }} - secret_token: {{ random_string(length=64) }} - csrf_token: {{ random_string(length=40, charset="hex") }} - session_secret: {{ random_string(length=32, charset="alphanumeric") }} - - # Password hashing (example - use proper password hashing in production!) - {% set admin_pwd = get_env(name="ADMIN_PASSWORD", default="changeme123") %} - admin_password_hash: {{ sha512(string=admin_pwd) }} - -{# ============================================ - Database Configuration - ============================================ #} -database: - # Filter all DB_* environment variables - {% set db_vars = filter_env(pattern="DB_*") %} - {% if db_vars | length > 0 %} - # From environment: - {% for var in db_vars %} - {{ var.key | lower | replace(from="db_", to="") }}: {{ var.value }} - {% endfor %} - {% else %} - # Default configuration: - host: {{ get_env(name="DB_HOST", default="localhost") }} - port: {{ get_env(name="DB_PORT", default="5432") }} - name: {{ get_env(name="DB_NAME", default="myapp_db") }} - user: {{ get_env(name="DB_USER", default="app_user") }} - {% endif %} - - # Connection pool - max_connections: {{ get_env(name="DB_MAX_CONNECTIONS", default="20") }} - connection_id: {{ uuid() }} - -{# ============================================ - Server Configuration - ============================================ #} -server: - {% set servers = filter_env(pattern="SERVER_*") %} - {% if servers | length > 0 %} - # Detected server configuration: - {% for srv in servers %} - {{ srv.key | lower | replace(from="server_", to="") }}: {{ srv.value }} - {% endfor %} - {% else %} - # Default server configuration: - host: {{ get_env(name="HOST", default="0.0.0.0") }} - port: {{ get_env(name="PORT", default="8080") }} - protocol: {{ get_env(name="PROTOCOL", default="http") }} - {% endif %} - - # TLS/SSL - {% set enable_tls = get_env(name="ENABLE_TLS", default="false") %} - {% if enable_tls == "true" %} - tls: - enabled: true - cert_path: {{ get_env(name="TLS_CERT_PATH", default="/etc/ssl/cert.pem") }} - key_path: {{ get_env(name="TLS_KEY_PATH", default="/etc/ssl/key.pem") }} - {% else %} - tls: - enabled: false - {% endif %} - -{# ============================================ - Logging Configuration - ============================================ #} -logging: - {% set env_type = get_env(name="ENV", default="development") %} - {% if env_type == "production" %} - level: ERROR - format: json - output: /var/log/app/production.log - {% elif env_type == "staging" %} - level: WARN - format: json - output: /var/log/app/staging.log - {% else %} - level: DEBUG - format: text - output: stdout - {% endif %} - - # Log rotation ID - rotation_id: {{ uuid() }} - -{# ============================================ - Feature Flags - ============================================ #} -features: - {% set features = get_env(name="FEATURES", default="api,web,admin") | split(pat=",") %} - enabled: [{% for feature in features %}"{{ feature | trim }}"{% if not loop.last %}, {% endif %}{% endfor %}] - count: {{ features | length }} - - # Feature-specific settings - {% for feature in features %} - {{ feature | trim | slugify }}: - enabled: true - token: {{ random_string(length=16, charset="hex") }} - {% endfor %} - -{# ============================================ - External Services - ============================================ #} -external_services: - # All API_* environment variables - {% set api_vars = filter_env(pattern="API_*") %} - {% if api_vars | length > 0 %} - apis: - {% for api in api_vars %} - {{ api.key | lower | replace(from="api_", to="") }}: - url: {{ api.value }} - key: {{ random_string(length=32, charset="hex") }} - checksum: {{ md5(string=api.value) }} - {% endfor %} - {% else %} - apis: [] - {% endif %} - -{# ============================================ - Cache Configuration - ============================================ #} -cache: - {% set cache_type = get_env(name="CACHE_TYPE", default="memory") %} - type: {{ cache_type }} - ttl: {{ get_env(name="CACHE_TTL", default="3600") }} - - {% if cache_type == "redis" %} - redis: - host: {{ get_env(name="REDIS_HOST", default="localhost") }} - port: {{ get_env(name="REDIS_PORT", default="6379") }} - db: {{ get_env(name="REDIS_DB", default="0") }} - password_hash: {{ sha256(string=get_env(name="REDIS_PASSWORD", default="")) }} - {% endif %} - -{# ============================================ - Monitoring & Metrics - ============================================ #} -monitoring: - enabled: {{ get_env(name="ENABLE_MONITORING", default="true") }} - endpoint: {{ get_env(name="METRICS_ENDPOINT", default="/metrics") }} - - # Unique tracking IDs - cluster_id: {{ uuid() }} - node_id: {{ uuid() }} - - # Sample intervals (in seconds) - {% set intervals = get_env(name="SAMPLE_INTERVALS", default="10,30,60") | split(pat=",") %} - sample_intervals: [{% for interval in intervals %}{{ interval }}{% if not loop.last %}, {% endif %}{% endfor %}] - -{# ============================================ - Recovery & Backup - ============================================ #} -recovery: - # Recovery codes (for 2FA backup) - codes: - {% for i in range(end=5) %} - - {{ random_string(length=8, charset="uppercase") }}-{{ random_string(length=8, charset="uppercase") }} - {% endfor %} - - # Backup encryption key - backup_key: {{ random_string(length=64, charset="hex") }} - backup_key_hash: {{ sha256(string=get_env(name="BACKUP_PASSPHRASE", default="default-passphrase")) }} - -{# ============================================ - Metadata - ============================================ #} -metadata: - generated_at: {{ now() }} - generated_by: tmpltool - template_version: "2.0" - config_hash: {{ sha1(string="comprehensive-config-v2.0") }} - - # All environment variables used - environment_variables: - {% set all_env = filter_env(pattern="*") %} - total_count: {{ all_env | length }} - app_vars: {{ filter_env(pattern="APP_*") | length }} - db_vars: {{ filter_env(pattern="DB_*") | length }} - server_vars: {{ filter_env(pattern="SERVER_*") | length }} -``` - -Set environment variables and render: -```bash -# Set application variables -export APP_NAME="mywebapp" -export APP_VERSION="2.1.0" -export ENV="production" - -# Set database variables -export DB_HOST="db.example.com" -export DB_PORT="5432" -export DB_NAME="production_db" -export DB_USER="app_prod" -export DB_MAX_CONNECTIONS="50" - -# Set server variables -export SERVER_HOST="api.example.com" -export SERVER_PORT="443" -export SERVER_PROTOCOL="https" - -# Enable features -export ENABLE_TLS="true" -export TLS_CERT_PATH="/etc/ssl/certs/app.crt" -export TLS_KEY_PATH="/etc/ssl/private/app.key" - -# Set security -export ADMIN_PASSWORD="SecureP@ssw0rd123" -export LICENSE_KEY="PROD-ABC123-XYZ789" - -# Set features -export FEATURES="api,web,admin,analytics,reporting" - -# External services -export API_PAYMENT_URL="https://api.payment.example.com" -export API_EMAIL_URL="https://api.email.example.com" - -# Cache configuration -export CACHE_TYPE="redis" -export REDIS_HOST="cache.example.com" -export REDIS_PORT="6379" -export REDIS_PASSWORD="redis-secure-pass" - -# Render the configuration -tmpltool comprehensive-app-config.tmpl -o app-config.yaml -``` - -This example demonstrates: -- ✅ All hash functions: `md5()`, `sha1()`, `sha256()`, `sha512()` -- ✅ UUID generation: `uuid()` -- ✅ Random strings: `random_string()` with various charsets -- ✅ Environment variables: `get_env()` with defaults -- ✅ Pattern filtering: `filter_env()` -- ✅ Conditionals: `if/elif/else` -- ✅ Loops: `for` loops with ranges and arrays -- ✅ Filters: `upper`, `lower`, `trim`, `slugify`, `replace`, `split`, `length` -- ✅ Comments: `{# ... #}` -- ✅ String operations: concatenation and formatting -- ✅ Complex logic: nested conditions and loops - -**Note:** The comprehensive example does not include filesystem functions. For filesystem function examples, see the [Filesystem Functions](#filesystem-functions) section. - -## Examples - -The `examples/` directory contains ready-to-use template examples demonstrating various features: - -- **`basic.tmpl`** - Basic variable substitution and conditionals -- **`greeting.tmpl`** - Simple greeting with `get_env()` function -- **`config.tmpl`** - Application configuration file generation -- **`docker-compose.tmpl`** - Docker Compose with sensible defaults -- **`config-with-defaults.tmpl`** - Advanced config using `get_env()` function (recommended) -- **`server-config.tmpl`** - Server configuration using `filter_env()` pattern matching -- **`hash-crypto.tmpl`** - Demonstrates all hash functions, UUID, and random string generation -- **`comprehensive-app-config.tmpl`** - Complete showcase of ALL features (recommended for learning) - -### Try an Example - +**Render:** ```bash -# Basic example with environment variables -CUSTOM_VAR="Hello World" tmpltool examples/basic.tmpl - -# Greeting with defaults -tmpltool examples/greeting.tmpl - -# Generate a docker-compose.yml with all defaults (works out of the box!) -tmpltool examples/docker-compose.tmpl -o docker-compose.yml - -# Generate docker-compose.yml with custom values -SERVICE_NAME=web \ -DATABASE_URL=postgres://db:5432/myapp \ -ENABLE_VOLUMES=true \ -tmpltool examples/docker-compose.tmpl -o docker-compose.yml - -# Config with get_env() function and defaults -tmpltool examples/config-with-defaults.tmpl - -# Hash and crypto functions -tmpltool examples/hash-crypto.tmpl - -# Comprehensive example with ALL features (great for learning!) -tmpltool examples/comprehensive-app-config.tmpl - -# Comprehensive example with environment variables -APP_NAME="MyWebApp" \ -ENV="production" \ -DB_HOST="db.example.com" \ -FEATURES="api,web,admin" \ -tmpltool examples/comprehensive-app-config.tmpl -o app-config.yaml +tmpltool build-report.tmpl ``` -See the [examples/README.md](examples/README.md) for detailed documentation of each example. - ## Template Syntax -tmpltool uses the [Tera](https://keats.github.io/tera/) template engine. Here are some common syntax patterns: +tmpltool uses the [Tera](https://keats.github.io/tera/) template engine. For complete documentation, visit: https://keats.github.io/tera/docs/ ### Variables + ``` {{ variable_name }} ``` -**Note:** Environment variables are NOT automatically available as variables. Use the `get_env()` function to access them (see below). +**Note:** Environment variables are NOT automatically available. Use the `get_env()` function to access them. ### Conditionals + ``` {% if CONDITION %} ... @@ -795,250 +307,264 @@ tmpltool uses the [Tera](https://keats.github.io/tera/) template engine. Here ar {% endif %} ``` +**Important:** `get_env()` cannot be used directly in `{% if %}` conditions. Use `{% set %}` to assign to a variable first: + +``` +{% set debug = get_env(name="DEBUG", default="false") %} +{% if debug == "true" %} + Debug mode enabled +{% endif %} +``` + ### Loops + ``` {% for item in items %} {{ item }} {% endfor %} ``` +Access loop metadata: +``` +{% for item in items %} + {{ loop.index }}: {{ item }} + {% if loop.first %}(first){% endif %} + {% if loop.last %}(last){% endif %} +{% endfor %} +``` + ### Filters + ``` {{ variable | filter_name }} {{ variable | filter_name(arg=value) }} ``` -### Built-in `get_env()` Function +**Common filters:** +- `upper`, `lower`, `title` - Case conversion +- `trim`, `truncate` - String operations +- `slugify` - Convert to URL-friendly slug +- `urlencode` - URL encoding +- `filesizeformat` - Format bytes (e.g., "1.5 KB") +- `date(format="%Y-%m-%d")` - Date formatting +- `split(pat=",")` - Split string into array +- `length` - Get array/string length + +### Comments + +``` +{# This is a comment #} +``` -tmpltool uses Tera's built-in `get_env()` function for accessing environment variables with optional defaults: +### Setting Variables ``` -{{ get_env(name="VARIABLE_NAME", default="fallback_value") }} +{% set variable_name = value %} +{% set name = get_env(name="USER", default="guest") %} ``` +## Function Reference + +### Environment Variables + +#### `get_env(name, default)` + +Access environment variables with optional default values. + +**Arguments:** +- `name` (required) - Environment variable name +- `default` (optional) - Fallback value if variable doesn't exist + +**Returns:** String value of the environment variable or default + **Examples:** ``` -# With default value (recommended) +{# With default (recommended) #} port = {{ get_env(name="PORT", default="8080") }} -database = {{ get_env(name="DB_URL", default="postgres://localhost/mydb") }} -# Without default (will error if variable doesn't exist) +{# Without default (will error if variable doesn't exist) #} api_key = {{ get_env(name="API_KEY") }} -# Use in conditionals (requires {% set %} first) +{# Use in variables (requires {% set %}) #} {% set debug = get_env(name="DEBUG", default="false") %} {% if debug == "true" %} Debug mode enabled {% endif %} ``` -**Benefits:** -- No template errors when environment variables are missing -- Sensible defaults for development -- Easy to override in production -- Self-documenting configuration - -**Important Notes:** -- `get_env()` cannot be used directly in `{% if %}` conditions - use `{% set %}` to assign to a variable first -- Direct environment variable access (e.g., `{{ ENV_VAR }}`) is not supported - always use `get_env()` +#### `filter_env(pattern)` -See [examples/config-with-defaults.tmpl](examples/config-with-defaults.tmpl) for a complete example. - -### Custom `filter_env()` Function - -tmpltool provides a custom `filter_env()` function to filter environment variables by glob pattern: - -``` -{% for var in filter_env(pattern="PATTERN") %} - {{ var.key }}={{ var.value }} -{% endfor %} -``` +Filter environment variables by glob pattern. **Arguments:** -- `pattern` (required) - A glob pattern to match environment variable names - - `*` matches any characters - - `?` matches exactly one character +- `pattern` (required) - Glob pattern (`*` matches any characters, `?` matches one character) -**Returns:** -- An array of objects, each with: - - `key` - The environment variable name - - `value` - The environment variable value -- Results are sorted alphabetically by key +**Returns:** Array of objects with `key` and `value` fields, sorted alphabetically **Examples:** ``` -# Match all SERVER_* variables +{# Match all SERVER_* variables #} {% for var in filter_env(pattern="SERVER_*") %} export {{ var.key }}="{{ var.value }}" {% endfor %} -# Match all database variables -{% set db_vars = filter_env(pattern="DATABASE_*") %} -{% if db_vars | length > 0 %} -Found {{ db_vars | length }} database variables -{% endif %} - -# Match any variable ending with _PORT +{# Match any variable ending with _PORT #} {% for var in filter_env(pattern="*_PORT") %} {{ var.key }}: {{ var.value }} {% endfor %} -``` -See [examples/server-config.tmpl](examples/server-config.tmpl) for a complete example. - -### Hash Functions +{# Count matching variables #} +{% set db_vars = filter_env(pattern="DATABASE_*") %} +Found {{ db_vars | length }} database variables +``` -tmpltool provides cryptographic hash functions for generating checksums and hashes: +### Hash & Crypto Functions #### `md5(string)` -Calculates MD5 hash of a string. + +Calculate MD5 hash of a string. ``` Checksum: {{ md5(string="hello world") }} -# Output: 5eb63bbbe01eeed093cb22bb8f5acdc3 +{# Output: 5eb63bbbe01eeed093cb22bb8f5acdc3 #} ``` #### `sha1(string)` -Calculates SHA1 hash of a string. + +Calculate SHA1 hash of a string. ``` Hash: {{ sha1(string="tmpltool") }} -# Output: c054a2a60ca2fe935ea1056bd90386194116f14f +{# Output: c054a2a60ca2fe935ea1056bd90386194116f14f #} ``` #### `sha256(string)` -Calculates SHA256 hash of a string (recommended for password hashing). + +Calculate SHA256 hash (recommended for password hashing). ``` {% set password = get_env(name="PASSWORD", default="secret") %} Password hash: {{ sha256(string=password) }} -# Output: fcf730b6d95236ecd3c9fc2d92d7b6b2bb061514961aec041d6c7a7192f592e4 ``` #### `sha512(string)` -Calculates SHA512 hash of a string (most secure). + +Calculate SHA512 hash (most secure). ``` Secure hash: {{ sha512(string="secure-data") }} -# Output: a5c18d86d1d07cc2b22b12284e2f8e5b9705761003f149467995927e36f0e447ddfb158b89a28c0b4d5ac419c979c1fc435a3378b619aed1bab0d15c3b583db9 ``` **Important:** These hash functions are for checksums and general-purpose hashing. For production password storage, use dedicated password hashing libraries with salt and proper key derivation functions (bcrypt, argon2, etc.). -### UUID Generation - #### `uuid()` -Generates a random UUID v4 (Universally Unique Identifier). + +Generate a random UUID v4 (Universally Unique Identifier). ``` Request ID: {{ uuid() }} Session ID: {{ uuid() }} -# Output: -# Request ID: c5b78641-89f8-4d04-a4c9-d53ba4d433f9 -# Session ID: aabc7fe1-f8ed-45ff-944d-9c24f3823ac0 +{# Each call generates a unique identifier #} ``` -Each call to `uuid()` generates a unique identifier. +#### `random_string(length, charset)` -### Random String Generation - -#### `random_string(length, charset)` -Generates a random string with customizable length and character set. +Generate a random string with customizable length and character set. **Arguments:** -- `length` (required) - Length of the string to generate (1-10000) -- `charset` (optional) - Character set to use (default: `alphanumeric`) +- `length` (required) - Length of string (1-10000) +- `charset` (optional) - Character set preset or custom string (default: `alphanumeric`) **Character Set Presets:** -- `alphanumeric` - Letters (a-z, A-Z) and digits (0-9) - **default** +- `alphanumeric` - Letters and digits (a-z, A-Z, 0-9) - **default** - `alphabetic` or `alpha` - Letters only (a-z, A-Z) -- `lowercase` or `lower` - Lowercase letters only (a-z) -- `uppercase` or `upper` - Uppercase letters only (A-Z) +- `lowercase` or `lower` - Lowercase letters (a-z) +- `uppercase` or `upper` - Uppercase letters (A-Z) - `numeric` or `digits` - Digits only (0-9) -- `hex` or `hexadecimal` - Hexadecimal characters (0-9, a-f) +- `hex` or `hexadecimal` - Hexadecimal (0-9, a-f) - `hex_upper` - Hexadecimal uppercase (0-9, A-F) - Custom string - Any custom character set (e.g., `"abc123"`) **Examples:** ``` -# Alphanumeric (default) +{# Alphanumeric (default) #} API Key: {{ random_string(length=32) }} -# Output: 0QY92XIYYKIvMVuVc8a7u8O4v19VacO9 -# Lowercase only +{# Lowercase only #} Username: user_{{ random_string(length=8, charset="lowercase") }} -# Output: user_lvaycaxa -# Uppercase only -Code: {{ random_string(length=6, charset="uppercase") }} -# Output: YFVLRV - -# Numeric only +{# Numeric PIN #} PIN: {{ random_string(length=4, charset="numeric") }} -# Output: 5858 -# Hexadecimal +{# Hexadecimal token #} Token: {{ random_string(length=16, charset="hex") }} -# Output: bd2954f90019649b -# Custom charset +{# Custom charset #} Password: {{ random_string(length=12, charset="abc123") }} -# Output: 3bb3c31bb23c ``` -**Practical Example - Secure Configuration:** +**Practical Example:** ```yaml application: instance_id: {{ uuid() }} secret_key: {{ random_string(length=64) }} api_token: {{ random_string(length=32, charset="hex") }} + csrf_token: {{ random_string(length=40, charset="hex") }} security: password_hash: {{ sha256(string=get_env(name="PASSWORD")) }} - csrf_token: {{ random_string(length=40, charset="hex") }} ``` -See [examples/hash-crypto.tmpl](examples/hash-crypto.tmpl) for a complete example. - ### Filesystem Functions -tmpltool provides secure filesystem functions for reading files and querying file information within templates. All filesystem functions enforce security restrictions to prevent unauthorized access. +All filesystem functions enforce security restrictions to prevent unauthorized access. Only relative paths within the current working directory are allowed unless `--trust` mode is enabled. + +**Security Restrictions:** +- ✗ No absolute paths (e.g., `/etc/passwd`) +- ✗ No parent directory traversal (e.g., `../../secret.txt`) +- ✓ Only relative paths within current directory -**Security Note:** All filesystem functions only allow access to relative paths within the current working directory. Absolute paths (starting with `/`) and parent directory traversal (`..`) are explicitly blocked. +**Trust Mode:** Use `--trust` flag to bypass these restrictions for trusted templates. + +```bash +tmpltool --trust template.tmpl # Can access any file +``` #### `read_file(path)` -Reads the content of a file into the template. + +Read the content of a file into the template. **Arguments:** -- `path` (required) - Relative path to the file to read +- `path` (required) - Relative path to the file -**Returns:** String containing the file content +**Returns:** String containing file content **Examples:** ``` -# Read a configuration file +{# Read a configuration file #} {% set config = read_file(path="config.txt") %} {{ config }} -# Read and include file content +{# Read and include LICENSE #} License: {{ read_file(path="LICENSE") }} -# Use with filters +{# Use with filters #} First 100 chars: {{ read_file(path="README.md") | truncate(length=100) }} ``` #### `file_exists(path)` -Checks if a file exists at the specified path. + +Check if a file exists at the specified path. **Arguments:** - `path` (required) - Relative path to check -**Returns:** Boolean (`true` if file exists, `false` otherwise) +**Returns:** Boolean (`true` if exists, `false` otherwise) **Examples:** ``` -# Conditional file inclusion +{# Conditional file inclusion #} {% if file_exists(path="custom-config.txt") %} Custom config found! {{ read_file(path="custom-config.txt") }} @@ -1046,7 +572,7 @@ Custom config found! Using default configuration {% endif %} -# Check multiple files +{# Check multiple files #} {% set has_readme = file_exists(path="README.md") %} {% set has_license = file_exists(path="LICENSE") %} Documentation: {% if has_readme %}✓{% else %}✗{% endif %} @@ -1054,7 +580,8 @@ License: {% if has_license %}✓{% else %}✗{% endif %} ``` #### `list_dir(path)` -Lists all files and directories in a directory. + +List all files and directories in a directory. **Arguments:** - `path` (required) - Relative path to the directory @@ -1063,17 +590,17 @@ Lists all files and directories in a directory. **Examples:** ``` -# List files in a directory +{# List files in a directory #} Files in data/: {% for file in list_dir(path="data") %} - {{ file }} {% endfor %} -# Count files +{# Count files #} {% set files = list_dir(path="templates") %} Total templates: {{ files | length }} -# Filter by extension +{# Filter by extension #} {% set all_files = list_dir(path="src") %} Rust files: {% for file in all_files %} @@ -1084,37 +611,35 @@ Rust files: ``` #### `glob(pattern)` -Lists all files matching a glob pattern. + +List all files matching a glob pattern. **Arguments:** -- `pattern` (required) - Glob pattern to match files - - `*` matches any characters - - `?` matches exactly one character - - `**` matches any number of directories +- `pattern` (required) - Glob pattern (`*` matches any characters, `?` matches one character, `**` matches any number of directories) **Returns:** Array of file paths (sorted alphabetically) **Examples:** ``` -# Find all text files +{# Find all text files #} Text files: {% for file in glob(pattern="*.txt") %} - {{ file }} {% endfor %} -# Find files in subdirectories +{# Find files in subdirectories #} All Rust files: {% for file in glob(pattern="src/**/*.rs") %} - {{ file }} {% endfor %} -# Match specific patterns +{# Match specific patterns #} Config files: {% for file in glob(pattern="config*.{json,yaml,toml}") %} - {{ file }} {% endfor %} -# Use in conditionals +{# Use in conditionals #} {% set test_files = glob(pattern="tests/**/*.rs") %} {% if test_files | length > 0 %} Found {{ test_files | length }} test files @@ -1122,7 +647,8 @@ Found {{ test_files | length }} test files ``` #### `file_size(path)` -Gets the size of a file in bytes. + +Get the size of a file in bytes. **Arguments:** - `path` (required) - Relative path to the file @@ -1131,13 +657,13 @@ Gets the size of a file in bytes. **Examples:** ``` -# Get file size +{# Get file size #} README size: {{ file_size(path="README.md") }} bytes -# Format with built-in filter +{# Format with built-in filter #} README size: {{ file_size(path="README.md") | filesizeformat }} -# Compare file sizes +{# Compare file sizes #} {% set size_a = file_size(path="file_a.txt") %} {% set size_b = file_size(path="file_b.txt") %} {% if size_a > size_b %} @@ -1146,7 +672,7 @@ file_a.txt is larger file_b.txt is larger {% endif %} -# Calculate total size +{# Calculate total size #} {% set files = glob(pattern="data/*.json") %} {% set total_size = 0 %} {% for file in files %} @@ -1156,23 +682,24 @@ Total data size: {{ total_size | filesizeformat }} ``` #### `file_modified(path)` -Gets the last modification time of a file as a Unix timestamp (seconds since epoch). + +Get the last modification time of a file as a Unix timestamp. **Arguments:** - `path` (required) - Relative path to the file -**Returns:** Unix timestamp (number of seconds since January 1, 1970) +**Returns:** Unix timestamp (seconds since January 1, 1970) **Examples:** ``` -# Get modification timestamp +{# Get modification timestamp #} Last modified: {{ file_modified(path="config.json") }} -# Format with date filter +{# Format with date filter #} {% set timestamp = file_modified(path="README.md") %} Last updated: {{ timestamp | date(format="%Y-%m-%d %H:%M:%S") }} -# Check if file is recent +{# Check if file is recent #} {% set mod_time = file_modified(path="cache.dat") %} {% set now_time = now() %} {% set age_seconds = now_time - mod_time %} @@ -1181,19 +708,6 @@ Cache is fresh (less than 1 hour old) {% else %} Cache is stale ({{ age_seconds / 3600 }} hours old) {% endif %} - -# Find most recently modified file -{% set files = glob(pattern="logs/*.log") %} -{% set newest_time = 0 %} -{% set newest_file = "" %} -{% for file in files %} -{% set mod_time = file_modified(path=file) %} -{% if mod_time > newest_time %} -{% set newest_time = mod_time %} -{% set newest_file = file %} -{% endif %} -{% endfor %} -Most recent log: {{ newest_file }} ``` **Practical Example - Build Report:** @@ -1221,185 +735,24 @@ Total Rust files: {{ rs_files | length }} ## Tests {% set test_files = glob(pattern="tests/**/*.rs") %} Test files: {{ test_files | length }} -{% for test in test_files %} -- {{ test }} -{% endfor %} -``` - -**Security Restrictions:** - -All filesystem functions enforce the following security rules: - -1. **No Absolute Paths** - Paths starting with `/` are rejected - ``` - {{ read_file(path="/etc/passwd") }} # ✗ ERROR: Security violation - ``` - -2. **No Parent Directory Traversal** - Paths containing `..` are rejected - ``` - {{ read_file(path="../../secret.txt") }} # ✗ ERROR: Security violation - ``` - -3. **Relative Paths Only** - Only files within the current working directory are accessible - ``` - {{ read_file(path="config.txt") }} # ✓ OK - {{ read_file(path="data/file.txt") }} # ✓ OK - {{ file_exists(path="subdir/test.txt") }} # ✓ OK - ``` - -These restrictions ensure templates can only access files in the current working directory and its subdirectories, preventing unauthorized access to system files or files outside the project. - -**Trust Mode:** - -You can bypass these security restrictions by using the `--trust` command-line flag: - -```bash -# Without --trust: Security error -tmpltool template.tmpl # ERROR if template tries to read /etc/passwd - -# With --trust: Unrestricted access -tmpltool --trust template.tmpl # OK, can read any file -``` - -**When to use `--trust`:** -- When you need to access system files or configuration outside your project -- When reading files from absolute paths (e.g., `/etc/hosts`, `/var/log/app.log`) -- When accessing parent directories (e.g., `../config/settings.yml`) -- When you fully trust the template source and know what files it accesses - -**WARNING:** Only use `--trust` with templates you completely trust. Malicious templates could read sensitive files like SSH keys, passwords, or system configurations. - -### Validation Functions - -tmpltool provides validation functions to check if strings match specific formats. These are useful for validating user input, configuration values, or data from external sources. - -#### `is_email(string)` - -Validates if a string is a valid email address format. - -``` -Email: user@example.com -Valid: {{ is_email(string="user@example.com") }} -{# Output: Valid: true #} - -Email: invalid-email -Valid: {{ is_email(string="invalid-email") }} -{# Output: Valid: false #} -``` - -#### `is_url(string)` - -Validates if a string is a valid URL (supports http, https, ftp, file schemes). - -``` -URL: https://example.com/path -Valid: {{ is_url(string="https://example.com/path") }} -{# Output: Valid: true #} - -URL: not-a-url -Valid: {{ is_url(string="not-a-url") }} -{# Output: Valid: false #} -``` - -#### `is_ip(string)` - -Validates if a string is a valid IP address (IPv4 or IPv6). - -``` -IPv4: 192.168.1.1 -Valid: {{ is_ip(string="192.168.1.1") }} -{# Output: Valid: true #} - -IPv6: 2001:db8::1 -Valid: {{ is_ip(string="2001:db8::1") }} -{# Output: Valid: true #} - -Invalid: 256.1.1.1 -Valid: {{ is_ip(string="256.1.1.1") }} -{# Output: Valid: false #} ``` -#### `is_uuid(string)` - -Validates if a string is a valid UUID format. - -``` -UUID: 550e8400-e29b-41d4-a716-446655440000 -Valid: {{ is_uuid(string="550e8400-e29b-41d4-a716-446655440000") }} -{# Output: Valid: true #} - -Invalid: not-a-uuid -Valid: {{ is_uuid(string="not-a-uuid") }} -{# Output: Valid: false #} -``` - -#### `matches_regex(pattern, string)` - -Checks if a string matches a regular expression pattern. - -``` -{# Validate alphanumeric #} -{% if matches_regex(pattern="^[A-Za-z0-9]+$", string="Test123") %} - Valid alphanumeric string -{% endif %} - -{# Validate phone number format #} -{% set phone = get_env(name="PHONE", default="") %} -{% if matches_regex(pattern="^\\d{3}-\\d{3}-\\d{4}$", string=phone) %} - Phone number format: XXX-XXX-XXXX -{% endif %} - -{# Check for specific pattern #} -{% if matches_regex(pattern="^prod-", string="prod-server-01") %} - This is a production server -{% endif %} -``` - -**Practical Example - Configuration Validation:** -``` -# Configuration Validation Report - -{% set email = get_env(name="ADMIN_EMAIL", default="") %} -Admin Email: {{ email }} -{% if is_email(string=email) %} -✓ Valid email format -{% else %} -✗ Invalid email format -{% endif %} - -{% set api_url = get_env(name="API_URL", default="") %} -API URL: {{ api_url }} -{% if is_url(string=api_url) %} -✓ Valid URL format -{% else %} -✗ Invalid URL format -{% endif %} +### Data Parsing Functions -{% set server_ip = get_env(name="SERVER_IP", default="") %} -Server IP: {{ server_ip }} -{% if is_ip(string=server_ip) %} -✓ Valid IP address -{% else %} -✗ Invalid IP address -{% endif %} +Parse structured data formats (JSON, YAML, TOML) from strings or files. Useful for loading configuration files, processing API responses, or working with structured data. -{% set correlation_id = get_env(name="CORRELATION_ID", default="") %} -Correlation ID: {{ correlation_id }} -{% if is_uuid(string=correlation_id) %} -✓ Valid UUID format -{% else %} -✗ Invalid UUID format -{% endif %} -``` +**Security Note:** File-reading functions enforce the same security restrictions as other filesystem functions. -### Data Parsing Functions +#### `parse_json(string)` -tmpltool provides functions to parse structured data formats (JSON, YAML, TOML) from strings or files. These functions are useful for loading configuration files, processing API responses, or working with structured data in templates. +Parse a JSON string into an object. -#### `parse_json(string)` +**Arguments:** +- `string` (required) - JSON string to parse -Parse a JSON string into an object that can be used in templates. +**Returns:** Parsed JSON object +**Examples:** ``` {% set config = parse_json(string='{"name": "myapp", "port": 8080, "debug": true}') %} Application: {{ config.name }} @@ -1411,6 +764,12 @@ Debug mode: {{ config.debug }} Parse a YAML string into an object. +**Arguments:** +- `string` (required) - YAML string to parse + +**Returns:** Parsed YAML object + +**Examples:** ``` {% set data = parse_yaml(string=" name: myapp @@ -1426,6 +785,12 @@ Theme: {{ data.settings.theme }} Parse a TOML string into an object. +**Arguments:** +- `string` (required) - TOML string to parse + +**Returns:** Parsed TOML object + +**Examples:** ``` {% set config = parse_toml(string=' [database] @@ -1441,7 +806,12 @@ Cache: {{ config.cache.enabled }} #### `read_json_file(path)` -Read and parse a JSON file. The path is resolved relative to the template file's directory. +Read and parse a JSON file. + +**Arguments:** +- `path` (required) - Relative path to JSON file + +**Returns:** Parsed JSON object **Example JSON file** (`config/settings.json`): ```json @@ -1473,6 +843,11 @@ Features: Read and parse a YAML file. +**Arguments:** +- `path` (required) - Relative path to YAML file + +**Returns:** Parsed YAML object + **Example YAML file** (`config.yaml`): ```yaml services: @@ -1499,6 +874,11 @@ Services: Read and parse a TOML file. +**Arguments:** +- `path` (required) - Relative path to TOML file + +**Returns:** Parsed TOML object + **Example TOML file** (`Cargo.toml`): ```toml [package] @@ -1524,7 +904,6 @@ Dependencies: ``` **Practical Example - Multi-format Configuration:** - ``` {# Load configuration from different sources #} {% set json_config = read_json_file(path="config.json") %} @@ -1548,537 +927,352 @@ Rust Version: {{ toml_config.package.edition }} Dependencies: {{ toml_config.dependencies | length }} ``` -**Security Note**: Like other filesystem functions, data parsing file functions enforce security restrictions: -- Only relative paths allowed (no absolute paths like `/etc/config.json`) -- No parent directory traversal (no `..` in paths) -- Access restricted to current working directory and subdirectories -- Use `--trust` flag to bypass these restrictions for trusted templates +### Validation Functions -### Comments -``` -{# This is a comment #} -``` +Validate strings against specific formats. Useful for validating user input, configuration values, or data from external sources. -For complete Tera syntax documentation, visit: https://keats.github.io/tera/docs/ +#### `is_email(string)` -## Error Handling +Validate if a string is a valid email address format. -- If a template file doesn't exist, tmpltool will exit with an error -- If a template has syntax errors, tmpltool will report the error location -- Environment variable handling: - - **Direct access not supported:** `{{ ENV_VAR }}` will cause an error - environment variables are not automatically available - - **With default (recommended):** `{{ get_env(name="VAR", default="...") }}` will use the default value if the variable doesn't exist - - **Without default:** `{{ get_env(name="VAR") }}` will error if the variable doesn't exist +**Arguments:** +- `string` (required) - String to validate -## Help +**Returns:** Boolean (`true` if valid email, `false` otherwise) -```bash -tmpltool --help +**Examples:** ``` +Email: user@example.com +Valid: {{ is_email(string="user@example.com") }} +{# Output: Valid: true #} -## Version - -```bash -tmpltool --version +Email: invalid-email +Valid: {{ is_email(string="invalid-email") }} +{# Output: Valid: false #} ``` -## Development - -### Prerequisites - -- Rust 1.70 or higher -- Cargo (comes with Rust) - -Install Rust from [rustup.rs](https://rustup.rs/) if you haven't already: - -```bash -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -``` +#### `is_url(string)` -### Project Structure - -``` -tmpltool/ -├── Cargo.toml # Project dependencies and metadata -├── Cargo.lock # Locked dependency versions -├── Makefile.toml # cargo-make task definitions -├── src/ -│ ├── main.rs # Entry point (binary) -│ ├── lib.rs # Library root -│ ├── cli.rs # CLI argument parsing -│ ├── renderer.rs # Template rendering logic -│ └── functions/ # Custom Tera functions (extensibility point) -│ └── mod.rs # Functions module (for registering custom functions) -├── tests/ # Integration tests (one test per file) -│ ├── common.rs # Shared test utilities -│ ├── fixtures/ # Test fixtures (templates & expected outputs) -│ │ ├── templates/ # Input template files -│ │ ├── expected/ # Expected output files -│ │ └── README.md # Fixtures documentation -│ ├── test_successful_rendering.rs -│ ├── test_missing_template_file.rs -│ ├── test_invalid_template_syntax.rs -│ ├── test_environment_variable_substitution.rs -│ ├── test_template_with_conditionals.rs -│ ├── test_template_with_missing_variable.rs -│ ├── test_multiline_template.rs -│ ├── test_stdout_output.rs -│ └── test_direct_var_access_fails.rs -├── examples/ # Example templates -│ ├── basic.tmpl # Basic usage example -│ ├── greeting.tmpl # Simple greeting with defaults -│ ├── config.tmpl # Config file generation -│ ├── config-with-defaults.tmpl # Advanced config with get_env() function -│ ├── docker-compose.tmpl # Docker Compose with defaults -│ └── README.md # Examples documentation -├── .gitignore # Git ignore rules -├── .editorconfig # Editor configuration -└── README.md # This file -``` - -#### Module Organization - -**Source Code (`src/`)** - No tests in source files: -- **`main.rs`** - Minimal binary entry point, just parses CLI args and calls the library -- **`lib.rs`** - Public library API, exports main functionality -- **`cli.rs`** - CLI argument parsing using clap -- **`renderer.rs`** - Core template rendering logic (no unit tests) -- **`functions/`** - Custom Tera functions (modular, one file per function) - - **`mod.rs`** - Registers custom functions (currently empty - built-in functions like `get_env()` work automatically) - -**Tests (`tests/`)** - All tests as integration tests: -- **`common.rs`** - Shared test utilities and fixture helpers -- **`fixtures/`** - Test fixtures (templates and expected outputs) -- **Individual test files** - One test per file for better organization (11 tests total) - -#### Adding New Custom Functions - -To add a new custom function: - -1. Create a new file in `src/functions/` (e.g., `my_function.rs`) -2. Implement your function following the Tera function signature -3. Add the module declaration to `src/functions/mod.rs` -4. Register your function in the `register_all()` function -5. Write tests in your function file - -Note: Tera's built-in functions like `get_env()`, `now()`, and `get_random()` are automatically available when the "builtins" feature is enabled. +Validate if a string is a valid URL (supports http, https, ftp, file schemes). -### Building +**Arguments:** +- `string` (required) - String to validate -Build the project in debug mode: +**Returns:** Boolean (`true` if valid URL, `false` otherwise) -```bash -cargo build +**Examples:** ``` +URL: https://example.com/path +Valid: {{ is_url(string="https://example.com/path") }} +{# Output: Valid: true #} -Build optimized release binary: - -```bash -cargo build --release +URL: not-a-url +Valid: {{ is_url(string="not-a-url") }} +{# Output: Valid: false #} ``` -The release binary will be located at `./target/release/tmpltool`. - -### Using cargo-make (Task Runner) +#### `is_ip(string)` -This project includes a comprehensive `Makefile.toml` for [cargo-make](https://github.com/sagiegurari/cargo-make), providing standardized tasks for building, testing, and cross-platform compilation. +Validate if a string is a valid IP address (IPv4 or IPv6). -#### Installation +**Arguments:** +- `string` (required) - String to validate -Install cargo-make globally: +**Returns:** Boolean (`true` if valid IP, `false` otherwise) -```bash -cargo install --force cargo-make +**Examples:** ``` +IPv4: 192.168.1.1 +Valid: {{ is_ip(string="192.168.1.1") }} +{# Output: Valid: true #} -#### Available Tasks - -View all available tasks: +IPv6: 2001:db8::1 +Valid: {{ is_ip(string="2001:db8::1") }} +{# Output: Valid: true #} -```bash -cargo make +Invalid: 256.1.1.1 +Valid: {{ is_ip(string="256.1.1.1") }} +{# Output: Valid: false #} ``` -**Common Development Tasks:** +#### `is_uuid(string)` -```bash -# Build and test -cargo make build # Build debug binary -cargo make build-release # Build optimized binary -cargo make test # Run all tests -cargo make test-verbose # Run tests with output -cargo make run # Run with example template +Validate if a string is a valid UUID format. + +**Arguments:** +- `string` (required) - String to validate -# Code quality -cargo make format # Format code with rustfmt -cargo make clippy # Run clippy linter -cargo make qa # Full quality check (format + clippy + test) -cargo make ci # CI checks (format-check + clippy + test) +**Returns:** Boolean (`true` if valid UUID, `false` otherwise) -# Utilities -cargo make clean # Clean build artifacts -cargo make check # Fast compile check -cargo make docs # Generate and open documentation -cargo make test-examples # Test all example templates +**Examples:** ``` +UUID: 550e8400-e29b-41d4-a716-446655440000 +Valid: {{ is_uuid(string="550e8400-e29b-41d4-a716-446655440000") }} +{# Output: Valid: true #} -**Cross-Platform Builds:** +Invalid: not-a-uuid +Valid: {{ is_uuid(string="not-a-uuid") }} +{# Output: Valid: false #} +``` -Build release binaries for different platforms: +#### `matches_regex(pattern, string)` -```bash -# Individual platforms -cargo make build-linux-x86_64 # Linux x86_64 -cargo make build-linux-musl # Linux (static, musl libc) -cargo make build-macos-x86_64 # macOS Intel -cargo make build-macos-aarch64 # macOS Apple Silicon -cargo make build-windows-x86_64 # Windows x86_64 +Check if a string matches a regular expression pattern. -# Build for all platforms -cargo make build-all-platforms -``` +**Arguments:** +- `pattern` (required) - Regular expression pattern +- `string` (required) - String to match against -**Note:** Cross-compilation may require installing additional targets: +**Returns:** Boolean (`true` if matches, `false` otherwise) -```bash -# Add targets for cross-compilation -rustup target add x86_64-unknown-linux-gnu -rustup target add x86_64-unknown-linux-musl -rustup target add x86_64-apple-darwin -rustup target add aarch64-apple-darwin -rustup target add x86_64-pc-windows-gnu +**Examples:** ``` +{# Validate alphanumeric #} +{% if matches_regex(pattern="^[A-Za-z0-9]+$", string="Test123") %} + Valid alphanumeric string +{% endif %} -**Workflow Tasks:** +{# Validate phone number format #} +{% set phone = get_env(name="PHONE", default="") %} +{% if matches_regex(pattern="^\\d{3}-\\d{3}-\\d{4}$", string=phone) %} + Phone number format: XXX-XXX-XXXX +{% endif %} -```bash -cargo make dev # Quick dev check (check + test) -cargo make pre-commit # Pre-commit checks -cargo make release-prepare # Full release preparation -cargo make all # Complete build and test suite +{# Check for specific pattern #} +{% if matches_regex(pattern="^prod-", string="prod-server-01") %} + This is a production server +{% endif %} ``` -**Additional Tools:** - -```bash -cargo make audit # Security audit (requires cargo-audit) -cargo make outdated # Check outdated dependencies (requires cargo-outdated) -cargo make bloat # Analyze binary size (requires cargo-bloat) +**Practical Example - Configuration Validation:** ``` +# Configuration Validation Report -### Running in Development - -Run without building a binary: +{% set email = get_env(name="ADMIN_EMAIL", default="") %} +Admin Email: {{ email }} +{% if is_email(string=email) %} +✓ Valid email format +{% else %} +✗ Invalid email format +{% endif %} -```bash -cargo run -- template.txt -o output.txt -``` +{% set api_url = get_env(name="API_URL", default="") %} +API URL: {{ api_url }} +{% if is_url(string=api_url) %} +✓ Valid URL format +{% else %} +✗ Invalid URL format +{% endif %} -Run with environment variables: +{% set server_ip = get_env(name="SERVER_IP", default="") %} +Server IP: {{ server_ip %} +{% if is_ip(string=server_ip) %} +✓ Valid IP address +{% else %} +✗ Invalid IP address +{% endif %} -```bash -CUSTOM_VAR="test" cargo run -- template.txt +{% set correlation_id = get_env(name="CORRELATION_ID", default="") %} +Correlation ID: {{ correlation_id }} +{% if is_uuid(string=correlation_id) %} +✓ Valid UUID format +{% else %} +✗ Invalid UUID format +{% endif %} ``` -### Testing - -Run all unit tests: +## Advanced Examples -```bash -cargo test -``` +### Docker Compose Generator -Run tests with output: +**Template** (`docker-compose.tmpl`): +```yaml +version: '3.8' -```bash -cargo test -- --nocapture +services: + {{ get_env(name="SERVICE_NAME", default="app") }}: + image: {{ get_env(name="DOCKER_IMAGE", default="node:18") }} + ports: + - "{{ get_env(name="HOST_PORT", default="3000") }}:{{ get_env(name="CONTAINER_PORT", default="3000") }}" + environment: + - NODE_ENV={{ get_env(name="NODE_ENV", default="development") }} + {% set db_url = get_env(name="DATABASE_URL", default="") %} + {% if db_url %} + - DATABASE_URL={{ db_url }} + {% endif %} + {% set enable_volumes = get_env(name="ENABLE_VOLUMES", default="false") %} + {% if enable_volumes == "true" %} + volumes: + - ./app:/app + {% endif %} ``` -Run a specific test: - +**Render with custom values:** ```bash -cargo test test_successful_rendering +SERVICE_NAME=web \ +DOCKER_IMAGE=node:20 \ +HOST_PORT=8080 \ +NODE_ENV=production \ +DATABASE_URL=postgres://db:5432/mydb \ +ENABLE_VOLUMES=true \ +tmpltool docker-compose.tmpl -o docker-compose.yml ``` -Run tests in verbose mode: - +**Or use defaults:** ```bash -cargo test -- --test-threads=1 --nocapture +tmpltool docker-compose.tmpl -o docker-compose.yml ``` -#### Test Coverage - -The project includes comprehensive test coverage. **All tests are located in `tests/` directory** - there are no unit tests in `src/` files. - -**Integration Tests in `tests/`** (11 tests, one per file): -- `test_simple_rendering.rs` - Simple static template rendering -- `test_successful_rendering.rs` - Template rendering with environment variables -- `test_env_with_default.rs` - Environment variable with default fallback -- `test_missing_template_file.rs` - Missing template file handling -- `test_invalid_template_syntax.rs` - Invalid template syntax handling -- `test_environment_variable_substitution.rs` - Environment variable substitution with `get_env()` -- `test_template_with_conditionals.rs` - Conditional logic (if/else) -- `test_template_with_missing_variable.rs` - Missing variable detection -- `test_multiline_template.rs` - Multiline templates -- `test_stdout_output.rs` - Stdout output functionality -- `test_direct_var_access_fails.rs` - Direct variable access fails (security test) - -**Unit Tests in `tests/`** (58 tests across multiple test files): -- `test_filter_env_unit.rs` - Environment variable filtering (6 tests) -- `test_hash_unit.rs` - Hash functions (6 tests) -- `test_uuid_unit.rs` - UUID generation (3 tests) -- `test_random_string_unit.rs` - Random string generation (11 tests) -- `test_filesystem_unit.rs` - Filesystem functions (23 tests) -- `test_hash_crypto_functions.rs` - Hash and crypto integration (17 tests) -- `test_comprehensive.rs` - Comprehensive template validation (2 tests) - -**Test Infrastructure:** -- `common.rs` - Shared test utilities and fixture helpers -- `fixtures/` - Test fixtures (templates and expected outputs) - -**Documentation Tests** (2 tests): -- Library documentation examples - -Total: **71 tests** covering integration, unit tests, and documentation scenarios. - -#### Adding New Integration Tests - -To add a new integration test: - -1. Create a new file in `tests/` (e.g., `tests/test_my_feature.rs`) -2. Import the common utilities: `mod common;` and `use common::*;` -3. Import dependencies: `use tmpltool::render_template;` -4. Write your test function with `#[test]` attribute -5. Use helper functions: `get_test_file_path()` and `cleanup_test_file()` - -**Example:** - -```rust -mod common; - -use common::{cleanup_test_file, get_test_file_path}; -use std::fs; -use tmpltool::render_template; +### Comprehensive Application Configuration -#[test] -fn test_my_feature() { - let template_path = get_test_file_path("my_template.txt"); - let output_path = get_test_file_path("my_output.txt"); +See [examples/comprehensive-app-config.tmpl](examples/comprehensive-app-config.tmpl) for a complete example demonstrating all features: - // Create test template - fs::write(&template_path, "{{ get_env(name=\"TEST\") }}").unwrap(); +- All hash functions (MD5, SHA1, SHA256, SHA512) +- UUID generation +- Random strings with various charsets +- Environment variables with defaults +- Pattern filtering with `filter_env()` +- Conditionals (if/elif/else) +- Loops with ranges and arrays +- Filters (upper, lower, trim, slugify, replace, split, length) +- Comments +- Complex nested logic - // Run render_template - let result = render_template( - Some(template_path.to_str().unwrap()), - Some(output_path.to_str().unwrap()), - ); +### Pipeline Usage - // Assert results - assert!(result.is_ok()); - - // Cleanup - cleanup_test_file(&template_path); - cleanup_test_file(&output_path); -} -``` - -Each test file is compiled as a separate test binary, making tests more isolated and easier to debug. - -#### Using Test Fixtures +```bash +# Generate and validate JSON config +tmpltool config.json.tmpl | jq . -The project uses test fixtures to make tests easier to maintain. Fixtures are template files and their expected outputs stored in `tests/fixtures/`. +# Generate and apply Kubernetes config +tmpltool k8s-deployment.yaml.tmpl | kubectl apply -f - -**Fixture Directory Structure:** +# Generate nginx config and test it +tmpltool nginx.conf.tmpl | nginx -t -c /dev/stdin +# Combine multiple templates +cat header.tmpl body.tmpl footer.tmpl | tmpltool > complete.html ``` -tests/fixtures/ -├── templates/ # Input template files -│ ├── simple.tmpl -│ ├── with_env.tmpl -│ ├── multiline.tmpl -│ ├── conditionals.tmpl -│ └── docker-compose.tmpl -└── expected/ # Expected output files - ├── simple.txt - ├── with_env.txt - ├── multiline.txt - └── docker-compose.txt -``` - -**Using Fixtures in Tests:** - -```rust -mod common; -use common::{ - cleanup_test_file, get_test_file_path, - read_fixture_expected, read_fixture_template -}; -use std::fs; -use tmpltool::render_template; +### Trust Mode - System Files -#[test] -fn test_my_feature() { - let output_path = get_test_file_path("output.txt"); - - // Read template from fixtures - let template_content = read_fixture_template("my_template.tmpl"); - let template_path = get_test_file_path("template.txt"); - fs::write(&template_path, template_content).unwrap(); - - // Render template - let result = render_template( - Some(template_path.to_str().unwrap()), - Some(output_path.to_str().unwrap()), - ); - - // Compare with expected output - assert!(result.is_ok()); - let output = fs::read_to_string(&output_path).unwrap(); - let expected = read_fixture_expected("my_template.txt"); - assert_eq!(output, expected); - - // Cleanup - cleanup_test_file(&template_path); - cleanup_test_file(&output_path); -} +**Template** (`system_info.tmpl`): ``` +# System Information -**Benefits:** -- ✅ Test data separated from test logic -- ✅ Easy to maintain and review template changes -- ✅ Reusable across multiple tests -- ✅ Can use real-world template examples - -See [tests/fixtures/README.md](tests/fixtures/README.md) for more details. - -### Code Quality +## Hostname +{{ read_file(path="/etc/hostname") }} -Format code: +## Hosts File (first 200 chars) +{{ read_file(path="/etc/hosts") | truncate(length=200) }} -```bash -cargo fmt +## Files in /etc (first 10) +{% for file in list_dir(path="/etc") | slice(end=10) %} +- {{ file }} +{% endfor %} ``` -Run linter (clippy): - +**Render:** ```bash -cargo clippy -``` - -Run clippy with all warnings: +# Without --trust: Security error +tmpltool system_info.tmpl +# Error: Security: Absolute paths are not allowed -```bash -cargo clippy -- -W clippy::all +# With --trust: Works! +tmpltool --trust system_info.tmpl -o system_info.md ``` -### Dependencies +**WARNING:** Only use `--trust` with templates you completely trust. Malicious templates could read sensitive files like SSH keys, passwords, or system configurations. -The project uses minimal dependencies: +## Error Handling -- **[tera](https://crates.io/crates/tera)** (v1.x) - Template engine with `builtins` feature enabled - - Provides built-in filters: `slugify`, `date`, `filesizeformat`, `urlencode`, etc. - - Provides built-in functions: `get_env()`, `now()`, `get_random()` -- **[clap](https://crates.io/crates/clap)** (v4.x) - Command-line argument parsing -- **[regex](https://crates.io/crates/regex)** (v1.x) - Regular expressions for pattern matching -- **[md-5](https://crates.io/crates/md-5)** (v0.10) - MD5 hash implementation -- **[sha1](https://crates.io/crates/sha1)** (v0.10) - SHA1 hash implementation -- **[sha2](https://crates.io/crates/sha2)** (v0.10) - SHA256 and SHA512 hash implementations -- **[uuid](https://crates.io/crates/uuid)** (v1.x) - UUID generation -- **[rand](https://crates.io/crates/rand)** (v0.8) - Random number generation -- **[glob](https://crates.io/crates/glob)** (v0.3) - Glob pattern matching for filesystem operations +- **Missing template file:** tmpltool exits with an error +- **Invalid template syntax:** Error location is reported +- **Environment variables:** + - Direct access not supported: `{{ ENV_VAR }}` causes an error + - With default (recommended): `{{ get_env(name="VAR", default="...") }}` uses default if missing + - Without default: `{{ get_env(name="VAR") }}` errors if variable doesn't exist +- **Filesystem errors:** Clear error messages for missing files, permission issues, or security violations -To update dependencies: +## Development -```bash -cargo update -``` +### Prerequisites -### Manual Testing +- Rust 1.70 or higher +- Cargo (comes with Rust) -Create a test template: +Install Rust from [rustup.rs](https://rustup.rs/): ```bash -cat > test.tmpl << 'EOF' -User: {{ USER }} -Home: {{ HOME }} -Custom: {{ CUSTOM_VAR }} -EOF +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` -Test it: +### Building ```bash -CUSTOM_VAR="Hello World" ./target/release/tmpltool test.tmpl -``` +# Debug build +cargo build -### Debugging +# Release build +cargo build --release +# Binary at: ./target/release/tmpltool +``` -Run with debug output: +### Testing ```bash -RUST_BACKTRACE=1 cargo run -- template.txt -``` +# Run all tests +cargo test -Full backtrace: +# Run with output +cargo test -- --nocapture -```bash -RUST_BACKTRACE=full cargo run -- template.txt +# Run specific test +cargo test test_name ``` -### Performance - -Benchmark the binary size: +### Code Quality ```bash -ls -lh target/release/tmpltool -``` +# Format code +cargo fmt -Profile with release build: +# Run linter +cargo clippy -```bash -cargo build --release -time ./target/release/tmpltool large_template.txt -o output.txt +# Full QA check (format + clippy + test) +cargo make qa ``` -### Installing Locally +### Using cargo-make -Install from the project directory: +Install cargo-make: ```bash -cargo install --path . +cargo install --force cargo-make ``` -This installs the binary to `~/.cargo/bin/tmpltool` (make sure this is in your PATH). - -Uninstall: - +**Common tasks:** ```bash -cargo uninstall tmpltool +cargo make build # Build debug +cargo make build-release # Build release +cargo make test # Run tests +cargo make qa # Full QA (format + clippy + test) +cargo make ci # CI checks +cargo make docs # Generate docs ``` -### Using as a Library - -tmpltool can also be used as a library in other Rust projects: - -```rust -use tmpltool::render_template; - -fn main() -> Result<(), Box> { - // Render to file - render_template("template.txt", Some("output.txt"))?; - - // Render to stdout - render_template("template.txt", None)?; - - Ok(()) -} +**Cross-platform builds:** +```bash +cargo make build-linux-x86_64 # Linux x86_64 +cargo make build-linux-musl # Linux (static) +cargo make build-macos-x86_64 # macOS Intel +cargo make build-macos-aarch64 # macOS Apple Silicon +cargo make build-windows-x86_64 # Windows +cargo make build-all-platforms # All platforms ``` -The library exposes: -- `render_template(template_path: &str, output_path: Option<&str>)` - Main rendering function -- `Cli` - Command-line argument structure +For more development details, see the [full development guide](README.md.backup#development) in the backup. ## CI/CD @@ -2086,80 +1280,70 @@ This project uses GitHub Actions for continuous integration and automated releas ### Continuous Integration -Every pull request and push to master/main triggers: - -- **Code Formatting Check** - Ensures code follows Rust style guidelines (`rustfmt`) -- **Linting** - Runs `clippy` with strict warnings -- **Multi-Platform Tests** - Tests on Ubuntu, macOS, and Windows -- **Code Coverage** - Generates coverage reports with `cargo-tarpaulin` and uploads to Codecov -- **cargo-make QA** - Runs comprehensive quality checks -- **Example Testing** - Tests all example templates to ensure they work +Every pull request and push to master triggers: +- Code formatting check (`rustfmt`) +- Linting (`clippy`) +- Multi-platform tests (Ubuntu, macOS, Windows) +- Code coverage (uploaded to Codecov) +- Example template testing ### Automated Releases -Releases are fully automated using [semantic-release](https://github.com/semantic-release/semantic-release): - -1. **Commit Analysis** - Analyzes commit messages to determine the next version -2. **Version Bumping** - Updates `Cargo.toml` with the new version -3. **CHANGELOG Generation** - Automatically generates `CHANGELOG.md` from commits -4. **Multi-Platform Builds** - Builds release binaries for: - - Linux (x86_64, x86_64-musl, aarch64) - - macOS (x86_64 Intel, aarch64 Apple Silicon) - - Windows (x86_64) -5. **GitHub Release** - Creates a new GitHub release with all binaries -6. **Docker Image** - Builds and publishes multi-arch Docker image to GHCR +Releases use [semantic-release](https://github.com/semantic-release/semantic-release): +1. Analyzes commit messages +2. Determines next version +3. Updates `Cargo.toml` +4. Generates `CHANGELOG.md` +5. Builds multi-platform binaries +6. Creates GitHub release +7. Publishes Docker images to GHCR ### Commit Convention -This project follows [Conventional Commits](https://www.conventionalcommits.org/) for automatic versioning: +Follow [Conventional Commits](https://www.conventionalcommits.org/): -- `feat: description` - New feature (minor version bump: 1.2.0 → 1.3.0) -- `fix: description` - Bug fix (patch version bump: 1.2.0 → 1.2.1) -- `feat!: description` or `BREAKING CHANGE:` - Breaking change (major version bump: 1.2.0 → 2.0.0) -- `docs:`, `refactor:`, `perf:`, `build:` - Other changes (patch bump) +- `feat: description` - New feature (minor bump: 1.2.0 → 1.3.0) +- `fix: description` - Bug fix (patch bump: 1.2.0 → 1.2.1) +- `feat!: description` - Breaking change (major bump: 1.2.0 → 2.0.0) +- `docs:`, `refactor:`, `perf:` - Other changes (patch bump) - `style:`, `test:`, `chore:`, `ci:` - No version bump **Examples:** - ```bash -# Feature (minor bump) -git commit -m "feat: add slugify filter support" +git commit -m "feat: add validation functions" +git commit -m "fix: correct path resolution" +git commit -m "feat!: change output behavior -# Bug fix (patch bump) -git commit -m "fix: correct multiline template rendering" - -# Breaking change (major bump) -git commit -m "feat!: change default output behavior - -BREAKING CHANGE: Output now goes to stdout by default instead of file" +BREAKING CHANGE: Output now defaults to stdout" ``` -See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed contribution guidelines. - -## License - -This project is open source. - ## Contributing -Contributions are welcome! Please feel free to submit issues or pull requests. - -For detailed contribution guidelines, including commit conventions, development workflow, and testing requirements, see [CONTRIBUTING.md](CONTRIBUTING.md). +Contributions are welcome! For detailed guidelines, see [CONTRIBUTING.md](CONTRIBUTING.md). ### Quick Start 1. Fork the repository -2. Clone your fork and install dependencies: +2. Clone and install dependencies: ```bash - git clone https://github.com/bordeux/tmpltool.git + git clone https://github.com/YOUR_USERNAME/tmpltool.git cd tmpltool - npm install # Installs commit validation hooks + npm install # Installs commit hooks ``` -3. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Create a feature branch 4. Make your changes -5. Run tests and QA checks (`cargo make qa`) -6. Commit using [conventional commits](#commit-convention) - invalid commits will be automatically rejected -7. Push to your fork -8. Open a Pull Request +5. Run QA checks: `cargo make qa` +6. Commit using conventional commits +7. Push and open a Pull Request -**Note:** Commit messages are automatically validated. If your commit is rejected, make sure it follows the [conventional commit format](#commit-convention). +**Note:** Commit messages are automatically validated. Invalid commits will be rejected. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +--- + +For more examples, see the [examples/](examples/) directory and [examples/README.md](examples/README.md). + +For complete Tera syntax documentation, visit: https://keats.github.io/tera/docs/ diff --git a/README.md.backup b/README.md.backup new file mode 100644 index 0000000..22b9306 --- /dev/null +++ b/README.md.backup @@ -0,0 +1,2165 @@ +# tmpltool + +[![CI](https://github.com/bordeux/tmpltool/actions/workflows/ci.yml/badge.svg)](https://github.com/bordeux/tmpltool/actions/workflows/ci.yml) +[![Release](https://github.com/bordeux/tmpltool/actions/workflows/release.yml/badge.svg)](https://github.com/bordeux/tmpltool/actions/workflows/release.yml) +[![codecov](https://codecov.io/gh/bordeux/tmpltool/branch/master/graph/badge.svg)](https://codecov.io/gh/bordeux/tmpltool) +[![GitHub release](https://img.shields.io/github/v/release/bordeux/tmpltool)](https://github.com/bordeux/tmpltool/releases) +[![Docker](https://img.shields.io/badge/docker-ghcr.io-blue)](https://github.com/bordeux/tmpltool/pkgs/container/tmpltool) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) + +A fast and simple command-line template rendering tool using [Tera](https://keats.github.io/tera/) templates with environment variables. + +## Features + +- Render Tera templates with environment variable support via `get_env()` function +- Filter environment variables by pattern with `filter_env()` function +- Cryptographic hash functions: `md5()`, `sha1()`, `sha256()`, `sha512()` +- UUID generation with `uuid()` function +- Random string generation with `random_string()` function +- Filesystem functions: `read_file()`, `file_exists()`, `list_dir()`, `glob()`, `file_size()`, `file_modified()` +- Output to file or stdout (for piping) +- Simple CLI interface +- Single binary executable +- Full Tera template syntax support (variables, conditionals, loops, filters, etc.) +- Built-in filters: slugify, date, urlencode, filesizeformat, and more +- Default values for environment variables (no errors when variables are missing) + +## Installation + +### From GitHub Releases + +Download pre-built binaries for your platform from the [releases page](https://github.com/bordeux/tmpltool/releases): + +- **Linux**: `tmpltool-linux-x86_64`, `tmpltool-linux-x86_64-musl` (static), `tmpltool-linux-aarch64` (ARM64) +- **macOS**: `tmpltool-macos-x86_64` (Intel), `tmpltool-macos-aarch64` (Apple Silicon) +- **Windows**: `tmpltool-windows-x86_64.exe` + +Extract and place in your PATH: + +```bash +# Linux/macOS example +tar -xzf tmpltool-linux-x86_64.tar.gz +sudo mv tmpltool /usr/local/bin/ +chmod +x /usr/local/bin/tmpltool +``` + +### Using Docker + +Pull from GitHub Container Registry: + +```bash +docker pull ghcr.io/bordeux/tmpltool:latest +``` + +Run with Docker: + +```bash +# Using a template file +docker run --rm -v $(pwd):/workspace -w /workspace ghcr.io/bordeux/tmpltool:latest template.tmpl + +# With environment variables +docker run --rm -e NAME=Alice -v $(pwd):/workspace -w /workspace \ + ghcr.io/bordeux/tmpltool:latest greeting.tmpl + +# Output to file +docker run --rm -v $(pwd):/workspace -w /workspace \ + ghcr.io/bordeux/tmpltool:latest template.tmpl -o output.txt +``` + +Create a shell alias for convenience: + +```bash +alias tmpltool='docker run --rm -v $(pwd):/workspace -w /workspace ghcr.io/bordeux/tmpltool:latest' +``` + +### From Source + +```bash +cargo install --path . +``` + +Or build manually: + +```bash +cargo build --release +# Binary will be at: ./target/release/tmpltool +``` + +## Usage + +```bash +# Read from file +tmpltool [TEMPLATE] [OPTIONS] + +# Read from stdin +cat template.txt | tmpltool [OPTIONS] +``` + +### Arguments + +- `[TEMPLATE]` - Path to the template file (optional) + - If omitted, reads template from stdin + +### Options + +- `-o, --output ` - Output file path (optional) + - If not specified, output is printed to stdout +- `--trust` - Trust mode: Allow filesystem functions to access absolute paths and parent directories (optional) + - **WARNING:** This disables security restrictions. Only use with trusted templates. + - Without this flag, filesystem functions are restricted to relative paths within the current working directory + - With this flag, you can access any file on the system (e.g., `/etc/passwd`, `../../secret.txt`) + +### Input/Output Combinations + +tmpltool supports all standard Unix I/O patterns: + +| Input | Output | Command | +|--------|--------|---------| +| File | stdout | `tmpltool template.txt` | +| File | File | `tmpltool template.txt -o output.txt` | +| stdin | stdout | `cat template.txt \| tmpltool` | +| stdin | File | `cat template.txt \| tmpltool -o output.txt` | + +### Examples + +#### File to stdout + +```bash +tmpltool template.txt +``` + +#### File to file + +```bash +tmpltool template.txt -o output.txt +``` + +#### stdin to stdout (pipe) + +```bash +cat template.txt | tmpltool +echo "Hello {{ get_env(name=\"NAME\", default=\"World\") }}!" | tmpltool +``` + +#### stdin to file + +```bash +cat template.txt | tmpltool -o output.txt +``` + +#### Chaining with other tools + +```bash +# Generate and validate +tmpltool config.json.tmpl | jq . + +# Generate from stdin and apply +cat k8s-deployment.yaml.tmpl | tmpltool | kubectl apply -f - + +# Combine multiple templates +cat header.tmpl body.tmpl footer.tmpl | tmpltool > complete.html +``` + +#### Using Trust Mode for System Files + +```bash +# Create a template that reads system files +cat > system_info.tmpl << 'EOF' +# System Information + +## Hostname +{{ read_file(path="/etc/hostname") }} + +## Hosts File (first 200 chars) +{{ read_file(path="/etc/hosts") | truncate(length=200) }} + +## Files in /etc (first 10) +{% for file in list_dir(path="/etc") | slice(end=10) %} +- {{ file }} +{% endfor %} +EOF + +# Without --trust: Security error +tmpltool system_info.tmpl +# Error: Security: Absolute paths and parent directory (..) access are not allowed + +# With --trust: Works! +tmpltool --trust system_info.tmpl -o system_info.md +``` + +#### Using Environment Variables + +Create a template file `greeting.tmpl`: +``` +Hello {{ get_env(name="USER") }}! +Your home directory is: {{ get_env(name="HOME") }} +Your shell: {{ get_env(name="SHELL") }} +``` + +Render it: +```bash +tmpltool greeting.tmpl +``` + +Output: +``` +Hello username! +Your home directory is: /home/username +Your shell: /bin/bash +``` + +**Note:** These environment variables (USER, HOME, SHELL) are typically available on Unix systems. + +#### Setting Custom Environment Variables + +Create a template `config.tmpl`: +``` +Database: {{ get_env(name="DB_HOST", default="localhost") }}:{{ get_env(name="DB_PORT", default="5432") }} +Environment: {{ get_env(name="APP_ENV", default="development") }} +Debug: {{ get_env(name="DEBUG", default="false") }} +``` + +Render with custom variables: +```bash +DB_HOST=postgres DB_PORT=5432 APP_ENV=production DEBUG=true tmpltool config.tmpl +``` + +Output: +``` +Database: postgres:5432 +Environment: production +Debug: true +``` + +Or render without any environment variables (using defaults): +```bash +tmpltool config.tmpl +``` + +Output: +``` +Database: localhost:5432 +Environment: development +Debug: false +``` + +#### Using Conditionals + +Template `status.tmpl`: +``` +{% set debug = get_env(name="DEBUG", default="false") %} +{% if debug == "true" %} +DEBUG MODE ENABLED +Log level: verbose +{% else %} +Production mode +Log level: error +{% endif %} +``` + +Render with DEBUG enabled: +```bash +DEBUG=true tmpltool status.tmpl +``` + +Output: +``` +DEBUG MODE ENABLED +Log level: verbose +``` + +Render without DEBUG (uses default): +```bash +tmpltool status.tmpl +``` + +Output: +``` +Production mode +Log level: error +``` + +#### Using Loops + +You can use environment variables with Tera's split filter to create lists: + +Template `list.tmpl`: +``` +{% set items_str = get_env(name="ITEMS", default="apple,banana,orange") %} +{% set items = items_str | split(pat=",") %} +Items: +{% for item in items %} + - {{ item }} +{% endfor %} +``` + +Render with custom list: +```bash +ITEMS="apple,banana,orange,grape" tmpltool list.tmpl +``` + +Output: +``` +Items: + - apple + - banana + - orange + - grape +``` + +Or use the default list: +```bash +tmpltool list.tmpl +``` + +Output: +``` +Items: + - apple + - banana + - orange +``` + +#### Using Filters + +Template `formatted.tmpl`: +``` +{% set name = get_env(name="NAME", default="john doe") %} +Uppercase: {{ name | upper }} +Lowercase: {{ name | lower }} +Title Case: {{ name | title }} +Slugified: {{ name | slugify }} +``` + +Render with custom name: +```bash +NAME="Jane Smith" tmpltool formatted.tmpl +``` + +Output: +``` +Uppercase: JANE SMITH +Lowercase: jane smith +Title Case: Jane Smith +Slugified: jane-smith +``` + +Or use default: +```bash +tmpltool formatted.tmpl +``` + +Output: +``` +Uppercase: JOHN DOE +Lowercase: john doe +Title Case: John Doe +Slugified: john-doe +``` + +#### Filtering Environment Variables by Pattern + +Use the `filter_env()` function to get all environment variables matching a pattern: + +Template `server-vars.tmpl`: +``` +Server Configuration: +{% for var in filter_env(pattern="SERVER_*") %} + {{ var.key }}={{ var.value }} +{% endfor %} +``` + +Set environment variables: +```bash +SERVER_HOST=localhost \ +SERVER_PORT=8080 \ +SERVER_NAME=myapp \ +OTHER_VAR=ignored \ +tmpltool server-vars.tmpl +``` + +Output: +``` +Server Configuration: + SERVER_HOST=localhost + SERVER_NAME=myapp + SERVER_PORT=8080 +``` + +**Pattern Syntax:** +- `*` - matches any characters (e.g., `SERVER_*` matches `SERVER_HOST`, `SERVER_PORT`, etc.) +- `?` - matches exactly one character (e.g., `DB_?` matches `DB_A`, `DB_B`, but not `DB_AB`) +- Patterns can be at the beginning, middle, or end (e.g., `*_PORT`, `APP_*_NAME`) + +The results are returned as an array of objects with `key` and `value` fields, sorted alphabetically by key. + +#### Complex Example - Docker Compose Generator + +Template `docker-compose.tmpl`: +```yaml +version: '3.8' + +services: + {{ get_env(name="SERVICE_NAME", default="app") }}: + image: {{ get_env(name="DOCKER_IMAGE", default="node:18") }} + ports: + - "{{ get_env(name="HOST_PORT", default="3000") }}:{{ get_env(name="CONTAINER_PORT", default="3000") }}" + environment: + - NODE_ENV={{ get_env(name="NODE_ENV", default="development") }} + {% set db_url = get_env(name="DATABASE_URL", default="") %} + {% if db_url %} + - DATABASE_URL={{ db_url }} + {% endif %} + {% set enable_volumes = get_env(name="ENABLE_VOLUMES", default="false") %} + {% if enable_volumes == "true" %} + volumes: + - ./app:/app + {% endif %} +``` + +Render with all custom values: +```bash +SERVICE_NAME=web \ +DOCKER_IMAGE=node:20 \ +HOST_PORT=8080 \ +CONTAINER_PORT=3000 \ +NODE_ENV=production \ +DATABASE_URL=postgres://db:5432/mydb \ +ENABLE_VOLUMES=true \ +tmpltool docker-compose.tmpl -o docker-compose.yml +``` + +Or use all defaults (works out of the box!): +```bash +tmpltool docker-compose.tmpl -o docker-compose.yml +``` + +This generates a working docker-compose.yml with sensible defaults. + +#### Pipeline Usage + +Generate and validate JSON config: +```bash +tmpltool config.json.tmpl | jq . +``` + +Generate and apply Kubernetes config: +```bash +tmpltool k8s-deployment.yaml.tmpl | kubectl apply -f - +``` + +Generate nginx config and test it: +```bash +tmpltool nginx.conf.tmpl | nginx -t -c /dev/stdin +``` + +#### Comprehensive Example - All Features + +This example demonstrates all tmpltool features in a single template: + +Template `comprehensive-app-config.tmpl`: +```yaml +# Application Configuration +# Generated: {{ now() }} +# Instance ID: {{ uuid() }} + +{# ============================================ + Service Configuration + ============================================ #} +service: + name: {{ get_env(name="APP_NAME", default="myapp") | upper }} + version: {{ get_env(name="APP_VERSION", default="1.0.0") }} + environment: {{ get_env(name="ENV", default="development") | upper }} + + # Unique identifiers + instance_id: {{ uuid() }} + deployment_id: {{ uuid() }} + +{# ============================================ + Security & Authentication + ============================================ #} +security: + # Hash functions for integrity checks + config_checksum: {{ md5(string="v1.0-config") }} + license_hash: {{ sha256(string=get_env(name="LICENSE_KEY", default="trial-license")) }} + + # Generated secrets + api_key: {{ random_string(length=32, charset="hex") }} + secret_token: {{ random_string(length=64) }} + csrf_token: {{ random_string(length=40, charset="hex") }} + session_secret: {{ random_string(length=32, charset="alphanumeric") }} + + # Password hashing (example - use proper password hashing in production!) + {% set admin_pwd = get_env(name="ADMIN_PASSWORD", default="changeme123") %} + admin_password_hash: {{ sha512(string=admin_pwd) }} + +{# ============================================ + Database Configuration + ============================================ #} +database: + # Filter all DB_* environment variables + {% set db_vars = filter_env(pattern="DB_*") %} + {% if db_vars | length > 0 %} + # From environment: + {% for var in db_vars %} + {{ var.key | lower | replace(from="db_", to="") }}: {{ var.value }} + {% endfor %} + {% else %} + # Default configuration: + host: {{ get_env(name="DB_HOST", default="localhost") }} + port: {{ get_env(name="DB_PORT", default="5432") }} + name: {{ get_env(name="DB_NAME", default="myapp_db") }} + user: {{ get_env(name="DB_USER", default="app_user") }} + {% endif %} + + # Connection pool + max_connections: {{ get_env(name="DB_MAX_CONNECTIONS", default="20") }} + connection_id: {{ uuid() }} + +{# ============================================ + Server Configuration + ============================================ #} +server: + {% set servers = filter_env(pattern="SERVER_*") %} + {% if servers | length > 0 %} + # Detected server configuration: + {% for srv in servers %} + {{ srv.key | lower | replace(from="server_", to="") }}: {{ srv.value }} + {% endfor %} + {% else %} + # Default server configuration: + host: {{ get_env(name="HOST", default="0.0.0.0") }} + port: {{ get_env(name="PORT", default="8080") }} + protocol: {{ get_env(name="PROTOCOL", default="http") }} + {% endif %} + + # TLS/SSL + {% set enable_tls = get_env(name="ENABLE_TLS", default="false") %} + {% if enable_tls == "true" %} + tls: + enabled: true + cert_path: {{ get_env(name="TLS_CERT_PATH", default="/etc/ssl/cert.pem") }} + key_path: {{ get_env(name="TLS_KEY_PATH", default="/etc/ssl/key.pem") }} + {% else %} + tls: + enabled: false + {% endif %} + +{# ============================================ + Logging Configuration + ============================================ #} +logging: + {% set env_type = get_env(name="ENV", default="development") %} + {% if env_type == "production" %} + level: ERROR + format: json + output: /var/log/app/production.log + {% elif env_type == "staging" %} + level: WARN + format: json + output: /var/log/app/staging.log + {% else %} + level: DEBUG + format: text + output: stdout + {% endif %} + + # Log rotation ID + rotation_id: {{ uuid() }} + +{# ============================================ + Feature Flags + ============================================ #} +features: + {% set features = get_env(name="FEATURES", default="api,web,admin") | split(pat=",") %} + enabled: [{% for feature in features %}"{{ feature | trim }}"{% if not loop.last %}, {% endif %}{% endfor %}] + count: {{ features | length }} + + # Feature-specific settings + {% for feature in features %} + {{ feature | trim | slugify }}: + enabled: true + token: {{ random_string(length=16, charset="hex") }} + {% endfor %} + +{# ============================================ + External Services + ============================================ #} +external_services: + # All API_* environment variables + {% set api_vars = filter_env(pattern="API_*") %} + {% if api_vars | length > 0 %} + apis: + {% for api in api_vars %} + {{ api.key | lower | replace(from="api_", to="") }}: + url: {{ api.value }} + key: {{ random_string(length=32, charset="hex") }} + checksum: {{ md5(string=api.value) }} + {% endfor %} + {% else %} + apis: [] + {% endif %} + +{# ============================================ + Cache Configuration + ============================================ #} +cache: + {% set cache_type = get_env(name="CACHE_TYPE", default="memory") %} + type: {{ cache_type }} + ttl: {{ get_env(name="CACHE_TTL", default="3600") }} + + {% if cache_type == "redis" %} + redis: + host: {{ get_env(name="REDIS_HOST", default="localhost") }} + port: {{ get_env(name="REDIS_PORT", default="6379") }} + db: {{ get_env(name="REDIS_DB", default="0") }} + password_hash: {{ sha256(string=get_env(name="REDIS_PASSWORD", default="")) }} + {% endif %} + +{# ============================================ + Monitoring & Metrics + ============================================ #} +monitoring: + enabled: {{ get_env(name="ENABLE_MONITORING", default="true") }} + endpoint: {{ get_env(name="METRICS_ENDPOINT", default="/metrics") }} + + # Unique tracking IDs + cluster_id: {{ uuid() }} + node_id: {{ uuid() }} + + # Sample intervals (in seconds) + {% set intervals = get_env(name="SAMPLE_INTERVALS", default="10,30,60") | split(pat=",") %} + sample_intervals: [{% for interval in intervals %}{{ interval }}{% if not loop.last %}, {% endif %}{% endfor %}] + +{# ============================================ + Recovery & Backup + ============================================ #} +recovery: + # Recovery codes (for 2FA backup) + codes: + {% for i in range(end=5) %} + - {{ random_string(length=8, charset="uppercase") }}-{{ random_string(length=8, charset="uppercase") }} + {% endfor %} + + # Backup encryption key + backup_key: {{ random_string(length=64, charset="hex") }} + backup_key_hash: {{ sha256(string=get_env(name="BACKUP_PASSPHRASE", default="default-passphrase")) }} + +{# ============================================ + Metadata + ============================================ #} +metadata: + generated_at: {{ now() }} + generated_by: tmpltool + template_version: "2.0" + config_hash: {{ sha1(string="comprehensive-config-v2.0") }} + + # All environment variables used + environment_variables: + {% set all_env = filter_env(pattern="*") %} + total_count: {{ all_env | length }} + app_vars: {{ filter_env(pattern="APP_*") | length }} + db_vars: {{ filter_env(pattern="DB_*") | length }} + server_vars: {{ filter_env(pattern="SERVER_*") | length }} +``` + +Set environment variables and render: +```bash +# Set application variables +export APP_NAME="mywebapp" +export APP_VERSION="2.1.0" +export ENV="production" + +# Set database variables +export DB_HOST="db.example.com" +export DB_PORT="5432" +export DB_NAME="production_db" +export DB_USER="app_prod" +export DB_MAX_CONNECTIONS="50" + +# Set server variables +export SERVER_HOST="api.example.com" +export SERVER_PORT="443" +export SERVER_PROTOCOL="https" + +# Enable features +export ENABLE_TLS="true" +export TLS_CERT_PATH="/etc/ssl/certs/app.crt" +export TLS_KEY_PATH="/etc/ssl/private/app.key" + +# Set security +export ADMIN_PASSWORD="SecureP@ssw0rd123" +export LICENSE_KEY="PROD-ABC123-XYZ789" + +# Set features +export FEATURES="api,web,admin,analytics,reporting" + +# External services +export API_PAYMENT_URL="https://api.payment.example.com" +export API_EMAIL_URL="https://api.email.example.com" + +# Cache configuration +export CACHE_TYPE="redis" +export REDIS_HOST="cache.example.com" +export REDIS_PORT="6379" +export REDIS_PASSWORD="redis-secure-pass" + +# Render the configuration +tmpltool comprehensive-app-config.tmpl -o app-config.yaml +``` + +This example demonstrates: +- ✅ All hash functions: `md5()`, `sha1()`, `sha256()`, `sha512()` +- ✅ UUID generation: `uuid()` +- ✅ Random strings: `random_string()` with various charsets +- ✅ Environment variables: `get_env()` with defaults +- ✅ Pattern filtering: `filter_env()` +- ✅ Conditionals: `if/elif/else` +- ✅ Loops: `for` loops with ranges and arrays +- ✅ Filters: `upper`, `lower`, `trim`, `slugify`, `replace`, `split`, `length` +- ✅ Comments: `{# ... #}` +- ✅ String operations: concatenation and formatting +- ✅ Complex logic: nested conditions and loops + +**Note:** The comprehensive example does not include filesystem functions. For filesystem function examples, see the [Filesystem Functions](#filesystem-functions) section. + +## Examples + +The `examples/` directory contains ready-to-use template examples demonstrating various features: + +- **`basic.tmpl`** - Basic variable substitution and conditionals +- **`greeting.tmpl`** - Simple greeting with `get_env()` function +- **`config.tmpl`** - Application configuration file generation +- **`docker-compose.tmpl`** - Docker Compose with sensible defaults +- **`config-with-defaults.tmpl`** - Advanced config using `get_env()` function (recommended) +- **`server-config.tmpl`** - Server configuration using `filter_env()` pattern matching +- **`hash-crypto.tmpl`** - Demonstrates all hash functions, UUID, and random string generation +- **`comprehensive-app-config.tmpl`** - Complete showcase of ALL features (recommended for learning) + +### Try an Example + +```bash +# Basic example with environment variables +CUSTOM_VAR="Hello World" tmpltool examples/basic.tmpl + +# Greeting with defaults +tmpltool examples/greeting.tmpl + +# Generate a docker-compose.yml with all defaults (works out of the box!) +tmpltool examples/docker-compose.tmpl -o docker-compose.yml + +# Generate docker-compose.yml with custom values +SERVICE_NAME=web \ +DATABASE_URL=postgres://db:5432/myapp \ +ENABLE_VOLUMES=true \ +tmpltool examples/docker-compose.tmpl -o docker-compose.yml + +# Config with get_env() function and defaults +tmpltool examples/config-with-defaults.tmpl + +# Hash and crypto functions +tmpltool examples/hash-crypto.tmpl + +# Comprehensive example with ALL features (great for learning!) +tmpltool examples/comprehensive-app-config.tmpl + +# Comprehensive example with environment variables +APP_NAME="MyWebApp" \ +ENV="production" \ +DB_HOST="db.example.com" \ +FEATURES="api,web,admin" \ +tmpltool examples/comprehensive-app-config.tmpl -o app-config.yaml +``` + +See the [examples/README.md](examples/README.md) for detailed documentation of each example. + +## Template Syntax + +tmpltool uses the [Tera](https://keats.github.io/tera/) template engine. Here are some common syntax patterns: + +### Variables +``` +{{ variable_name }} +``` + +**Note:** Environment variables are NOT automatically available as variables. Use the `get_env()` function to access them (see below). + +### Conditionals +``` +{% if CONDITION %} + ... +{% elif OTHER_CONDITION %} + ... +{% else %} + ... +{% endif %} +``` + +### Loops +``` +{% for item in items %} + {{ item }} +{% endfor %} +``` + +### Filters +``` +{{ variable | filter_name }} +{{ variable | filter_name(arg=value) }} +``` + +### Built-in `get_env()` Function + +tmpltool uses Tera's built-in `get_env()` function for accessing environment variables with optional defaults: + +``` +{{ get_env(name="VARIABLE_NAME", default="fallback_value") }} +``` + +**Examples:** +``` +# With default value (recommended) +port = {{ get_env(name="PORT", default="8080") }} +database = {{ get_env(name="DB_URL", default="postgres://localhost/mydb") }} + +# Without default (will error if variable doesn't exist) +api_key = {{ get_env(name="API_KEY") }} + +# Use in conditionals (requires {% set %} first) +{% set debug = get_env(name="DEBUG", default="false") %} +{% if debug == "true" %} + Debug mode enabled +{% endif %} +``` + +**Benefits:** +- No template errors when environment variables are missing +- Sensible defaults for development +- Easy to override in production +- Self-documenting configuration + +**Important Notes:** +- `get_env()` cannot be used directly in `{% if %}` conditions - use `{% set %}` to assign to a variable first +- Direct environment variable access (e.g., `{{ ENV_VAR }}`) is not supported - always use `get_env()` + +See [examples/config-with-defaults.tmpl](examples/config-with-defaults.tmpl) for a complete example. + +### Custom `filter_env()` Function + +tmpltool provides a custom `filter_env()` function to filter environment variables by glob pattern: + +``` +{% for var in filter_env(pattern="PATTERN") %} + {{ var.key }}={{ var.value }} +{% endfor %} +``` + +**Arguments:** +- `pattern` (required) - A glob pattern to match environment variable names + - `*` matches any characters + - `?` matches exactly one character + +**Returns:** +- An array of objects, each with: + - `key` - The environment variable name + - `value` - The environment variable value +- Results are sorted alphabetically by key + +**Examples:** +``` +# Match all SERVER_* variables +{% for var in filter_env(pattern="SERVER_*") %} +export {{ var.key }}="{{ var.value }}" +{% endfor %} + +# Match all database variables +{% set db_vars = filter_env(pattern="DATABASE_*") %} +{% if db_vars | length > 0 %} +Found {{ db_vars | length }} database variables +{% endif %} + +# Match any variable ending with _PORT +{% for var in filter_env(pattern="*_PORT") %} +{{ var.key }}: {{ var.value }} +{% endfor %} +``` + +See [examples/server-config.tmpl](examples/server-config.tmpl) for a complete example. + +### Hash Functions + +tmpltool provides cryptographic hash functions for generating checksums and hashes: + +#### `md5(string)` +Calculates MD5 hash of a string. + +``` +Checksum: {{ md5(string="hello world") }} +# Output: 5eb63bbbe01eeed093cb22bb8f5acdc3 +``` + +#### `sha1(string)` +Calculates SHA1 hash of a string. + +``` +Hash: {{ sha1(string="tmpltool") }} +# Output: c054a2a60ca2fe935ea1056bd90386194116f14f +``` + +#### `sha256(string)` +Calculates SHA256 hash of a string (recommended for password hashing). + +``` +{% set password = get_env(name="PASSWORD", default="secret") %} +Password hash: {{ sha256(string=password) }} +# Output: fcf730b6d95236ecd3c9fc2d92d7b6b2bb061514961aec041d6c7a7192f592e4 +``` + +#### `sha512(string)` +Calculates SHA512 hash of a string (most secure). + +``` +Secure hash: {{ sha512(string="secure-data") }} +# Output: a5c18d86d1d07cc2b22b12284e2f8e5b9705761003f149467995927e36f0e447ddfb158b89a28c0b4d5ac419c979c1fc435a3378b619aed1bab0d15c3b583db9 +``` + +**Important:** These hash functions are for checksums and general-purpose hashing. For production password storage, use dedicated password hashing libraries with salt and proper key derivation functions (bcrypt, argon2, etc.). + +### UUID Generation + +#### `uuid()` +Generates a random UUID v4 (Universally Unique Identifier). + +``` +Request ID: {{ uuid() }} +Session ID: {{ uuid() }} +# Output: +# Request ID: c5b78641-89f8-4d04-a4c9-d53ba4d433f9 +# Session ID: aabc7fe1-f8ed-45ff-944d-9c24f3823ac0 +``` + +Each call to `uuid()` generates a unique identifier. + +### Random String Generation + +#### `random_string(length, charset)` +Generates a random string with customizable length and character set. + +**Arguments:** +- `length` (required) - Length of the string to generate (1-10000) +- `charset` (optional) - Character set to use (default: `alphanumeric`) + +**Character Set Presets:** +- `alphanumeric` - Letters (a-z, A-Z) and digits (0-9) - **default** +- `alphabetic` or `alpha` - Letters only (a-z, A-Z) +- `lowercase` or `lower` - Lowercase letters only (a-z) +- `uppercase` or `upper` - Uppercase letters only (A-Z) +- `numeric` or `digits` - Digits only (0-9) +- `hex` or `hexadecimal` - Hexadecimal characters (0-9, a-f) +- `hex_upper` - Hexadecimal uppercase (0-9, A-F) +- Custom string - Any custom character set (e.g., `"abc123"`) + +**Examples:** +``` +# Alphanumeric (default) +API Key: {{ random_string(length=32) }} +# Output: 0QY92XIYYKIvMVuVc8a7u8O4v19VacO9 + +# Lowercase only +Username: user_{{ random_string(length=8, charset="lowercase") }} +# Output: user_lvaycaxa + +# Uppercase only +Code: {{ random_string(length=6, charset="uppercase") }} +# Output: YFVLRV + +# Numeric only +PIN: {{ random_string(length=4, charset="numeric") }} +# Output: 5858 + +# Hexadecimal +Token: {{ random_string(length=16, charset="hex") }} +# Output: bd2954f90019649b + +# Custom charset +Password: {{ random_string(length=12, charset="abc123") }} +# Output: 3bb3c31bb23c +``` + +**Practical Example - Secure Configuration:** +```yaml +application: + instance_id: {{ uuid() }} + secret_key: {{ random_string(length=64) }} + api_token: {{ random_string(length=32, charset="hex") }} + +security: + password_hash: {{ sha256(string=get_env(name="PASSWORD")) }} + csrf_token: {{ random_string(length=40, charset="hex") }} +``` + +See [examples/hash-crypto.tmpl](examples/hash-crypto.tmpl) for a complete example. + +### Filesystem Functions + +tmpltool provides secure filesystem functions for reading files and querying file information within templates. All filesystem functions enforce security restrictions to prevent unauthorized access. + +**Security Note:** All filesystem functions only allow access to relative paths within the current working directory. Absolute paths (starting with `/`) and parent directory traversal (`..`) are explicitly blocked. + +#### `read_file(path)` +Reads the content of a file into the template. + +**Arguments:** +- `path` (required) - Relative path to the file to read + +**Returns:** String containing the file content + +**Examples:** +``` +# Read a configuration file +{% set config = read_file(path="config.txt") %} +{{ config }} + +# Read and include file content +License: +{{ read_file(path="LICENSE") }} + +# Use with filters +First 100 chars: {{ read_file(path="README.md") | truncate(length=100) }} +``` + +#### `file_exists(path)` +Checks if a file exists at the specified path. + +**Arguments:** +- `path` (required) - Relative path to check + +**Returns:** Boolean (`true` if file exists, `false` otherwise) + +**Examples:** +``` +# Conditional file inclusion +{% if file_exists(path="custom-config.txt") %} +Custom config found! +{{ read_file(path="custom-config.txt") }} +{% else %} +Using default configuration +{% endif %} + +# Check multiple files +{% set has_readme = file_exists(path="README.md") %} +{% set has_license = file_exists(path="LICENSE") %} +Documentation: {% if has_readme %}✓{% else %}✗{% endif %} +License: {% if has_license %}✓{% else %}✗{% endif %} +``` + +#### `list_dir(path)` +Lists all files and directories in a directory. + +**Arguments:** +- `path` (required) - Relative path to the directory + +**Returns:** Array of filenames (sorted alphabetically) + +**Examples:** +``` +# List files in a directory +Files in data/: +{% for file in list_dir(path="data") %} + - {{ file }} +{% endfor %} + +# Count files +{% set files = list_dir(path="templates") %} +Total templates: {{ files | length }} + +# Filter by extension +{% set all_files = list_dir(path="src") %} +Rust files: +{% for file in all_files %} +{% if file is ending_with(".rs") %} + - {{ file }} +{% endif %} +{% endfor %} +``` + +#### `glob(pattern)` +Lists all files matching a glob pattern. + +**Arguments:** +- `pattern` (required) - Glob pattern to match files + - `*` matches any characters + - `?` matches exactly one character + - `**` matches any number of directories + +**Returns:** Array of file paths (sorted alphabetically) + +**Examples:** +``` +# Find all text files +Text files: +{% for file in glob(pattern="*.txt") %} + - {{ file }} +{% endfor %} + +# Find files in subdirectories +All Rust files: +{% for file in glob(pattern="src/**/*.rs") %} + - {{ file }} +{% endfor %} + +# Match specific patterns +Config files: +{% for file in glob(pattern="config*.{json,yaml,toml}") %} + - {{ file }} +{% endfor %} + +# Use in conditionals +{% set test_files = glob(pattern="tests/**/*.rs") %} +{% if test_files | length > 0 %} +Found {{ test_files | length }} test files +{% endif %} +``` + +#### `file_size(path)` +Gets the size of a file in bytes. + +**Arguments:** +- `path` (required) - Relative path to the file + +**Returns:** File size as a number (in bytes) + +**Examples:** +``` +# Get file size +README size: {{ file_size(path="README.md") }} bytes + +# Format with built-in filter +README size: {{ file_size(path="README.md") | filesizeformat }} + +# Compare file sizes +{% set size_a = file_size(path="file_a.txt") %} +{% set size_b = file_size(path="file_b.txt") %} +{% if size_a > size_b %} +file_a.txt is larger +{% else %} +file_b.txt is larger +{% endif %} + +# Calculate total size +{% set files = glob(pattern="data/*.json") %} +{% set total_size = 0 %} +{% for file in files %} +{% set total_size = total_size + file_size(path=file) %} +{% endfor %} +Total data size: {{ total_size | filesizeformat }} +``` + +#### `file_modified(path)` +Gets the last modification time of a file as a Unix timestamp (seconds since epoch). + +**Arguments:** +- `path` (required) - Relative path to the file + +**Returns:** Unix timestamp (number of seconds since January 1, 1970) + +**Examples:** +``` +# Get modification timestamp +Last modified: {{ file_modified(path="config.json") }} + +# Format with date filter +{% set timestamp = file_modified(path="README.md") %} +Last updated: {{ timestamp | date(format="%Y-%m-%d %H:%M:%S") }} + +# Check if file is recent +{% set mod_time = file_modified(path="cache.dat") %} +{% set now_time = now() %} +{% set age_seconds = now_time - mod_time %} +{% if age_seconds < 3600 %} +Cache is fresh (less than 1 hour old) +{% else %} +Cache is stale ({{ age_seconds / 3600 }} hours old) +{% endif %} + +# Find most recently modified file +{% set files = glob(pattern="logs/*.log") %} +{% set newest_time = 0 %} +{% set newest_file = "" %} +{% for file in files %} +{% set mod_time = file_modified(path=file) %} +{% if mod_time > newest_time %} +{% set newest_time = mod_time %} +{% set newest_file = file %} +{% endif %} +{% endfor %} +Most recent log: {{ newest_file }} +``` + +**Practical Example - Build Report:** +``` +# Build Report +Generated: {{ now() | date(format="%Y-%m-%d %H:%M:%S") }} + +## Source Files +{% set rs_files = glob(pattern="src/**/*.rs") %} +Total Rust files: {{ rs_files | length }} + +{% for file in rs_files %} +- {{ file }} + Size: {{ file_size(path=file) | filesizeformat }} + Modified: {{ file_modified(path=file) | date(format="%Y-%m-%d") }} +{% endfor %} + +## Configuration +{% if file_exists(path="Cargo.toml") %} +✓ Cargo.toml found ({{ file_size(path="Cargo.toml") }} bytes) +{% else %} +✗ Cargo.toml missing +{% endif %} + +## Tests +{% set test_files = glob(pattern="tests/**/*.rs") %} +Test files: {{ test_files | length }} +{% for test in test_files %} +- {{ test }} +{% endfor %} +``` + +**Security Restrictions:** + +All filesystem functions enforce the following security rules: + +1. **No Absolute Paths** - Paths starting with `/` are rejected + ``` + {{ read_file(path="/etc/passwd") }} # ✗ ERROR: Security violation + ``` + +2. **No Parent Directory Traversal** - Paths containing `..` are rejected + ``` + {{ read_file(path="../../secret.txt") }} # ✗ ERROR: Security violation + ``` + +3. **Relative Paths Only** - Only files within the current working directory are accessible + ``` + {{ read_file(path="config.txt") }} # ✓ OK + {{ read_file(path="data/file.txt") }} # ✓ OK + {{ file_exists(path="subdir/test.txt") }} # ✓ OK + ``` + +These restrictions ensure templates can only access files in the current working directory and its subdirectories, preventing unauthorized access to system files or files outside the project. + +**Trust Mode:** + +You can bypass these security restrictions by using the `--trust` command-line flag: + +```bash +# Without --trust: Security error +tmpltool template.tmpl # ERROR if template tries to read /etc/passwd + +# With --trust: Unrestricted access +tmpltool --trust template.tmpl # OK, can read any file +``` + +**When to use `--trust`:** +- When you need to access system files or configuration outside your project +- When reading files from absolute paths (e.g., `/etc/hosts`, `/var/log/app.log`) +- When accessing parent directories (e.g., `../config/settings.yml`) +- When you fully trust the template source and know what files it accesses + +**WARNING:** Only use `--trust` with templates you completely trust. Malicious templates could read sensitive files like SSH keys, passwords, or system configurations. + +### Validation Functions + +tmpltool provides validation functions to check if strings match specific formats. These are useful for validating user input, configuration values, or data from external sources. + +#### `is_email(string)` + +Validates if a string is a valid email address format. + +``` +Email: user@example.com +Valid: {{ is_email(string="user@example.com") }} +{# Output: Valid: true #} + +Email: invalid-email +Valid: {{ is_email(string="invalid-email") }} +{# Output: Valid: false #} +``` + +#### `is_url(string)` + +Validates if a string is a valid URL (supports http, https, ftp, file schemes). + +``` +URL: https://example.com/path +Valid: {{ is_url(string="https://example.com/path") }} +{# Output: Valid: true #} + +URL: not-a-url +Valid: {{ is_url(string="not-a-url") }} +{# Output: Valid: false #} +``` + +#### `is_ip(string)` + +Validates if a string is a valid IP address (IPv4 or IPv6). + +``` +IPv4: 192.168.1.1 +Valid: {{ is_ip(string="192.168.1.1") }} +{# Output: Valid: true #} + +IPv6: 2001:db8::1 +Valid: {{ is_ip(string="2001:db8::1") }} +{# Output: Valid: true #} + +Invalid: 256.1.1.1 +Valid: {{ is_ip(string="256.1.1.1") }} +{# Output: Valid: false #} +``` + +#### `is_uuid(string)` + +Validates if a string is a valid UUID format. + +``` +UUID: 550e8400-e29b-41d4-a716-446655440000 +Valid: {{ is_uuid(string="550e8400-e29b-41d4-a716-446655440000") }} +{# Output: Valid: true #} + +Invalid: not-a-uuid +Valid: {{ is_uuid(string="not-a-uuid") }} +{# Output: Valid: false #} +``` + +#### `matches_regex(pattern, string)` + +Checks if a string matches a regular expression pattern. + +``` +{# Validate alphanumeric #} +{% if matches_regex(pattern="^[A-Za-z0-9]+$", string="Test123") %} + Valid alphanumeric string +{% endif %} + +{# Validate phone number format #} +{% set phone = get_env(name="PHONE", default="") %} +{% if matches_regex(pattern="^\\d{3}-\\d{3}-\\d{4}$", string=phone) %} + Phone number format: XXX-XXX-XXXX +{% endif %} + +{# Check for specific pattern #} +{% if matches_regex(pattern="^prod-", string="prod-server-01") %} + This is a production server +{% endif %} +``` + +**Practical Example - Configuration Validation:** +``` +# Configuration Validation Report + +{% set email = get_env(name="ADMIN_EMAIL", default="") %} +Admin Email: {{ email }} +{% if is_email(string=email) %} +✓ Valid email format +{% else %} +✗ Invalid email format +{% endif %} + +{% set api_url = get_env(name="API_URL", default="") %} +API URL: {{ api_url }} +{% if is_url(string=api_url) %} +✓ Valid URL format +{% else %} +✗ Invalid URL format +{% endif %} + +{% set server_ip = get_env(name="SERVER_IP", default="") %} +Server IP: {{ server_ip }} +{% if is_ip(string=server_ip) %} +✓ Valid IP address +{% else %} +✗ Invalid IP address +{% endif %} + +{% set correlation_id = get_env(name="CORRELATION_ID", default="") %} +Correlation ID: {{ correlation_id }} +{% if is_uuid(string=correlation_id) %} +✓ Valid UUID format +{% else %} +✗ Invalid UUID format +{% endif %} +``` + +### Data Parsing Functions + +tmpltool provides functions to parse structured data formats (JSON, YAML, TOML) from strings or files. These functions are useful for loading configuration files, processing API responses, or working with structured data in templates. + +#### `parse_json(string)` + +Parse a JSON string into an object that can be used in templates. + +``` +{% set config = parse_json(string='{"name": "myapp", "port": 8080, "debug": true}') %} +Application: {{ config.name }} +Port: {{ config.port }} +Debug mode: {{ config.debug }} +``` + +#### `parse_yaml(string)` + +Parse a YAML string into an object. + +``` +{% set data = parse_yaml(string=" +name: myapp +settings: + theme: dark + notifications: true +") %} +App: {{ data.name }} +Theme: {{ data.settings.theme }} +``` + +#### `parse_toml(string)` + +Parse a TOML string into an object. + +``` +{% set config = parse_toml(string=' +[database] +host = "localhost" +port = 5432 + +[cache] +enabled = true +') %} +Database: {{ config.database.host }}:{{ config.database.port }} +Cache: {{ config.cache.enabled }} +``` + +#### `read_json_file(path)` + +Read and parse a JSON file. The path is resolved relative to the template file's directory. + +**Example JSON file** (`config/settings.json`): +```json +{ + "app_name": "MyApp", + "version": "1.0.0", + "features": { + "auth": true, + "api": true + } +} +``` + +**Template:** +``` +{% set config = read_json_file(path="config/settings.json") %} +# {{ config.app_name }} v{{ config.version }} + +Features: +{% if config.features.auth %} +- Authentication: Enabled +{% endif %} +{% if config.features.api %} +- API: Enabled +{% endif %} +``` + +#### `read_yaml_file(path)` + +Read and parse a YAML file. + +**Example YAML file** (`config.yaml`): +```yaml +services: + - name: web + port: 8080 + - name: api + port: 3000 + +environment: production +``` + +**Template:** +``` +{% set config = read_yaml_file(path="config.yaml") %} +Environment: {{ config.environment }} + +Services: +{% for service in config.services %} + - {{ service.name }}: port {{ service.port }} +{% endfor %} +``` + +#### `read_toml_file(path)` + +Read and parse a TOML file. + +**Example TOML file** (`Cargo.toml`): +```toml +[package] +name = "myapp" +version = "1.0.0" + +[dependencies] +serde = "1.0" +tokio = "1.0" +``` + +**Template:** +``` +{% set cargo = read_toml_file(path="Cargo.toml") %} +# {{ cargo.package.name }} + +Version: {{ cargo.package.version }} + +Dependencies: +{% for dep, version in cargo.dependencies %} +- {{ dep }}: {{ version }} +{% endfor %} +``` + +**Practical Example - Multi-format Configuration:** + +``` +{# Load configuration from different sources #} +{% set json_config = read_json_file(path="config.json") %} +{% set yaml_config = read_yaml_file(path="config.yaml") %} +{% set toml_config = read_toml_file(path="Cargo.toml") %} + +# Application Configuration Report + +## From JSON ({{ json_config.app_name }}) +- Version: {{ json_config.version }} +- Debug: {{ json_config.debug }} + +## From YAML +Environment: {{ yaml_config.environment }} +{% for service in yaml_config.services %} +- Service {{ service.name }}: {{ service.host }}:{{ service.port }} +{% endfor %} + +## From TOML ({{ toml_config.package.name }}) +Rust Version: {{ toml_config.package.edition }} +Dependencies: {{ toml_config.dependencies | length }} +``` + +**Security Note**: Like other filesystem functions, data parsing file functions enforce security restrictions: +- Only relative paths allowed (no absolute paths like `/etc/config.json`) +- No parent directory traversal (no `..` in paths) +- Access restricted to current working directory and subdirectories +- Use `--trust` flag to bypass these restrictions for trusted templates + +### Comments +``` +{# This is a comment #} +``` + +For complete Tera syntax documentation, visit: https://keats.github.io/tera/docs/ + +## Error Handling + +- If a template file doesn't exist, tmpltool will exit with an error +- If a template has syntax errors, tmpltool will report the error location +- Environment variable handling: + - **Direct access not supported:** `{{ ENV_VAR }}` will cause an error - environment variables are not automatically available + - **With default (recommended):** `{{ get_env(name="VAR", default="...") }}` will use the default value if the variable doesn't exist + - **Without default:** `{{ get_env(name="VAR") }}` will error if the variable doesn't exist + +## Help + +```bash +tmpltool --help +``` + +## Version + +```bash +tmpltool --version +``` + +## Development + +### Prerequisites + +- Rust 1.70 or higher +- Cargo (comes with Rust) + +Install Rust from [rustup.rs](https://rustup.rs/) if you haven't already: + +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +### Project Structure + +``` +tmpltool/ +├── Cargo.toml # Project dependencies and metadata +├── Cargo.lock # Locked dependency versions +├── Makefile.toml # cargo-make task definitions +├── src/ +│ ├── main.rs # Entry point (binary) +│ ├── lib.rs # Library root +│ ├── cli.rs # CLI argument parsing +│ ├── renderer.rs # Template rendering logic +│ └── functions/ # Custom Tera functions (extensibility point) +│ └── mod.rs # Functions module (for registering custom functions) +├── tests/ # Integration tests (one test per file) +│ ├── common.rs # Shared test utilities +│ ├── fixtures/ # Test fixtures (templates & expected outputs) +│ │ ├── templates/ # Input template files +│ │ ├── expected/ # Expected output files +│ │ └── README.md # Fixtures documentation +│ ├── test_successful_rendering.rs +│ ├── test_missing_template_file.rs +│ ├── test_invalid_template_syntax.rs +│ ├── test_environment_variable_substitution.rs +│ ├── test_template_with_conditionals.rs +│ ├── test_template_with_missing_variable.rs +│ ├── test_multiline_template.rs +│ ├── test_stdout_output.rs +│ └── test_direct_var_access_fails.rs +├── examples/ # Example templates +│ ├── basic.tmpl # Basic usage example +│ ├── greeting.tmpl # Simple greeting with defaults +│ ├── config.tmpl # Config file generation +│ ├── config-with-defaults.tmpl # Advanced config with get_env() function +│ ├── docker-compose.tmpl # Docker Compose with defaults +│ └── README.md # Examples documentation +├── .gitignore # Git ignore rules +├── .editorconfig # Editor configuration +└── README.md # This file +``` + +#### Module Organization + +**Source Code (`src/`)** - No tests in source files: +- **`main.rs`** - Minimal binary entry point, just parses CLI args and calls the library +- **`lib.rs`** - Public library API, exports main functionality +- **`cli.rs`** - CLI argument parsing using clap +- **`renderer.rs`** - Core template rendering logic (no unit tests) +- **`functions/`** - Custom Tera functions (modular, one file per function) + - **`mod.rs`** - Registers custom functions (currently empty - built-in functions like `get_env()` work automatically) + +**Tests (`tests/`)** - All tests as integration tests: +- **`common.rs`** - Shared test utilities and fixture helpers +- **`fixtures/`** - Test fixtures (templates and expected outputs) +- **Individual test files** - One test per file for better organization (11 tests total) + +#### Adding New Custom Functions + +To add a new custom function: + +1. Create a new file in `src/functions/` (e.g., `my_function.rs`) +2. Implement your function following the Tera function signature +3. Add the module declaration to `src/functions/mod.rs` +4. Register your function in the `register_all()` function +5. Write tests in your function file + +Note: Tera's built-in functions like `get_env()`, `now()`, and `get_random()` are automatically available when the "builtins" feature is enabled. + +### Building + +Build the project in debug mode: + +```bash +cargo build +``` + +Build optimized release binary: + +```bash +cargo build --release +``` + +The release binary will be located at `./target/release/tmpltool`. + +### Using cargo-make (Task Runner) + +This project includes a comprehensive `Makefile.toml` for [cargo-make](https://github.com/sagiegurari/cargo-make), providing standardized tasks for building, testing, and cross-platform compilation. + +#### Installation + +Install cargo-make globally: + +```bash +cargo install --force cargo-make +``` + +#### Available Tasks + +View all available tasks: + +```bash +cargo make +``` + +**Common Development Tasks:** + +```bash +# Build and test +cargo make build # Build debug binary +cargo make build-release # Build optimized binary +cargo make test # Run all tests +cargo make test-verbose # Run tests with output +cargo make run # Run with example template + +# Code quality +cargo make format # Format code with rustfmt +cargo make clippy # Run clippy linter +cargo make qa # Full quality check (format + clippy + test) +cargo make ci # CI checks (format-check + clippy + test) + +# Utilities +cargo make clean # Clean build artifacts +cargo make check # Fast compile check +cargo make docs # Generate and open documentation +cargo make test-examples # Test all example templates +``` + +**Cross-Platform Builds:** + +Build release binaries for different platforms: + +```bash +# Individual platforms +cargo make build-linux-x86_64 # Linux x86_64 +cargo make build-linux-musl # Linux (static, musl libc) +cargo make build-macos-x86_64 # macOS Intel +cargo make build-macos-aarch64 # macOS Apple Silicon +cargo make build-windows-x86_64 # Windows x86_64 + +# Build for all platforms +cargo make build-all-platforms +``` + +**Note:** Cross-compilation may require installing additional targets: + +```bash +# Add targets for cross-compilation +rustup target add x86_64-unknown-linux-gnu +rustup target add x86_64-unknown-linux-musl +rustup target add x86_64-apple-darwin +rustup target add aarch64-apple-darwin +rustup target add x86_64-pc-windows-gnu +``` + +**Workflow Tasks:** + +```bash +cargo make dev # Quick dev check (check + test) +cargo make pre-commit # Pre-commit checks +cargo make release-prepare # Full release preparation +cargo make all # Complete build and test suite +``` + +**Additional Tools:** + +```bash +cargo make audit # Security audit (requires cargo-audit) +cargo make outdated # Check outdated dependencies (requires cargo-outdated) +cargo make bloat # Analyze binary size (requires cargo-bloat) +``` + +### Running in Development + +Run without building a binary: + +```bash +cargo run -- template.txt -o output.txt +``` + +Run with environment variables: + +```bash +CUSTOM_VAR="test" cargo run -- template.txt +``` + +### Testing + +Run all unit tests: + +```bash +cargo test +``` + +Run tests with output: + +```bash +cargo test -- --nocapture +``` + +Run a specific test: + +```bash +cargo test test_successful_rendering +``` + +Run tests in verbose mode: + +```bash +cargo test -- --test-threads=1 --nocapture +``` + +#### Test Coverage + +The project includes comprehensive test coverage. **All tests are located in `tests/` directory** - there are no unit tests in `src/` files. + +**Integration Tests in `tests/`** (11 tests, one per file): +- `test_simple_rendering.rs` - Simple static template rendering +- `test_successful_rendering.rs` - Template rendering with environment variables +- `test_env_with_default.rs` - Environment variable with default fallback +- `test_missing_template_file.rs` - Missing template file handling +- `test_invalid_template_syntax.rs` - Invalid template syntax handling +- `test_environment_variable_substitution.rs` - Environment variable substitution with `get_env()` +- `test_template_with_conditionals.rs` - Conditional logic (if/else) +- `test_template_with_missing_variable.rs` - Missing variable detection +- `test_multiline_template.rs` - Multiline templates +- `test_stdout_output.rs` - Stdout output functionality +- `test_direct_var_access_fails.rs` - Direct variable access fails (security test) + +**Unit Tests in `tests/`** (58 tests across multiple test files): +- `test_filter_env_unit.rs` - Environment variable filtering (6 tests) +- `test_hash_unit.rs` - Hash functions (6 tests) +- `test_uuid_unit.rs` - UUID generation (3 tests) +- `test_random_string_unit.rs` - Random string generation (11 tests) +- `test_filesystem_unit.rs` - Filesystem functions (23 tests) +- `test_hash_crypto_functions.rs` - Hash and crypto integration (17 tests) +- `test_comprehensive.rs` - Comprehensive template validation (2 tests) + +**Test Infrastructure:** +- `common.rs` - Shared test utilities and fixture helpers +- `fixtures/` - Test fixtures (templates and expected outputs) + +**Documentation Tests** (2 tests): +- Library documentation examples + +Total: **71 tests** covering integration, unit tests, and documentation scenarios. + +#### Adding New Integration Tests + +To add a new integration test: + +1. Create a new file in `tests/` (e.g., `tests/test_my_feature.rs`) +2. Import the common utilities: `mod common;` and `use common::*;` +3. Import dependencies: `use tmpltool::render_template;` +4. Write your test function with `#[test]` attribute +5. Use helper functions: `get_test_file_path()` and `cleanup_test_file()` + +**Example:** + +```rust +mod common; + +use common::{cleanup_test_file, get_test_file_path}; +use std::fs; +use tmpltool::render_template; + +#[test] +fn test_my_feature() { + let template_path = get_test_file_path("my_template.txt"); + let output_path = get_test_file_path("my_output.txt"); + + // Create test template + fs::write(&template_path, "{{ get_env(name=\"TEST\") }}").unwrap(); + + // Run render_template + let result = render_template( + Some(template_path.to_str().unwrap()), + Some(output_path.to_str().unwrap()), + ); + + // Assert results + assert!(result.is_ok()); + + // Cleanup + cleanup_test_file(&template_path); + cleanup_test_file(&output_path); +} +``` + +Each test file is compiled as a separate test binary, making tests more isolated and easier to debug. + +#### Using Test Fixtures + +The project uses test fixtures to make tests easier to maintain. Fixtures are template files and their expected outputs stored in `tests/fixtures/`. + +**Fixture Directory Structure:** + +``` +tests/fixtures/ +├── templates/ # Input template files +│ ├── simple.tmpl +│ ├── with_env.tmpl +│ ├── multiline.tmpl +│ ├── conditionals.tmpl +│ └── docker-compose.tmpl +└── expected/ # Expected output files + ├── simple.txt + ├── with_env.txt + ├── multiline.txt + └── docker-compose.txt +``` + +**Using Fixtures in Tests:** + +```rust +mod common; + +use common::{ + cleanup_test_file, get_test_file_path, + read_fixture_expected, read_fixture_template +}; +use std::fs; +use tmpltool::render_template; + +#[test] +fn test_my_feature() { + let output_path = get_test_file_path("output.txt"); + + // Read template from fixtures + let template_content = read_fixture_template("my_template.tmpl"); + let template_path = get_test_file_path("template.txt"); + fs::write(&template_path, template_content).unwrap(); + + // Render template + let result = render_template( + Some(template_path.to_str().unwrap()), + Some(output_path.to_str().unwrap()), + ); + + // Compare with expected output + assert!(result.is_ok()); + let output = fs::read_to_string(&output_path).unwrap(); + let expected = read_fixture_expected("my_template.txt"); + assert_eq!(output, expected); + + // Cleanup + cleanup_test_file(&template_path); + cleanup_test_file(&output_path); +} +``` + +**Benefits:** +- ✅ Test data separated from test logic +- ✅ Easy to maintain and review template changes +- ✅ Reusable across multiple tests +- ✅ Can use real-world template examples + +See [tests/fixtures/README.md](tests/fixtures/README.md) for more details. + +### Code Quality + +Format code: + +```bash +cargo fmt +``` + +Run linter (clippy): + +```bash +cargo clippy +``` + +Run clippy with all warnings: + +```bash +cargo clippy -- -W clippy::all +``` + +### Dependencies + +The project uses minimal dependencies: + +- **[tera](https://crates.io/crates/tera)** (v1.x) - Template engine with `builtins` feature enabled + - Provides built-in filters: `slugify`, `date`, `filesizeformat`, `urlencode`, etc. + - Provides built-in functions: `get_env()`, `now()`, `get_random()` +- **[clap](https://crates.io/crates/clap)** (v4.x) - Command-line argument parsing +- **[regex](https://crates.io/crates/regex)** (v1.x) - Regular expressions for pattern matching +- **[md-5](https://crates.io/crates/md-5)** (v0.10) - MD5 hash implementation +- **[sha1](https://crates.io/crates/sha1)** (v0.10) - SHA1 hash implementation +- **[sha2](https://crates.io/crates/sha2)** (v0.10) - SHA256 and SHA512 hash implementations +- **[uuid](https://crates.io/crates/uuid)** (v1.x) - UUID generation +- **[rand](https://crates.io/crates/rand)** (v0.8) - Random number generation +- **[glob](https://crates.io/crates/glob)** (v0.3) - Glob pattern matching for filesystem operations + +To update dependencies: + +```bash +cargo update +``` + +### Manual Testing + +Create a test template: + +```bash +cat > test.tmpl << 'EOF' +User: {{ USER }} +Home: {{ HOME }} +Custom: {{ CUSTOM_VAR }} +EOF +``` + +Test it: + +```bash +CUSTOM_VAR="Hello World" ./target/release/tmpltool test.tmpl +``` + +### Debugging + +Run with debug output: + +```bash +RUST_BACKTRACE=1 cargo run -- template.txt +``` + +Full backtrace: + +```bash +RUST_BACKTRACE=full cargo run -- template.txt +``` + +### Performance + +Benchmark the binary size: + +```bash +ls -lh target/release/tmpltool +``` + +Profile with release build: + +```bash +cargo build --release +time ./target/release/tmpltool large_template.txt -o output.txt +``` + +### Installing Locally + +Install from the project directory: + +```bash +cargo install --path . +``` + +This installs the binary to `~/.cargo/bin/tmpltool` (make sure this is in your PATH). + +Uninstall: + +```bash +cargo uninstall tmpltool +``` + +### Using as a Library + +tmpltool can also be used as a library in other Rust projects: + +```rust +use tmpltool::render_template; + +fn main() -> Result<(), Box> { + // Render to file + render_template("template.txt", Some("output.txt"))?; + + // Render to stdout + render_template("template.txt", None)?; + + Ok(()) +} +``` + +The library exposes: +- `render_template(template_path: &str, output_path: Option<&str>)` - Main rendering function +- `Cli` - Command-line argument structure + +## CI/CD + +This project uses GitHub Actions for continuous integration and automated releases. + +### Continuous Integration + +Every pull request and push to master/main triggers: + +- **Code Formatting Check** - Ensures code follows Rust style guidelines (`rustfmt`) +- **Linting** - Runs `clippy` with strict warnings +- **Multi-Platform Tests** - Tests on Ubuntu, macOS, and Windows +- **Code Coverage** - Generates coverage reports with `cargo-tarpaulin` and uploads to Codecov +- **cargo-make QA** - Runs comprehensive quality checks +- **Example Testing** - Tests all example templates to ensure they work + +### Automated Releases + +Releases are fully automated using [semantic-release](https://github.com/semantic-release/semantic-release): + +1. **Commit Analysis** - Analyzes commit messages to determine the next version +2. **Version Bumping** - Updates `Cargo.toml` with the new version +3. **CHANGELOG Generation** - Automatically generates `CHANGELOG.md` from commits +4. **Multi-Platform Builds** - Builds release binaries for: + - Linux (x86_64, x86_64-musl, aarch64) + - macOS (x86_64 Intel, aarch64 Apple Silicon) + - Windows (x86_64) +5. **GitHub Release** - Creates a new GitHub release with all binaries +6. **Docker Image** - Builds and publishes multi-arch Docker image to GHCR + +### Commit Convention + +This project follows [Conventional Commits](https://www.conventionalcommits.org/) for automatic versioning: + +- `feat: description` - New feature (minor version bump: 1.2.0 → 1.3.0) +- `fix: description` - Bug fix (patch version bump: 1.2.0 → 1.2.1) +- `feat!: description` or `BREAKING CHANGE:` - Breaking change (major version bump: 1.2.0 → 2.0.0) +- `docs:`, `refactor:`, `perf:`, `build:` - Other changes (patch bump) +- `style:`, `test:`, `chore:`, `ci:` - No version bump + +**Examples:** + +```bash +# Feature (minor bump) +git commit -m "feat: add slugify filter support" + +# Bug fix (patch bump) +git commit -m "fix: correct multiline template rendering" + +# Breaking change (major bump) +git commit -m "feat!: change default output behavior + +BREAKING CHANGE: Output now goes to stdout by default instead of file" +``` + +See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed contribution guidelines. + +## License + +This project is open source. + +## Contributing + +Contributions are welcome! Please feel free to submit issues or pull requests. + +For detailed contribution guidelines, including commit conventions, development workflow, and testing requirements, see [CONTRIBUTING.md](CONTRIBUTING.md). + +### Quick Start + +1. Fork the repository +2. Clone your fork and install dependencies: + ```bash + git clone https://github.com/bordeux/tmpltool.git + cd tmpltool + npm install # Installs commit validation hooks + ``` +3. Create a feature branch (`git checkout -b feature/amazing-feature`) +4. Make your changes +5. Run tests and QA checks (`cargo make qa`) +6. Commit using [conventional commits](#commit-convention) - invalid commits will be automatically rejected +7. Push to your fork +8. Open a Pull Request + +**Note:** Commit messages are automatically validated. If your commit is rejected, make sure it follows the [conventional commit format](#commit-convention).