Add the --command subcommand (#824)

This commit is contained in:
Casey Rodarmor 2021-05-09 20:35:35 -07:00 committed by GitHub
parent 4cb82e0c1f
commit 50cd24d37b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 316 additions and 50 deletions

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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'

View File

@ -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')

View File

@ -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]' \

View File

@ -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

View File

@ -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},

View File

@ -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(arg::ARGUMENTS)
.multiple(true)
.help("Overrides and recipe(s) to run, defaulting to the first recipe in the justfile"),
)
.arg(Arg::with_name(cmd::CHOOSE).long("choose").help(CHOOSE_HELP))
.arg(
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::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]

View File

@ -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);
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
);
}
}
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() {

View File

@ -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,

View File

@ -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,
},

134
tests/command.rs Normal file
View File

@ -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());
}

View File

@ -1,6 +1,7 @@
pub(crate) use std::{
collections::BTreeMap,
env, fs,
env::{self, consts::EXE_SUFFIX},
fs,
io::Write,
iter,
path::Path,

View File

@ -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"], "");
}
}

View File

@ -4,6 +4,7 @@ mod test;
mod common;
mod choose;
mod command;
mod completions;
mod conditional;
mod delimiters;