Repository: casey/just
Pull request #1400 (2022-11-03 06:32:45)

Add [no-cd] attribute


Patch:
diff --git a/README.md b/README.md
index 7600c834..cfd67723 100644
--- a/README.md
+++ b/README.md
@@ -1203,13 +1203,14 @@ These functions can fail, for example if a path does not have an extension, whic
 
 Recipes may be annotated with attributes that change their behavior.
 
-| Name                | Description                                       |
-| ------------------- | ------------------------------------------------- |
-| `[no-exit-message]` | Don't print an error message when a recipe fails. |
-| `[linux]`           | Enable recipe on Linux.                           |
-| `[macos]`           | Enable recipe on MacOS.                           |
-| `[unix]`            | Enable recipe on Unixes.                          |
-| `[windows]`         | Enable recipe on Windows.                         |
+| Name                | Description                                     |
+| ------------------- | ----------------------------------------------- |
+| `[no-cd]`           | Don't change directory before executing recipe. |
+| `[no-exit-message]` | Don't print an error message if recipe fails.   |
+| `[linux]`           | Enable recipe on Linux.                         |
+| `[macos]`           | Enable recipe on MacOS.                         |
+| `[unix]`            | Enable recipe on Unixes.                        |
+| `[windows]`         | Enable recipe on Windows.                       |
 
 #### Enabling and Disabling Recipes
 
@@ -1235,6 +1236,27 @@ run:
   main.exe
 ```
 
+#### Disabling Changing Directory<sup>master</sup>
+
+`just` normally executes recipes with the current directory set to the
+directory that contains the `justfile`. This can be disabled using the
+`[no-cd]` attribute. This can be used to create recipes which use paths
+relative to the invocation directory, or which operate on the current
+directory.
+
+For exmaple, this `commit` recipe:
+
+```make
+[no-cd]
+commit file:
+  git add {{file}}
+  git commit
+```
+
+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`.
+
 ### Command Evaluation Using Backticks
 
 Backticks can be used to store the result of commands:
