mod accumulate;
mod cli;
mod explain;
use std::io::Write;
use std::path::PathBuf;
use clap::Parser;
use knf::{
ConfigFormat, Layers, MergeOptions, ProcessEnv, format, interpolate, load_layers, merge,
};
use cli::{Cli, ShallowAt};
use explain::{explain_pipeline, name_the_set_flag, name_the_shallow_flag};
#[allow(dead_code)]
fn main() {
main_from(std::env::args_os());
}
pub fn main_from<I, T>(args: I)
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
let cli = Cli::parse_from(args);
if let Err(err) = run(cli) {
eprintln!("error: {err}");
for cause in err.chain().skip(1) {
eprintln!(" caused by: {cause}");
}
std::process::exit(1);
}
}
fn run(cli: Cli) -> anyhow::Result<()> {
for leaf in &cli.set {
leaf.validate_keys().map_err(name_the_set_flag)?;
}
let opts = merge_options(&cli)?;
let mut files = if let Some(target) = &cli.accumulate {
knf::fs::accumulate(target, None).map_err(explain::explain_accumulate)?
} else {
cli.files.clone()
};
if let Some(pattern) = &cli.glob {
files = knf::fs::filter_paths(&files, pattern, false);
} else if let Some(pattern) = &cli.glob_filename {
files = knf::fs::filter_paths(&files, pattern, true);
}
if cli.list_files {
let mut text = String::new();
for path in &files {
use std::fmt::Write;
writeln!(text, "{}", path.display()).expect("writing to a String cannot fail");
}
return write_stdout(&text);
}
run_pipeline(&cli, &files, &opts)
}
fn run_pipeline(cli: &Cli, files: &[PathBuf], opts: &MergeOptions) -> anyhow::Result<()> {
let layers = load_layers(files, cli.format.map(Into::into)).map_err(explain_pipeline)?;
match layers {
Layers::Json(layers) => run_native(cli, layers, opts),
Layers::Toml(layers) => run_native(cli, layers, opts),
}
}
fn run_native<V: ConfigFormat>(
cli: &Cli,
mut layers: Vec<V>,
opts: &MergeOptions,
) -> anyhow::Result<()> {
for leaf in &cli.set {
layers.push(leaf.clone().into_layer().map_err(name_the_set_flag)?);
}
let merged = merge(layers, opts).map_err(explain_pipeline)?;
let merged = if cli.interpolate {
interpolate(merged, &ProcessEnv).map_err(explain_pipeline)?
} else {
merged
};
let text = format::emit(merged, !cli.compact).map_err(explain_pipeline)?;
write_stdout(&text)
}
fn merge_options(cli: &Cli) -> anyhow::Result<MergeOptions> {
let mut shallow = Vec::with_capacity(cli.shallow.len());
for occurrence in &cli.shallow {
let keys = match occurrence {
ShallowAt::Root => Vec::new(),
ShallowAt::Path(path) => path
.clone()
.try_into_keys()
.map_err(name_the_shallow_flag)?,
};
shallow.push(keys);
}
Ok(MergeOptions {
strict: cli.strict,
shallow,
})
}
fn write_stdout(text: &str) -> anyhow::Result<()> {
use anyhow::Context;
match std::io::stdout().write_all(text.as_bytes()) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => Ok(()),
Err(e) => Err(e).context("writing to stdout"),
}
}