use std::env;
use std::process::{ExitCode, Termination};
use std::str::FromStr;
use anyhow::{anyhow, bail, Context, Error, Result};
use clap::{CommandFactory, Parser};
use feature_check::defs::{Config, Mode, Obtained, DEFAULT_OPTION_NAME, DEFAULT_PREFIX};
use feature_check::expr::{self as fexpr, CalcResult};
use feature_check::obtain;
const FEATURES: [(&str, &str); 4] = [
("feature-check", env!("CARGO_PKG_VERSION")),
("single", "1.0"),
("list", "1.0"),
("simple", "1.0"),
];
#[derive(Debug, Clone, Copy)]
enum OutputFormat {
Json,
Tsv,
}
impl OutputFormat {
const JSON_NAME: &'static str = "json";
const TSV_NAME: &'static str = "tsv";
}
impl FromStr for OutputFormat {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
Self::JSON_NAME => Ok(Self::Json),
Self::TSV_NAME => Ok(Self::Tsv),
other => Err(anyhow!(format!(
"Unrecognized output format '{}'",
other.to_owned()
))),
}
}
}
impl AsRef<str> for OutputFormat {
fn as_ref(&self) -> &str {
match *self {
Self::Json => Self::JSON_NAME,
Self::Tsv => Self::TSV_NAME,
}
}
}
#[derive(Debug)]
struct MainConfig {
config: Config,
output_format: OutputFormat,
show_version: bool,
}
#[derive(Debug)]
enum MainMode {
Do(MainConfig),
Handled,
}
#[derive(Debug)]
enum MainResult {
Ok,
FeatureNotSupported,
Error,
}
impl Termination for MainResult {
fn report(self) -> ExitCode {
ExitCode::from(match self {
Self::Ok => 0,
Self::FeatureNotSupported => 1,
Self::Error => 2,
})
}
}
#[derive(Debug, Parser)]
#[clap(disable_help_flag(true))]
#[allow(clippy::struct_excessive_bools)]
struct Cli {
#[clap(short, long)]
help: bool,
#[clap(long)]
features: bool,
#[clap(short)]
list: bool,
#[clap(short = 'O', default_value = DEFAULT_OPTION_NAME)]
optname: String,
#[clap(short = 'o', default_value = OutputFormat::TSV_NAME)]
output_format: OutputFormat,
#[clap(short = 'P', default_value = DEFAULT_PREFIX)]
prefix: String,
#[clap(short = 'v')]
show_version: bool,
#[clap(short = 'V', long)]
version: bool,
expressions: Vec<String>,
}
#[allow(clippy::print_stdout)]
fn version_help_features<F: FnOnce() -> String>(
opt_version: bool,
opt_help: bool,
opt_features: bool,
get_help: F,
) -> Result<bool> {
if opt_version {
let version = FEATURES
.iter()
.find_map(|&(name, version)| (name == "feature-check").then(|| version))
.with_context(|| {
format!(
"Internal error: 'feature-check' not found in the features list: {:?}",
FEATURES
)
})?;
println!("feature-check {}", version);
}
if opt_help {
println!("{}", get_help());
}
if opt_features {
let features: Vec<String> = FEATURES
.iter()
.map(|&(name, version)| format!("{}={}", name, version))
.collect();
println!("Features: {}", features.join(" "));
}
Ok(opt_version || opt_help || opt_features)
}
fn parse_args() -> Result<MainMode> {
let opts = Cli::parse();
let get_help = || format!("{}", Cli::command().render_long_help());
if version_help_features(opts.version, opts.help, opts.features, get_help)? {
return Ok(MainMode::Handled);
}
let (program, opts_expr) = opts.expressions.split_first().with_context(get_help)?;
let build = |mode| {
Ok(MainMode::Do(MainConfig {
config: Config::default()
.with_option_name(opts.optname)
.with_prefix(opts.prefix)
.with_program(program.clone())
.with_mode(mode),
output_format: opts.output_format,
show_version: opts.show_version,
}))
};
if opts_expr.is_empty() {
if !opts.list {
bail!(get_help());
}
build(Mode::List)
} else {
if opts.list {
bail!(get_help());
}
let expr = opts_expr.join(" ");
let mode = fexpr::parse(&expr).context("Invalid expression specified")?;
build(mode)
}
}
#[allow(clippy::print_stdout)]
#[allow(clippy::wildcard_enum_match_arm)]
fn run(config: MainConfig) -> Result<MainResult> {
Ok(
match obtain::obtain_features(&config.config)
.context("Could not obtain the program's list of features")?
{
Obtained::Failed(_) | Obtained::NotSupported => MainResult::Error,
Obtained::Features(features) => match config.config.mode {
Mode::Single(feature) => {
let value = feature
.get_value(&features)
.context("Could not evaluate the expression")?;
match value {
CalcResult::Version(ver) => {
if config.show_version {
println!("{}", ver.as_ref());
}
MainResult::Ok
}
CalcResult::Null => MainResult::FeatureNotSupported,
other => bail!(format!("single.get_value() returned {:?}", other)),
}
}
Mode::List => match config.output_format {
OutputFormat::Tsv => {
let mut res: Vec<String> = features
.iter()
.map(|(name, value)| format!("{}\t{}", name, value))
.collect();
res.sort();
println!("{}", res.join("\n"));
MainResult::Ok
}
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&features)
.context("Internal error: could not serialize the features data")?
);
MainResult::Ok
}
},
Mode::Simple(expr) => {
let value = expr
.get_value(&features)
.context("Could not evaluate the expression")?;
match value {
CalcResult::Bool(true) => MainResult::Ok,
CalcResult::Bool(false) => MainResult::FeatureNotSupported,
other => bail!(format!("parse_simple().get_value() returned {:?}", other)),
}
}
other => bail!(format!(
"Internal error: unexpected mode {:?} after obtain_features()",
other
)),
},
other => bail!(format!(
"Internal error: obtain_features() returned something weird: {:?}",
other
)),
},
)
}
fn main() -> Result<MainResult> {
Ok(match parse_args()? {
MainMode::Handled => MainResult::Ok,
MainMode::Do(config) => run(config)?,
})
}