diff --git a/src/attribute.rs b/src/attribute.rs
index 375f2bb6..c205f340 100644
--- a/src/attribute.rs
+++ b/src/attribute.rs
@@ -8,6 +8,7 @@ use super::*;
 pub(crate) enum Attribute {
   Linux,
   Macos,
+  NoCd,
   NoExitMessage,
   Unix,
   Windows,
diff --git a/src/platform.rs b/src/platform.rs
index 965fac21..9fd9b161 100644
--- a/src/platform.rs
+++ b/src/platform.rs
@@ -6,13 +6,15 @@ pub(crate) struct Platform;
 impl PlatformInterface for Platform {
   fn make_shebang_command(
     path: &Path,
-    working_directory: &Path,
+    working_directory: Option<&Path>,
     _shebang: Shebang,
   ) -> Result<Command, OutputError> {
     // shebang scripts can be executed directly on unix
     let mut cmd = Command::new(path);
 
-    cmd.current_dir(working_directory);
+    if let Some(working_directory) = working_directory {
+      cmd.current_dir(working_directory);
+    }
 
     Ok(cmd)
   }
@@ -48,7 +50,7 @@ impl PlatformInterface for Platform {
 impl PlatformInterface for Platform {
   fn make_shebang_command(
     path: &Path,
-    working_directory: &Path,
+    working_directory: Option<&Path>,
     shebang: Shebang,
   ) -> Result<Command, OutputError> {
     use std::borrow::Cow;
@@ -57,7 +59,9 @@ impl PlatformInterface for Platform {
     let command = if shebang.interpreter.contains('/') {
       // …translate path to the interpreter from unix style to windows style.
       let mut cygpath = Command::new("cygpath");
-      cygpath.current_dir(working_directory);
+      if let Some(working_directory) = working_directory {
+        cygpath.current_dir(working_directory);
+      }
       cygpath.arg("--windows");
       cygpath.arg(shebang.interpreter);
 
@@ -69,7 +73,9 @@ impl PlatformInterface for Platform {
 
     let mut cmd = Command::new(command.as_ref());
 
-    cmd.current_dir(working_directory);
+    if let Some(working_directory) = working_directory {
+      cmd.current_dir(working_directory);
+    }
 
     if let Some(argument) = shebang.argument {
       cmd.arg(argument);
diff --git a/src/platform_interface.rs b/src/platform_interface.rs
index fcab222b..0028e09d 100644
--- a/src/platform_interface.rs
+++ b/src/platform_interface.rs
@@ -5,7 +5,7 @@ pub(crate) trait PlatformInterface {
   /// shebang line `shebang`
   fn make_shebang_command(
     path: &Path,
-    working_directory: &Path,
+    working_directory: Option<&Path>,
     shebang: Shebang,
   ) -> Result<Command, OutputError>;
 
diff --git a/src/recipe.rs b/src/recipe.rs
index 7da2a614..5fda4496 100644
--- a/src/recipe.rs
+++ b/src/recipe.rs
@@ -66,6 +66,10 @@ impl<'src, D> Recipe<'src, D> {
     !self.private
   }
 
+  pub(crate) fn change_directory(&self) -> bool {
+    !self.attributes.contains(&Attribute::NoCd)
+  }
+
   pub(crate) fn enabled(&self) -> bool {
     let windows = self.attributes.contains(&Attribute::Windows);
     let linux = self.attributes.contains(&Attribute::Linux);
@@ -190,7 +194,9 @@ impl<'src, D> Recipe<'src, D> {
 
       let mut cmd = context.settings.shell_command(config);
 
-      cmd.current_dir(&context.search.working_directory);
+      if self.change_directory() {
+        cmd.current_dir(&context.search.working_directory);
+      }
 
       cmd.arg(command);
 
@@ -322,13 +328,19 @@ impl<'src, D> Recipe<'src, D> {
     })?;
 
     // create a command to run the script
-    let mut command =
-      Platform::make_shebang_command(&path, &context.search.working_directory, shebang).map_err(
-        |output_error| Error::Cygpath {
-          recipe: self.name(),
-          output_error,
-        },
-      )?;
+    let mut command = Platform::make_shebang_command(
+      &path,
+      if self.change_directory() {
+        Some(&context.search.working_directory)
+      } else {
+        None
+      },
+      shebang,
+    )
+    .map_err(|output_error| Error::Cygpath {
+      recipe: self.name(),
+      output_error,
+    })?;
 
     if context.settings.positional_arguments {
       command.args(positional);


Test patch:
diff --git a/tests/lib.rs b/tests/lib.rs
index 7a999fe2..e6c7ed49 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -60,6 +60,7 @@ mod json;
 mod line_prefixes;
 mod misc;
 mod multibyte_char;
+mod no_cd;
 mod no_exit_message;
 mod os_attributes;
 mod parser;
diff --git a/tests/no_cd.rs b/tests/no_cd.rs
new file mode 100644
index 00000000..ecdc006b
--- /dev/null
+++ b/tests/no_cd.rs
@@ -0,0 +1,43 @@
+use super::*;
+
+#[test]
+fn linewise() {
+  Test::new()
+    .justfile(
+      "
+      [no-cd]
+      foo:
+        cat bar
+    ",
+    )
+    .current_dir("foo")
+    .tree(tree! {
+      foo: {
+        bar: "hello",
+      }
+    })
+    .stderr("cat bar\n")
+    .stdout("hello")
+    .run();
+}
+
+#[test]
+fn shebang() {
+  Test::new()
+    .justfile(
+      "
+      [no-cd]
+      foo:
+        #!/bin/sh
+        cat bar
+    ",
+    )
+    .current_dir("foo")
+    .tree(tree! {
+      foo: {
+        bar: "hello",
+      }
+    })
+    .stdout("hello")
+    .run();
+}