diff --git a/src/find/matchers/mod.rs b/src/find/matchers/mod.rs index 94a81e61..d1f3d837 100644 --- a/src/find/matchers/mod.rs +++ b/src/find/matchers/mod.rs @@ -447,6 +447,68 @@ fn get_or_create_file(path: &str) -> Result> { Ok(file) } +/// A "global option" (GNU find's term) applies to the whole command instead +/// of a specific test, so unlike a test it takes effect no matter where in +/// the expression it appears; `-maxdepth` after `-type f`, for instance, +/// still limits the depth of every path find visits, not just descendants of +/// paths matching `-type f`. GNU find warns when one appears after a test, as +/// implemented by `warn_if_global_option_follows_test` below. +fn is_global_option(argument: &str) -> bool { + matches!( + argument, + "-d" | "-depth" + | "-files0-from" + | "-help" + | "--help" + | "-maxdepth" + | "-mindepth" + | "-mount" + | "-xdev" + | "-noleaf" + | "-sorted" + | "-version" + | "--version" + ) +} + +fn is_testing_criterion(argument: &str) -> bool { + !is_global_option(argument) + && !matches!( + argument, + "-daystart" + | "-follow" + | "-regextype" + | "-warn" + | "-nowarn" + | "-not" + | "!" + | "-and" + | "-a" + | "-or" + | "-o" + | "," + | "(" + | ")" + ) +} + +fn is_warnable_global_option(argument: &str) -> bool { + is_global_option(argument) && !matches!(argument, "-help" | "--help" | "-version" | "--version") +} + +fn warn_if_global_option_follows_test(argument: &str, config: &Config) { + if config.warnings_enabled && is_warnable_global_option(argument) { + if let Some(previous) = &config.last_non_option { + eprintln!( + "find: warning: you have specified the global option {argument} after the argument \ + {previous}, but global options are not positional, i.e., {argument} \ + affects tests specified before it as well as those specified after it. Please \ + specify global options before other arguments." + ); + } + } +} + /// The main "translate command-line args into a matcher" function. Will call /// itself recursively if it encounters an opening bracket. A successful return /// consists of a tuple containing the new index into the args array to use (if @@ -468,7 +530,9 @@ fn build_matcher_tree( let mut i = arg_index; let mut invert_next_matcher = false; while i < args.len() { - let possible_submatcher = match args[i] { + let argument = args[i]; + warn_if_global_option_follows_test(argument, config); + let possible_submatcher = match argument { "-print" => Some(Printer::new(PrintDelimiter::Newline, None).into_box()), "-print0" => Some(Printer::new(PrintDelimiter::Null, None).into_box()), "-printf" => { @@ -914,17 +978,14 @@ fn build_matcher_tree( Some(TrueMatcher.into_box()) } "-d" | "-depth" => { - // TODO add warning if it appears after actual testing criterion config.depth_first = true; Some(TrueMatcher.into_box()) } "-mount" | "-xdev" => { - // TODO add warning if it appears after actual testing criterion config.same_file_system = true; Some(TrueMatcher.into_box()) } "-sorted" => { - // TODO add warning if it appears after actual testing criterion config.sorted_output = true; Some(TrueMatcher.into_box()) } @@ -952,6 +1013,14 @@ fn build_matcher_tree( config.version_requested = true; None } + "-warn" => { + config.warnings_enabled = true; + Some(TrueMatcher.into_box()) + } + "-nowarn" => { + config.warnings_enabled = false; + Some(TrueMatcher.into_box()) + } "-files0-from" => { if i >= args.len() - 1 { return Err(From::from(format!("missing argument to {}", args[i]))); @@ -1013,6 +1082,9 @@ fn build_matcher_tree( top_level_matcher.new_and_condition(submatcher); } } + if is_testing_criterion(argument) { + config.last_non_option = Some(argument.to_string()); + } } if expecting_bracket { return Err(From::from( diff --git a/src/find/mod.rs b/src/find/mod.rs index ff047c82..b6e09a2f 100644 --- a/src/find/mod.rs +++ b/src/find/mod.rs @@ -8,8 +8,8 @@ pub mod matchers; use matchers::{Follow, WalkEntry}; use std::cell::RefCell; +use std::env; use std::error::Error; -#[cfg(unix)] use std::io::IsTerminal; use std::io::{self, stderr, stdout, BufRead, BufReader, Write}; use std::path::PathBuf; @@ -27,6 +27,8 @@ pub struct Config { version_requested: bool, today_start: bool, no_leaf_dirs: bool, + warnings_enabled: bool, + last_non_option: Option, follow: Follow, files0_argument: Option, /// Whether the expression uses -ok or -okdir, which prompt on stderr and @@ -49,6 +51,8 @@ impl Default for Config { // and this configuration field will exist as // a compatibility item for GNU findutils. no_leaf_dirs: false, + warnings_enabled: false, + last_non_option: None, follow: Follow::Never, files0_argument: None, // This option exclusively for -files0-from argument. interactive_exec: false, @@ -206,7 +210,11 @@ impl Iterator for Files0Paths { fn parse_args(args: &[&str]) -> Result> { let mut paths = vec![]; let mut i = 0; - let mut config = Config::default(); + let mut config = Config { + warnings_enabled: std::io::stdin().is_terminal() + && env::var_os("POSIXLY_CORRECT").is_none(), + ..Config::default() + }; while i < args.len() { match args[i] { @@ -412,6 +420,7 @@ Positional options: -sorted Sort directory contents by name (non-standard) -regextype type Set regex syntax (default: emacs) -files0-from file Read starting points from file, NUL-separated + -warn / -nowarn Turn warning messages on or off Tests: -name pattern Base name matches shell pattern diff --git a/tests/test_find.rs b/tests/test_find.rs index 5af44364..f6cc8482 100644 --- a/tests/test_find.rs +++ b/tests/test_find.rs @@ -97,6 +97,36 @@ fn two_matchers_one_matches() { .no_output(); } +#[test] +fn warns_when_global_option_follows_test() { + ucmd() + .args(&["-warn", "-type", "d", "-maxdepth", "0"]) + .succeeds() + .stdout_is(".\n") + .stderr_is( + "find: warning: you have specified the global option -maxdepth after the argument \ + -type, but global options are not positional, i.e., -maxdepth affects tests \ + specified before it as well as those specified after it. Please specify global \ + options before other arguments.\n", + ); + + ucmd() + .args(&["-warn", "-type", "d", "-nowarn", "-maxdepth", "0"]) + .succeeds() + .stdout_only(".\n") + .no_stderr(); +} + +#[test] +fn does_not_warn_for_help_or_version_after_test() { + for option in ["-help", "--help", "-version", "--version"] { + ucmd() + .args(&["-warn", "-type", "d", option]) + .succeeds() + .no_stderr(); + } +} + #[test] fn multiple_matcher_success() { ucmd()