Repository: casey/just
Pull request #804 (2021-04-25 01:22:39)

Add `positional-arguments` setting
This setting causes recipe arguments to be passed as positional arguments to commands.


Patch:
diff --git a/GRAMMAR.md b/GRAMMAR.md
index f0d6fbf2..cf907df1 100644
--- a/GRAMMAR.md
+++ b/GRAMMAR.md
@@ -60,6 +60,7 @@ export        : 'export' assignment
 
 setting       : 'set' 'dotenv-load' boolean?
               | 'set' 'export' boolean?
+              | 'set' 'positional-arguments' boolean?
               | 'set' 'shell' ':=' '[' string (',' string)* ','? ']'
 
 boolean       : ':=' ('true' | 'false')
diff --git a/README.adoc b/README.adoc
index d364cdf6..b8d70200 100644
--- a/README.adoc
+++ b/README.adoc
@@ -388,9 +388,10 @@ foo:
 [options="header"]
 |=================
 | Name | Value | Description
-| `dotenv-load` | `true` or `false` | Load a `.env` file, if present.
-| `export` | `true` or `false` | Export all variables as environment variables.
-|`shell` | `[COMMAND, ARGS...]` | Set the command used to invoke recipes and evaluate backticks.
+| `dotenv-load` | boolean | Load a `.env` file, if present.
+| `export` | boolean | Export all variables as environment variables.
+| `positional-arguments` | boolean | Pass positional arguments.
+| `shell` | `[COMMAND, ARGS...]` | Set the command used to invoke recipes and evaluate backticks.
 |=================
 
 Boolean settings can be written as:
