Skip to content

Commit

Permalink
Add the --command subcommand (#824)
Browse files Browse the repository at this point in the history
  • Loading branch information
casey authored May 10, 2021
1 parent 4cb82e0 commit 50cd24d
Show file tree
Hide file tree
Showing 16 changed files with 314 additions and 48 deletions.
2 changes: 1 addition & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 9 additions & 1 deletion completions/just.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions completions/just.elvish
Original file line number Diff line number Diff line change
Expand Up @@ -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>'
Expand All @@ -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'
Expand Down
2 changes: 2 additions & 0 deletions completions/just.fish
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ 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'
complete -c just -n "__fish_use_subcommand" -l highlight -d 'Highlight echoed recipe lines in bold'
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'
Expand Down
3 changes: 3 additions & 0 deletions completions/just.powershell
Original file line number Diff line number Diff line change
Expand Up @@ -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>')
Expand All @@ -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')
Expand Down
3 changes: 3 additions & 0 deletions completions/just.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -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' \
Expand All @@ -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]' \
Expand Down
2 changes: 1 addition & 1 deletion rustfmt.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
52 changes: 45 additions & 7 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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";
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand All @@ -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]
Expand Down
82 changes: 59 additions & 23 deletions src/justfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
21 changes: 21 additions & 0 deletions src/runtime_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 50cd24d

Please sign in to comment.