Allow matching search path arguments (#1475)

This commit is contained in:
Greg Shuflin 2023-01-11 23:06:17 -08:00 committed by GitHub
parent b29f72ceb5
commit d499227dcb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 190 additions and 22 deletions

View File

@ -2153,6 +2153,29 @@ $ just foo/build
$ just foo/ $ 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 ### Hiding `justfile`s
`just` looks for `justfile`s named `justfile` and `.justfile`, which can be used to keep a `justfile` hidden. `just` looks for `justfile`s named `justfile` and `.justfile`, which can be used to keep a `justfile` hidden.

View File

@ -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 { for (name, value) in positional.overrides {
overrides.insert(name.clone(), value.clone()); overrides.insert(name.clone(), value.clone());

View File

@ -11,6 +11,8 @@ pub(crate) enum ConfigError {
message message
))] ))]
Internal { message: String }, Internal { message: String },
#[snafu(display("Conflicting path arguments: `{}` and `{}`", seen, conflicting))]
ConflictingSearchDirArgs { seen: String, conflicting: String },
#[snafu(display( #[snafu(display(
"Path-prefixed recipes may not be used with `--working-directory` or `--justfile`." "Path-prefixed recipes may not be used with `--working-directory` or `--justfile`."
))] ))]

View File

@ -36,41 +36,75 @@ pub struct Positional {
pub arguments: Vec<String>, pub arguments: Vec<String>,
} }
#[derive(Copy, Clone)]
enum ProcessingStep {
Overrides,
SearchDir,
Arguments,
}
impl Positional { impl Positional {
pub fn from_values<'values>(values: Option<impl IntoIterator<Item = &'values str>>) -> Self { pub(crate) fn from_values<'values>(
values: Option<impl IntoIterator<Item = &'values str>>,
) -> Result<Self, ConfigError> {
let mut overrides = Vec::new(); let mut overrides = Vec::new();
let mut search_directory = None; let mut search_directory = None;
let mut arguments = Vec::new(); let mut arguments = Vec::new();
let mut processing_step = ProcessingStep::Overrides;
if let Some(values) = values { if let Some(values) = values {
for value in values { let mut values = values.into_iter().peekable();
if search_directory.is_none() && arguments.is_empty() { while let Some(value) = values.peek() {
if let Some(o) = Self::override_from_value(value) { let value = *value;
overrides.push(o); match processing_step {
} else if value == "." || value == ".." { ProcessingStep::Overrides => {
search_directory = Some(value.to_owned()); if let Some(o) = Self::override_from_value(value) {
} else if let Some(i) = value.rfind('/') { overrides.push(o);
let (dir, tail) = value.split_at(i + 1); values.next();
} else {
search_directory = Some(dir.to_owned()); processing_step = ProcessingStep::SearchDir;
if !tail.is_empty() {
arguments.push(tail.to_owned());
} }
} else {
arguments.push(value.to_owned());
} }
} else { ProcessingStep::SearchDir => {
arguments.push(value.to_owned()); 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, overrides,
search_directory, search_directory,
arguments, arguments,
} })
} }
/// Parse an override from a value of the form `NAME=.*`. /// Parse an override from a value of the form `NAME=.*`.
@ -107,7 +141,7 @@ mod tests {
#[test] #[test]
fn $name() { fn $name() {
assert_eq! ( assert_eq! (
Positional::from_values(Some($vals.iter().cloned())), Positional::from_values(Some($vals.iter().cloned())).unwrap(),
Positional { Positional {
overrides: $overrides overrides: $overrides
.iter() .iter()
@ -225,4 +259,35 @@ mod tests {
search_directory: None, search_directory: None,
arguments: ["a", "a=b"], 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/");
}
} }

View File

@ -72,6 +72,7 @@ mod recursion_limit;
mod regexes; mod regexes;
mod run; mod run;
mod search; mod search;
mod search_arguments;
mod shadowing_parameters; mod shadowing_parameters;
mod shebang; mod shebang;
mod shell; mod shell;

77
tests/search_arguments.rs Normal file
View File

@ -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"
);
}