nebius/SWE-rebench-V2-PRs huggingface dataset:
Repository: casey/just
Pull request #957 (2021-08-27 23:30:37)

Bump `target` version to 2.0


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 612d2c23..f12bcd50 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -532,9 +532,9 @@ dependencies = [
 
 [[package]]
 name = "target"
-version = "1.1.0"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d1e4fbc9c74e1521bc75f7780d2546587e2ade1822f4c5107ce6e262162a2a8c"
+checksum = "ba852e71502340e2eaf2fa51f9b3ec6aa25750da1aa65771491c69d67789b05c"
 
 [[package]]
 name = "tempfile"
diff --git a/Cargo.toml b/Cargo.toml
index f5641336..a9b7d981 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,7 +30,7 @@ libc          = "0.2.0"
 log           = "0.4.4"
 snafu         = "0.6.0"
 strum_macros  = "0.21.1"
-target        = "1.0.0"
+target        = "2.0.0"
 tempfile      = "3.0.0"
 typed-arena   = "2.0.1"
 unicode-width = "0.1.0"
diff --git a/src/function.rs b/src/function.rs
index 80394c11..70c619e9 100644
--- a/src/function.rs
+++ b/src/function.rs
@@ -179,7 +179,7 @@ fn os(_context: &FunctionContext) -> Result<String, String> {
 }
 
 fn os_family(_context: &FunctionContext) -> Result<String, String> {
-  Ok(target::os_family().to_owned())
+  Ok(target::family().to_owned())
 }
 
 fn parent_directory(_context: &FunctionContext, path: &str) -> Result<String, String> {


Test patch:
diff --git a/tests/functions.rs b/tests/functions.rs
index dc8be3ee..62cf0d02 100644
--- a/tests/functions.rs
+++ b/tests/functions.rs
@@ -6,8 +6,8 @@ test! {
 foo:
   echo {{arch()}} {{os()}} {{os_family()}}
 "#,
-  stdout:   format!("{} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
-  stderr:   format!("echo {} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
+  stdout:   format!("{} {} {}\n", target::arch(), target::os(), target::family()).as_str(),
+  stderr:   format!("echo {} {} {}\n", target::arch(), target::os(), target::family()).as_str(),
 }
 
 test! {
@@ -20,8 +20,8 @@ f := os_family()
 foo:
   echo {{a}} {{o}} {{f}}
 "#,
-  stdout:   format!("{} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
-  stderr:   format!("echo {} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
+  stdout:   format!("{} {} {}\n", target::arch(), target::os(), target::family()).as_str(),
+  stderr:   format!("echo {} {} {}\n", target::arch(), target::os(), target::family()).as_str(),
 }
 
 #[cfg(not(windows))]
@@ -249,8 +249,8 @@ test! {
 foo a=arch() o=os() f=os_family():
   echo {{a}} {{o}} {{f}}
 "#,
-  stdout:   format!("{} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
-  stderr:   format!("echo {} {} {}\n", target::arch(), target::os(), target::os_family()).as_str(),
+  stdout:   format!("{} {} {}\n", target::arch(), target::os(), target::family()).as_str(),
+  stderr:   format!("echo {} {} {}\n", target::arch(), target::os(), target::family()).as_str(),
 }
 
 test! {
Repository: casey/just
Pull request #1022 (2021-11-08 19:15:36)

Add `quote` function for escaping strings
`quote(s)` surrounds `s` with single quotes and replaces all single
quotes in `s` with `'\''`, serving to escape special characters for most
shells, including Bourne shell descendants.


Patch:
diff --git a/README.adoc b/README.adoc
index 9a405cbc..5496a32c 100644
--- a/README.adoc
+++ b/README.adoc
@@ -888,6 +888,7 @@ The executable is at: /bin/just
 ==== String Manipulation
 
 - `lowercase(s)` - Convert `s` to lowercase.
+- `quote(s)` - Replace all single quotes with `'\''` and prepend and append single quotes to `s`. This is sufficient to escape special characters for many shells, including most Bourne shell descendants.
 - `replace(s, from, to)` - Replace all occurrences of `from` in `s` to `to`.
 - `trim(s)` - Remove leading and trailing whitespace from `s`.
 - `trim_end(s)` - Remove trailing whitespace from `s`.
diff --git a/src/function.rs b/src/function.rs
index db972205..5232814e 100644
--- a/src/function.rs
+++ b/src/function.rs
@@ -27,6 +27,7 @@ lazy_static! {
     ("os", Nullary(os)),
     ("os_family", Nullary(os_family)),
     ("parent_directory", Unary(parent_directory)),
+    ("quote", Unary(quote)),
     ("replace", Ternary(replace)),
     ("trim", Unary(trim)),
     ("trim_end", Unary(trim_end)),
@@ -206,6 +207,10 @@ fn parent_directory(_context: &FunctionContext, path: &str) -> Result<String, St
     .ok_or_else(|| format!("Could not extract parent directory from `{}`", path))
 }
 
+fn quote(_context: &FunctionContext, s: &str) -> Result<String, String> {
+  Ok(format!("'{}'", s.replace('\'', "'\\''")))
+}
+
 fn replace(_context: &FunctionContext, s: &str, from: &str, to: &str) -> Result<String, String> {
   Ok(s.replace(from, to))
 }


Test patch:
diff --git a/tests/lib.rs b/tests/lib.rs
index fe9819af..250eeaf4 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -26,6 +26,7 @@ mod invocation_directory;
 mod misc;
 mod positional_arguments;
 mod quiet;
+mod quote;
 mod readme;
 mod regexes;
 mod search;
diff --git a/tests/quote.rs b/tests/quote.rs
new file mode 100644
index 00000000..4cbdc94f
--- /dev/null
+++ b/tests/quote.rs
@@ -0,0 +1,43 @@
+use crate::common::*;
+
+#[test]
+fn single_quotes_are_prepended_and_appended() {
+  Test::new()
+    .justfile(
+      "
+      x := quote('abc')
+    ",
+    )
+    .args(&["--evaluate", "x"])
+    .stdout("'abc'")
+    .run();
+}
+
+#[test]
+fn quotes_are_escaped() {
+  Test::new()
+    .justfile(
+      r#"
+      x := quote("'")
+    "#,
+    )
+    .args(&["--evaluate", "x"])
+    .stdout(r"''\'''")
+    .run();
+}
+
+#[test]
+fn quoted_strings_can_be_used_as_arguments() {
+  Test::new()
+    .justfile(
+      r#"
+      file := quote("foo ' bar")
+
+      @foo:
+        touch {{ file }}
+        ls -1
+    "#,
+    )
+    .stdout("foo ' bar\njustfile\n")
+    .run();
+}
Repository: casey/just
Pull request #1122 (2022-02-28 03:04:50)

Make path_exists() relative to current working directory


Patch:
diff --git a/src/function.rs b/src/function.rs
index eba04e75..943c5347 100644
--- a/src/function.rs
+++ b/src/function.rs
@@ -154,7 +154,7 @@ fn just_executable(_context: &FunctionContext) -> Result<String, String> {
   exe_path.to_str().map(str::to_owned).ok_or_else(|| {
     format!(
       "Executable path is not valid unicode: {}",
-      exe_path.to_string_lossy()
+      exe_path.display()
     )
   })
 }
@@ -168,7 +168,7 @@ fn justfile(context: &FunctionContext) -> Result<String, String> {
     .ok_or_else(|| {
       format!(
         "Justfile path is not valid unicode: {}",
-        context.search.justfile.to_string_lossy()
+        context.search.justfile.display()
       )
     })
 }
@@ -187,7 +187,7 @@ fn justfile_directory(context: &FunctionContext) -> Result<String, String> {
     .ok_or_else(|| {
       format!(
         "Justfile directory is not valid unicode: {}",
-        justfile_directory.to_string_lossy()
+        justfile_directory.display()
       )
     })
 }
@@ -211,8 +211,15 @@ fn parent_directory(_context: &FunctionContext, path: &str) -> Result<String, St
     .ok_or_else(|| format!("Could not extract parent directory from `{}`", path))
 }
 
-fn path_exists(_context: &FunctionContext, path: &str) -> Result<String, String> {
-  Ok(Utf8Path::new(path).exists().to_string())
+fn path_exists(context: &FunctionContext, path: &str) -> Result<String, String> {
+  Ok(
+    context
+      .search
+      .working_directory
+      .join(path)
+      .exists()
+      .to_string(),
+  )
 }
 
 fn quote(_context: &FunctionContext, s: &str) -> Result<String, String> {


Test patch:
diff --git a/tests/functions.rs b/tests/functions.rs
index e499ea21..ffbf2f60 100644
--- a/tests/functions.rs
+++ b/tests/functions.rs
@@ -403,3 +403,18 @@ fn test_path_exists_filepath_doesnt_exist() {
     .stdout("false")
     .run();
 }
+
+#[test]
+fn path_exists_subdir() {
+  Test::new()
+    .tree(tree! {
+      foo: "",
+      bar: {
+      }
+    })
+    .justfile("x := path_exists('foo')")
+    .current_dir("bar")
+    .args(&["--evaluate", "x"])
+    .stdout("true")
+    .run();
+}
Repository: casey/just
Pull request #1816 (2024-01-03 21:07:46)

Fix recipe doc alignment in just --list
`recipe.private` doesn't take into account recipes with the `[private]` attribute, so private recipes affect the alignment calculation of the the recipe docs. This causes them to align too far to the right.

Use `recipe.public()` instead which appears to be the canonical way of differentiating between public and private.

casey: Nice, good catch! Can you add a test for this?
crdx: Yep. Should it go in `tests/modules.rs` or a new file `tests/subcommand.rs`?
crdx: I went ahead and added it to `tests/modules.rs` as that felt like the right place. Verified it fails before the change and passes after.
casey: Whoops, sorry for forgetting about this. Looks good to me!

Patch:
diff --git a/src/justfile.rs b/src/justfile.rs
index 55cc70df..0b862d29 100644
--- a/src/justfile.rs
+++ b/src/justfile.rs
@@ -468,7 +468,7 @@ impl<'src> Justfile<'src> {
       .recipes
       .values()
       .map(AsRef::as_ref)
-      .filter(|recipe| recipe.public())
+      .filter(|recipe| recipe.is_public())
       .collect::<Vec<&Recipe<Dependency>>>();
 
     if source_order {
diff --git a/src/recipe.rs b/src/recipe.rs
index 30e9d9ed..8f91bb01 100644
--- a/src/recipe.rs
+++ b/src/recipe.rs
@@ -94,7 +94,7 @@ impl<'src, D> Recipe<'src, D> {
     Ok(())
   }
 
-  pub(crate) fn public(&self) -> bool {
+  pub(crate) fn is_public(&self) -> bool {
     !self.private && !self.attributes.contains(&Attribute::Private)
   }
 
diff --git a/src/subcommand.rs b/src/subcommand.rs
index ad016eb1..8dbcbaea 100644
--- a/src/subcommand.rs
+++ b/src/subcommand.rs
@@ -445,7 +445,7 @@ impl Subcommand {
     let mut line_widths: BTreeMap<&str, usize> = BTreeMap::new();
 
     for (name, recipe) in &justfile.recipes {
-      if recipe.private {
+      if !recipe.is_public() {
         continue;
       }
 


Test patch:
diff --git a/tests/modules.rs b/tests/modules.rs
index 63e067fe..75aaf7fc 100644
--- a/tests/modules.rs
+++ b/tests/modules.rs
@@ -685,6 +685,37 @@ fn module_paths_beginning_with_tilde_are_expanded_to_homdir() {
     .run();
 }
 
+#[test]
+fn module_recipe_list_alignment_ignores_private_recipes() {
+  Test::new()
+    .write(
+      "foo.just",
+      "
+# foos
+foo:
+ @echo FOO
+
+[private]
+barbarbar:
+ @echo BAR
+
+@_bazbazbaz:
+ @echo BAZ
+      ",
+    )
+    .justfile("mod foo")
+    .test_round_trip(false)
+    .arg("--unstable")
+    .arg("--list")
+    .stdout(
+      "Available recipes:
+    foo:
+        foo # foos
+",
+    )
+    .run();
+}
+
 #[test]
 fn recipes_with_same_name_are_both_run() {
   Test::new()
Repository: casey/just
Pull request #1859 (2024-01-19 20:02:05)

Add function to canonicalize paths


Patch:
diff --git a/README.md b/README.md
index b65fccfb..7e669400 100644
--- a/README.md
+++ b/README.md
@@ -1381,7 +1381,10 @@ The process ID is: 420
 ##### Fallible
 
 - `absolute_path(path)` - Absolute path to relative `path` in the working
-  directory. `absolute_path("./bar.txt")` in directory `/foo` is `/foo/bar.txt`.
+  directory. `absolute_path("./bar.txt")` in directory `/foo` is
+  `/foo/bar.txt`.
+- `canonicalize(path)` - Canonicalize `path` by resolving symlinks and removing
+  `.`, `..`, and extra `/`s where possible.
 - `extension(path)` - Extension of `path`. `extension("/foo/bar.txt")` is
   `txt`.
 - `file_name(path)` - File name of `path` with any leading directory components
diff --git a/src/function.rs b/src/function.rs
index 464e64a1..1262076b 100644
--- a/src/function.rs
+++ b/src/function.rs
@@ -21,6 +21,7 @@ pub(crate) fn get(name: &str) -> Option<Function> {
   let function = match name {
     "absolute_path" => Unary(absolute_path),
     "arch" => Nullary(arch),
+    "canonicalize" => Unary(canonicalize),
     "cache_directory" => Nullary(|_| dir("cache", dirs::cache_dir)),
     "capitalize" => Unary(capitalize),
     "clean" => Unary(clean),
@@ -106,6 +107,18 @@ fn arch(_context: &FunctionContext) -> Result<String, String> {
   Ok(target::arch().to_owned())
 }
 
+fn canonicalize(_context: &FunctionContext, path: &str) -> Result<String, String> {
+  let canonical =
+    std::fs::canonicalize(path).map_err(|err| format!("I/O error canonicalizing path: {err}"))?;
+
+  canonical.to_str().map(str::to_string).ok_or_else(|| {
+    format!(
+      "Canonical path is not valid unicode: {}",
+      canonical.display(),
+    )
+  })
+}
+
 fn capitalize(_context: &FunctionContext, s: &str) -> Result<String, String> {
   let mut capitalized = String::new();
   for (i, c) in s.chars().enumerate() {


Test patch:
diff --git a/tests/functions.rs b/tests/functions.rs
index e1da40ad..c3565242 100644
--- a/tests/functions.rs
+++ b/tests/functions.rs
@@ -662,3 +662,14 @@ fn just_pid() {
 
   assert_eq!(stdout.parse::<u32>().unwrap(), pid);
 }
+
+#[cfg(unix)]
+#[test]
+fn canonicalize() {
+  Test::new()
+    .args(["--evaluate", "x"])
+    .justfile("x := canonicalize('foo')")
+    .symlink("justfile", "foo")
+    .stdout_regex(".*/justfile")
+    .run();
+}
diff --git a/tests/test.rs b/tests/test.rs
index a615fb65..2a3a6ac3 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -113,6 +113,17 @@ impl Test {
     self.tempdir.path().join("justfile")
   }
 
+  #[cfg(unix)]
+  #[track_caller]
+  pub(crate) fn symlink(self, original: &str, link: &str) -> Self {
+    std::os::unix::fs::symlink(
+      self.tempdir.path().join(original),
+      self.tempdir.path().join(link),
+    )
+    .unwrap();
+    self
+  }
+
   pub(crate) fn no_justfile(mut self) -> Self {
     self.justfile = None;
     self
Repository: casey/just
Pull request #2040 (2024-05-15 04:29:31)

Make `dotenv-path` relative to working directory


Patch:
diff --git a/src/load_dotenv.rs b/src/load_dotenv.rs
index fb129d2b..f543d02d 100644
--- a/src/load_dotenv.rs
+++ b/src/load_dotenv.rs
@@ -23,7 +23,7 @@ pub(crate) fn load_dotenv(
   }
 
   if let Some(path) = dotenv_path {
-    return load_from_file(path);
+    return load_from_file(&working_directory.join(path));
   }
 
   let filename = dotenv_filename.map_or(DEFAULT_DOTENV_FILENAME, |s| s.as_str());


Test patch:
diff --git a/tests/dotenv.rs b/tests/dotenv.rs
index 7c5a6303..43f5e6ba 100644
--- a/tests/dotenv.rs
+++ b/tests/dotenv.rs
@@ -186,7 +186,7 @@ fn can_set_dotenv_path_from_justfile() {
   Test::new()
     .justfile(
       r#"
-        set dotenv-path:= "subdir/.env"
+        set dotenv-path := "subdir/.env"
 
         foo:
           @echo $JUST_TEST_VARIABLE
@@ -228,7 +228,7 @@ fn program_argument_has_priority_for_dotenv_path() {
   Test::new()
     .justfile(
       r#"
-        set dotenv-path:= "subdir/.env"
+        set dotenv-path := "subdir/.env"
 
         foo:
           @echo $JUST_TEST_VARIABLE
@@ -245,3 +245,20 @@ fn program_argument_has_priority_for_dotenv_path() {
     .status(EXIT_SUCCESS)
     .run();
 }
+
+#[test]
+fn dotenv_path_is_relative_to_working_directory() {
+  Test::new()
+    .justfile(
+      "
+        set dotenv-path := '.env'
+
+        foo:
+          @echo $DOTENV_KEY
+      ",
+    )
+    .tree(tree! { subdir: { } })
+    .current_dir("subdir")
+    .stdout("dotenv-value\n")
+    .run();
+}
diff --git a/tests/test.rs b/tests/test.rs
index a200de7c..65ee30ae 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -203,9 +203,7 @@ impl Test {
     };
     let stderr = unindent(&self.stderr);
 
-    let mut dotenv_path = self.tempdir.path().to_path_buf();
-    dotenv_path.push(".env");
-    fs::write(dotenv_path, "DOTENV_KEY=dotenv-value").unwrap();
+    fs::write(self.tempdir.path().join(".env"), "DOTENV_KEY=dotenv-value").unwrap();
 
     let mut command = Command::new(executable_path("just"));
Repository: casey/just
Pull request #2046 (2024-05-16 20:53:32)

Support append()
`append(suffix, base)` takes a `append` and a `base` string with space-separated words,
and append `suffix` to the end of each word in the base string.

For example, `append('.c', 'foo bar baz')` -> 'foo.c bar.c baz.c'


casey: Seems reasonable to me! How about calling it `append`, since that's shorter, and using `split_whitespace` instead of `split(" ")`?
gyreas: Shorter names are okay too. This is carried over from when it was about porting make functions.

I'll incorporate the suggestion changes. Thanks for your feedbacks
casey: Nice, LGTM! I added `<sup>master</sup>` in the readme, which indicates that it still hasn't been released. I'll remove that when it makes it into a release.
casey: Gah, didn't mean to close.

Patch:
diff --git a/README.md b/README.md
index 8937fbef..23ee60d9 100644
--- a/README.md
+++ b/README.md
@@ -1397,7 +1397,8 @@ The process ID is: 420
 
 
 #### String Manipulation
-
+- `append(suffix, s)`<sup>master</sup> Append `suffix` to whitespace-separated
+  strings in `s`.
 - `quote(s)` - Replace all single quotes with `'\''` and prepend and append
   single quotes to `s`. This is sufficient to escape special characters for
   many shells, including most Bourne shell descendants.
diff --git a/src/function.rs b/src/function.rs
index b7e03e55..2ed09836 100644
--- a/src/function.rs
+++ b/src/function.rs
@@ -20,6 +20,7 @@ pub(crate) enum Function {
 pub(crate) fn get(name: &str) -> Option<Function> {
   let function = match name {
     "absolute_path" => Unary(absolute_path),
+    "append" => Binary(append),
     "arch" => Nullary(arch),
     "blake3" => Unary(blake3),
     "blake3_file" => Unary(blake3_file),
@@ -105,6 +106,15 @@ fn absolute_path(context: &FunctionContext, path: &str) -> Result<String, String
   }
 }
 
+fn append(_context: &FunctionContext, suffix: &str, s: &str) -> Result<String, String> {
+  Ok(
+    s.split_whitespace()
+      .map(|s| format!("{s}{suffix}"))
+      .collect::<Vec<String>>()
+      .join(" "),
+  )
+}
+
 fn arch(_context: &FunctionContext) -> Result<String, String> {
   Ok(target::arch().to_owned())
 }


Test patch:
diff --git a/tests/functions.rs b/tests/functions.rs
index 2c0e8143..3cc1d40e 100644
--- a/tests/functions.rs
+++ b/tests/functions.rs
@@ -481,6 +481,26 @@ fn trim_end() {
   assert_eval_eq("trim_end('  f  ')", "  f");
 }
 
+#[test]
+fn append() {
+  assert_eval_eq("append('8', 'r s t')", "r8 s8 t8");
+  assert_eval_eq("append('.c', 'main sar x11')", "main.c sar.c x11.c");
+  assert_eval_eq("append('-', 'c v h y')", "c- v- h- y-");
+  assert_eval_eq(
+    "append('0000', '11 10 01 00')",
+    "110000 100000 010000 000000",
+  );
+  assert_eval_eq(
+    "append('tion', '
+    Determina
+    Acquisi
+    Motiva
+    Conjuc
+    ')",
+    "Determination Acquisition Motivation Conjuction",
+  );
+}
+
 #[test]
 #[cfg(not(windows))]
 fn join() {
Repository: casey/just
Pull request #2059 (2024-05-19 23:45:56)

Allow shell expanded strings in source paths


Patch:
diff --git a/src/parser.rs b/src/parser.rs
index e3abcbfb..77c6147b 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -339,6 +339,7 @@ impl<'run, 'src> Parser<'run, 'src> {
           }
           Some(Keyword::Import)
             if self.next_are(&[Identifier, StringToken])
+              || self.next_are(&[Identifier, Identifier, StringToken])
               || self.next_are(&[Identifier, QuestionMark]) =>
           {
             self.presume_keyword(Keyword::Import)?;
@@ -353,6 +354,7 @@ impl<'run, 'src> Parser<'run, 'src> {
           }
           Some(Keyword::Mod)
             if self.next_are(&[Identifier, Identifier, StringToken])
+              || self.next_are(&[Identifier, Identifier, Identifier, StringToken])
               || self.next_are(&[Identifier, Identifier, Eof])
               || self.next_are(&[Identifier, Identifier, Eol])
               || self.next_are(&[Identifier, QuestionMark]) =>
@@ -363,7 +365,8 @@ impl<'run, 'src> Parser<'run, 'src> {
 
             let name = self.parse_name()?;
 
-            let relative = if self.next_is(StringToken) {
+            let relative = if self.next_is(StringToken) || self.next_are(&[Identifier, StringToken])
+            {
               Some(self.parse_string_literal()?)
             } else {
               None


Test patch:
diff --git a/tests/shell_expansion.rs b/tests/shell_expansion.rs
index b748c500..f91fd000 100644
--- a/tests/shell_expansion.rs
+++ b/tests/shell_expansion.rs
@@ -65,3 +65,35 @@ fn shell_expanded_strings_can_be_used_in_settings() {
     .stdout("dotenv-value\n")
     .run();
 }
+
+#[test]
+fn shell_expanded_strings_can_be_used_in_import_paths() {
+  Test::new()
+    .justfile(
+      "
+        import x'$JUST_TEST_VARIABLE'
+
+        foo: bar
+      ",
+    )
+    .write("import.just", "@bar:\n echo BAR")
+    .env("JUST_TEST_VARIABLE", "import.just")
+    .stdout("BAR\n")
+    .run();
+}
+
+#[test]
+fn shell_expanded_strings_can_be_used_in_mod_paths() {
+  Test::new()
+    .justfile(
+      "
+        mod foo x'$JUST_TEST_VARIABLE'
+      ",
+    )
+    .write("mod.just", "@bar:\n echo BAR")
+    .env("JUST_TEST_VARIABLE", "mod.just")
+    .args(["--unstable", "foo", "bar"])
+    .stdout("BAR\n")
+    .test_round_trip(false)
+    .run();
+}
Repository: casey/just
Pull request #2061 (2024-05-20 04:09:03)

Pass command as first argument to `shell`


Patch:
diff --git a/README.md b/README.md
index fd0e7db0..d67efc52 100644
--- a/README.md
+++ b/README.md
@@ -1347,6 +1347,18 @@ file.
   interpret `command` is the same shell that is used to evaluate recipe lines,
   and can be changed with `set shell := […]`.
 
+  `command` is passed as the first argument, so if the command is `'echo $@'`,
+  the full command line, with the default shell command `shell -cu` and `args`
+  `'foo'` and `'bar'` will be:
+
+  ```
+  'shell' '-cu' 'echo $@' 'echo $@' 'foo' 'bar'
+  ```
+
+  This is so that `$@` works as expected, and `$1` refers to the first
+  argument. `$@` does not include the first positional argument, which is
+  expected to be the name of the program being run.
+
 ```just
 # arguments can be variables
 file := '/sys/class/power_supply/BAT0/status'
@@ -1362,9 +1374,10 @@ full := shell('echo $1', 'foo')
 ```
 
 ```just
-# using python as the shell
+# Using python as the shell. Since `python -c` sets `sys.argv[0]` to `'-c'`,
+# the first "real" positional argument will be `sys.argv[2]`.
 set shell := ["python3", "-c"]
-olleh := shell('import sys; print(sys.argv[1][::-1]))', 'hello')
+olleh := shell('import sys; print(sys.argv[2][::-1]))', 'hello')
 ```
 
 #### Environment Variables
diff --git a/src/evaluator.rs b/src/evaluator.rs
index 1b0f9460..0ccafb98 100644
--- a/src/evaluator.rs
+++ b/src/evaluator.rs
@@ -226,7 +226,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
       })
   }
 
-  pub(crate) fn run_command(&self, command: &str, args: &[String]) -> Result<String, OutputError> {
+  pub(crate) fn run_command(&self, command: &str, args: &[&str]) -> Result<String, OutputError> {
     let mut cmd = self.settings.shell_command(self.config);
     cmd.arg(command);
     cmd.args(args);
diff --git a/src/function.rs b/src/function.rs
index 0133d9ef..0321508a 100644
--- a/src/function.rs
+++ b/src/function.rs
@@ -460,8 +460,12 @@ fn sha256_file(evaluator: &Evaluator, path: &str) -> Result<String, String> {
 }
 
 fn shell(evaluator: &Evaluator, command: &str, args: &[String]) -> Result<String, String> {
+  let args = iter::once(command)
+    .chain(args.iter().map(String::as_str))
+    .collect::<Vec<&str>>();
+
   evaluator
-    .run_command(command, args)
+    .run_command(command, &args)
     .map_err(|output_error| output_error.to_string())
 }
 


Test patch:
diff --git a/tests/functions.rs b/tests/functions.rs
index c2d718c7..4631f55b 100644
--- a/tests/functions.rs
+++ b/tests/functions.rs
@@ -779,7 +779,17 @@ fn shell_no_argument() {
 
 #[test]
 fn shell_minimal() {
-  assert_eval_eq("shell('echo $0 $1', 'justice', 'legs')", "justice legs");
+  assert_eval_eq("shell('echo $1 $2', 'justice', 'legs')", "justice legs");
+}
+
+#[test]
+fn shell_args() {
+  assert_eval_eq("shell('echo $@', 'justice', 'legs')", "justice legs");
+}
+
+#[test]
+fn shell_first_arg() {
+  assert_eval_eq("shell('echo $0')", "echo $0");
 }
 
 #[test]