feature-check 2.0.0

Query a program for supported features
Documentation
/*
 * Copyright (c) 2021, 2022  Peter Pentchev <roam@ringlet.net>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */
//! 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::{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;

/// 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(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,
        }
    }
}

/// 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))]
#[allow(clippy::struct_excessive_bools)]
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)]
    optname: String,

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

    /// The features prefix in the program output.
    #[clap(short = 'P', default_value = DEFAULT_PREFIX)]
    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`.
#[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)
}

/// Parse the command-line arguments, handle `--help`, `--version`, and `--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)
    }
}

/// Obtain the features of a program, display them as requested.
#[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)?,
    })
}