use std::env; use std::ffi::OsString; use std::iter; use std::ops::AddAssign; use std::path::PathBuf; use clap::Parser; #[derive(clap::Parser, Debug, PartialEq, Eq, Clone)] #[clap(author, about)] #[allow(clippy::struct_excessive_bools)] pub struct Args { /// Cause environment variables, including those with null values, to override macro /// assignments within makefiles. #[clap(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. #[clap( short = 'f', long = "file", visible_alias = "makefile", number_of_values = 1, value_parser )] 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. #[clap(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. #[clap( 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. #[clap( 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. #[clap(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. #[clap(short, long)] pub question: bool, /// Clear the suffix list and do not use the built-in rules. #[clap(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. #[clap( short = 'S', long, visible_alias = "stop", hide = 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. #[clap(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. #[clap(short, long)] pub touch: bool, /// Change to the given directory before running. #[cfg(feature = "full")] #[clap(short = 'C', long, value_parser)] pub directory: Option, /// Print the working directory when starting. // TODO implement // TODO automatically with -C or recursion or decide that this is a bad GNU feature #[cfg(feature = "full")] #[clap( short = 'w', long, overrides_with = "print_directory", overrides_with = "no_print_directory" )] pub print_directory: bool, /// Do not print the working directory when starting, even when running with -C or recursively. #[cfg(feature = "full")] #[clap( long, overrides_with = "print_directory", overrides_with = "no_print_directory" )] pub no_print_directory: 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, ) -> 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." // 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.contains('='); 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).chain(args); Self::parse_from(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 { self.targets_or_macros .iter() .map(AsRef::as_ref) .filter(|x: &&str| !x.contains('=')) } pub fn macros(&self) -> impl Iterator { self.targets_or_macros .iter() .map(AsRef::as_ref) .filter(|x: &&str| x.contains('=')) } pub fn makeflags(&self) -> String { let mut flags = String::new(); if self.environment_overrides { flags.push('e'); } if self.ignore_errors { flags.push('i'); } if self.keep_going { flags.push('k'); } if self.dry_run { flags.push('n'); } if self.print_everything { flags.push('p'); } if self.question { flags.push('q'); } if self.no_builtin_rules { flags.push('r'); } if self.no_keep_going { flags.push('S'); } if self.silent { flags.push('s'); } if self.touch { flags.push('t'); } // TODO decide whether or not print_directory should go here too let macros = self .targets_or_macros .iter() .map(|x| x.as_ref()) .filter(|x: &&str| x.contains('=')) .collect::>() .join(" "); let mut result = String::new(); if !flags.is_empty() && !macros.is_empty() { result.push('-'); } result.add_assign(&flags); // TODO consider -- to separate flags from macros - GNU does it but it would require // gnarly splicing to not override recursive macros if !flags.is_empty() && !macros.is_empty() { result += " "; } result.add_assign(¯os); result } } #[cfg(test)] mod test { use super::*; #[test] fn clap_validate() { use clap::CommandFactory; Args::command().debug_assert(); } #[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, #[cfg(feature = "full")] directory: None, #[cfg(feature = "full")] print_directory: false, #[cfg(feature = "full")] no_print_directory: 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, #[cfg(feature = "full")] directory: None, #[cfg(feature = "full")] print_directory: false, #[cfg(feature = "full")] no_print_directory: false, 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, #[cfg(feature = "full")] directory: None, #[cfg(feature = "full")] print_directory: false, #[cfg(feature = "full")] no_print_directory: 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, #[cfg(feature = "full")] directory: None, #[cfg(feature = "full")] print_directory: false, #[cfg(feature = "full")] no_print_directory: false, targets_or_macros: vec![], } ); } #[test] #[cfg(feature = "full")] fn print_directory_wrestling() { let args = "makers -w --no-print-directory -w -w --no-print-directory --no-print-directory -w --no-print-directory"; 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: false, silent: false, touch: false, #[cfg(feature = "full")] directory: None, #[cfg(feature = "full")] print_directory: false, #[cfg(feature = "full")] no_print_directory: true, 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, #[cfg(feature = "full")] directory: None, #[cfg(feature = "full")] print_directory: false, #[cfg(feature = "full")] no_print_directory: 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, #[cfg(feature = "full")] directory: None, #[cfg(feature = "full")] print_directory: false, #[cfg(feature = "full")] no_print_directory: 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, #[cfg(feature = "full")] directory: None, #[cfg(feature = "full")] print_directory: false, #[cfg(feature = "full")] no_print_directory: false, targets_or_macros: vec!["bar".into(), "baz=yeet".into(), "foo=bar".into()], } ); assert_eq!(args.makeflags(), "-einpqrSst -- baz=yeet foo=bar"); } }