Repository: casey/just
Pull request #2374 (2024-09-18 07:43:27)

Add `--one` flag to forbid multiple recipes from being invoked on the command line


Patch:
diff --git a/README.md b/README.md
index 1299762c..1bee80f5 100644
--- a/README.md
+++ b/README.md
@@ -753,6 +753,48 @@ $ just --list --list-heading ''
     build
 ```
 
+### Invoking Multiple Recipes
+
+Multiple recipes may be invoked on the command line at once:
+
+```just
+build:
+  make web
+
+serve:
+  python3 -m http.server -d out 8000
+```
+
+```sh
+$ just build serve
+make web
+python3 -m http.server -d out 8000
+```
+
+Keep in mind that recipes with parameters will swallow arguments, even if they
+match the names of other recipes:
+
+```just
+build project:
+  make {{project}}
+
+serve:
+  python3 -m http.server -d out 8000
+```
+
+```sh
+$ just build serve
+make: *** No rule to make target `serve'.  Stop.
+```
+
+The `--one` flag can be used to restrict command-line invocations to a single
+recipe:
+
+```sh
+$ just --one build serve
+error: Expected 1 command-line recipe invocation but found 2.
+```
+
 ### Working Directory
 
 By default, recipes run with the working directory set to the directory that
diff --git a/src/config.rs b/src/config.rs
index 356c663f..d0b91d67 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -25,6 +25,7 @@ pub(crate) struct Config {
   pub(crate) load_dotenv: bool,
   pub(crate) no_aliases: bool,
   pub(crate) no_dependencies: bool,
+  pub(crate) one: bool,
   pub(crate) search_config: SearchConfig,
   pub(crate) shell: Option<String>,
   pub(crate) shell_args: Option<Vec<String>>,
@@ -100,6 +101,7 @@ mod arg {
   pub(crate) const NO_DEPS: &str = "NO-DEPS";
   pub(crate) const NO_DOTENV: &str = "NO-DOTENV";
   pub(crate) const NO_HIGHLIGHT: &str = "NO-HIGHLIGHT";
+  pub(crate) const ONE: &str = "ONE";
   pub(crate) const QUIET: &str = "QUIET";
   pub(crate) const SET: &str = "SET";
   pub(crate) const SHELL: &str = "SHELL";
@@ -297,6 +299,13 @@ impl Config {
           .help("Don't highlight echoed recipe lines in bold")
           .overrides_with(arg::HIGHLIGHT),
       )
+      .arg(
+        Arg::new(arg::ONE)
+          .long("one")
+          .env("JUST_ONE")
+          .action(ArgAction::SetTrue)
+          .help("Forbid multiple recipes from being invoked on the command line"),
+      )
       .arg(
         Arg::new(arg::QUIET)
           .short('q')
@@ -721,6 +730,7 @@ impl Config {
       load_dotenv: !matches.get_flag(arg::NO_DOTENV),
       no_aliases: matches.get_flag(arg::NO_ALIASES),
       no_dependencies: matches.get_flag(arg::NO_DEPS),
+      one: matches.get_flag(arg::ONE),
       search_config,
       shell: matches.get_one::<String>(arg::SHELL).map(Into::into),
       shell_args: if matches.get_flag(arg::CLEAR_SHELL_ARGS) {
diff --git a/src/error.rs b/src/error.rs
index cd22311e..dd6955fe 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -95,6 +95,9 @@ pub(crate) enum Error<'src> {
     variable: String,
     suggestion: Option<Suggestion<'src>>,
   },
+  ExcessInvocations {
+    invocations: usize,
+  },
   ExpectedSubmoduleButFoundRecipe {
     path: String,
   },
@@ -373,6 +376,9 @@ impl<'src> ColorDisplay for Error<'src> {
           write!(f, "\n{suggestion}")?;
         }
       }
+      ExcessInvocations { invocations } => {
+        write!(f, "Expected 1 command-line recipe invocation but found {invocations}.")?;
+      },
       ExpectedSubmoduleButFoundRecipe { path } => {
         write!(f, "Expected submodule at `{path}` but found recipe.")?;
       },
diff --git a/src/justfile.rs b/src/justfile.rs
index ab5fc6db..1a4043ff 100644
--- a/src/justfile.rs
+++ b/src/justfile.rs
@@ -202,6 +202,12 @@ impl<'src> Justfile<'src> {
       )?);
     }
 
+    if config.one && invocations.len() > 1 {
+      return Err(Error::ExcessInvocations {
+        invocations: invocations.len(),
+      });
+    }
+
     let mut ran = Ran::default();
     for invocation in invocations {
       let context = ExecutionContext {


Test patch:
diff --git a/tests/run.rs b/tests/run.rs
index 7f10cc99..9819023f 100644
--- a/tests/run.rs
+++ b/tests/run.rs
@@ -5,15 +5,40 @@ fn dont_run_duplicate_recipes() {
   Test::new()
     .justfile(
       "
-      foo:
-        # foo
-    ",
+        @foo:
+          echo foo
+      ",
     )
     .args(["foo", "foo"])
-    .stderr(
+    .stdout("foo\n")
+    .run();
+}
+
+#[test]
+fn one_flag_only_allows_one_invocation() {
+  Test::new()
+    .justfile(
       "
-      # foo
-    ",
+        @foo:
+          echo foo
+      ",
+    )
+    .args(["--one", "foo"])
+    .stdout("foo\n")
+    .run();
+
+  Test::new()
+    .justfile(
+      "
+        @foo:
+          echo foo
+
+        @bar:
+          echo bar
+      ",
     )
+    .args(["--one", "foo", "bar"])
+    .stderr("error: Expected 1 command-line recipe invocation but found 2.\n")
+    .status(1)
     .run();
 }