1use anyhow::{Context, Result};
2use clap::Parser;
3use std::io::Read;
4use std::path::PathBuf;
5
6const SHELL_BOILERPLATE: &str = r#"
7toml_keys=()
8tt_out=$(mktemp); tt_err=$(mktemp)
9if tigerturtle file.toml -- ${toml_keys[@]} >$tt_out 2>$tt_err; then
10 eval $(<$tt_out); rm $tt_out; rm $tt_err;
11else
12 echo "$(<$tt_err)" >&2; rm $tt_out; rm $tt_err; exit 1;
13fi
14"#;
15
16#[derive(Debug, Parser)]
17#[clap(name = "tigerturtle")]
18#[clap(about = "Parse and evaluate toml files in bash")]
19#[clap(author = "https://ariel.ninja")]
20#[clap(version)]
21pub struct Args {
22 #[arg()]
24 pub file: Option<PathBuf>,
25 #[arg(short = 'd', long, default_value_t = String::from("__"))]
27 pub delim: String,
28 #[arg(short = 'p', long)]
30 pub output_prefix: Option<String>,
31 #[arg(short = 'D', long)]
33 pub default: Option<String>,
34 #[arg(short = 'W', long)]
36 pub write_missing: bool,
37 #[arg(short = 'r', long, default_value_t = String::from("_"))]
39 pub required_prefix: String,
40 #[arg(short = 'G', long)]
42 pub generate: bool,
43 #[arg(raw = true)]
45 pub keys: Vec<String>,
46}
47
48pub fn run() -> Result<()> {
49 let args = Args::parse();
50 if args.generate {
51 println!("{SHELL_BOILERPLATE}");
52 return Ok(());
53 }
54 if args.write_missing {
55 if let Some(default_content) = args.default.as_ref() {
56 if let Some(file) = args.file.as_ref() {
57 write_default_if_missing(file, default_content).context("write default toml")?;
58 }
59 }
60 }
61 let toml_contents = get_toml_content(args.file.as_ref(), args.default).context("get toml")?;
62 let evaluation_string = process_toml(
63 &toml_contents,
64 args.keys,
65 &args.output_prefix.unwrap_or_default(),
66 &args.required_prefix,
67 &args.delim,
68 )
69 .context("process toml")?;
70 println!("{evaluation_string}");
71 Ok(())
72}
73
74pub fn process_toml(
75 toml_contents: &str,
76 mut keys: Vec<String>,
77 output_prefix: &str,
78 required_prefix: &str,
79 delim: &str,
80) -> Result<String> {
81 let parsed_toml: toml::Table = toml::from_str(toml_contents).context("parse toml")?;
82 let mut lines = Vec::new();
83 for key in &mut keys {
84 let required = if let Some(stripped_key) = key.strip_prefix(required_prefix) {
85 *key = stripped_key.to_owned();
86 true
87 } else {
88 false
89 };
90 let key_path: Vec<String> = key
91 .split(delim)
92 .map(std::borrow::ToOwned::to_owned)
93 .collect();
94 let bash_key = key_path.join(delim);
95 let value = match (required, get_toml_value(&parsed_toml, &key_path)) {
96 (true, None) => anyhow::bail!(format!("missing required key: {key}")),
97 (false, None) => String::default(),
98 (_, Some(v)) => v,
99 };
100 lines.push(format!("{output_prefix}{bash_key}={value}"));
101 }
102 Ok(lines.join("\n"))
103}
104
105fn write_default_if_missing(file: &PathBuf, default: &String) -> Result<()> {
106 if file.exists() {
107 return Ok(());
108 }
109 if let Some(parent) = file.parent() {
110 if !parent.exists() {
111 std::fs::create_dir_all(parent).context("create directory for default file")?;
112 }
113 }
114 std::fs::write(file, default).context("write default file contents")
115}
116
117pub fn get_toml_content(file: Option<&PathBuf>, default: Option<String>) -> Result<String> {
118 let toml_contents: String = if let Some(toml_file) = file {
119 if toml_file.exists() {
120 std::fs::read_to_string(toml_file).context("read file")?
121 } else if let Some(default_content) = default {
122 default_content
123 } else {
124 anyhow::bail!("file does not exist and no default provided");
125 }
126 } else {
127 let mut stdin_input = String::new();
128 std::io::stdin()
129 .read_to_string(&mut stdin_input)
130 .context("read stdin")?;
131 stdin_input
132 };
133 Ok(toml_contents)
134}
135
136fn get_toml_value(table: &toml::Table, key_path: &[String]) -> Option<String> {
137 if let Some(next_key_part) = key_path.first() {
138 let next_value_part = table.get(next_key_part)?;
139 return match next_value_part {
140 toml::Value::Table(inner_table) => get_toml_value(inner_table, key_path.split_at(1).1),
141 value => (key_path.len() == 1).then_some(value.to_string()),
142 };
143 };
144 None
145}