From 479e3c5129d9fdaf14a1a47c25a75aa76b30d92a Mon Sep 17 00:00:00 2001 From: Sergio Medina Toledo Date: Fri, 6 Sep 2019 20:07:23 +0100 Subject: [PATCH] Change ergonomics of args and add README content --- README.md | 145 +++++++++++++++++++++++++++++++++++++++++++++++++++- src/main.rs | 126 ++++++++++++++++++++++++++++----------------- 2 files changed, 224 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 2521ab3..38464db 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # j2_render -j2_render is CLI tool to render [jinja 2]() templates, it can load the context from different sources. +j2_render is a static binary CLI tool to render [jinja 2]() templates, it can load the context from different sources. j2_render is writen in rust and uses [tera]() as jinja 2 engine, may be some differences between implementations, so look at tera's documentation for reference @@ -13,6 +13,149 @@ sudo curl -L "https://github.com/lumasepa/j2_render/releases/download/${J2_RENDE sudo chmod +x /usr/local/bin/j2_render ``` +## Working with the Context + +The context in j2_render is a json. +The context can be loaded from different formats and ways. +The context is populated in args order so last arg overwrites keys of previous ones + +### Supported Context formats + +* json +* yaml +* toml +* hcl/tf/tfvars +* key=value + +#### Future Supported +* http/s + json/yaml/toml/hcl/tf/tfvars `format+https?://...` + +### Supported Context Inputs + +* context file `--file` `-f` `file_path` or `format+file_path` +* environment variables `--env` `-e` +* arguments `--var` `-v` `key=value` or `format+key=value` +* stdin `--stdin` `-i` json/yaml/toml/hcl/tf/tfvars + +## Working with templates +Templates are jinja 2 templates implemented by tera, for reference of the jinja 2 template lenguaje go to [tera doc]() + +### Supported Templates Inputs + +* Template file `--file` `-f` `file_path` +* stdin `--stdin` `-i` `template` + ## Usage +``` +j2_render [FLAGS] + + OPTIONS: + + FORMATS = json,yaml,hcl,tfvars,tf,template,j2,tpl + FILE_PATH = file_path.FORMAT or FORMAT+file_path -- path to a file ctx, template or output + VAR = key=value or FORMAT+key=value -- if format provided value will be parsed as format + + FLAGS: + + --stdin/-i FORMAT -- read from stdin context or template + --out/-o file_path -- output file for rendered template, default stdout + --env/-e -- load env vars in ctx + --file/-f FILE_PATH -- loads a file as context or template depending on extension or format + --var/-v VAR -- adds a pair key value to the context or a template depending on format + --print-ctx/-p -- print the context as json and exits + --help/-h -- shows this help +``` + #### Usage Examples + +##### Render a template using a context file + +```bash +j2_render -f ctx.yaml -f template.j2 > result +``` + +##### Render a template using environment variables + +```bash +j2_render --env -f template.j2 > result +``` + +##### Render a template using context from stdin + +```bash +cat ctx.yaml | j2_render -i yaml -f template.j2 > result +``` + +#### Render a template using context from var + +```bash +echo "Hello {{name}}" | j2_render --var "name=world" -i j2 > result +``` + +##### Render a template from stdin + +```bash +cat template.j2 | j2_render -i j2 -f ctx.yaml > result +``` + +#### Render a template from vars + +```bash +TEMPLATE=$(cat template.j2) +j2_render --var "j2+=${TEMPLATE}" -f ctx.yaml > result +``` + +#### Render a template using context mixed by different inputs + +```bash +cat ctx.json | j2_render --var "name=batman" -i json --env -f ctx.yaml --var "json+list=[1,2,3]" -f template.j2 > result +``` + +##### Abuse to convert to json + +```bash +j2_render -f file.yaml --print-ctx > file.json +``` + +```bash +j2_render -f file.tfvars --print-ctx > file.json +``` + +```bash +j2_render -f file.toml --print-ctx > file.json +``` + +### Extensions to jinja 2 + +### filters + +* "bash" +* "sed" +* "glob" +* "read_file" +* "file_name" +* "file_dir" +* "strip_line_breaks" +* "remove_extension" +* "b64decode" +* "b64encode" +* "str" +* "to_json" +* "from_json" + +### functions + +* "tab_all_lines" +* "bash" +* "str" +* "to_json" +* "from_json" + +### testers + +* "file" +* "directory" + + + diff --git a/src/main.rs b/src/main.rs index 14c52a6..faeef87 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,51 +19,82 @@ mod functions; mod inners; mod testers; +pub struct Config { + pub template: String, + pub context: Context, + pub out_file: Option, + pub print_ctx: bool, +} + pub fn help() { println!( " -j2_render [opts] - - --stdin -i [json,yaml,hcl,tfvars,template] read from stdin context or template - --out [file_path] output file for rendered template, default stdout - --env -e load env vars in ctx - --ctx -c [file_path] context files to be loaded in context, multiple files allowed, default empty - --var -v key=value adds a pair key value to the context - --template -t [file_path] template to be rendered, default empty - --print-ctx -p print the context as json and exits - --help shows this help +j2_render [FLAGS] + + OPTIONS: + + FORMATS = json,yaml,hcl,tfvars,tf,template,j2,tpl + FILE_PATH = file_path.FORMAT or FORMAT+file_path -- path to a file ctx, template or output + VAR = key=value or FORMAT+key=value -- if format provided value will be parsed as format + + FLAGS: + + --stdin/-i FORMAT -- read from stdin context or template + --out/-o file_path -- output file for rendered template, default stdout + --env/-e -- load env vars in ctx + --file/-f FILE_PATH -- loads a file as context or template depending on extension or format + --var/-v VAR -- adds a pair key value to the context or a template depending on format + --print-ctx/-p -- print the context as json and exits + --help/-h -- shows this help " ) } -pub fn parse_args() -> (String, Option, bool, Context) { +fn extract_format(string: &str) -> Option<(String,String)> { + let mut parts : Vec<&str>= string.splitn(2, '+').collect(); + let format = parts.pop().expect(""); + if parts.len() == 0 { + return None + } + let other = parts.pop().expect(""); + return Some((format.to_string(), other.to_string())) +} + +pub fn parse_args() -> Config { let mut args = env::args().collect::>(); args.reverse(); - let mut template = String::new(); - let mut context = Context::new(); - let mut out = None; - let mut print_ctx = false; + let mut config = Config{ + template: String::new(), + context: Context::new(), + out_file: None, + print_ctx: false + }; args.pop(); // binary name while let Some(arg) = args.pop() { match arg.as_str() { - "--print-ctx" | "-p" => print_ctx = true, "--var" | "-v" => { let variable = args .pop() .expect("error specified --var/-v flag but not value provided"); - let mut parts : Vec<&str> = variable.split('=').collect(); + let mut parts : Vec<&str> = variable.splitn(2, '=').collect(); let key = parts.pop().expect("Error no key=value found"); - let value = parts.join("="); - context.insert(&key, &value) + let value = parts.pop().expect("Error no key=value found"); + + if let Some((format, key)) = extract_format(key) { + process_inputs(&mut config, format, value.to_string()); + } else { + config.context.insert(&key, &value) + } } + "--print-ctx" | "-p" => config.print_ctx = true, "--out" | "-o" => { let filepath = args .pop() .expect("error specified --out/-o flag but not file path provided"); - out = Some(filepath); + config.out_file = Some(filepath); } "--stdin" | "-i" => { let format = args @@ -71,36 +102,31 @@ pub fn parse_args() -> (String, Option, bool, Context) { .expect("error specified --stdin/-i flag but not format provided"); let mut data = String::new(); io::stdin().read_to_string(&mut data).expect("Error readinf from stdin"); - if format == "template" { - template = data + process_inputs(&mut config, format, data); + } + "--file" | "-f" => { + let path = args + .pop() + .expect("error specified --file/-f flag but not context file path provided"); + let (format, path) = if let Some((format, path)) = extract_format(&path) { + (format, path) } else { - populate_ctx(&mut context, format, data); - } + let extension = Path::new(&path) + .extension() + .and_then(OsStr::to_str) + .expect("Error no extension found in ctx file"); + (extension.to_string(), path) + }; + + let data = fs::read_to_string(&path).expect(&format!("Error reading context file {}", path)); + process_inputs(&mut config, format, data); } "--env" | "-e" => { let env_vars = env::vars().collect::>(); for (k, v) in env_vars.iter() { - context.insert(k, v); + config.context.insert(k, v); } } - "--ctx" | "-c" => { - let path = args - .pop() - .expect("error specified --ctx/-c flag but not context file path provided"); - let extension = Path::new(&path) - .extension() - .and_then(OsStr::to_str) - .expect("Error no extension found in ctx file"); - let data = fs::read_to_string(&path).expect(&format!("Error reading context file {}", path)); - - populate_ctx(&mut context, extension.to_string(), data); - } - "--template" | "-t" => { - let path = args - .pop() - .expect("error specified --template/-t flag but not template path provided"); - template = fs::read_to_string(path).expect("Error reading template") - } "--help" | "help" | "-h" => { help(); exit(0); @@ -108,7 +134,15 @@ pub fn parse_args() -> (String, Option, bool, Context) { _ => panic!("Error argument {} not recognized", arg), } } - return (template, out, print_ctx, context); + return config; +} + +pub fn process_inputs(mut config: &mut Config, format: String, data: String) { + if format == "template" || format == "tpl" || format == "j2" { + config.template = data + } else { + populate_ctx(&mut config.context, format, data); + } } pub fn populate_ctx(context: &mut Context, format: String, data: String) { @@ -151,7 +185,7 @@ pub fn populate_ctx(context: &mut Context, format: String, data: String) { } pub fn main() -> std::result::Result<(), String> { - let (template, out, print_ctx, context) = parse_args(); + let Config{template, context,print_ctx, out_file } = parse_args(); if print_ctx { println!("{}", context.as_json().expect("Error encoding ctx as json").to_string()); @@ -187,7 +221,7 @@ pub fn main() -> std::result::Result<(), String> { let rendered = tera.render("template", &context).expect("Error rendering template"); - if let Some(filepath) = out { + if let Some(filepath) = out_file { let mut file = fs::File::create(&filepath).expect("Error creating output file"); file.write_all(rendered.as_ref()).expect("Error writing to output file"); } else {