Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render Templates from Stdin and Files #3

Merged
merged 1 commit into from
Jun 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .cargo/config

This file was deleted.

77 changes: 76 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,86 @@
# jinjer [![Build Status][travis.svg]][travis]

A CLI utility for rendering Jinja templates.
A CLI tool for rendering Jinja-esque templates using the [Tera template engine][tera].

## Usage

```
USAGE:
jinjer [FLAGS] <SUBCOMMAND>

FLAGS:
-h, --help Prints help information
-V, --version Prints version information
-v Logging verbosity. By default, it is set to ERROR, pass -v multiple times to increase verbosity.

SUBCOMMANDS:
facts Dump all detected facts to standard output. Useful for understanding which facts are available at
runtime.
help Prints this message or the help of the given subcommand(s)
render Render one or more templates to standard output or a file.
```

`jinjer` exposes two subcommands at present:

- `facts`: dumps available system facts to standard output in JSON format.
- `render`: renders either standard input or a list of template files in order, either to standard output or to a
specified output file.

#### `jinjer facts`

```
USAGE:
jinjer facts

FLAGS:
-h, --help Prints help information
-V, --version Prints version information
```

Executing `jinjer facts` will simply dump a JSON dictionary of all facts discovered by `jinjer` at runtime. Use this
command to preview which facts will be available during template rendering.

#### `jinjer render`

```
USAGE:
jinjer render [FLAGS] [OPTIONS] [template_files]...

FLAGS:
-e, --auto-escape Enable HTML auto-escaping of templates in the template renderer. By default, output is not safe
for HTML.
-h, --help Prints help information
-V, --version Prints version information

OPTIONS:
-o, --output <output_file> Render output to a file rather than to standard output.

ARGS:
<template_files>... A list of template files to render in order.
```

Use `jinjer render` to render templates. If no template files are passed, `jinjer` will attempt to read a template from
standard input. By default, rendering is done to standard output, use `-o|--output` to send rendering output to a
file on the filesystem.

Template output is unsanitized by default. If rendering HTML-safe output is required, pass `-e|--auto-escape` to
attempt to render output safely for HTML.

## Facts

`jinjer` has a fact plugin system for facts at runtime. Presently, there are two plugins:

- `"basic"`: basic system facts such as CPU count.
- `"env"`: environment variables.

Ultimately, it would be easy and straightforward to add support for systems like `facter` and perhaps Ansible to
leverage these as fact providers. Contributions welcome. :wave:

## License

- [Apache Software License, Version 2.0](./LICENSE-APACHE)
- [MIT License](./LICENSE-MIT)

[tera]: https://tera.netlify.com/
[travis]: https://travis-ci.org/naftulikay/jinjer
[travis.svg]: https://travis-ci.org/naftulikay/jinjer.svg?branch=master
1 change: 1 addition & 0 deletions examples/cpu_count.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
There are {{ basic.cpu_cores.logical }} logical core(s) and {{ basic.cpu_cores.physical }} physical core(s).
3 changes: 3 additions & 0 deletions examples/path.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{%- for path in env.PATH | split(pat=":") -%}
{{ path }}
{% endfor -%}
6 changes: 5 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::path::PathBuf;

use structopt::StructOpt;

