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/
|
||||
```
|
||||
|
||||
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.
|
||||
|
@ -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());
|
||||
|
@ -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`."
|
||||
))]
|
||||
|
@ -36,41 +36,75 @@ pub struct Positional {
|
||||
pub arguments: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum ProcessingStep {
|
||||
Overrides,
|
||||
SearchDir,
|
||||
Arguments,
|
||||
}
|
||||
|
||||
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 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/");
|
||||
}
|
||||
}
|
||||
|
@ -72,6 +72,7 @@ mod recursion_limit;
|
||||
mod regexes;
|
||||
mod run;
|
||||
mod search;
|
||||
mod search_arguments;
|
||||
mod shadowing_parameters;
|
||||
mod shebang;
|
||||
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