feature-check 2.3.2

Query a program for supported features
Documentation
// SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
// SPDX-License-Identifier: BSD-2-Clause
//! A command-line tool used for querying a program for its features.

use std::env;
use std::process::{ExitCode, Termination};
use std::str::FromStr;

use anyhow::{Context as _, Error, Result, anyhow, bail};
use clap::{CommandFactory as _, Parser as _};
use clap_derive::Parser;

use feature_check::defs::{Config, DEFAULT_OPTION_NAME, DEFAULT_PREFIX, Mode, Obtained};
use feature_check::expr::{self as fexpr, CalcResult};
use feature_check::obtain;

/// The list of the program's features.
const FEATURES: [(&str, &str); 4] = [
    ("feature-check", env!("CARGO_PKG_VERSION")),
    ("single", "1.0"),
    ("list", "1.0"),
    ("simple", "1.0"),
];

/// The format in which the queried program's features should be displayed.
#[derive(Debug, Clone, Copy)]
enum OutputFormat {
    /// Display the feature list as a JSON object.
    Json,
    /// Display the feature list as a series of tab-separated fields on separate lines.
    Tsv,
}

impl OutputFormat {
    /// The string used to request JSON output.
    const JSON_NAME: &'static str = "json";
    /// The string used to request tab-separated values output.
    const TSV_NAME: &'static str = "tsv";
}

impl FromStr for OutputFormat {
    type Err = Error;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        match value {
            Self::JSON_NAME => Ok(Self::Json),
            Self::TSV_NAME => Ok(Self::Tsv),
            other => Err(anyhow!("Unrecognized output format '{other}'")),
        }
    }
}

impl AsRef<str> for OutputFormat {
    fn as_ref(&self) -> &str {
        match *self {
            Self::Json => Self::JSON_NAME,
            Self::Tsv => Self::TSV_NAME,
        }
    }
}

/// Runtime configuration for the feature-check command-line tool.
#[derive(Debug)]
struct MainConfig {
    /// The library crate's configuration settings.
    config: Config,
    /// The selected output format.
    output_format: OutputFormat,
    /// Display the version strings of the features, or only their names?
    show_version: bool,
}

/// Has the [`parse_args`] function managed to handle e.g. `--features` or `--version` output,
/// or should the real "query a program's features" work be done?
#[derive(Debug)]
enum MainMode {
    /// Query a program's features.
    Do(MainConfig),
    /// Requested action already taken successfully.
    Handled,
}

/// What should the program return?
#[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))]
#[expect(clippy::struct_excessive_bools, reason = "we do take a lot of options")]
struct Cli {
    /// Display `feature-check` usage information and exit.
    #[clap(short, long)]
    help: bool,

    /// List the features supported by `feature-check` itself and exit.
    #[clap(long)]
    features: bool,

    /// List the features supported by the specified program.
    #[clap(short)]
    list: bool,

    /// Specify the query-features option to pass to the program.
    #[clap(short = 'O', default_value = DEFAULT_OPTION_NAME, allow_hyphen_values = true)]
    optname: String,

    /// Specify the output format for the list of features.
    #[clap(short = 'o', default_value = OutputFormat::TSV_NAME, allow_hyphen_values = true)]
    output_format: OutputFormat,

    /// The features prefix in the program output.
    #[clap(short = 'P', default_value = DEFAULT_PREFIX, allow_hyphen_values = true)]
    prefix: String,

    /// Output the feature version when querying a single feature".
    #[clap(short = 'v')]
    show_version: bool,

    /// Display `feature-check` version information and exit.
    #[clap(short = 'V', long)]
    version: bool,

    /// The feature name or `feature op version` expression to check for.
    expressions: Vec<String>,
}

/// Handle `--version`, `--help`, and/or `--features`.
#[expect(clippy::print_stdout, reason = "this is the whole point")]
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_some(version))
            .with_context(|| {
                format!(
                    "Internal error: 'feature-check' not found in the features list: {FEATURES:?}",
                )
            })?;
        println!("feature-check {version}");
    }

    if opt_help {
        println!("{usage}", usage = get_help());
    }

    if opt_features {
        let features: Vec<String> = FEATURES
            .iter()
            .map(|&(name, version)| format!("{name}={version}"))
            .collect();
        println!("Features: {features}", features = features.join(" "));
    }

    Ok(opt_version || opt_help || opt_features)
}

/// Parse the command-line arguments, handle `--help`, `--version`, and `--features`.
fn parse_args() -> Result<MainMode> {
    let opts = Cli::parse();
    let get_help = || format!("{usage}", usage = 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)
    }
}

/// Obtain the features of a program, display them as requested.
#[expect(clippy::print_stdout, reason = "this is the whole point")]
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")?;
                    #[expect(
                        clippy::wildcard_enum_match_arm,
                        reason = "we do only expect two outcomes"
                    )]
                    match value {
                        CalcResult::Version(ver) => {
                            if config.show_version {
                                println!("{ver}", ver = ver.as_ref());
                            }
                            MainResult::Ok
                        }
                        CalcResult::Null => MainResult::FeatureNotSupported,
                        other => bail!("single.get_value() returned {other:?}"),
                    }
                }
                Mode::List => match config.output_format {
                    OutputFormat::Tsv => {
                        let mut res: Vec<String> = features
                            .iter()
                            .map(|(name, value)| format!("{name}\t{value}"))
                            .collect();
                        res.sort();
                        println!("{res}", res = res.join("\n"));
                        MainResult::Ok
                    }
                    OutputFormat::Json => {
                        println!(
                            "{features}",
                            features = 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")?;
                    #[expect(
                        clippy::wildcard_enum_match_arm,
                        reason = "we do only expect two outcomes"
                    )]
                    match value {
                        CalcResult::Bool(true) => MainResult::Ok,
                        CalcResult::Bool(false) => MainResult::FeatureNotSupported,
                        other => bail!("parse_simple().get_value() returned {other:?}"),
                    }
                }
                other => bail!("Internal error: unexpected mode {other:?} after obtain_features()"),
            },
            other => bail!("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)?,
    })
}