feature-check 1.0.1

Query a program for supported features
Documentation
/*
 * Copyright (c) 2021  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.
 */
use std::env;
use std::process;
use std::str::FromStr;

use expect_exit::ExpectedWithError;

#[macro_use]
extern crate quick_error;

use feature_check::defs;
use feature_check::expr as fexpr;
use feature_check::obtain;

quick_error! {
    #[derive(Debug)]
    enum OptsError {
        InvalidOutputFormat(format: String) {
            display("Unrecognized output format '{}'", format)
        }
    }
}

const USAGE: &str =
    "Usage:\tfeature-check feature-check [-v] [-O optname] [-P prefix] program feature-name
\tfeature-check [-O optname] [-P prefix] program feature-name op version
\tfeature-check [-O optname] [-o json|tsv] [-P prefix] -l program
\tfeature-check -h | --help
\tfeature-check -V | --version";

const FEATURES: [(&str, &str); 4] = [
    ("feature-check", env!("CARGO_PKG_VERSION")),
    ("single", "1.0"),
    ("list", "1.0"),
    ("simple", "1.0"),
];

#[derive(Debug)]
enum OutputFormat {
    Json,
    Tsv,
}

impl OutputFormat {
    const JSON_NAME: &'static str = "json";
    const TSV_NAME: &'static str = "tsv";
}

impl FromStr for OutputFormat {
    type Err = OptsError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            OutputFormat::JSON_NAME => Ok(OutputFormat::Json),
            OutputFormat::TSV_NAME => Ok(OutputFormat::Tsv),
            other => Err(OptsError::InvalidOutputFormat(other.to_string())),
        }
    }
}

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

#[derive(Debug)]
struct MainConfig {
    config: defs::Config,
    output_format: OutputFormat,
    show_version: bool,
}

#[derive(Debug)]
enum MainMode {
    Do(MainConfig),
    Exit(i32),
}

fn parse_args() -> MainMode {
    let args: Vec<String> = env::args().collect();
    let mut optargs = getopts::Options::new();

    optargs.optflag("h", "help", "display program usage output and exit");
    optargs.optflag(
        "l",
        "",
        "list the features supported by the specified program",
    );
    optargs.optopt(
        "O",
        "",
        "specify the query-features option to pass to the program (default: '--features')",
        "optname",
    );
    optargs.optopt(
        "o",
        "",
        "specify the output format for the list of features (default: tsv)",
        "format",
    );
    optargs.optopt(
        "P",
        "",
        "the features prefix in the program output",
        "prefix",
    );
    optargs.optflag("V", "version", "display program version output and exit");
    optargs.optflag(
        "v",
        "",
        "output the feature version when querying a single feature",
    );
    optargs.optflag("", "features", "list supported program features and exit");

    match optargs.parse(&args[1..]) {
        Ok(opts) => {
            match opts.opt_present("V") || opts.opt_present("h") || opts.opt_present("features") {
                true => {
                    if opts.opt_present("V") {
                        let (_, version) = FEATURES
                            .iter()
                            .find(|(name, _)| *name == "feature-check")
                            .unwrap();
                        println!("feature-check {}", version);
                    }
                    if opts.opt_present("h") {
                        println!("{}", optargs.usage(USAGE));
                    }
                    if opts.opt_present("features") {
                        let features: Vec<String> = FEATURES
                            .iter()
                            .map(|(name, version)| format!("{}={}", name, version))
                            .collect();
                        println!("Features: {}", features.join(" "));
                    }
                    MainMode::Exit(0)
                }
                false => match opts.free.get(0) {
                    Some(program) => match opts.free.len() > 1 {
                        true => match opts.opt_present("l") {
                            false => MainMode::Do(MainConfig {
                                config: defs::Config {
                                    option_name: opts
                                        .opt_get_default("O", defs::DEFAULT_OPTION_NAME.to_string())
                                        .unwrap(),
                                    prefix: opts
                                        .opt_get_default("P", defs::DEFAULT_PREFIX.to_string())
                                        .unwrap(),
                                    program: program.clone(),
                                    mode: match opts.free.len() > 2
                                        || opts.free[1].find(' ').is_some()
                                    {
                                        true => defs::Mode::Simple(opts.free[1..].join(" ")),
                                        false => defs::Mode::Single(opts.free[1].clone()),
                                    },
                                },
                                output_format: opts
                                    .opt_get_default("o", OutputFormat::TSV_NAME.to_string())
                                    .unwrap()
                                    .parse()
                                    .or_exit_e_("Invalid output format specified"),
                                show_version: opts.opt_present("v"),
                            }),
                            true => {
                                eprintln!("{}", optargs.usage(USAGE));
                                MainMode::Exit(1)
                            }
                        },
                        false => match opts.opt_present("l") {
                            true => MainMode::Do(MainConfig {
                                config: defs::Config {
                                    option_name: opts
                                        .opt_get_default("O", defs::DEFAULT_OPTION_NAME.to_string())
                                        .unwrap(),
                                    prefix: opts
                                        .opt_get_default("P", defs::DEFAULT_PREFIX.to_string())
                                        .unwrap(),
                                    program: program.clone(),
                                    mode: defs::Mode::List,
                                },
                                output_format: opts
                                    .opt_get_default("o", OutputFormat::TSV_NAME.to_string())
                                    .unwrap()
                                    .parse()
                                    .or_exit_e_("Invalid output format specified"),
                                show_version: opts.opt_present("v"),
                            }),
                            false => {
                                eprintln!("{}", optargs.usage(USAGE));
                                MainMode::Exit(1)
                            }
                        },
                    },
                    None => {
                        eprintln!("{}", optargs.usage(USAGE));
                        MainMode::Exit(1)
                    }
                },
            }
        }
        Err(err) => {
            eprintln!("{}", err);
            eprintln!("{}", optargs.usage(USAGE));
            MainMode::Exit(1)
        }
    }
}

fn run(config: MainConfig) -> i32 {
    match obtain::obtain_features(&config.config)
        .or_exit_e_("Could not obtain the program's list of features")
    {
        defs::Obtained::Failed(_) => 2,
        defs::Obtained::NotSupported => 2,
        defs::Obtained::Features(features) => match config.config.mode {
            defs::Mode::Single(feature) => match features.get(&feature) {
                Some(value) => {
                    if config.show_version {
                        println!("{}", value);
                    }
                    0
                }
                None => 1,
            },
            defs::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"));
                    0
                }
                OutputFormat::Json => {
                    println!("{}", serde_json::to_string_pretty(&features).unwrap());
                    0
                }
            },
            defs::Mode::Simple(expr) => {
                let tree = fexpr::parse_simple(&expr).or_exit_e_("Invalid expression");
                let value = tree
                    .get_value(&features)
                    .or_exit_e_("Could not evaluate the expression");
                match value {
                    fexpr::CalcResult::Bool(true) => 0,
                    fexpr::CalcResult::Bool(false) => 1,
                    other => panic!("parse_simple().get_value() returned {:?}", other),
                }
            }
        },
    }
}

fn main() {
    match parse_args() {
        MainMode::Exit(rcode) => process::exit(rcode),
        MainMode::Do(config) => process::exit(run(config)),
    }
}