@@ -429,6 +430,28 @@ hello
 goodbye
 ```
 
+==== Positional Arguments
+
+If `positional-arguments` is `true`, recipe arguments will be passed as positional arguments to commands. For linewise recipes, argument `$0` will be the name of the Recipe.
+
+For example, running this recipe:
+
+```make
+set positional-arguments
+
+@foo bar:
+  echo $0
+  echo $1
+```
+
+Will produce the following output:
+
+```
+$ just foo hello
+foo
+hello
+```
+
 ==== Shell
 
 The `shell` setting controls the command used to invoke recipe lines and backticks. Shebang recipes are unaffected.
diff --git a/src/analyzer.rs b/src/analyzer.rs
index 542125e5..55cf6487 100644
--- a/src/analyzer.rs
+++ b/src/analyzer.rs
@@ -71,6 +71,9 @@ impl<'src> Analyzer<'src> {
         Setting::Export(export) => {
           settings.export = export;
         },
+        Setting::PositionalArguments(positional_arguments) => {
+          settings.positional_arguments = positional_arguments;
+        },
         Setting::Shell(shell) => {
           assert!(settings.shell.is_none());
           settings.shell = Some(shell);
diff --git a/src/justfile.rs b/src/justfile.rs
index 8145d28b..749243a1 100644
--- a/src/justfile.rs
+++ b/src/justfile.rs
@@ -266,7 +266,7 @@ impl<'src> Justfile<'src> {
       }
     }
 
-    recipe.run(context, dotenv, scope.child(), search)?;
+    recipe.run(context, dotenv, scope.child(), search, arguments)?;
 
     let mut invocation = vec![recipe.name().to_owned()];
     for argument in arguments.iter().cloned() {
diff --git a/src/keyword.rs b/src/keyword.rs
index a3f63143..c7c97bf3 100644
--- a/src/keyword.rs
+++ b/src/keyword.rs
@@ -4,14 +4,15 @@ use crate::common::*;
 #[strum(serialize_all = "kebab_case")]
 pub(crate) enum Keyword {
   Alias,
+  DotenvLoad,
   Else,
   Export,
-  DotenvLoad,
-  True,
   False,
   If,
+  PositionalArguments,
   Set,
   Shell,
+  True,
 }
 
 impl Keyword {
diff --git a/src/node.rs b/src/node.rs
index 433d45c2..b0413ad7 100644
--- a/src/node.rs
+++ b/src/node.rs
@@ -191,7 +191,8 @@ impl<'src> Node<'src> for Set<'src> {
 
     use Setting::*;
     match &self.value {
-      DotenvLoad(value) | Export(value) => set.push_mut(value.to_string()),
+      DotenvLoad(value) | Export(value) | PositionalArguments(value) =>
+        set.push_mut(value.to_string()),
       Shell(setting::Shell { command, arguments }) => {
         set.push_mut(Tree::string(&command.cooked));
         for argument in arguments {
diff --git a/src/parser.rs b/src/parser.rs
index 69b5d294..37bcf806 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -717,6 +717,12 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
         value: Setting::Export(value),
         name,
       });
+    } else if Keyword::PositionalArguments == lexeme {
+      let value = self.parse_set_bool()?;
+      return Ok(Set {
+        value: Setting::PositionalArguments(value),
+        name,
+      });
     }
 
     self.expect(ColonEquals)?;
@@ -1678,6 +1684,24 @@ mod tests {
     tree: (justfile (set dotenv_load false)),
   }
 
+  test! {
+    name: set_positional_arguments_implicit,
+    text: "set positional-arguments",
+    tree: (justfile (set positional_arguments true)),
+  }
+
+  test! {
+    name: set_positional_arguments_true,
+    text: "set positional-arguments := true",
+    tree: (justfile (set positional_arguments true)),
+  }
+
+  test! {
+    name: set_positional_arguments_false,
+    text: "set positional-arguments := false",
+    tree: (justfile (set positional_arguments false)),
+  }
+
   test! {
     name: set_shell_no_arguments,
     text: "set shell := ['tclsh']",
diff --git a/src/recipe.rs b/src/recipe.rs
index 48ae7e13..f1fc40eb 100644
--- a/src/recipe.rs
+++ b/src/recipe.rs
@@ -74,6 +74,7 @@ impl<'src, D> Recipe<'src, D> {
     dotenv: &BTreeMap<String, String>,
     scope: Scope<'src, 'run>,
     search: &'run Search,
+    arguments: &[&'run str],
   ) -> RunResult<'src, ()> {
     let config = &context.config;
 
@@ -176,6 +177,10 @@ impl<'src, D> Recipe<'src, D> {
         output_error,
       })?;
 
+      if context.settings.positional_arguments {
+        command.args(arguments);
+      }
+
       command.export(context.settings, dotenv, &scope);
 
       // run it!
@@ -260,6 +265,11 @@ impl<'src, D> Recipe<'src, D> {
 
         cmd.arg(command);
 
+        if context.settings.positional_arguments {
+          cmd.arg(self.name.lexeme());
+          cmd.args(arguments);
+        }
+
         if config.verbosity.quiet() {
           cmd.stderr(Stdio::null());
           cmd.stdout(Stdio::null());
diff --git a/src/setting.rs b/src/setting.rs
index 082baf19..011e3c02 100644
--- a/src/setting.rs
+++ b/src/setting.rs
@@ -2,9 +2,10 @@ use crate::common::*;
 
 #[derive(Debug)]
 pub(crate) enum Setting<'src> {
-  Shell(Shell<'src>),
-  Export(bool),
   DotenvLoad(bool),
+  Export(bool),
+  PositionalArguments(bool),
+  Shell(Shell<'src>),
 }
 
 #[derive(Debug, PartialEq)]
diff --git a/src/settings.rs b/src/settings.rs
index b86dc240..8e4e0460 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -2,17 +2,19 @@ use crate::common::*;
 
 #[derive(Debug, PartialEq)]
 pub(crate) struct Settings<'src> {
-  pub(crate) dotenv_load: Option<bool>,
-  pub(crate) export:      bool,
-  pub(crate) shell:       Option<setting::Shell<'src>>,
+  pub(crate) dotenv_load:          Option<bool>,
+  pub(crate) export:               bool,
+  pub(crate) positional_arguments: bool,
+  pub(crate) shell:                Option<setting::Shell<'src>>,
 }
 
 impl<'src> Settings<'src> {
   pub(crate) fn new() -> Settings<'src> {
     Settings {
-      dotenv_load: None,
-      export:      false,
-      shell:       None,
+      dotenv_load:          None,
+      export:               false,
+      positional_arguments: false,
+      shell:                None,
     }
   }
 


Test patch:
diff --git a/tests/lib.rs b/tests/lib.rs
index 2ec5a2fa..78cc53f7 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -17,6 +17,7 @@ mod init;
 mod interrupts;
 mod invocation_directory;
 mod misc;
+mod positional_arguments;
 mod quiet;
 mod readme;
 mod search;
diff --git a/tests/positional_arguments.rs b/tests/positional_arguments.rs
new file mode 100644
index 00000000..2d242980
--- /dev/null
+++ b/tests/positional_arguments.rs
@@ -0,0 +1,66 @@
+test! {
+  name: linewise,
+  justfile: r#"
+    set positional-arguments
+
+    foo bar baz:
+      echo $0
+      echo $1
+      echo $2
+      echo "$@"
+  "#,
+  args:   ("foo", "hello", "goodbye"),
+  stdout: "
+    foo
+    hello
+    goodbye
+    hello goodbye
+  ",
+  stderr: r#"
+    echo $0
+    echo $1
+    echo $2
+    echo "$@"
+  "#,
+}
+
+test! {
+  name: variadic_linewise,
+  justfile: r#"
+    set positional-arguments
+
+    foo *bar:
+      echo $1
+      echo "$@"
+  "#,
+  args:   ("foo", "a", "b", "c"),
+  stdout: "a\na b c\n",
+  stderr: "echo $1\necho \"$@\"\n",
+}
+
+test! {
+  name: shebang,
+  justfile: "
+    set positional-arguments
+
+    foo bar:
+      #!/bin/sh
+      echo $1
+  ",
+  args:   ("foo", "hello"),
+  stdout: "hello\n",
+}
+
+test! {
+  name: variadic_shebang,
+  justfile: r#"
+    set positional-arguments
+
+    foo *bar:
+      #!/bin/sh
+      echo $1
+      echo "$@"
+  "#,
+  args:   ("foo", "a", "b", "c"),
+  stdout: "a\na b c\n",
+}