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
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -884,8 +884,8 @@ $ just bar
/subdir
```

You can override working directory with `set working-directory := '…'`, whose value
is relative to the default working directory.
You can override the working directory for all recipes with
`set working-directory := '…'`:

```just
set working-directory := 'bar'
Expand All @@ -901,6 +901,26 @@ $ just foo
/home/bob/bar
```

You can override the working directory for a specific recipe with the
`working-directory` attribute<sup>master</sup>:

```just
[working-directory: 'bar']
@foo:
pwd
```

```console
$ pwd
/home/bob
$ just foo
/home/bob/bar
```

The argument to the `working-directory` setting or `working-directory`
attribute may be absolute or relative. If it is relative it is interpreted
relative to the default working directory.

### Aliases

Aliases allow recipes to be invoked on the command line with alternative names:
Expand Down Expand Up @@ -1972,6 +1992,7 @@ change their behavior.
| `[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(PATH)]`<sup>master</sup> | recipe | Set recipe working directory. `PATH` may be relative or absolute. If relative, it is interpreted relative to the default working directory. |

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

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 @@ -117,7 +121,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
5 changes: 5 additions & 0 deletions src/compile_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ impl Display for CompileError<'_> {
consist of tabs or spaces, but not both",
ShowWhitespace(whitespace)
),
NoCdAndWorkingDirectoryAttribute { recipe } => write!(
f,
"Recipe `{recipe}` has both `[no-cd]` and `[working-directory]` attributes"
),
ParameterFollowsVariadicParameter { parameter } => {
write!(f, "Parameter `{parameter}` follows variadic parameter")
}
Expand Down Expand Up @@ -286,6 +290,7 @@ impl Display for CompileError<'_> {
UnterminatedBacktick => write!(f, "Unterminated backtick"),
UnterminatedInterpolation => write!(f, "Unterminated interpolation"),
UnterminatedString => write!(f, "Unterminated string"),
UnterminatedString => write!(f, "Unterminated string"),
}
}
}
3 changes: 3 additions & 0 deletions src/compile_error_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ pub(crate) enum CompileErrorKind<'src> {
MixedLeadingWhitespace {
whitespace: &'src str,
},
NoCdAndWorkingDirectoryAttribute {
recipe: &'src str,
},
ParameterFollowsVariadicParameter {
parameter: &'src str,
},
Expand Down
16 changes: 16 additions & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,22 @@ impl<'run, 'src> Parser<'run, 'src> {
}));
}

let working_directory = attributes
.iter()
.any(|attribute| matches!(attribute, Attribute::WorkingDirectory(_)));

if working_directory {
for attribute in &attributes {
if let Attribute::NoCd = attribute {
return Err(
name.error(CompileErrorKind::NoCdAndWorkingDirectoryAttribute {
recipe: name.lexeme(),
}),
);
}
}
}

let private = name.lexeme().starts_with('_') || attributes.contains(&Attribute::Private);

let mut doc = doc.map(ToOwned::to_owned);
Expand Down
16 changes: 12 additions & 4 deletions src/recipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,19 @@ 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_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)
}

fn no_quiet(&self) -> bool {
Expand Down
22 changes: 21 additions & 1 deletion tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pub(crate) struct Test {
pub(crate) args: Vec<String>,
pub(crate) current_dir: PathBuf,
pub(crate) env: BTreeMap<String, String>,
pub(crate) expected_files: BTreeMap<PathBuf, Vec<u8>>,
pub(crate) justfile: Option<String>,
pub(crate) shell: bool,
pub(crate) status: i32,
Expand All @@ -71,6 +72,7 @@ impl Test {
args: Vec::new(),
current_dir: PathBuf::new(),
env: BTreeMap::new(),
expected_files: BTreeMap::new(),
justfile: Some(String::new()),
shell: true,
status: EXIT_SUCCESS,
Expand Down Expand Up @@ -98,7 +100,7 @@ impl Test {
}

pub(crate) fn create_dir(self, path: impl AsRef<Path>) -> Self {
fs::create_dir_all(self.tempdir.path().join(path.as_ref())).unwrap();
fs::create_dir_all(self.tempdir.path().join(path)).unwrap();
self
}

Expand Down Expand Up @@ -195,6 +197,14 @@ impl Test {
fs::write(path, content).unwrap();
self
}

pub(crate) fn expect_file(mut self, path: impl AsRef<Path>, content: impl AsRef<[u8]>) -> Self {
let path = path.as_ref();
self
.expected_files
.insert(path.into(), content.as_ref().into());
self
}
}

impl Test {
Expand Down Expand Up @@ -283,6 +293,16 @@ impl Test {
panic!("Output mismatch.");
}

for (path, expected) in &self.expected_files {
let actual = fs::read(self.tempdir.path().join(path)).unwrap();
assert_eq!(
actual,
expected.as_slice(),
"mismatch for expected file at path {}",
path.display(),
);
}

if self.test_round_trip && self.status == EXIT_SUCCESS {
self.round_trip();
}
Expand Down
77 changes: 77 additions & 0 deletions tests/working_directory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,3 +331,80 @@ file := shell('cat file.txt')
.stdout("FILE\n")
.run();
}

#[test]
fn attribute_duplicate() {
Test::new()
.justfile(
"
[working-directory('bar')]
[working-directory('baz')]
foo:
",
)
.stderr(
"error: Recipe attribute `working-directory` first used on line 1 is duplicated on line 2
——▶ justfile:2:2
2 │ [working-directory('baz')]
│ ^^^^^^^^^^^^^^^^^
",
)
.status(EXIT_FAILURE)
.run();
}

#[test]
fn attribute() {
Test::new()
.justfile(
"
[working-directory('foo')]
@qux:
echo baz > bar
",
)
.create_dir("foo")
.expect_file("foo/bar", "baz\n")
.run();
}

#[test]
fn attribute_with_nocd_is_forbidden() {
Test::new()
.justfile(
"
[working-directory('foo')]
[no-cd]
bar:
",
)
.stderr(
"
error: Recipe `bar` has both `[no-cd]` and `[working-directory]` attributes
——▶ justfile:3:1
3 │ bar:
│ ^^^
",
)
.status(EXIT_FAILURE)
.run();
}

#[test]
fn setting_and_attribute() {
Test::new()
.justfile(
"
set working-directory := 'foo'

[working-directory('bar')]
@baz:
echo bob > fred
",
)
.create_dir("foo/bar")
.expect_file("foo/bar/fred", "bob\n")
.run();
}
Loading