/// An almost-but-not-quite drop-in for the official `jsonnet` /// executable. use clap::{crate_authors, crate_version, value_t, App, AppSettings, Arg, ArgMatches, SubCommand}; use jsonnet::{jsonnet_version, JsonnetVm}; use std::borrow::Cow; use std::error::Error as StdError; use std::io::{self, Write}; use std::{env, process}; fn typed_arg_or_exit<'a, T>(matches: &ArgMatches<'a>, arg: &str) -> Option where T: std::str::FromStr, { if matches.is_present(arg) { let v = value_t!(matches, arg, T).unwrap_or_else(|e| e.exit()); Some(v) } else { None } } /// Parse "foo" into ("foo", None) and "foo=bar" into ("foo", Some("bar")) fn parse_kv(s: &str) -> (&str, Option<&str>) { match s.find('=') { Some(i) => (&s[..i], Some(&s[i + 1..])), None => (s, None), } } #[test] fn test_parse_kv() { assert_eq!(parse_kv("foo=bar"), ("foo", Some("bar"))); assert_eq!(parse_kv("foo"), ("foo", None)); assert_eq!(parse_kv("foo="), ("foo", Some(""))); } fn build_cli<'a, 'b>(version: &'b str) -> App<'a, 'b> { App::new("jsonnet") .version(version) .author(crate_authors!()) .about("Jsonnet command line tool") .setting(AppSettings::VersionlessSubcommands) .setting(AppSettings::SubcommandRequiredElseHelp) .subcommand(SubCommand::with_name("eval") .about("Evaluate a jsonnet file/expression") .arg(Arg::with_name("exec") .short("e") .long("exec") .help("Treat INPUT as code")) .arg(Arg::with_name("incdir") .short("j") .long("jpath") .value_name("DIR") .multiple(true) .use_delimiter(false) .number_of_values(1) .help("Specify an additional library search dir")) .arg(Arg::with_name("string") .short("S") .long("string") .help("Expect a string, manifest as plain text")) .arg(Arg::with_name("max-stack") .short("s") .long("max-stack") .value_name("N") .help("Number of allowed stack frames")) .arg(Arg::with_name("max-trace") .short("t") .long("max-trace") .value_name("N") .help("Max length of stack trace before cropping")) .arg(Arg::with_name("gc-min-objects") .long("gc-min-objects") .value_name("N") .help("Do not run garbage collector until this many")) .arg(Arg::with_name("gc-growth-trigger") .long("gc-growth-trigger") .value_name("N") .help("Run garbage collector after this amount of object growth")) .arg(Arg::with_name("ext-var") .short("V") .long("ext-str") .value_name("VAR[=VAL]") .multiple(true) .number_of_values(1) .use_delimiter(false) .help("Provide 'external' variable as a string. If is omitted, get from env var ")) .arg(Arg::with_name("ext-code") .long("ext-code") .value_name("VAR[=CODE]") .multiple(true) .number_of_values(1) .use_delimiter(false) .help("Provide 'external' variable as code. If is omitted, get from env var ")) .arg(Arg::with_name("tla-var") .short("A") .long("tla-str") .value_name("VAR[=VAL]") .multiple(true) .number_of_values(1) .use_delimiter(false) .help("Provide 'top-level argument' as a string. If is omitted, get from env var ")) .arg(Arg::with_name("tla-code") .long("tla-code") .value_name("VAR[=CODE]") .multiple(true) .number_of_values(1) .use_delimiter(false) .help("Provide 'top-level argument' as code. If is omitted, get from env var ")) .arg(Arg::with_name("INPUT") .required(true) .help("Input jsonnet file"))) .subcommand(SubCommand::with_name("fmt") .about("Reformat a jsonnet file/expression") .arg(Arg::with_name("exec") .short("e") .long("exec") .help("Treat INPUT as code")) .arg(Arg::with_name("indent") .short("n") .long("indent") .value_name("N") .default_value("0") .help("Number of spaces to indent by, 0 means no change")) .arg(Arg::with_name("max-blank-lines") .long("max-blank-lines") .value_name("N") .default_value("2") .help("Max vertical spacing, 0 means no change")) .arg(Arg::with_name("string-style") .long("string-style") .possible_values(&["d", "s", "l"]) .default_value("l") .value_name("C") .help("Enforce double, single quotes or 'leave'")) .arg(Arg::with_name("comment-style") .long("comment-style") .possible_values(&["h", "s", "l"]) .default_value("l") .value_name("C") .help("# (h) // (s) or 'leave'. Never changes she-bang")) .arg(Arg::with_name("no-pretty-field-names") .long("no-pretty-field-names") .help("Don't use syntax sugar for fields and indexing")) .arg(Arg::with_name("pad-arrays") .long("pad-arrays") .help("[ 1, 2, 3 ] instead of [1, 2, 3]")) .arg(Arg::with_name("no-pad-objects") .long("no-pad-objects") .help("{ x: 1, x: 2 } instead of {x: 1, y: 2}")) .arg(Arg::with_name("no-sort-imports") .long("no-sort-imports") .help("Don't sort imports")) .arg(Arg::with_name("debug-desugaring") .long("debug-desugaring") .help("Unparse the desugared AST without executing it")) .arg(Arg::with_name("INPUT") .required(true) .help("Input jsonnet file"))) } fn eval<'a, 'b>( vm: &'a mut JsonnetVm, matches: &ArgMatches<'b>, ) -> Result<(), Box> { if let Some(n) = typed_arg_or_exit(matches, "max-stack") { vm.max_stack(n); } if let Some(n) = typed_arg_or_exit(matches, "max-trace") { vm.max_trace(Some(n)); } if let Some(n) = typed_arg_or_exit(matches, "gc-min-objects") { vm.gc_min_objects(n); } if let Some(n) = typed_arg_or_exit(matches, "gc-growth-trigger") { vm.gc_growth_trigger(n); } if matches.is_present("string") { vm.string_output(true); } if let Some(paths) = matches.values_of_os("incdir") { for path in paths { vm.jpath_add(path); } } if let Some(vars) = matches.values_of("ext-var") { for (var, val) in vars.map(parse_kv) { let val = val .map(Cow::from) .or_else(|| env::var(var).ok().map(Cow::from)) .unwrap_or("".into()); vm.ext_var(var, &val); } } if let Some(vars) = matches.values_of("ext-code") { for (var, val) in vars.map(parse_kv) { let val = val .map(Cow::from) .or_else(|| env::var(var).ok().map(Cow::from)) .unwrap_or("".into()); vm.ext_code(var, &val); } } if let Some(vars) = matches.values_of("tla-var") { for (var, val) in vars.map(parse_kv) { let val = val .map(Cow::from) .or_else(|| env::var(var).ok().map(Cow::from)) .unwrap_or("".into()); vm.tla_var(var, &val); } } if let Some(vars) = matches.values_of("tla-code") { for (var, val) in vars.map(parse_kv) { let val = val .map(Cow::from) .or_else(|| env::var(var).ok().map(Cow::from)) .unwrap_or("".into()); vm.tla_code(var, &val); } } let output = if matches.is_present("exec") { let expr = matches.value_of("INPUT").unwrap(); vm.evaluate_snippet("INPUT", expr)? } else { let file = matches.value_of_os("INPUT").unwrap(); vm.evaluate_file(file)? }; print!("{}", output); Ok(()) } fn fmt<'a, 'b>( vm: &'a mut JsonnetVm, matches: &ArgMatches<'b>, ) -> Result<(), Box> { if let Some(n) = typed_arg_or_exit(matches, "indent") { vm.fmt_indent(n); } if let Some(n) = typed_arg_or_exit(matches, "max-blank-lines") { vm.fmt_max_blank_lines(n); } if let Some(v) = matches.value_of("string-style") { let v = match v { "d" => jsonnet::FmtString::Double, "s" => jsonnet::FmtString::Single, "l" => jsonnet::FmtString::Leave, _ => unreachable!(), }; vm.fmt_string(v); } if let Some(v) = matches.value_of("comment-style") { let v = match v { "h" => jsonnet::FmtComment::Hash, "s" => jsonnet::FmtComment::Slash, "l" => jsonnet::FmtComment::Leave, _ => unreachable!(), }; vm.fmt_comment(v); } if matches.is_present("no-pretty-field-names") { vm.fmt_pretty_field_names(false); } if matches.is_present("pad-arrays") { vm.fmt_pad_arrays(true); } if matches.is_present("no-pad-objects") { vm.fmt_pad_objects(false); } if matches.is_present("no-sort-imports") { vm.fmt_sort_import(false); } if matches.is_present("debug-desugaring") { vm.fmt_debug_desugaring(true); } let output = if matches.is_present("exec") { let expr = matches.value_of("INPUT").unwrap(); vm.fmt_snippet("INPUT", expr)? } else { let file = matches.value_of_os("INPUT").unwrap(); vm.fmt_file(file)? }; print!("{}", output); Ok(()) } fn main() { let version = format!("{} (libjsonnet {})", crate_version!(), jsonnet_version()); let matches = build_cli(&version).get_matches(); let mut vm = JsonnetVm::new(); let result = if let Some(matches) = matches.subcommand_matches("eval") { eval(&mut vm, matches) } else if let Some(matches) = matches.subcommand_matches("fmt") { fmt(&mut vm, matches) } else { unreachable!(); }; match result { Ok(()) => (), Err(e) => { write!(&mut io::stderr(), "{}", e).unwrap(); process::exit(1); } }; }