From d499227dcba0583acf0f0a8a7045794a7a730096 Mon Sep 17 00:00:00 2001 From: Greg Shuflin Date: Wed, 11 Jan 2023 23:06:17 -0800 Subject: [PATCH] Allow matching search path arguments (#1475) --- README.md | 23 ++++++++ src/config.rs | 2 +- src/config_error.rs | 2 + src/positional.rs | 107 ++++++++++++++++++++++++++++++-------- tests/lib.rs | 1 + tests/search_arguments.rs | 77 +++++++++++++++++++++++++++ 6 files changed, 190 insertions(+), 22 deletions(-) create mode 100644 tests/search_arguments.rs diff --git a/README.md b/README.md index f5b9584..725e975 100644 --- a/README.md +++ b/README.md @@ -2153,6 +2153,29 @@ $ just foo/build $ just foo/ ``` +Additional recipes after the first are sought in the same `justfile`. For +example, the following are both equivalent: + +```sh +$ just foo/a b +$ (cd foo && just a b) +``` + +And will both invoke recipes `a` and `b` in `foo/justfile`. + +For consistency, it possible to use path prefixes for all recipes: + +```sh +$ just foo/a foo/b +``` + +But they must match: + +```sh +$ just foo/a bar/b +error: Conflicting path arguments: `foo/` and `bar/` +``` + ### Hiding `justfile`s `just` looks for `justfile`s named `justfile` and `.justfile`, which can be used to keep a `justfile` hidden. diff --git a/src/config.rs b/src/config.rs index ce9438d..8c31b4e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -433,7 +433,7 @@ impl Config { } } - let positional = Positional::from_values(matches.values_of(arg::ARGUMENTS)); + let positional = Positional::from_values(matches.values_of(arg::ARGUMENTS))?; for (name, value) in positional.overrides { overrides.insert(name.clone(), value.clone()); diff --git a/src/config_error.rs b/src/config_error.rs index 36a2274..0fd62b3 100644 --- a/src/config_error.rs +++ b/src/config_error.rs @@ -11,6 +11,8 @@ pub(crate) enum ConfigError { message ))] Internal { message: String }, + #[snafu(display("Conflicting path arguments: `{}` and `{}`", seen, conflicting))] + ConflictingSearchDirArgs { seen: String, conflicting: String }, #[snafu(display( "Path-prefixed recipes may not be used with `--working-directory` or `--justfile`." ))] diff --git a/src/positional.rs b/src/positional.rs index a74a474..c5643cc 100644 --- a/src/positional.rs +++ b/src/positional.rs @@ -36,41 +36,75 @@ pub struct Positional { pub arguments: Vec, } +#[derive(Copy, Clone)] +enum ProcessingStep { + Overrides, + SearchDir, + Arguments, +} + impl Positional { - pub fn from_values<'values>(values: Option>) -> Self { + pub(crate) fn from_values<'values>( + values: Option>, + ) -> Result { let mut overrides = Vec::new(); let mut search_directory = None; let mut arguments = Vec::new(); + let mut processing_step = ProcessingStep::Overrides; + if let Some(values) = values { - for value in values { - if search_directory.is_none() && arguments.is_empty() { - if let Some(o) = Self::override_from_value(value) { - overrides.push(o); - } else if value == "." || value == ".." { - search_directory = Some(value.to_owned()); - } else if let Some(i) = value.rfind('/') { - let (dir, tail) = value.split_at(i + 1); - - search_directory = Some(dir.to_owned()); - - if !tail.is_empty() { - arguments.push(tail.to_owned()); + let mut values = values.into_iter().peekable(); + while let Some(value) = values.peek() { + let value = *value; + match processing_step { + ProcessingStep::Overrides => { + if let Some(o) = Self::override_from_value(value) { + overrides.push(o); + values.next(); + } else { + processing_step = ProcessingStep::SearchDir; } - } else { - arguments.push(value.to_owned()); } - } else { - arguments.push(value.to_owned()); + ProcessingStep::SearchDir => { + if value == "." || value == ".." { + search_directory = Some(value.to_owned()); + values.next(); + } else if let Some(i) = value.rfind('/') { + let (dir, tail) = value.split_at(i + 1); + + if let Some(ref seen) = search_directory { + if seen != dir { + return Err(ConfigError::ConflictingSearchDirArgs { + seen: seen.clone(), + conflicting: dir.into(), + }); + } + } else { + search_directory = Some(dir.to_owned()); + } + + if !tail.is_empty() { + arguments.push(tail.to_owned()); + } + values.next(); + } else { + processing_step = ProcessingStep::Arguments; + } + } + ProcessingStep::Arguments => { + arguments.push(value.to_owned()); + values.next(); + } } } } - Self { + Ok(Self { overrides, search_directory, arguments, - } + }) } /// Parse an override from a value of the form `NAME=.*`. @@ -107,7 +141,7 @@ mod tests { #[test] fn $name() { assert_eq! ( - Positional::from_values(Some($vals.iter().cloned())), + Positional::from_values(Some($vals.iter().cloned())).unwrap(), Positional { overrides: $overrides .iter() @@ -225,4 +259,35 @@ mod tests { search_directory: None, arguments: ["a", "a=b"], } + + test! { + name: search_dir_and_recipe_only, + values: ["some/path/recipe_a"], + overrides: [], + search_directory: Some("some/path/"), + arguments: ["recipe_a"], + } + + test! { + name: multiple_same_valued_search_directories, + values: ["some/path/recipe_a", "some/path/recipe_b"], + overrides: [], + search_directory: Some("some/path/"), + arguments: ["recipe_a", "recipe_b"], + } + + #[test] + fn invalid_multiple_search_paths() { + let err = Positional::from_values(Some( + [ + "some/path/recipe_a", + "some/path/recipe_b", + "other/path/recipe_c", + ] + .iter() + .copied(), + )) + .unwrap_err(); + assert_matches!(err, ConfigError::ConflictingSearchDirArgs { seen, conflicting } if seen == "some/path/" && conflicting == "other/path/"); + } } diff --git a/tests/lib.rs b/tests/lib.rs index cceacc8..29ddf2c 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -72,6 +72,7 @@ mod recursion_limit; mod regexes; mod run; mod search; +mod search_arguments; mod shadowing_parameters; mod shebang; mod shell; diff --git a/tests/search_arguments.rs b/tests/search_arguments.rs new file mode 100644 index 0000000..e842f6d --- /dev/null +++ b/tests/search_arguments.rs @@ -0,0 +1,77 @@ +use super::*; + +#[test] +fn same_path_argument() { + let justfile_contents = unindent( + r#" + recipe_a: + echo "A" + + recipe_b: + echo "B" + "#, + ); + let tmp = temptree! { + subdir: { + justfile: justfile_contents + } + }; + + for arg_list in [ + ["subdir/recipe_a", "recipe_b"], + ["subdir/recipe_a", "subdir/recipe_b"], + ] { + let mut command = Command::new(executable_path("just")); + command.current_dir(tmp.path()); + + for arg in arg_list { + command.arg(arg); + } + + let output = command.output().unwrap(); + + let stdout = String::from_utf8(output.stdout).unwrap(); + + assert!(output.status.success()); + assert_eq!(stdout, "A\nB\n"); + } +} + +#[test] +fn different_path_arguments() { + let justfile_contents1 = unindent( + r#" + recipe_a: + echo "A" + + "#, + ); + let justfile_contents2 = unindent( + r#" + recipe_b: + echo "B" + "#, + ); + let tmp = temptree! { + subdir: { + justfile: justfile_contents1 + }, + subdir2: { + justfile: justfile_contents2 + } + }; + + let output = Command::new(executable_path("just")) + .current_dir(tmp.path()) + .arg("subdir/recipe_a") + .arg("subdir2/recipe_b") + .output() + .unwrap(); + + let stderr = String::from_utf8(output.stderr).unwrap(); + + assert_eq!( + stderr, + "error: Conflicting path arguments: `subdir/` and `subdir2/`\n" + ); +}