Allow matching search path arguments (#1475)
This commit is contained in:
parent
b29f72ceb5
commit
d499227dcb
23
README.md
23
README.md
@ -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.
|
||||||
|
@ -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());
|
||||||
|
@ -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`."
|
||||||
))]
|
))]
|
||||||
|
@ -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/");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
77
tests/search_arguments.rs
Normal 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"
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user