makers 0.7.0

a POSIX-compatible make implemented in Rust
use std::env;
use std::ffi::OsString;
use std::iter;
use std::path::PathBuf;

use structopt::StructOpt;

#[derive(StructOpt, Debug, PartialEq, Eq, Clone)]
#[structopt(author, about)]
#[allow(clippy::struct_excessive_bools)]
pub struct Args {
    /// Cause environment variables, including those with null values, to override macro
    /// assignments within makefiles.
    #[structopt(short, long)]
    pub environment_overrides: bool,

    /// Specify a different makefile (or '-' for standard input).
    ///
    /// The argument makefile is a pathname of a description file, which is also referred
    /// to as the makefile. A pathname of '-' shall denote the standard input. There can
    /// be multiple instances of this option, and they shall be processed in the order
    /// specified. The effect of specifying the same option-argument more than once is
    /// unspecified.
    #[structopt(
        short = "f",
        long = "file",
        visible_alias = "makefile",
        number_of_values = 1,
        parse(from_os_str)
    )]
    pub makefile: Vec<PathBuf>,

    /// Ignore error codes returned by invoked commands.
    ///
    /// This mode is the same as if the special target .IGNORE were specified without
    /// prerequisites.
    #[structopt(short, long)]
    pub ignore_errors: bool,

    /// Continue to update other targets that do not depend on the current target if a
    /// non-ignored error occurs while executing the commands to bring a target
    /// up-to-date.
    #[structopt(
        short,
        long,
        overrides_with = "keep-going",
        overrides_with = "no-keep-going"
    )]
    pub keep_going: bool,

    /// Write commands that would be executed on standard output, but do not execute them
    /// (but execute lines starting with '+').
    ///
    /// However, lines with a <plus-sign> ( '+' ) prefix shall be executed. In this mode,
    /// lines with an at-sign ( '@' ) character prefix shall be written to standard
    /// output.
    #[structopt(
        short = "n",
        long,
        visible_alias = "just-print",
        visible_alias = "recon"
    )]
    pub dry_run: bool,

    /// Write to standard output the complete set of macro definitions and target
    /// descriptions.
    ///
    /// The output format is unspecified.
    #[structopt(short, long, visible_alias = "print-data-base")]
    pub print_everything: bool,

    /// Return a zero exit value if the target file is up-to-date; otherwise, return an
    /// exit value of 1.
    ///
    /// Targets shall not be updated if this option is specified. However, a makefile
    /// command line (associated with the targets) with a <plus-sign> ( '+' ) prefix
    /// shall be executed.
    #[structopt(short, long)]
    pub question: bool,

    /// Clear the suffix list and do not use the built-in rules.
    #[structopt(short = "r", long)]
    pub no_builtin_rules: bool,

    /// Terminate make if an error occurs while executing the commands to bring a target
    /// up-to-date (default behavior, required by POSIX to be also a flag for some
    /// reason).
    ///
    /// This shall be the default and the opposite of -k.
    #[structopt(
        short = "S",
        long,
        visible_alias = "stop",
        hidden = true,
        overrides_with = "keep-going",
        overrides_with = "no-keep-going"
    )]
    pub no_keep_going: bool,

    /// Do not write makefile command lines or touch messages to standard output before
    /// executing.
    ///
    /// This mode shall be the same as if the special target .SILENT were specified
    /// without prerequisites.
    #[structopt(short, long, visible_alias = "quiet")]
    pub silent: bool,

    /// Update the modification time of each target as though a touch target had been
    /// executed.
    ///
    /// Targets that have prerequisites but no commands, or that are already up-to-date,
    /// shall not be touched in this manner. Write messages to standard output for each
    /// target file indicating the name of the file and that it was touched. Normally,
    /// the makefile command lines associated with each target are not executed. However,
    /// a command line with a <plus-sign> ( '+' ) prefix shall be executed.
    #[structopt(short, long)]
    pub touch: bool,

    /// Target names or macro definitions.
    ///
    /// If no target is specified, while make is processing the makefiles, the first
    /// target that make encounters that is not a special target or an inference rule
    /// shall be used.
    pub targets_or_macros: Vec<String>,
}

impl Args {
    fn from_given_args_and_given_env(
        mut args: impl Iterator<Item = OsString>,
        env_makeflags: String,
    ) -> Self {
        // POSIX spec says "Any options specified in the MAKEFLAGS environment variable
        // shall be evaluated before any options specified on the make utility command
        // line."
        // TODO allow macro definitions in MAKEFLAGS
        // POSIX says we have to accept
        // > The characters are option letters without the leading <hyphen-minus>
        // > characters or <blank> separation used on a make utility command line.
        let makeflags_given = !env_makeflags.is_empty();
        let makeflags_spaces = env_makeflags.contains(' ');
        let makeflags_leading_dash = env_makeflags.starts_with('-');
        let makeflags_has_equals = env_makeflags.starts_with('=');
        let makeflags_obviously_full =
            makeflags_spaces || makeflags_leading_dash || makeflags_has_equals;
        let env_makeflags = if makeflags_given && !makeflags_obviously_full {
            format!("-{}", env_makeflags)
        } else {
            env_makeflags
        };
        let env_makeflags = env_makeflags.split_whitespace().map(OsString::from);
        // per the structopt docs, the first argument will be used as the binary name,
        // so we need to make sure it goes in before MAKEFLAGS
        let arg_0 = args.next().unwrap_or_else(|| env!("CARGO_PKG_NAME").into());

        let args = iter::once(arg_0)
            .chain(env_makeflags.into_iter())
            .chain(args);

        Self::from_iter(args)
    }

