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)] 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, /// 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 ( '+' ) 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 ( '+' ) 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 ( '+' ) 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, } impl Args { fn from_given_args_and_given_env(mut args: impl Iterator, env_makeflags: String) -> Args { // 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 // > characters or 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(|x| OsString::from(x)); // 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 arg0 = args.next().unwrap_or_else(|| env!("CARGO_PKG_NAME").into()); let args = iter::once(arg0) .chain(env_makeflags.into_iter()) .chain(args); let args = args.collect::>(); dbg!(&args); let args = args.into_iter(); Args::from_iter(args) } pub fn from_env_and_args() -> Args { let env_makeflags = env::var("MAKEFLAGS").unwrap_or_default(); let args = env::args_os(); Self::from_given_args_and_given_env(args, env_makeflags) } pub fn targets(&self) -> impl Iterator { self.targets_or_macros.iter().filter(|x| !x.contains('=')) } pub fn macros(&self) -> impl Iterator { self.targets_or_macros.iter().filter(|x| x.contains('=')) } } #[cfg(test)] mod test { use super::*; #[test] fn no_args() { let args: Vec = 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()], }); } }