diff --git a/README.md b/README.md
index 994ec488..6427ed9a 100644
--- a/README.md
+++ b/README.md
@@ -1875,6 +1875,7 @@ Recipes, `mod` statements, and aliases may be annotated with attributes that cha
| `[script(COMMAND)]`1.32.0 | recipe | Execute recipe as a script interpreted by `COMMAND`. See [script recipes](#script-recipes) for more details. |
| `[unix]`1.8.0 | recipe | Enable recipe on Unixes. (Includes MacOS). |
| `[windows]`1.8.0 | recipe | Enable recipe on Windows. |
+| `[working-directory('bar')]`1.37.0 | recipe | Set the working directory for the recipe, relative to the default working directory. |
A recipe can have multiple attributes, either on multiple lines:
@@ -1938,6 +1939,23 @@ Can be used with paths that are relative to the current directory, because
`[no-cd]` prevents `just` from changing the current directory when executing
`commit`.
+#### Changing Working Directory1.37.0
+
+`just` normally executes recipes with the current directory set to the directory
+that contains the `justfile`. The execution directory can be changed with the
+`[working-directory('dir')]` attribute. This can be used to create recipes which
+are executed in a directory relative to the default directory.
+
+For example, this `example` recipe:
+
+```just
+[working-directory('dir')]
+example:
+ echo "$(pwd)"
+```
+
+Which will run in the `dir` directory.
+
#### Requiring Confirmation for Recipes1.17.0
`just` normally executes all recipes unless there is an error. The `[confirm]`
diff --git a/src/attribute.rs b/src/attribute.rs
index ebdc2127..e90aa0b9 100644
--- a/src/attribute.rs
+++ b/src/attribute.rs
@@ -23,13 +23,14 @@ pub(crate) enum Attribute<'src> {
Script(Option>),
Unix,
Windows,
+ WorkingDirectory(StringLiteral<'src>),
}
impl AttributeDiscriminant {
fn argument_range(self) -> RangeInclusive {
match self {
Self::Confirm | Self::Doc => 0..=1,
- Self::Group | Self::Extension => 1..=1,
+ Self::Group | Self::Extension | Self::WorkingDirectory => 1..=1,
Self::Linux
| Self::Macos
| Self::NoCd
@@ -93,6 +94,9 @@ impl<'src> Attribute<'src> {
}),
AttributeDiscriminant::Unix => Self::Unix,
AttributeDiscriminant::Windows => Self::Windows,
+ AttributeDiscriminant::WorkingDirectory => {
+ Self::WorkingDirectory(arguments.into_iter().next().unwrap())
+ }
})
}
@@ -109,7 +113,8 @@ impl<'src> Display for Attribute<'src> {
Self::Confirm(Some(argument))
| Self::Doc(Some(argument))
| Self::Extension(argument)
- | Self::Group(argument) => write!(f, "({argument})")?,
+ | Self::Group(argument)
+ | Self::WorkingDirectory(argument) => write!(f, "({argument})")?,
Self::Script(Some(shell)) => write!(f, "({shell})")?,
Self::Confirm(None)
| Self::Doc(None)
diff --git a/src/recipe.rs b/src/recipe.rs
index 05516225..fdf6b27a 100644
--- a/src/recipe.rs
+++ b/src/recipe.rs
@@ -131,11 +131,24 @@ impl<'src, D> Recipe<'src, D> {
}
fn working_directory<'a>(&'a self, context: &'a ExecutionContext) -> Option {
- if self.change_directory() {
- Some(context.working_directory())
- } else {
- None
+ if !self.change_directory() {
+ return None;
}
+
+ let working_dir = self
+ .attributes
+ .iter()
+ .filter_map(|attribute| match attribute {
+ Attribute::WorkingDirectory(dir) => Some(dir),
+ _ => None,
+ })
+ .last();
+
+ Some(
+ working_dir
+ .map(|dir| context.working_directory().join(dir.raw))
+ .unwrap_or(context.working_directory()),
+ )
}
fn no_quiet(&self) -> bool {
diff --git a/tests/working_directory.rs b/tests/working_directory.rs
index 3396b73e..461a5e24 100644
--- a/tests/working_directory.rs
+++ b/tests/working_directory.rs
@@ -331,3 +331,70 @@ file := shell('cat file.txt')
.stdout("FILE\n")
.run();
}
+
+#[test]
+fn attribute() {
+ Test::new()
+ .justfile(
+ r#"
+ [working-directory('bar')]
+ print1:
+ echo "$(basename "$PWD")"
+
+ [working-directory('bar')]
+ [working-directory('baz')]
+ print2:
+ echo "$(basename "$PWD")"
+
+ [working-directory('bar')]
+ [no-cd]
+ print3:
+ echo "$(basename "$PWD")"
+ "#,
+ )
+ .current_dir("foo")
+ .tree(tree! {
+ foo: {},
+ bar: {},
+ baz: {},
+ })
+ .args(["print1", "print2", "print3"])
+ .stderr(
+ r#"echo "$(basename "$PWD")"
+echo "$(basename "$PWD")"
+echo "$(basename "$PWD")"
+"#,
+ )
+ .stdout("bar\nbaz\nfoo\n")
+ .run();
+}
+
+#[test]
+fn setting_and_attribute() {
+ Test::new()
+ .justfile(
+ r#"
+ set working-directory := 'bar'
+
+ [working-directory('baz')]
+ print1:
+ echo "$(basename "$PWD")"
+ echo "$(basename "$(dirname "$PWD")")"
+ "#,
+ )
+ .current_dir("foo")
+ .tree(tree! {
+ foo: {},
+ bar: {
+ baz: {},
+ },
+ })
+ .args(["print1"])
+ .stderr(
+ r#"echo "$(basename "$PWD")"
+echo "$(basename "$(dirname "$PWD")")"
+"#,
+ )
+ .stdout("baz\nbar\n")
+ .run();
+}