    pub fn from_env_and_args() -> Self {
        let env_makeflags = env::var("MAKEFLAGS").unwrap_or_default();
        let args = env::args_os();
        Self::from_given_args_and_given_env(args, env_makeflags)
    }

    #[cfg(test)]
    pub fn empty() -> Self {
        let env_makeflags = String::new();
        let args = vec![OsString::from("makers")];
        Self::from_given_args_and_given_env(args.into_iter(), env_makeflags)
    }

    pub fn targets(&self) -> impl Iterator<Item = &str> {
        self.targets_or_macros
            .iter()
            .map(AsRef::as_ref)
            .filter(|x: &&str| !x.contains('='))
    }

    pub fn macros(&self) -> impl Iterator<Item = &str> {
        self.targets_or_macros
            .iter()
            .map(AsRef::as_ref)
            .filter(|x: &&str| x.contains('='))
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn no_args() {
        let args: Vec<OsString> = vec!["makers".into()];
        let args = Args::from_given_args_and_given_env(args.into_iter(), String::new());
        assert_eq!(
            args,
            Args {
                environment_overrides: false,
                makefile: vec![],
                ignore_errors: false,
                keep_going: false,
                dry_run: false,
                print_everything: false,
                question: false,
                no_builtin_rules: false,
                no_keep_going: false,
                silent: false,
                touch: false,
                targets_or_macros: vec![],
            }
        );
    }

    #[test]
    fn kitchen_sink_args() {
        let args = "makers -eiknpqrstf foo -f bruh bar baz=yeet";
        let args = Args::from_given_args_and_given_env(
            args.split_whitespace().map(OsString::from),
            String::new(),
        );
        assert_eq!(
            args,
            Args {
                environment_overrides: true,
                makefile: vec!["foo".into(), "bruh".into()],
                ignore_errors: true,
                keep_going: true,
                dry_run: true,
                print_everything: true,
                question: true,
                no_builtin_rules: true,
                no_keep_going: false,
                silent: true,
                touch: true,
                targets_or_macros: vec!["bar".into(), "baz=yeet".into()],
            }
        );
    }

    #[test]
    fn keep_going_wrestling() {
        let args = "makers -kSkSkSSSkSkkSk -k -S -k -k -S -S -k";
        let args = Args::from_given_args_and_given_env(
            args.split_whitespace().map(OsString::from),
            String::new(),
        );
        assert_eq!(
            args,
            Args {
                environment_overrides: false,
                makefile: vec![],
                ignore_errors: false,
                keep_going: true,
                dry_run: false,
                print_everything: false,
                question: false,
                no_builtin_rules: false,
                no_keep_going: false,
                silent: false,
                touch: false,
                targets_or_macros: vec![],
            }
        );
    }

    #[test]
    fn keep_going_wrestling_alt() {
        let args = "makers -kSkSkSSSkSkkSk -k -S -k -k -S -S -kS";
        let args = Args::from_given_args_and_given_env(
            args.split_whitespace().map(OsString::from),
            String::new(),
        );
        assert_eq!(
            args,
            Args {
                environment_overrides: false,
                makefile: vec![],
                ignore_errors: false,
                keep_going: false,
                dry_run: false,
                print_everything: false,
                question: false,
                no_builtin_rules: false,
                no_keep_going: true,
                silent: false,
                touch: false,
                targets_or_macros: vec![],
            }
        );
    }

    #[test]
    fn makeflags_lazy() {
        let args = "makers";
        let makeflags = "eiknp";
        let args = Args::from_given_args_and_given_env(iter::once(args.into()), makeflags.into());
        assert_eq!(
            args,
            Args {
                environment_overrides: true,
                makefile: vec![],
                ignore_errors: true,
                keep_going: true,
                dry_run: true,
                print_everything: true,
                question: false,
                no_builtin_rules: false,
                no_keep_going: false,
                silent: false,
                touch: false,
                targets_or_macros: vec![],
            }
        );
    }

    #[test]
    fn makeflags_full() {
        let args = "makers";
        let makeflags = "-i -knp";
        let args = Args::from_given_args_and_given_env(iter::once(args.into()), makeflags.into());
        assert_eq!(
            args,
            Args {
                environment_overrides: false,
                makefile: vec![],
                ignore_errors: true,
                keep_going: true,
                dry_run: true,
                print_everything: true,
                question: false,
                no_builtin_rules: false,
                no_keep_going: false,
                silent: false,
                touch: false,
                targets_or_macros: vec![],
            }
        );
    }

    #[test]
    fn nightmare() {
        let makeflags = "-nrs -k foo=bar";
        let args = "makers -eipqtSf foo -f bruh bar baz=yeet";
        let args = Args::from_given_args_and_given_env(
            args.split_whitespace().map(OsString::from),
            makeflags.into(),
        );
        assert_eq!(
            args,
            Args {
                environment_overrides: true,
                makefile: vec!["foo".into(), "bruh".into()],
                ignore_errors: true,
                keep_going: false,
                dry_run: true,
                print_everything: true,
                question: true,
                no_builtin_rules: true,
                no_keep_going: true,
                silent: true,
                touch: true,
                targets_or_macros: vec!["foo=bar".into(), "bar".into(), "baz=yeet".into()],
            }
        );
    }
}