/// A CLI utility for rendering Jinja templates.
/// A CLI utility for rendering Jinja-like templates using the Tera template engine.
#[derive(Debug, StructOpt)]
#[structopt(author = "")]
pub struct Args {
Expand Down Expand Up @@ -31,6 +31,10 @@ pub struct FactsCommand {}

#[derive(Debug, StructOpt)]
pub struct RenderCommand {
/// Enable HTML auto-escaping of templates in the template renderer. By default, output is not
/// safe for HTML.
#[structopt(short = "e", long = "auto-escape")]
pub autoescape: bool,
/// Render output to a file rather than to standard output.
#[structopt(short = "o", long = "output")]
pub output_file: Option<PathBuf>,
Expand Down
2 changes: 1 addition & 1 deletion src/cmd/facts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use serde_json::to_string_pretty;
use std::default::Default;

/// Dump out facts.
pub fn call(_config: &FactsCommand) {
pub fn call(_config: FactsCommand) {
println!(
"{}",
to_string_pretty(&Facts::default().discover()).unwrap()
Expand Down
133 changes: 132 additions & 1 deletion src/cmd/render.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,135 @@
use crate::cli::RenderCommand;

use crate::facts::Facts;

use log;

use serde_json::Value;

use std::default::Default;

use std::fs;

use std::io;
use std::io::prelude::*;
use std::io::BufWriter;

use std::process;

use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;

use tera::Context;
use tera::Tera;

/// Render the given templates.
pub fn call(_config: &RenderCommand) {}
pub fn call(config: RenderCommand) {
// our facts to be provided to templates
let context = Context::from_value(Value::Object(Facts::default().discover()))
.expect("Unable to create a context from discovered facts.");

// create a buffer around stdout/output file writes
let mut writer = get_output_writer(&config);

if config.template_files.len() == 0 {
// if no template files are passed, render from stdin
render_stdin(&config, context, &mut writer);
} else {
// user passed template files, render them in order
render_template_files(&config, context, &mut writer);
}
}

/// Open a writer either on standard output or to a given file passed in the config.
fn get_output_writer(config: &RenderCommand) -> BufWriter<Box<dyn Write>> {
let output: Box<dyn Write> = match &config.output_file {
Some(p) => Box::new(
fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&p)
.unwrap_or_else(|e| {
log::error!(
"Unable to open output file {} for writing: {}",
p.display(),
e
);
process::exit(1);
}),
),
None => Box::new(io::stdout()),
};

// create a buffer around stdout/output file writes
BufWriter::new(output)
}

/// Render the template present on standard input.
fn render_stdin(config: &RenderCommand, context: Context, writer: &mut Write) {
log::debug!("Rendering template from standard input as no template files were passed...");
let mut buffer = String::new();

io::stdin().read_to_string(&mut buffer).unwrap_or_else(|e| {
log::error!("Unable to read template from standard input: {}", e);
process::exit(1);
});

match Tera::one_off(buffer.as_str(), context, config.autoescape) {
Ok(s) => {
write!(writer, "{}", s).unwrap_or_else(|e| {
log::error!("Unable to write rendered template to output: {}", e);
process::exit(1);
});
}
Err(e) => {
log::error!("Unable to render template from standard input: {:?}", e);
process::exit(1);
}
}
}

/// Render a list of template files from the configuration.
fn render_template_files(config: &RenderCommand, context: Context, writer: &mut Write) {
let success = AtomicBool::new(false);

for template_file in &config.template_files {
log::info!("Rendering {}...", template_file.display());

match fs::read_to_string(template_file) {
Ok(s) => match Tera::one_off(s.as_str(), context.clone(), config.autoescape) {
// cool, we've been able to read the template file
Ok(s) => {
// we have successfully rendered the template
write!(writer, "{}", s).unwrap_or_else(|e| {
log::error!("Unable to write rendered template to output: {}", e);
process::exit(1);
});

success.store(true, Ordering::SeqCst);
}
Err(e) => {
// rendering the template has failed
log::error!(
"Unable to render template file {}: {:?}",
template_file.display(),
e
);
}
},
Err(e) => {
// we weren't able to read the template file
log::error!(
"Unable to read template file {}: {}",
template_file.display(),
e
);
}
}
}

if !success.load(Ordering::SeqCst) {
log::error!("Unable to successfully render any provided templates.");
process::exit(1);
}
}
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ fn main() {

// execute the given command
match args.command {
cli::Command::Facts(f) => cmd::facts::call(&f),
cli::Command::Render(r) => cmd::render::call(&r),
cli::Command::Facts(f) => cmd::facts::call(f),
cli::Command::Render(r) => cmd::render::call(r),
}
}