From 23a53fb50f144d78f1874b85a21b3f544b223f9a Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Tue, 30 Jul 2024 20:50:32 -0400 Subject: [PATCH] Add `working-directory` setting (#2283) --- GRAMMAR.md | 1 + README.md | 1 + src/evaluator.rs | 12 ++++- src/keyword.rs | 1 + src/node.rs | 5 ++- src/parser.rs | 7 +++ src/recipe.rs | 31 ++++++++----- src/setting.rs | 6 ++- src/settings.rs | 4 ++ tests/json.rs | 21 +++++++++ tests/working_directory.rs | 90 ++++++++++++++++++++++++++++++++++++++ 11 files changed, 165 insertions(+), 14 deletions(-) diff --git a/GRAMMAR.md b/GRAMMAR.md index c7752d3f87..683b5a6509 100644 --- a/GRAMMAR.md +++ b/GRAMMAR.md @@ -80,6 +80,7 @@ setting : 'allow-duplicate-recipes' boolean? | 'unstable' boolean? | 'windows-powershell' boolean? | 'windows-shell' ':=' string_list + | 'working-directory' ':=' string boolean : ':=' ('true' | 'false') diff --git a/README.md b/README.md index 8de9796d74..275da4ffcc 100644 --- a/README.md +++ b/README.md @@ -824,6 +824,7 @@ foo: | `unstable`1.31.0 | boolean | `false` | Enable unstable features. | | `windows-powershell` | boolean | `false` | Use PowerShell on Windows as default shell. (Deprecated. Use `windows-shell` instead. | | `windows-shell` | `[COMMAND, ARGSā€¦]` | - | Set the command used to invoke recipes and evaluate backticks. | +| `working-directory`master | string | - | Set the working directory for recipes and backticks, relative to the default working directory. | Boolean settings can be written as: diff --git a/src/evaluator.rs b/src/evaluator.rs index 82260b84a1..191b7120fa 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -239,7 +239,17 @@ impl<'src, 'run> Evaluator<'src, 'run> { let mut cmd = self.context.settings.shell_command(self.context.config); cmd.arg(command); cmd.args(args); - cmd.current_dir(&self.context.search.working_directory); + if let Some(working_directory) = &self.context.settings.working_directory { + cmd.current_dir( + self + .context + .search + .working_directory + .join(working_directory), + ) + } else { + cmd.current_dir(&self.context.search.working_directory) + }; cmd.export( self.context.settings, self.context.dotenv, diff --git a/src/keyword.rs b/src/keyword.rs index 1461c0c952..ff06c39d27 100644 --- a/src/keyword.rs +++ b/src/keyword.rs @@ -30,6 +30,7 @@ pub(crate) enum Keyword { Unstable, WindowsPowershell, WindowsShell, + WorkingDirectory, X, } diff --git a/src/node.rs b/src/node.rs index bd5585df1a..3ccf862d57 100644 --- a/src/node.rs +++ b/src/node.rs @@ -307,7 +307,10 @@ impl<'src> Node<'src> for Set<'src> { set.push_mut(Tree::string(&argument.cooked)); } } - Setting::DotenvFilename(value) | Setting::DotenvPath(value) | Setting::Tempdir(value) => { + Setting::DotenvFilename(value) + | Setting::DotenvPath(value) + | Setting::Tempdir(value) + | Setting::WorkingDirectory(value) => { set.push_mut(Tree::string(&value.cooked)); } } diff --git a/src/parser.rs b/src/parser.rs index d9985a326f..a7b6243e0b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -967,6 +967,7 @@ impl<'run, 'src> Parser<'run, 'src> { Keyword::Shell => Some(Setting::Shell(self.parse_interpreter()?)), Keyword::Tempdir => Some(Setting::Tempdir(self.parse_string_literal()?)), Keyword::WindowsShell => Some(Setting::WindowsShell(self.parse_interpreter()?)), + Keyword::WorkingDirectory => Some(Setting::WorkingDirectory(self.parse_string_literal()?)), _ => None, }; @@ -2146,6 +2147,12 @@ mod tests { tree: (justfile (set windows_powershell false)), } + test! { + name: set_working_directory, + text: "set working-directory := 'foo'", + tree: (justfile (set working_directory "foo")), + } + test! { name: conditional, text: "a := if b == c { d } else { e }", diff --git a/src/recipe.rs b/src/recipe.rs index 7ae9641aee..0d40307e8a 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -136,15 +136,21 @@ impl<'src, D> Recipe<'src, D> { !self.attributes.contains(&Attribute::NoExitMessage) } - fn working_directory<'a>(&'a self, search: &'a Search) -> Option<&Path> { - if self.change_directory() { - Some(if self.submodule_depth > 0 { - &self.working_directory - } else { - &search.working_directory - }) + fn working_directory<'a>(&'a self, context: &'a ExecutionContext) -> Option { + if !self.change_directory() { + return None; + } + + let base = if self.submodule_depth > 0 { + &self.working_directory + } else { + &context.search.working_directory + }; + + if let Some(setting) = &context.settings.working_directory { + Some(base.join(setting)) } else { - None + Some(base.into()) } } @@ -265,7 +271,7 @@ impl<'src, D> Recipe<'src, D> { let mut cmd = context.settings.shell_command(config); - if let Some(working_directory) = self.working_directory(context.search) { + if let Some(working_directory) = self.working_directory(context) { cmd.current_dir(working_directory); } @@ -408,8 +414,11 @@ impl<'src, D> Recipe<'src, D> { io_error: error, })?; - let mut command = - executor.command(&path, self.name(), self.working_directory(context.search))?; + let mut command = executor.command( + &path, + self.name(), + self.working_directory(context).as_deref(), + )?; if self.takes_positional_arguments(context.settings) { command.args(positional); diff --git a/src/setting.rs b/src/setting.rs index 342cd878e3..2b68713580 100644 --- a/src/setting.rs +++ b/src/setting.rs @@ -19,6 +19,7 @@ pub(crate) enum Setting<'src> { Unstable(bool), WindowsPowerShell(bool), WindowsShell(Interpreter<'src>), + WorkingDirectory(StringLiteral<'src>), } impl<'src> Display for Setting<'src> { @@ -38,7 +39,10 @@ impl<'src> Display for Setting<'src> { Self::ScriptInterpreter(shell) | Self::Shell(shell) | Self::WindowsShell(shell) => { write!(f, "[{shell}]") } - Self::DotenvFilename(value) | Self::DotenvPath(value) | Self::Tempdir(value) => { + Self::DotenvFilename(value) + | Self::DotenvPath(value) + | Self::Tempdir(value) + | Self::WorkingDirectory(value) => { write!(f, "{value}") } } diff --git a/src/settings.rs b/src/settings.rs index 7ea64dba6a..795ddebd1f 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -25,6 +25,7 @@ pub(crate) struct Settings<'src> { pub(crate) unstable: bool, pub(crate) windows_powershell: bool, pub(crate) windows_shell: Option>, + pub(crate) working_directory: Option, } impl<'src> Settings<'src> { @@ -84,6 +85,9 @@ impl<'src> Settings<'src> { Setting::Tempdir(tempdir) => { settings.tempdir = Some(tempdir.cooked); } + Setting::WorkingDirectory(working_directory) => { + settings.working_directory = Some(working_directory.cooked.into()); + } } } diff --git a/tests/json.rs b/tests/json.rs index 19135aca58..961bdfa4ff 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -62,6 +62,7 @@ fn alias() { "unstable": false, "windows_powershell": false, "windows_shell": null, + "working_directory" : null, }, "unexports": [], "warnings": [], @@ -105,6 +106,7 @@ fn assignment() { "unstable": false, "windows_powershell": false, "windows_shell": null, + "working_directory" : null, }, "unexports": [], "warnings": [], @@ -162,6 +164,7 @@ fn body() { "unstable": false, "windows_powershell": false, "windows_shell": null, + "working_directory" : null, }, "unexports": [], "warnings": [], @@ -231,6 +234,7 @@ fn dependencies() { "unstable": false, "windows_powershell": false, "windows_shell": null, + "working_directory" : null, }, "unexports": [], "warnings": [], @@ -338,6 +342,7 @@ fn dependency_argument() { "unstable": false, "windows_powershell": false, "windows_shell": null, + "working_directory" : null, }, "unexports": [], "warnings": [], @@ -407,6 +412,7 @@ fn duplicate_recipes() { "unstable": false, "windows_powershell": false, "windows_shell": null, + "working_directory" : null, }, "unexports": [], "warnings": [], @@ -454,6 +460,7 @@ fn duplicate_variables() { "unstable": false, "windows_powershell": false, "windows_shell": null, + "working_directory" : null, }, "unexports": [], "warnings": [], @@ -504,6 +511,7 @@ fn doc_comment() { "unstable": false, "windows_powershell": false, "windows_shell": null, + "working_directory" : null, }, "unexports": [], "warnings": [], @@ -540,6 +548,7 @@ fn empty_justfile() { "unstable": false, "windows_powershell": false, "windows_shell": null, + "working_directory" : null, }, "unexports": [], "warnings": [], @@ -697,6 +706,7 @@ fn parameters() { "unstable": false, "windows_powershell": false, "windows_shell": null, + "working_directory" : null, }, "unexports": [], "warnings": [], @@ -787,6 +797,7 @@ fn priors() { "unstable": false, "windows_powershell": false, "windows_shell": null, + "working_directory" : null, }, "unexports": [], "warnings": [], @@ -837,6 +848,7 @@ fn private() { "unstable": false, "windows_powershell": false, "windows_shell": null, + "working_directory" : null, }, "unexports": [], "warnings": [], @@ -887,6 +899,7 @@ fn quiet() { "unstable": false, "windows_powershell": false, "windows_shell": null, + "working_directory" : null, }, "unexports": [], "warnings": [], @@ -952,6 +965,7 @@ fn settings() { "unstable": false, "windows_powershell": false, "windows_shell": null, + "working_directory" : null, }, "unexports": [], "warnings": [], @@ -1005,6 +1019,7 @@ fn shebang() { "unstable": false, "windows_powershell": false, "windows_shell": null, + "working_directory" : null, }, "unexports": [], "warnings": [], @@ -1055,6 +1070,7 @@ fn simple() { "unstable": false, "windows_powershell": false, "windows_shell": null, + "working_directory" : null, }, "unexports": [], "warnings": [], @@ -1108,6 +1124,7 @@ fn attribute() { "ignore_comments": false, "windows_powershell": false, "windows_shell": null, + "working_directory" : null, }, "unexports": [], "warnings": [], @@ -1176,6 +1193,7 @@ fn module() { "ignore_comments": false, "windows_powershell": false, "windows_shell": null, + "working_directory" : null, }, "unexports": [], "warnings": [], @@ -1199,6 +1217,7 @@ fn module() { "ignore_comments": false, "windows_powershell": false, "windows_shell": null, + "working_directory" : null, }, "unexports": [], "warnings": [], @@ -1269,6 +1288,7 @@ fn module_group() { "ignore_comments": false, "windows_powershell": false, "windows_shell": null, + "working_directory" : null, }, "unexports": [], "warnings": [], @@ -1292,6 +1312,7 @@ fn module_group() { "ignore_comments": false, "windows_powershell": false, "windows_shell": null, + "working_directory" : null, }, "unexports": [], "warnings": [], diff --git a/tests/working_directory.rs b/tests/working_directory.rs index 02296967e6..eb6618c6ec 100644 --- a/tests/working_directory.rs +++ b/tests/working_directory.rs @@ -181,3 +181,93 @@ fn search_dir_parent() -> Result<(), Box> { Ok(()) } + +#[test] +fn setting() { + Test::new() + .justfile( + r#" + set working-directory := 'bar' + + print1: + echo "$(basename "$PWD")" + + [no-cd] + print2: + echo "$(basename "$PWD")" + "#, + ) + .current_dir("foo") + .tree(tree! { + foo: {}, + bar: {} + }) + .args(["print1", "print2"]) + .stderr( + r#"echo "$(basename "$PWD")" +echo "$(basename "$PWD")" +"#, + ) + .stdout("bar\nfoo\n") + .run(); +} + +#[test] +fn no_cd_overrides_setting() { + Test::new() + .justfile( + " + set working-directory := 'bar' + + [no-cd] + foo: + cat bar + ", + ) + .current_dir("foo") + .tree(tree! { + foo: { + bar: "hello", + } + }) + .stderr("cat bar\n") + .stdout("hello") + .run(); +} + +#[test] +fn working_dir_in_submodule_is_relative_to_module_path() { + Test::new() + .write( + "foo/mod.just", + " +set working-directory := 'bar' + +@foo: + cat file.txt +", + ) + .justfile("mod foo") + .write("foo/bar/file.txt", "FILE") + .arg("foo") + .stdout("FILE") + .run(); +} + +#[test] +fn working_dir_applies_to_backticks() { + Test::new() + .justfile( + " + set working-directory := 'foo' + + file := `cat file.txt` + + @foo: + echo {{ file }} + ", + ) + .write("foo/file.txt", "FILE") + .stdout("FILE\n") + .run(); +}