Repository: casey/just
Pull request #824 (2021-05-10 02:53:45)

Add the `--command` subcommand


Patch:
diff --git a/README.adoc b/README.adoc
index ec003d97..1cde04f1 100644
--- a/README.adoc
+++ b/README.adoc
@@ -1111,7 +1111,7 @@ echo:
 The interpreter path `/bin/sh` will be translated to a Windows-style path using
 `cygpath` before being executed.
 
-If the interpreter path does not contain a `/` it will be executed without being translated. This is useful if `cygpath` is not available, or you wish to use a Windows style path to the interpreter.
+If the interpreter path does not contain a `/` it will be executed without being translated. This is useful if `cygpath` is not available, or you wish to pass a Windows style path to the interpreter.
 
 === Setting Variables in a Recipe
 
diff --git a/completions/just.bash b/completions/just.bash
index fa5716e8..4de4bcfb 100644
--- a/completions/just.bash
+++ b/completions/just.bash
@@ -20,7 +20,7 @@ _just() {
 
     case "${cmd}" in
         just)
-            opts=" -q -u -v -e -l -h -V -f -d -s  --dry-run --highlight --no-dotenv --no-highlight --quiet --clear-shell-args --unsorted --verbose --choose --dump --edit --evaluate --init --list --summary --variables --help --version --chooser --color --list-heading --list-prefix --justfile --set --shell --shell-arg --working-directory --completions --show  <ARGUMENTS>... "
+            opts=" -q -u -v -e -l -h -V -f -d -c -s  --dry-run --highlight --no-dotenv --no-highlight --quiet --shell-command --clear-shell-args --unsorted --verbose --choose --dump --edit --evaluate --init --list --summary --variables --help --version --chooser --color --list-heading --list-prefix --justfile --set --shell --shell-arg --working-directory --command --completions --show  <ARGUMENTS>... "
                 if [[ ${cur} == -* ]] ; then
                     COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
                     return 0
@@ -77,6 +77,14 @@ _just() {
                     COMPREPLY=($(compgen -f "${cur}"))
                     return 0
                     ;;
+                --command)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                    -c)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
                 --completions)
                     COMPREPLY=($(compgen -W "zsh bash fish powershell elvish" -- "${cur}"))
                     return 0
