Repository: casey/just
Pull request #2276 (2024-07-26 07:34:58)

Allow multiline shebangs in scripts
Fix for issue #2024, allowing multiline shebangs, most importantly for use with Nix scripts, I don't know if they are relevant anywhere else.

Manual testing with Nix works for me, and I think error line numbers are retained correctly. The test added is a bit fake (just reads it like a shebang and shell like a comment), but a realistic integration test would require adding Nix to the dependencies which maybe isn't a good idea.

First time contributing, thank you for any feedback!

vkstrm: f20f493f53c441c3e6b7de90cc22a603a50aa95b A suggestion how to do that. Looks a bit more messy, but won't look at shebangs after the ones following the first line. The middle iteration is annoying since it still needs to handle a non-shebang line so that it is added correctly. The last one is cleaner though.
vkstrm: > > Looks a bit more messy,
> 
> FWIW there is also #2073 which started to attempt a different approach to implement multi-line shebang functionality. If avoiding messy-looking code is important, might that other PR suggest some way to improve this?

I did look at that first but gave it a go myself since it seems it wasn't being worked on. I guess it depends on how you look at it if that solutions is more appropriate, if the Shebang struct is interested in more than just the actual real shebang? I can see how collecting them there and then using them in script() could be done. If you think that is better I can try.

Patch:
diff --git a/src/executor.rs b/src/executor.rs
index 6c31aacf..08fadf98 100644
--- a/src/executor.rs
+++ b/src/executor.rs
@@ -92,44 +92,32 @@ impl<'a> Executor<'a> {
   // numbers in errors from generated script match justfile source lines.
   pub(crate) fn script<D>(&self, recipe: &Recipe<D>, lines: &[String]) -> String {
     let mut script = String::new();
-
-    match self {
-      Self::Shebang(shebang) => {
-        let mut n = 0;
-
-        for (i, (line, evaluated)) in recipe.body.iter().zip(lines).enumerate() {
-          if i == 0 {
-            if shebang.include_shebang_line() {
-              script.push_str(evaluated);
-              script.push('\n');
-              n += 1;
-            }
-          } else {
-            while n < line.number {
-              script.push('\n');
-              n += 1;
-            }
-
-            script.push_str(evaluated);
-            script.push('\n');
-            n += 1;
-          }
+    let mut n = 0;
+    let shebangs = recipe
+      .body
+      .iter()
+      .take_while(|line| line.is_shebang())
+      .count();
+
+    if let Self::Shebang(shebang) = self {
+      for shebang_line in &lines[..shebangs] {
+        if shebang.include_shebang_line() {
+          script.push_str(shebang_line);
         }
+        script.push('\n');
+        n += 1;
       }
-      Self::Command(_) => {
-        let mut n = 0;
-
-        for (line, evaluated) in recipe.body.iter().zip(lines) {
-          while n < line.number {
-            script.push('\n');
-            n += 1;
-          }
+    }
 
-          script.push_str(evaluated);
-          script.push('\n');
-          n += 1;
-        }
+    for (line, text) in recipe.body.iter().zip(lines).skip(n) {
+      while n < line.number {
+        script.push('\n');
+        n += 1;
       }
+
+      script.push_str(text);
+      script.push('\n');
+      n += 1;
     }
 
     script


Test patch:
diff --git a/tests/script.rs b/tests/script.rs
index 0a293165..d5d0e2cb 100644
--- a/tests/script.rs
+++ b/tests/script.rs
@@ -243,6 +243,43 @@ b
 
 
 
+c
+",
+    )
+    .run();
+}
+
+#[cfg(not(windows))]
+#[test]
+fn multiline_shebang_line_numbers() {
+  Test::new()
+    .justfile(
+      "foo:
+  #!/usr/bin/env cat
+  #!shebang
+  #!shebang
+
+  a
+
+  b
+
+
+  c
+
+
+",
+    )
+    .stdout(
+      "#!/usr/bin/env cat
+#!shebang
+#!shebang
+
+
+a
+
+b
+
+
 c
 ",
     )
diff --git a/tests/shebang.rs b/tests/shebang.rs
index e5ae23d9..a7cceece 100644
--- a/tests/shebang.rs
+++ b/tests/shebang.rs
@@ -44,6 +44,18 @@ default:
   stdout: "Hello-World\r\n",
 }
 
+#[cfg(windows)]
+test! {
+  name:     multi_line_cmd_shebangs_are_removed,
+  justfile: r#"
+default:
+  #!cmd.exe /c
+  #!foo
+  @echo Hello-World
+"#,
+  stdout: "Hello-World\r\n",
+}
+
 #[test]
 fn simple() {
   Test::new()