diff --git a/README.md b/README.md
index 3175aeefe2..06786e247b 100644
--- a/README.md
+++ b/README.md
@@ -950,6 +950,12 @@ $ just bar
/subdir
```
+Use `set no-cd`master to make all recipes in the current module
+default to the same behavior.
+
+`set no-cd` and `set working-directory` can be overridden on a per-recipe basis
+with the `[no-cd]` and `[working-directory]` attributes.
+
You can override the working directory for all recipes with
`set working-directory := '…'`:
@@ -1043,6 +1049,7 @@ foo:
| `ignore-comments` | boolean | `false` | Ignore recipe lines beginning with `#`. |
| `no-exit-message`1.39.0 | boolean | `false` | Don't print exit messages if recipes fail. |
| `lazy`1.47.0 | boolean | `false` | Don't evaluate unused variables. |
+| `no-cd`master | boolean | `false` | Don't change directory when executing recipes by recipe attribute. |
| `positional-arguments` | boolean | `false` | Pass positional arguments. |
| `quiet` | boolean | `false` | Disable echoing recipe lines before executing. |
| `script-interpreter`1.33.0 | `[COMMAND, ARGS…]` | `['sh', '-eu']` | Set command used to invoke recipes with empty `[script]` attribute. |
diff --git a/src/analyzer.rs b/src/analyzer.rs
index 2b1fa9dbc0..1125d29247 100644
--- a/src/analyzer.rs
+++ b/src/analyzer.rs
@@ -417,6 +417,24 @@ impl<'run, 'src> Analyzer<'run, 'src> {
}));
}
+ if let Some(second) = Keyword::from_lexeme(set.name.lexeme()) {
+ let first = match second {
+ Keyword::NoCd => Keyword::WorkingDirectory,
+ Keyword::WorkingDirectory => Keyword::NoCd,
+ _ => {
+ return Ok(());
+ }
+ };
+
+ if let Some(conflict) = self.sets.get(first.lexeme()) {
+ return Err(set.name.error(NoCdAndWorkingDirectorySetting {
+ first,
+ first_line: conflict.name.line,
+ second,
+ }));
+ }
+ }
+
Ok(())
}
diff --git a/src/compile_error.rs b/src/compile_error.rs
index 823a682784..1dd15ed4f0 100644
--- a/src/compile_error.rs
+++ b/src/compile_error.rs
@@ -254,6 +254,17 @@ impl Display for CompileError<'_> {
f,
"recipe `{recipe}` has both `[no-cd]` and `[working-directory]` attributes"
),
+ NoCdAndWorkingDirectorySetting {
+ first,
+ first_line,
+ second,
+ } => write!(
+ f,
+ "`{}` set on line {} is incompatible with `{}`",
+ first.lexeme(),
+ first_line.ordinal(),
+ second.lexeme()
+ ),
OptionNameContainsEqualSign { parameter } => {
write!(
f,
diff --git a/src/compile_error_kind.rs b/src/compile_error_kind.rs
index a65bf953bb..456778ba78 100644
--- a/src/compile_error_kind.rs
+++ b/src/compile_error_kind.rs
@@ -114,6 +114,11 @@ pub(crate) enum CompileErrorKind<'src> {
NoCdAndWorkingDirectoryAttribute {
recipe: &'src str,
},
+ NoCdAndWorkingDirectorySetting {
+ first: Keyword,
+ first_line: usize,
+ second: Keyword,
+ },
OptionNameContainsEqualSign {
parameter: String,
},
diff --git a/src/evaluator.rs b/src/evaluator.rs
index b950f816cf..cf2512306d 100644
--- a/src/evaluator.rs
+++ b/src/evaluator.rs
@@ -101,6 +101,9 @@ impl<'src, 'run> Evaluator<'src, 'run> {
Setting::Lazy(value) => {
settings.lazy = value;
}
+ Setting::NoCd(value) => {
+ settings.no_cd = value;
+ }
Setting::NoExitMessage(value) => {
settings.no_exit_message = value;
}
diff --git a/src/keyword.rs b/src/keyword.rs
index b2b1d52967..7b75d0a3a3 100644
--- a/src/keyword.rs
+++ b/src/keyword.rs
@@ -24,6 +24,7 @@ pub(crate) enum Keyword {
Import,
Lazy,
Mod,
+ NoCd,
NoExitMessage,
PositionalArguments,
Quiet,
diff --git a/src/node.rs b/src/node.rs
index a60a80f661..188353014a 100644
--- a/src/node.rs
+++ b/src/node.rs
@@ -283,6 +283,7 @@ impl<'src> Node<'src> for Set<'src> {
| Setting::Guards(value)
| Setting::IgnoreComments(value)
| Setting::Lazy(value)
+ | Setting::NoCd(value)
| Setting::NoExitMessage(value)
| Setting::PositionalArguments(value)
| Setting::Quiet(value)
diff --git a/src/parser.rs b/src/parser.rs
index cdcbb17c9e..ec2456126d 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -1416,6 +1416,7 @@ impl<'run, 'src> Parser<'run, 'src> {
Keyword::Guards => Some(Setting::Guards(self.parse_set_bool()?)),
Keyword::IgnoreComments => Some(Setting::IgnoreComments(self.parse_set_bool()?)),
Keyword::Lazy => Some(Setting::Lazy(self.parse_set_bool()?)),
+ Keyword::NoCd => Some(Setting::NoCd(self.parse_set_bool()?)),
Keyword::NoExitMessage => Some(Setting::NoExitMessage(self.parse_set_bool()?)),
Keyword::PositionalArguments => Some(Setting::PositionalArguments(self.parse_set_bool()?)),
Keyword::Quiet => Some(Setting::Quiet(self.parse_set_bool()?)),
@@ -2650,6 +2651,12 @@ mod tests {
tree: (justfile (set quiet false)),
}
+ test! {
+ name: set_no_cd,
+ text: "set no-cd := true",
+ tree: (justfile (set no_cd true)),
+ }
+
test! {
name: set_positional_arguments_false,
text: "set positional-arguments := false",
diff --git a/src/recipe.rs b/src/recipe.rs
index 6e0eda3580..f897558d1a 100644
--- a/src/recipe.rs
+++ b/src/recipe.rs
@@ -169,8 +169,19 @@ impl<'src> Recipe<'src> {
.contains(AttributeDiscriminant::PositionalArguments)
}
- pub(crate) fn change_directory(&self) -> bool {
- !self.attributes.contains(AttributeDiscriminant::NoCd)
+ pub(crate) fn change_directory(&self, settings: &Settings) -> bool {
+ if self
+ .attributes
+ .contains(AttributeDiscriminant::WorkingDirectory)
+ {
+ return true;
+ }
+
+ if self.attributes.contains(AttributeDiscriminant::NoCd) {
+ return false;
+ }
+
+ !settings.no_cd
}
fn print_exit_message(&self, settings: &Settings) -> bool {
@@ -186,7 +197,7 @@ impl<'src> Recipe<'src> {
}
fn working_directory<'a>(&'a self, context: &'a ExecutionContext) -> Option {
- if !self.change_directory() {
+ if !self.change_directory(&context.module.settings) {
return None;
}
diff --git a/src/setting.rs b/src/setting.rs
index d0de410cbb..58c8ed10fb 100644
--- a/src/setting.rs
+++ b/src/setting.rs
@@ -14,6 +14,7 @@ pub(crate) enum Setting<'src> {
Guards(bool),
IgnoreComments(bool),
Lazy(bool),
+ NoCd(bool),
NoExitMessage(bool),
PositionalArguments(bool),
Quiet(bool),
@@ -39,6 +40,7 @@ impl<'src> Setting<'src> {
| Self::Guards(value)
| Self::IgnoreComments(value)
| Self::Lazy(value)
+ | Self::NoCd(value)
| Self::NoExitMessage(value)
| Self::PositionalArguments(value)
| Self::Quiet(value)
@@ -88,6 +90,7 @@ impl Display for Setting<'_> {
| Self::Guards(value)
| Self::IgnoreComments(value)
| Self::Lazy(value)
+ | Self::NoCd(value)
| Self::NoExitMessage(value)
| Self::PositionalArguments(value)
| Self::Quiet(value)
diff --git a/src/settings.rs b/src/settings.rs
index bdea9adda8..460061b160 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -19,6 +19,7 @@ pub(crate) struct Settings {
pub(crate) guards: bool,
pub(crate) ignore_comments: bool,
pub(crate) lazy: bool,
+ pub(crate) no_cd: bool,
pub(crate) no_exit_message: bool,
pub(crate) positional_arguments: bool,
pub(crate) quiet: bool,
diff --git a/tests/json.rs b/tests/json.rs
index 3702c28d94..ad7ae55e84 100644
--- a/tests/json.rs
+++ b/tests/json.rs
@@ -94,6 +94,7 @@ struct Settings<'a> {
guards: bool,
ignore_comments: bool,
lazy: bool,
+ no_cd: bool,
no_exit_message: bool,
positional_arguments: bool,
quiet: bool,
diff --git a/tests/modules.rs b/tests/modules.rs
index bf76b4f9df..af746534a0 100644
--- a/tests/modules.rs
+++ b/tests/modules.rs
@@ -215,6 +215,28 @@ foo:
.failure();
}
+#[test]
+fn submodules_do_not_inherit_no_cd_setting() {
+ Test::new()
+ .write(
+ "foo/mod.just",
+ "bar:
+ @cat data.txt
+",
+ )
+ .write("foo/data.txt", "MODULE\n")
+ .justfile(
+ "
+ set no-cd := true
+
+ mod foo
+ ",
+ )
+ .args(["foo", "bar"])
+ .stdout("MODULE\n")
+ .success();
+}
+
#[test]
fn modules_conflict_with_recipes() {
Test::new()
diff --git a/tests/no_cd.rs b/tests/no_cd.rs
index d389359b46..1df3336193 100644
--- a/tests/no_cd.rs
+++ b/tests/no_cd.rs
@@ -41,3 +41,64 @@ fn shebang() {
.stdout("hello")
.success();
}
+
+#[test]
+fn setting_applies_to_recipes() {
+ Test::new()
+ .justfile(
+ "
+ set no-cd := true
+
+ foo:
+ cat bar
+ ",
+ )
+ .current_dir("child")
+ .tree(tree! {
+ bar: "root",
+ child: {
+ bar: "child",
+ }
+ })
+ .stderr("cat bar\n")
+ .stdout("child")
+ .success();
+}
+
+#[test]
+fn working_directory_attribute_overrides_setting() {
+ Test::new()
+ .justfile(
+ "
+ set no-cd := true
+
+ [working-directory('workspace')]
+ foo:
+ cat data.txt
+ ",
+ )
+ .write("workspace/data.txt", "WORKSPACE")
+ .stderr("cat data.txt\n")
+ .stdout("WORKSPACE")
+ .success();
+}
+
+#[test]
+fn evaluator_paths_ignore_setting() {
+ Test::new()
+ .justfile(
+ "
+ set no-cd := true
+
+ file := `cat data.txt`
+
+ @foo:
+ echo {{file}}
+ ",
+ )
+ .current_dir("inv")
+ .write("data.txt", "MODULE")
+ .write("inv/data.txt", "INVOCATION")
+ .stdout("MODULE\n")
+ .success();
+}
diff --git a/tests/settings.rs b/tests/settings.rs
index 647a8e6927..80eaa662a2 100644
--- a/tests/settings.rs
+++ b/tests/settings.rs
@@ -261,6 +261,27 @@ fn variable() {
.success();
}
+#[test]
+fn no_cd_setting_conflicts_with_working_directory_setting() {
+ Test::new()
+ .justfile(
+ "
+ set no-cd := true
+ set working-directory := 'bar'
+ ",
+ )
+ .stderr(
+ "
+ error: `no-cd` set on line 1 is incompatible with `working-directory`
+ ——▶ justfile:2:5
+ │
+ 2 │ set working-directory := 'bar'
+ │ ^^^^^^^^^^^^^^^^^
+ ",
+ )
+ .failure();
+}
+
#[test]
fn unused_non_const_assignments() {
Test::new()
diff --git a/tests/working_directory.rs b/tests/working_directory.rs
index f5473df806..ff94bb36be 100644
--- a/tests/working_directory.rs
+++ b/tests/working_directory.rs
@@ -235,6 +235,27 @@ fn no_cd_overrides_setting() {
.success();
}
+#[test]
+fn working_directory_setting_conflicts_with_no_cd_setting() {
+ Test::new()
+ .justfile(
+ "
+ set working-directory := 'bar'
+ set no-cd := true
+ ",
+ )
+ .stderr(
+ "
+ error: `working-directory` set on line 1 is incompatible with `no-cd`
+ ——▶ justfile:2:5
+ │
+ 2 │ set no-cd := true
+ │ ^^^^^
+ ",
+ )
+ .failure();
+}
+
#[test]
fn working_dir_in_submodule_is_relative_to_module_path() {
Test::new()