diff --git a/completions/just.elvish b/completions/just.elvish
index e49cf35d..a2c3fe1c 100644
--- a/completions/just.elvish
+++ b/completions/just.elvish
@@ -25,6 +25,8 @@ edit:completion:arg-completer[just] = [@words]{
             cand --shell-arg 'Invoke shell with <SHELL-ARG> as an argument'
             cand -d 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set'
             cand --working-directory 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set'
+            cand -c 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set'
+            cand --command 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set'
             cand --completions 'Print shell completion script for <SHELL>'
             cand -s 'Show information about <RECIPE>'
             cand --show 'Show information about <RECIPE>'
@@ -34,6 +36,7 @@ edit:completion:arg-completer[just] = [@words]{
             cand --no-highlight 'Don''t highlight echoed recipe lines in bold'
             cand -q 'Suppress all output'
             cand --quiet 'Suppress all output'
+            cand --shell-command 'Invoke <COMMAND> with the shell used to run recipe lines and backticks'
             cand --clear-shell-args 'Clear shell arguments'
             cand -u 'Return list and summary entries in source order'
             cand --unsorted 'Return list and summary entries in source order'
diff --git a/completions/just.fish b/completions/just.fish
index 87f27cab..83f2267e 100644
--- a/completions/just.fish
+++ b/completions/just.fish
@@ -18,6 +18,7 @@ complete -c just -n "__fish_use_subcommand" -l set -d 'Override <VARIABLE> with
 complete -c just -n "__fish_use_subcommand" -l shell -d 'Invoke <SHELL> to run recipes'
 complete -c just -n "__fish_use_subcommand" -l shell-arg -d 'Invoke shell with <SHELL-ARG> as an argument'
 complete -c just -n "__fish_use_subcommand" -s d -l working-directory -d 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set'
+complete -c just -n "__fish_use_subcommand" -s c -l command -d 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set'
 complete -c just -n "__fish_use_subcommand" -l completions -d 'Print shell completion script for <SHELL>' -r -f -a "zsh bash fish powershell elvish"
 complete -c just -n "__fish_use_subcommand" -s s -l show -d 'Show information about <RECIPE>'
 complete -c just -n "__fish_use_subcommand" -l dry-run -d 'Print what just would do without doing it'
@@ -25,6 +26,7 @@ complete -c just -n "__fish_use_subcommand" -l highlight -d 'Highlight echoed re
 complete -c just -n "__fish_use_subcommand" -l no-dotenv -d 'Don\'t load `.env` file'
 complete -c just -n "__fish_use_subcommand" -l no-highlight -d 'Don\'t highlight echoed recipe lines in bold'
 complete -c just -n "__fish_use_subcommand" -s q -l quiet -d 'Suppress all output'
+complete -c just -n "__fish_use_subcommand" -l shell-command -d 'Invoke <COMMAND> with the shell used to run recipe lines and backticks'
 complete -c just -n "__fish_use_subcommand" -l clear-shell-args -d 'Clear shell arguments'
 complete -c just -n "__fish_use_subcommand" -s u -l unsorted -d 'Return list and summary entries in source order'
 complete -c just -n "__fish_use_subcommand" -s v -l verbose -d 'Use verbose output'
diff --git a/completions/just.powershell b/completions/just.powershell
index b33fdb07..64d8990c 100644
--- a/completions/just.powershell
+++ b/completions/just.powershell
@@ -30,6 +30,8 @@ Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock {
             [CompletionResult]::new('--shell-arg', 'shell-arg', [CompletionResultType]::ParameterName, 'Invoke shell with <SHELL-ARG> as an argument')
             [CompletionResult]::new('-d', 'd', [CompletionResultType]::ParameterName, 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set')
             [CompletionResult]::new('--working-directory', 'working-directory', [CompletionResultType]::ParameterName, 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set')
+            [CompletionResult]::new('-c', 'c', [CompletionResultType]::ParameterName, 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set')
+            [CompletionResult]::new('--command', 'command', [CompletionResultType]::ParameterName, 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set')
             [CompletionResult]::new('--completions', 'completions', [CompletionResultType]::ParameterName, 'Print shell completion script for <SHELL>')
             [CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Show information about <RECIPE>')
             [CompletionResult]::new('--show', 'show', [CompletionResultType]::ParameterName, 'Show information about <RECIPE>')
@@ -39,6 +41,7 @@ Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock {
             [CompletionResult]::new('--no-highlight', 'no-highlight', [CompletionResultType]::ParameterName, 'Don''t highlight echoed recipe lines in bold')
             [CompletionResult]::new('-q', 'q', [CompletionResultType]::ParameterName, 'Suppress all output')
             [CompletionResult]::new('--quiet', 'quiet', [CompletionResultType]::ParameterName, 'Suppress all output')
+            [CompletionResult]::new('--shell-command', 'shell-command', [CompletionResultType]::ParameterName, 'Invoke <COMMAND> with the shell used to run recipe lines and backticks')
             [CompletionResult]::new('--clear-shell-args', 'clear-shell-args', [CompletionResultType]::ParameterName, 'Clear shell arguments')
             [CompletionResult]::new('-u', 'u', [CompletionResultType]::ParameterName, 'Return list and summary entries in source order')
             [CompletionResult]::new('--unsorted', 'unsorted', [CompletionResultType]::ParameterName, 'Return list and summary entries in source order')
diff --git a/completions/just.zsh b/completions/just.zsh
index b08318d9..d31d1a5e 100644
--- a/completions/just.zsh
+++ b/completions/just.zsh
@@ -26,6 +26,8 @@ _just() {
 '*--shell-arg=[Invoke shell with <SHELL-ARG> as an argument]' \
 '-d+[Use <WORKING-DIRECTORY> as working directory. --justfile must also be set]' \
 '--working-directory=[Use <WORKING-DIRECTORY> as working directory. --justfile must also be set]' \
+'-c+[Run an arbitrary command with the working directory, `.env`, overrides, and exports set]' \
+'--command=[Run an arbitrary command with the working directory, `.env`, overrides, and exports set]' \
 '--completions=[Print shell completion script for <SHELL>]: :(zsh bash fish powershell elvish)' \
 '-s+[Show information about <RECIPE>]: :_just_commands' \
 '--show=[Show information about <RECIPE>]: :_just_commands' \
@@ -35,6 +37,7 @@ _just() {
 '--no-highlight[Don'\''t highlight echoed recipe lines in bold]' \
 '(--dry-run)-q[Suppress all output]' \
 '(--dry-run)--quiet[Suppress all output]' \
+'--shell-command[Invoke <COMMAND> with the shell used to run recipe lines and backticks]' \
 '--clear-shell-args[Clear shell arguments]' \
 '-u[Return list and summary entries in source order]' \
 '--unsorted[Return list and summary entries in source order]' \
diff --git a/rustfmt.toml b/rustfmt.toml
index f8ac04f6..4e7d74f7 100644
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -5,10 +5,10 @@ error_on_unformatted         = true
 format_code_in_doc_comments  = true
 format_macro_bodies          = true
 format_strings               = true
+imports_granularity          = "Crate"
 match_arm_blocks             = false
 match_block_trailing_comma   = true
 max_width                    = 100
-merge_imports                = true
 newline_style                = "Unix"
 normalize_comments           = true
 overflow_delimited_expr      = true
diff --git a/src/common.rs b/src/common.rs
index 5dd5ec03..d7d0db19 100644
--- a/src/common.rs
+++ b/src/common.rs
@@ -3,7 +3,7 @@ pub(crate) use std::{
   cmp,
   collections::{BTreeMap, BTreeSet},
   env,
-  ffi::OsString,
+  ffi::{OsStr, OsString},
   fmt::{self, Debug, Display, Formatter},
   fs,
   io::{self, Cursor, Write},
diff --git a/src/config.rs b/src/config.rs
index 10ed7e92..5da8b6d5 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -26,6 +26,7 @@ pub(crate) struct Config {
   pub(crate) shell:                String,
   pub(crate) shell_args:           Vec<String>,
   pub(crate) shell_present:        bool,
+  pub(crate) shell_command:        bool,
   pub(crate) subcommand:           Subcommand,
   pub(crate) unsorted:             bool,
   pub(crate) verbosity:            Verbosity,
@@ -42,14 +43,16 @@ mod cmd {
   pub(crate) const SHOW: &str = "SHOW";
   pub(crate) const SUMMARY: &str = "SUMMARY";
   pub(crate) const VARIABLES: &str = "VARIABLES";
+  pub(crate) const COMMAND: &str = "COMMAND";
 
   pub(crate) const ALL: &[&str] = &[
     CHOOSE,
+    COMMAND,
     COMPLETIONS,
     DUMP,
     EDIT,
-    INIT,
     EVALUATE,
+    INIT,
     LIST,
     SHOW,
     SUMMARY,
@@ -75,15 +78,16 @@ mod arg {
   pub(crate) const COLOR: &str = "COLOR";
   pub(crate) const DRY_RUN: &str = "DRY-RUN";
   pub(crate) const HIGHLIGHT: &str = "HIGHLIGHT";
+  pub(crate) const JUSTFILE: &str = "JUSTFILE";
   pub(crate) const LIST_HEADING: &str = "LIST-HEADING";
   pub(crate) const LIST_PREFIX: &str = "LIST-PREFIX";
-  pub(crate) const JUSTFILE: &str = "JUSTFILE";
   pub(crate) const NO_DOTENV: &str = "NO-DOTENV";
   pub(crate) const NO_HIGHLIGHT: &str = "NO-HIGHLIGHT";
   pub(crate) const QUIET: &str = "QUIET";
   pub(crate) const SET: &str = "SET";
   pub(crate) const SHELL: &str = "SHELL";
   pub(crate) const SHELL_ARG: &str = "SHELL-ARG";
+  pub(crate) const SHELL_COMMAND: &str = "SHELL-COMMAND";
   pub(crate) const UNSORTED: &str = "UNSORTED";
   pub(crate) const VERBOSE: &str = "VERBOSE";
   pub(crate) const WORKING_DIRECTORY: &str = "WORKING-DIRECTORY";
@@ -193,6 +197,12 @@ impl Config {
           .overrides_with(arg::CLEAR_SHELL_ARGS)
           .help("Invoke shell with <SHELL-ARG> as an argument"),
       )
+      .arg(
+        Arg::with_name(arg::SHELL_COMMAND)
+          .long("shell-command")
+          .requires(cmd::COMMAND)
+          .help("Invoke <COMMAND> with the shell used to run recipe lines and backticks"),
+      )
       .arg(
         Arg::with_name(arg::CLEAR_SHELL_ARGS)
           .long("clear-shell-args")
@@ -220,12 +230,18 @@ impl Config {
           .help("Use <WORKING-DIRECTORY> as working directory. --justfile must also be set")
           .requires(arg::JUSTFILE),
       )
+      .arg(Arg::with_name(cmd::CHOOSE).long("choose").help(CHOOSE_HELP))
       .arg(
-        Arg::with_name(arg::ARGUMENTS)
-          .multiple(true)
-          .help("Overrides and recipe(s) to run, defaulting to the first recipe in the justfile"),
+        Arg::with_name(cmd::COMMAND)
+          .long("command")
+          .short("c")
+          .min_values(1)
+          .allow_hyphen_values(true)
+          .help(
+            "Run an arbitrary command with the working directory, `.env`, overrides, and exports \
+             set",
+          ),
       )
-      .arg(Arg::with_name(cmd::CHOOSE).long("choose").help(CHOOSE_HELP))
       .arg(
         Arg::with_name(cmd::COMPLETIONS)
           .long("completions")
@@ -279,7 +295,12 @@ impl Config {
           .long("variables")
           .help("List names of variables"),
       )
-      .group(ArgGroup::with_name("SUBCOMMAND").args(cmd::ALL));
+      .group(ArgGroup::with_name("SUBCOMMAND").args(cmd::ALL))
+      .arg(
+        Arg::with_name(arg::ARGUMENTS)
+          .multiple(true)
+          .help("Overrides and recipe(s) to run, defaulting to the first recipe in the justfile"),
+      );
 
     if cfg!(feature = "help4help2man") {
       app.version(env!("CARGO_PKG_VERSION")).about(concat!(
@@ -401,6 +422,16 @@ impl Config {
         chooser: matches.value_of(arg::CHOOSER).map(str::to_owned),
         overrides,
       }
+    } else if let Some(values) = matches.values_of_os(cmd::COMMAND) {
+      let mut arguments = values
+        .into_iter()
+        .map(OsStr::to_owned)
+        .collect::<Vec<OsString>>();
+      Subcommand::Command {
+        binary: arguments.remove(0),
+        arguments,
+        overrides,
+      }
     } else if let Some(shell) = matches.value_of(cmd::COMPLETIONS) {
       Subcommand::Completions {
         shell: shell.to_owned(),
@@ -463,6 +494,7 @@ impl Config {
       highlight: !matches.is_present(arg::NO_HIGHLIGHT),
       shell: matches.value_of(arg::SHELL).unwrap().to_owned(),
       load_dotenv: !matches.is_present(arg::NO_DOTENV),
+      shell_command: matches.is_present(arg::SHELL_COMMAND),
       unsorted: matches.is_present(arg::UNSORTED),
       list_heading: matches
         .value_of(arg::LIST_HEADING)
@@ -522,6 +554,7 @@ impl Config {
     match &self.subcommand {
       Choose { overrides, chooser } =>
         self.choose(justfile, &search, overrides, chooser.as_deref())?,
+      Command { overrides, .. } => self.run(justfile, &search, overrides, &[])?,
       Dump => Self::dump(justfile),
       Evaluate { overrides, .. } => self.run(justfile, &search, overrides, &[])?,
       List => self.list(justfile),
@@ -893,6 +926,8 @@ FLAGS:
         --no-dotenv           Don't load `.env` file
         --no-highlight        Don't highlight echoed recipe lines in bold
     -q, --quiet               Suppress all output
+        --shell-command       Invoke <COMMAND> with the shell used to run recipe lines and \
+                                 backticks
         --summary             List names of available recipes
     -u, --unsorted            Return list and summary entries in source order
         --variables           List names of variables
@@ -903,6 +938,9 @@ OPTIONS:
         --color <COLOR>
             Print colorful output [default: auto]  [possible values: auto, always, never]
 
+    -c, --command <COMMAND>
+            Run an arbitrary command with the working directory, `.env`, overrides, and exports set
+
         --completions <SHELL>
             Print shell completion script for <SHELL> [possible values: zsh, bash, fish, \
                                  powershell, elvish]
diff --git a/src/justfile.rs b/src/justfile.rs
index ed20f3fc..cfd3cee5 100644
--- a/src/justfile.rs
+++ b/src/justfile.rs
@@ -127,34 +127,70 @@ impl<'src> Justfile<'src> {
       )?
     };
 
-    if let Subcommand::Evaluate { variable, .. } = &config.subcommand {
-      if let Some(variable) = variable {
-        if let Some(value) = scope.value(variable) {
-          print!("{}", value);
+    match &config.subcommand {
+      Subcommand::Command {
+        binary, arguments, ..
+      } => {
+        let mut command = if config.shell_command {
+          let mut command = self.settings.shell_command(&config);
+          command.arg(binary);
+          command
         } else {
-          return Err(RuntimeError::EvalUnknownVariable {
-            suggestion: self.suggest_variable(&variable),
-            variable:   variable.clone(),
-          });
-        }
-      } else {
-        let mut width = 0;
+          Command::new(binary)
+        };
 
-        for name in scope.names() {
-          width = cmp::max(name.len(), width);
-        }
+        command.args(arguments);
+
+        command.current_dir(&search.working_directory);
 
-        for binding in scope.bindings() {
-          println!(
-            "{0:1$} := \"{2}\"",
-            binding.name.lexeme(),
-            width,
-            binding.value
-          );
+        let scope = scope.child();
+
+        command.export(&self.settings, &dotenv, &scope);
+
+        let status = InterruptHandler::guard(|| command.status()).map_err(|io_error| {
+          RuntimeError::CommandInvocation {
+            binary: binary.clone(),
+            arguments: arguments.clone(),
+            io_error,
+          }
+        })?;
+
+        if !status.success() {
+          process::exit(status.code().unwrap_or(EXIT_FAILURE));
+        };
+
+        return Ok(());
+      },
+      Subcommand::Evaluate { variable, .. } => {
+        if let Some(variable) = variable {
+          if let Some(value) = scope.value(variable) {
+            print!("{}", value);
+          } else {
+            return Err(RuntimeError::EvalUnknownVariable {
+              suggestion: self.suggest_variable(&variable),
+              variable:   variable.clone(),
+            });
+          }
+        } else {
+          let mut width = 0;
+
+          for name in scope.names() {
+            width = cmp::max(name.len(), width);
+          }
+
+          for binding in scope.bindings() {
+            println!(
+              "{0:1$} := \"{2}\"",
+              binding.name.lexeme(),
+              width,
+              binding.value
+            );
+          }
         }
-      }
 
-      return Ok(());
+        return Ok(());
+      },
+      _ => {},
     }
 
     let argvec: Vec<&str> = if !arguments.is_empty() {
diff --git a/src/runtime_error.rs b/src/runtime_error.rs
index df2ad956..7aab4c85 100644
--- a/src/runtime_error.rs
+++ b/src/runtime_error.rs
@@ -18,6 +18,11 @@ pub(crate) enum RuntimeError<'src> {
     line_number: Option<usize>,
     code:        i32,
   },
+  CommandInvocation {
+    binary:    OsString,
+    arguments: Vec<OsString>,
+    io_error:  io::Error,
+  },
   Cygpath {
     recipe:       &'src str,
     output_error: OutputError,
@@ -201,6 +206,22 @@ impl<'src> Display for RuntimeError<'src> {
         } else {
           write!(f, "Recipe `{}` failed with exit code {}", recipe, code)?;
         },
+      CommandInvocation {
+        binary,
+        arguments,
+        io_error,
+      } => {
+        write!(
+          f,
+          "Failed to invoke {}: {}",
+          iter::once(binary)
+            .chain(arguments)
+            .map(|value| Enclosure::tick(value.to_string_lossy()).to_string())
+            .collect::<Vec<String>>()
+            .join(" "),
+          io_error,
+        )?;
+      },
       Cygpath {
         recipe,
         output_error,
diff --git a/src/subcommand.rs b/src/subcommand.rs
index 36b32731..a3084c63 100644
--- a/src/subcommand.rs
+++ b/src/subcommand.rs
@@ -6,6 +6,11 @@ pub(crate) enum Subcommand {
     overrides: BTreeMap<String, String>,
     chooser:   Option<String>,
   },
+  Command {
+    arguments: Vec<OsString>,
+    binary:    OsString,
+    overrides: BTreeMap<String, String>,
+  },
   Completions {
     shell: String,
   },


Test patch:
diff --git a/tests/command.rs b/tests/command.rs
new file mode 100644
index 00000000..f740135d
--- /dev/null
+++ b/tests/command.rs
@@ -0,0 +1,134 @@
+use crate::common::*;
+
+test! {
+  name: long,
+  justfile: "
+    x:
+      echo XYZ
+  ",
+  args: ("--command", "printf", "foo"),
+  stdout: "foo",
+}
+
+test! {
+  name: short,
+  justfile: "
+    x:
+      echo XYZ
+  ",
+  args: ("-c", "printf", "foo"),
+  stdout: "foo",
+}
+
+test! {
+  name: no_binary,
+  justfile: "
+    x:
+      echo XYZ
+  ",
+  args: ("--command"),
+  stderr: &format!("
+    error: The argument '--command <COMMAND>' requires a value but none was supplied
+
+    USAGE:
+        just{} --color <COLOR> --shell <SHELL> --shell-arg <SHELL-ARG>... \
+        <--choose|--command <COMMAND>|--completions <SHELL>|--dump|--edit|\
+        --evaluate|--init|--list|--show <RECIPE>|--summary|--variables>
+
+    For more information try --help
+  ", EXE_SUFFIX),
+  status: EXIT_FAILURE,
+}
+
+test! {
+  name: env_is_loaded,
+  justfile: "
+    x:
+      echo XYZ
+  ",
+  args: ("--command", "sh", "-c", "printf $DOTENV_KEY"),
+  stdout: "dotenv-value",
+}
+
+test! {
+  name: exports_are_available,
+  justfile: "
+    export FOO := 'bar'
+
+    x:
+      echo XYZ
+  ",
+  args: ("--command", "sh", "-c", "printf $FOO"),
+  stdout: "bar",
+}
+
+test! {
+  name: set_overrides_work,
+  justfile: "
+    export FOO := 'bar'
+
+    x:
+      echo XYZ
+  ",
+  args: ("--set", "FOO", "baz", "--command", "sh", "-c", "printf $FOO"),
+  stdout: "baz",
+}
+
+test! {
+  name: run_in_shell,
+  justfile: "
+    set shell := ['printf']
+  ",
+  args: ("--shell-command", "--command", "bar baz"),
+  stdout: "bar baz",
+  shell: false,
+}
+
+test! {
+  name: exit_status,
+  justfile: "
+    x:
+      echo XYZ
+  ",
+  args: ("--command", "false"),
+  status: EXIT_FAILURE,
+}
+
+#[test]
+fn working_directory_is_correct() {
+  let tmp = tempdir();
+
+  fs::write(tmp.path().join("justfile"), "").unwrap();
+  fs::write(tmp.path().join("bar"), "baz").unwrap();
+  fs::create_dir(tmp.path().join("foo")).unwrap();
+
+  let output = Command::new(&executable_path("just"))
+    .args(&["--command", "cat", "bar"])
+    .current_dir(tmp.path().join("foo"))
+    .output()
+    .unwrap();
+
+  assert_eq!(str::from_utf8(&output.stderr).unwrap(), "");
+
+  assert!(output.status.success());
+
+  assert_eq!(str::from_utf8(&output.stdout).unwrap(), "baz");
+}
+
+#[test]
+fn command_not_found() {
+  let tmp = tempdir();
+
+  fs::write(tmp.path().join("justfile"), "").unwrap();
+
+  let output = Command::new(&executable_path("just"))
+    .args(&["--command", "asdfasdfasdfasdfadfsadsfadsf", "bar"])
+    .output()
+    .unwrap();
+
+  assert!(str::from_utf8(&output.stderr)
+    .unwrap()
+    .starts_with("error: Failed to invoke `asdfasdfasdfasdfadfsadsfadsf` `bar`:"));
+
+  assert!(!output.status.success());
+}
diff --git a/tests/common.rs b/tests/common.rs
index 4602111a..43ce2270 100644
--- a/tests/common.rs
+++ b/tests/common.rs
@@ -1,6 +1,7 @@
 pub(crate) use std::{
   collections::BTreeMap,
-  env, fs,
+  env::{self, consts::EXE_SUFFIX},
+  fs,
   io::Write,
   iter,
   path::Path,
diff --git a/tests/interrupts.rs b/tests/interrupts.rs
index d230532e..7b68452f 100644
--- a/tests/interrupts.rs
+++ b/tests/interrupts.rs
@@ -1,6 +1,7 @@
 #[cfg(unix)]
 mod unix {
   use executable_path::executable_path;
+  use just::unindent;
   use std::{
     fs,
     process::Command,
@@ -14,16 +15,17 @@ mod unix {
     }
   }
 
-  fn interrupt_test(justfile: &str) {
+  fn interrupt_test(arguments: &[&str], justfile: &str) {
     let tmp = tempdir();
     let mut justfile_path = tmp.path().to_path_buf();
     justfile_path.push("justfile");
-    fs::write(justfile_path, justfile).unwrap();
+    fs::write(justfile_path, unindent(justfile)).unwrap();
 
     let start = Instant::now();
 
     let mut child = Command::new(&executable_path("just"))
       .current_dir(&tmp)
+      .args(arguments)
       .spawn()
       .expect("just invocation failed");
 
@@ -50,11 +52,12 @@ mod unix {
   #[ignore]
   fn interrupt_shebang() {
     interrupt_test(
+      &[],
       "
-default:
-  #!/usr/bin/env sh
-  sleep 1
-",
+        default:
+          #!/usr/bin/env sh
+          sleep 1
+      ",
     );
   }
 
@@ -62,10 +65,11 @@ default:
   #[ignore]
   fn interrupt_line() {
     interrupt_test(
+      &[],
       "
-default:
-  @sleep 1
-",
+        default:
+          @sleep 1
+      ",
     );
   }
 
@@ -73,12 +77,19 @@ default:
   #[ignore]
   fn interrupt_backtick() {
     interrupt_test(
+      &[],
       "
-foo := `sleep 1`
+        foo := `sleep 1`
 
-default:
-  @echo {{foo}}
-",
+        default:
+          @echo {{foo}}
+      ",
     );
   }
+
+  #[test]
+  #[ignore]
+  fn interrupt_command() {
+    interrupt_test(&["--command", "sleep", "1"], "");
+  }
 }
diff --git a/tests/lib.rs b/tests/lib.rs
index 78cc53f7..de8644a6 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -4,6 +4,7 @@ mod test;
 mod common;
 
 mod choose;
+mod command;
 mod completions;
 mod conditional;
 mod delimiters;