Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add working-directory attribute #2438

Merged
merged 12 commits into from
Nov 27, 2024
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1875,6 +1875,7 @@ Recipes, `mod` statements, and aliases may be annotated with attributes that cha
| `[script(COMMAND)]`<sup>1.32.0</sup> | recipe | Execute recipe as a script interpreted by `COMMAND`. See [script recipes](#script-recipes) for more details. |
| `[unix]`<sup>1.8.0</sup> | recipe | Enable recipe on Unixes. (Includes MacOS). |
| `[windows]`<sup>1.8.0</sup> | recipe | Enable recipe on Windows. |
| `[working-directory('bar')]`<sup>1.37.0</sup> | recipe | Set the working directory for the recipe, relative to the default working directory. |

A recipe can have multiple attributes, either on multiple lines:

Expand Down Expand Up @@ -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 Directory<sup>1.37.0</sup>

`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 Recipes<sup>1.17.0</sup>

`just` normally executes all recipes unless there is an error. The `[confirm]`
Expand Down
9 changes: 7 additions & 2 deletions src/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ pub(crate) enum Attribute<'src> {
Script(Option<Interpreter<'src>>),
Unix,
Windows,
WorkingDirectory(StringLiteral<'src>),
}

impl AttributeDiscriminant {
fn argument_range(self) -> RangeInclusive<usize> {
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
Expand Down Expand Up @@ -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())
}
})
}

Expand All @@ -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)
Expand Down
21 changes: 17 additions & 4 deletions src/recipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,24 @@ impl<'src, D> Recipe<'src, D> {
}

fn working_directory<'a>(&'a self, context: &'a ExecutionContext) -> Option<PathBuf> {
if self.change_directory() {
Some(context.working_directory())
} else {
None
if !self.change_directory() {
return None;
}

let working_dir = self
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is better implemented as a for loop:

let working_directory = context.working_directory();

for attribute in self.attributes {
  if let Attribute::WorkingDirectory(dir) = attribute {
    return Some(working_directory.join(dir.cooked);
  }
}

Some(working_directory)

Also note the use of cooked, which is the contents of the string literal after processing escape sequences.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is better implemented as a for loop

Yup, the suggested for loop reads better and is more concise. Just curious, is there any other reason you suggested this refactor?

Also note the use of cooked, which is the contents of the string literal after processing escape sequences.

Good to know - thanks for the info!

.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 {
Expand Down
67 changes: 67 additions & 0 deletions tests/working_directory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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')]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate working directory attributes should be an error.

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();
}