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()],
}
);
}
}