use std::process::{ExitCode, Termination};
use anyhow::{bail, ensure, Context, Result};
use clap::Parser;
use confget::defs::{BackendKind, Config, FileData};
use confget::format;
#[derive(Debug)]
enum MainResult {
OK,
Failure,
}
impl Termination for MainResult {
fn report(self) -> ExitCode {
ExitCode::from(match self {
Self::OK => 0,
Self::Failure => 1,
})
}
}
#[derive(Debug)]
enum Mode {
Handled(MainResult),
Query(Config),
Check(Config, String),
Sections(Config),
}
#[derive(Debug, Parser)]
#[clap(disable_help_flag(true))]
#[allow(clippy::struct_excessive_bools)]
struct Cli {
#[clap(short)]
check_only: bool,
#[clap(short)]
filename: Option<String>,
#[clap(short = 'h', long = "help")]
show_help: bool,
#[clap(short = 'L')]
match_var_names: bool,
#[clap(short)]
list_all: bool,
#[clap(short)]
match_var_values: Option<String>,
#[clap(short = 'N')]
show_var_name: bool,
#[clap(short = 'n')]
never_show_var_name: bool,
#[clap(short = 'O')]
section_override: bool,
#[clap(short = 'P')]
name_suffix: Option<String>,
#[clap(short = 'p')]
name_prefix: Option<String>,
#[clap(short)]
query: Option<String>,
#[clap(short = 'S')]
shell_escape: bool,
#[clap(short = 's')]
section: Option<String>,
#[clap(short = 't')]
backend_name: Option<String>,
#[clap(short = 'V', long = "version")]
show_version: bool,
#[clap(short = 'x')]
match_regex: bool,
varnames: Vec<String>,
}
const USAGE: &str = "Usage:
confget [-cOSx] [-N | -n] [-f filename] [-m pattern] [-P postfix] [-p prefix]
[-s section] [-t type] var...
confget [-OSx] [-N | -n] [-f filename] [-m pattern] [-P postfix] [-p prefix]
[-s section] [-t type] -L pattern...
confget [-OSx] [-N | -n] [-f filename] [-m pattern] [-P postfix] [-p prefix]
[-s section] [-t type] -l
confget [-f filename] -q sections [-t type]
confget -q features
confget -q feature NAME";
const fn supports_sections_query(backend: &BackendKind) -> bool {
match *backend {
#[cfg(feature = "ini-nom")]
BackendKind::IniNom => true,
#[cfg(feature = "ini-regex")]
BackendKind::IniRE => true,
_ => false,
}
}
#[allow(clippy::print_stdout)]
fn validate_options(opts: &Cli, config: Config) -> Result<Mode> {
ensure!(
[
opts.query.is_some(),
opts.match_var_names,
opts.list_all,
!(config.varnames.is_empty()
|| opts.match_var_names
|| opts
.query
.as_ref()
.map_or(false, |value| value == "feature")),
]
.into_iter()
.filter(|value| *value)
.count()
< 2,
"Only a single query at a time, please!"
);
if opts.show_version {
let (_, version) = confget::features()
.into_iter()
.find(|&(name, _)| name == "BASE")
.context("Internal error: no 'BASE' in the features list")?;
println!("confget {}", version);
}
if opts.show_help {
println!("{}", USAGE);
}
if opts.show_help || opts.show_version {
return Ok(Mode::Handled(MainResult::OK));
}
if let Some(ref query) = opts.query {
return Ok(match query.as_str() {
"sections" => {
ensure!(supports_sections_query(&config.backend),
"The query for sections is only supported for the 'ini' backend for the present",
);
Mode::Sections(config)
}
"features" => {
ensure!(config.varnames.is_empty(), "No arguments to -q features");
let features: Vec<String> = confget::features()
.into_iter()
.map(|(name, version)| format!("{}={}", name, version))
.collect();
println!("{}", features.join(" "));
Mode::Handled(MainResult::OK)
}
"feature" => {
let (feature_name, rest) = config
.varnames
.split_first()
.context("Expected a single feature name")?;
ensure!(rest.is_empty(), "Only a single feature name expected");
match confget::features()
.into_iter()
.find(|&(name, _)| name == feature_name)
{
Some((_, value)) => {
println!("{}", value);
Mode::Handled(MainResult::OK)
}
None => Mode::Handled(MainResult::Failure),
}
}
other => bail!(format!("Unrecognized query '{}'", other)),
});
}
if opts.list_all {
ensure!(
config.varnames.is_empty(),
"Only a single query at a time, please!"
);
} else if opts.match_var_names {
ensure!(!config.varnames.is_empty(), "No patterns to match against");
} else {
ensure!(
!config.varnames.is_empty(),
"No variables specified to query"
);
}
if opts.check_only {
let (varname, rest) = opts
.varnames
.split_first()
.context("Internal error: check_only with no config.varnames")?;
ensure!(rest.is_empty(), "Only a single query at a time, please!");
Ok(Mode::Check(config, varname.clone()))
} else {
Ok(Mode::Query(config))
}
}
fn parse_args() -> Result<Mode> {
let opts = Cli::parse();
let clone_or_empty =
|opt: Option<&String>| opt.map_or_else(String::new, |value| (*value).clone());
let config = Config {
backend: if let Some(value) = opts.backend_name.as_ref() {
value.parse().context("Invalid backend specified")?
} else {
BackendKind::get_preferred_ini_backend()
},
encoding: String::new(),
filename: opts.filename.clone(),
list_all: opts.list_all,
match_regex: opts.match_regex,
match_var_names: opts.match_var_names,
match_var_values: opts.match_var_values.clone(),
name_prefix: clone_or_empty(opts.name_prefix.as_ref()),
name_suffix: clone_or_empty(opts.name_suffix.as_ref()),
section: clone_or_empty(opts.section.as_ref()),
section_override: opts.section_override,
section_specified: opts.section.is_some(),
shell_escape: opts.shell_escape,
show_var_name: opts.show_var_name
|| ((opts.match_var_names || opts.list_all || opts.varnames.len() > 1)
&& !opts.never_show_var_name),
varnames: opts.varnames.clone(),
};
validate_options(&opts, config)
}
#[allow(clippy::print_stdout)]
fn output_vars(config: &Config, data: &FileData, section: &str) -> Result<MainResult> {
for var in format::filter_vars(config, data, section)
.context("Could not select the variables to output")?
{
println!("{}", var.output_full);
}
Ok(MainResult::OK)
}
fn output_check_only(
_config: &Config,
data: &FileData,
section: &str,
varname: &str,
) -> MainResult {
data.get(section).map_or(MainResult::Failure, |sect_data| {
if sect_data.contains_key(varname) {
MainResult::OK
} else {
MainResult::Failure
}
})
}
#[allow(clippy::print_stdout)]
fn output_sections(data: &FileData) -> MainResult {
let mut sections: Vec<&String> = data.keys().filter(|value| !value.is_empty()).collect();
sections.sort();
for name in sections {
println!("{}", name);
}
MainResult::OK
}
fn parse_config(config: &Config) -> Result<(FileData, String)> {
let (data, first_section) =
confget::read_ini_file(config).context("Could not parse the INI-style file")?;
let section = if config.section.is_empty() {
first_section
} else {
config.section.clone()
};
Ok((data, section))
}
fn main() -> Result<MainResult> {
Ok(match parse_args()? {
Mode::Handled(res) => res,
Mode::Check(config, varname) => {
let (data, section) = parse_config(&config)?;
output_check_only(&config, &data, §ion, &varname)
}
Mode::Sections(config) => {
let (data, _) = parse_config(&config)?;
output_sections(&data)
}
Mode::Query(config) => {
let (data, section) = parse_config(&config)?;
output_vars(&config, &data, §ion)?
}
})
}