Add shell() function for running external commands (#2047)
This commit is contained in:
parent
198b37c020
commit
c6612de760
27
README.md
27
README.md
@ -1340,6 +1340,33 @@ that work on various operating systems. For an example, see
|
|||||||
[cross-platform.just](https://github.com/casey/just/blob/master/examples/cross-platform.just)
|
[cross-platform.just](https://github.com/casey/just/blob/master/examples/cross-platform.just)
|
||||||
file.
|
file.
|
||||||
|
|
||||||
|
#### External Commands
|
||||||
|
|
||||||
|
- `shell(command, args...)` returns the standard output of shell script
|
||||||
|
`command` with zero or more positional arguments `args`. The shell used to
|
||||||
|
interpret `command` is the same shell that is used to evaluate recipe lines,
|
||||||
|
and can be changed with `set shell := […]`.
|
||||||
|
|
||||||
|
```just
|
||||||
|
# arguments can be variables
|
||||||
|
file := '/sys/class/power_supply/BAT0/status'
|
||||||
|
bat0stat := shell('cat $1', file)
|
||||||
|
|
||||||
|
# commands can be variables
|
||||||
|
command := 'wc -l $1'
|
||||||
|
output := shell(command, 'main.c')
|
||||||
|
|
||||||
|
# note that arguments must be used
|
||||||
|
empty := shell('echo', 'foo')
|
||||||
|
full := shell('echo $1', 'foo')
|
||||||
|
```
|
||||||
|
|
||||||
|
```just
|
||||||
|
# using python as the shell
|
||||||
|
set shell := ["python3", "-c"]
|
||||||
|
olleh := shell('import sys; print(sys.argv[1][::-1]))', 'hello')
|
||||||
|
```
|
||||||
|
|
||||||
#### Environment Variables
|
#### Environment Variables
|
||||||
|
|
||||||
- `env_var(key)` — Retrieves the environment variable with name `key`, aborting
|
- `env_var(key)` — Retrieves the environment variable with name `key`, aborting
|
||||||
|
@ -76,6 +76,15 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
Thunk::UnaryPlus {
|
||||||
|
args: (a, rest), ..
|
||||||
|
} => {
|
||||||
|
self.resolve_expression(a)?;
|
||||||
|
for arg in rest {
|
||||||
|
self.resolve_expression(arg)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Thunk::Binary { args: [a, b], .. } => {
|
Thunk::Binary { args: [a, b], .. } => {
|
||||||
self.resolve_expression(a)?;
|
self.resolve_expression(a)?;
|
||||||
self.resolve_expression(b)
|
self.resolve_expression(b)
|
||||||
|
@ -102,6 +102,22 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||||||
message,
|
message,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
UnaryPlus {
|
||||||
|
name,
|
||||||
|
function,
|
||||||
|
args: (a, rest),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let a = self.evaluate_expression(a)?;
|
||||||
|
let mut rest_evaluated = Vec::new();
|
||||||
|
for arg in rest {
|
||||||
|
rest_evaluated.push(self.evaluate_expression(arg)?);
|
||||||
|
}
|
||||||
|
function(self, &a, &rest_evaluated).map_err(|message| Error::FunctionCall {
|
||||||
|
function: *name,
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
}
|
||||||
Binary {
|
Binary {
|
||||||
name,
|
name,
|
||||||
function,
|
function,
|
||||||
@ -127,7 +143,6 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||||||
for arg in rest {
|
for arg in rest {
|
||||||
rest_evaluated.push(self.evaluate_expression(arg)?);
|
rest_evaluated.push(self.evaluate_expression(arg)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
function(self, &a, &b, &rest_evaluated).map_err(|message| Error::FunctionCall {
|
function(self, &a, &b, &rest_evaluated).map_err(|message| Error::FunctionCall {
|
||||||
function: *name,
|
function: *name,
|
||||||
message,
|
message,
|
||||||
@ -203,28 +218,27 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run_backtick(&self, raw: &str, token: &Token<'src>) -> RunResult<'src, String> {
|
fn run_backtick(&self, raw: &str, token: &Token<'src>) -> RunResult<'src, String> {
|
||||||
|
self
|
||||||
|
.run_command(raw, &[])
|
||||||
|
.map_err(|output_error| Error::Backtick {
|
||||||
|
token: *token,
|
||||||
|
output_error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn run_command(&self, command: &str, args: &[String]) -> Result<String, OutputError> {
|
||||||
let mut cmd = self.settings.shell_command(self.config);
|
let mut cmd = self.settings.shell_command(self.config);
|
||||||
|
cmd.arg(command);
|
||||||
cmd.arg(raw);
|
cmd.args(args);
|
||||||
|
|
||||||
cmd.current_dir(&self.search.working_directory);
|
cmd.current_dir(&self.search.working_directory);
|
||||||
|
|
||||||
cmd.export(self.settings, self.dotenv, &self.scope);
|
cmd.export(self.settings, self.dotenv, &self.scope);
|
||||||
|
|
||||||
cmd.stdin(Stdio::inherit());
|
cmd.stdin(Stdio::inherit());
|
||||||
|
|
||||||
cmd.stderr(if self.config.verbosity.quiet() {
|
cmd.stderr(if self.config.verbosity.quiet() {
|
||||||
Stdio::null()
|
Stdio::null()
|
||||||
} else {
|
} else {
|
||||||
Stdio::inherit()
|
Stdio::inherit()
|
||||||
});
|
});
|
||||||
|
InterruptHandler::guard(|| output(cmd))
|
||||||
InterruptHandler::guard(|| {
|
|
||||||
output(cmd).map_err(|output_error| Error::Backtick {
|
|
||||||
token: *token,
|
|
||||||
output_error,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn evaluate_line(
|
pub(crate) fn evaluate_line(
|
||||||
|
@ -14,6 +14,7 @@ pub(crate) enum Function {
|
|||||||
Nullary(fn(&Evaluator) -> Result<String, String>),
|
Nullary(fn(&Evaluator) -> Result<String, String>),
|
||||||
Unary(fn(&Evaluator, &str) -> Result<String, String>),
|
Unary(fn(&Evaluator, &str) -> Result<String, String>),
|
||||||
UnaryOpt(fn(&Evaluator, &str, Option<&str>) -> Result<String, String>),
|
UnaryOpt(fn(&Evaluator, &str, Option<&str>) -> Result<String, String>),
|
||||||
|
UnaryPlus(fn(&Evaluator, &str, &[String]) -> Result<String, String>),
|
||||||
Binary(fn(&Evaluator, &str, &str) -> Result<String, String>),
|
Binary(fn(&Evaluator, &str, &str) -> Result<String, String>),
|
||||||
BinaryPlus(fn(&Evaluator, &str, &str, &[String]) -> Result<String, String>),
|
BinaryPlus(fn(&Evaluator, &str, &str, &[String]) -> Result<String, String>),
|
||||||
Ternary(fn(&Evaluator, &str, &str, &str) -> Result<String, String>),
|
Ternary(fn(&Evaluator, &str, &str, &str) -> Result<String, String>),
|
||||||
@ -67,6 +68,7 @@ pub(crate) fn get(name: &str) -> Option<Function> {
|
|||||||
"semver_matches" => Binary(semver_matches),
|
"semver_matches" => Binary(semver_matches),
|
||||||
"sha256" => Unary(sha256),
|
"sha256" => Unary(sha256),
|
||||||
"sha256_file" => Unary(sha256_file),
|
"sha256_file" => Unary(sha256_file),
|
||||||
|
"shell" => UnaryPlus(shell),
|
||||||
"shoutykebabcase" => Unary(shoutykebabcase),
|
"shoutykebabcase" => Unary(shoutykebabcase),
|
||||||
"shoutysnakecase" => Unary(shoutysnakecase),
|
"shoutysnakecase" => Unary(shoutysnakecase),
|
||||||
"snakecase" => Unary(snakecase),
|
"snakecase" => Unary(snakecase),
|
||||||
@ -93,6 +95,7 @@ impl Function {
|
|||||||
Nullary(_) => 0..0,
|
Nullary(_) => 0..0,
|
||||||
Unary(_) => 1..1,
|
Unary(_) => 1..1,
|
||||||
UnaryOpt(_) => 1..2,
|
UnaryOpt(_) => 1..2,
|
||||||
|
UnaryPlus(_) => 1..usize::MAX,
|
||||||
Binary(_) => 2..2,
|
Binary(_) => 2..2,
|
||||||
BinaryPlus(_) => 2..usize::MAX,
|
BinaryPlus(_) => 2..usize::MAX,
|
||||||
Ternary(_) => 3..3,
|
Ternary(_) => 3..3,
|
||||||
@ -456,6 +459,12 @@ fn sha256_file(evaluator: &Evaluator, path: &str) -> Result<String, String> {
|
|||||||
Ok(format!("{hash:x}"))
|
Ok(format!("{hash:x}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn shell(evaluator: &Evaluator, command: &str, args: &[String]) -> Result<String, String> {
|
||||||
|
evaluator
|
||||||
|
.run_command(command, args)
|
||||||
|
.map_err(|output_error| output_error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
fn shoutykebabcase(_evaluator: &Evaluator, s: &str) -> Result<String, String> {
|
fn shoutykebabcase(_evaluator: &Evaluator, s: &str) -> Result<String, String> {
|
||||||
Ok(s.to_shouty_kebab_case())
|
Ok(s.to_shouty_kebab_case())
|
||||||
}
|
}
|
||||||
|
11
src/node.rs
11
src/node.rs
@ -125,6 +125,17 @@ impl<'src> Node<'src> for Expression<'src> {
|
|||||||
tree.push_mut(b.tree());
|
tree.push_mut(b.tree());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
UnaryPlus {
|
||||||
|
name,
|
||||||
|
args: (a, rest),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
tree.push_mut(name.lexeme());
|
||||||
|
tree.push_mut(a.tree());
|
||||||
|
for arg in rest {
|
||||||
|
tree.push_mut(arg.tree());
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary {
|
Binary {
|
||||||
name, args: [a, b], ..
|
name, args: [a, b], ..
|
||||||
} => {
|
} => {
|
||||||
|
@ -261,6 +261,20 @@ impl Expression {
|
|||||||
arguments,
|
arguments,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
full::Thunk::UnaryPlus {
|
||||||
|
name,
|
||||||
|
args: (a, rest),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let mut arguments = vec![Expression::new(a)];
|
||||||
|
for arg in rest {
|
||||||
|
arguments.push(Expression::new(arg));
|
||||||
|
}
|
||||||
|
Expression::Call {
|
||||||
|
name: name.lexeme().to_owned(),
|
||||||
|
arguments,
|
||||||
|
}
|
||||||
|
}
|
||||||
full::Thunk::Binary {
|
full::Thunk::Binary {
|
||||||
name, args: [a, b], ..
|
name, args: [a, b], ..
|
||||||
} => Self::Call {
|
} => Self::Call {
|
||||||
|
35
src/thunk.rs
35
src/thunk.rs
@ -20,6 +20,12 @@ pub(crate) enum Thunk<'src> {
|
|||||||
function: fn(&Evaluator, &str, Option<&str>) -> Result<String, String>,
|
function: fn(&Evaluator, &str, Option<&str>) -> Result<String, String>,
|
||||||
args: (Box<Expression<'src>>, Box<Option<Expression<'src>>>),
|
args: (Box<Expression<'src>>, Box<Option<Expression<'src>>>),
|
||||||
},
|
},
|
||||||
|
UnaryPlus {
|
||||||
|
name: Name<'src>,
|
||||||
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
|
function: fn(&Evaluator, &str, &[String]) -> Result<String, String>,
|
||||||
|
args: (Box<Expression<'src>>, Vec<Expression<'src>>),
|
||||||
|
},
|
||||||
Binary {
|
Binary {
|
||||||
name: Name<'src>,
|
name: Name<'src>,
|
||||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
@ -46,6 +52,7 @@ impl<'src> Thunk<'src> {
|
|||||||
Self::Nullary { name, .. }
|
Self::Nullary { name, .. }
|
||||||
| Self::Unary { name, .. }
|
| Self::Unary { name, .. }
|
||||||
| Self::UnaryOpt { name, .. }
|
| Self::UnaryOpt { name, .. }
|
||||||
|
| Self::UnaryPlus { name, .. }
|
||||||
| Self::Binary { name, .. }
|
| Self::Binary { name, .. }
|
||||||
| Self::BinaryPlus { name, .. }
|
| Self::BinaryPlus { name, .. }
|
||||||
| Self::Ternary { name, .. } => name,
|
| Self::Ternary { name, .. } => name,
|
||||||
@ -79,6 +86,15 @@ impl<'src> Thunk<'src> {
|
|||||||
name,
|
name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
(Function::UnaryPlus(function), 1..=usize::MAX) => {
|
||||||
|
let rest = arguments.drain(1..).collect();
|
||||||
|
let a = Box::new(arguments.pop().unwrap());
|
||||||
|
Ok(Thunk::UnaryPlus {
|
||||||
|
function,
|
||||||
|
args: (a, rest),
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
}
|
||||||
(Function::Binary(function), 2) => {
|
(Function::Binary(function), 2) => {
|
||||||
let b = arguments.pop().unwrap().into();
|
let b = arguments.pop().unwrap().into();
|
||||||
let a = arguments.pop().unwrap().into();
|
let a = arguments.pop().unwrap().into();
|
||||||
@ -133,6 +149,17 @@ impl Display for Thunk<'_> {
|
|||||||
write!(f, "{}({a})", name.lexeme())
|
write!(f, "{}({a})", name.lexeme())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
UnaryPlus {
|
||||||
|
name,
|
||||||
|
args: (a, rest),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
write!(f, "{}({a}", name.lexeme())?;
|
||||||
|
for arg in rest {
|
||||||
|
write!(f, ", {arg}")?;
|
||||||
|
}
|
||||||
|
write!(f, ")")
|
||||||
|
}
|
||||||
Binary {
|
Binary {
|
||||||
name, args: [a, b], ..
|
name, args: [a, b], ..
|
||||||
} => write!(f, "{}({a}, {b})", name.lexeme()),
|
} => write!(f, "{}({a}, {b})", name.lexeme()),
|
||||||
@ -175,6 +202,14 @@ impl<'src> Serialize for Thunk<'src> {
|
|||||||
seq.serialize_element(b)?;
|
seq.serialize_element(b)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Self::UnaryPlus {
|
||||||
|
args: (a, rest), ..
|
||||||
|
} => {
|
||||||
|
seq.serialize_element(a)?;
|
||||||
|
for arg in rest {
|
||||||
|
seq.serialize_element(arg)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
Self::Binary { args, .. } => {
|
Self::Binary { args, .. } => {
|
||||||
for arg in args {
|
for arg in args {
|
||||||
seq.serialize_element(arg)?;
|
seq.serialize_element(arg)?;
|
||||||
|
@ -28,6 +28,14 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> {
|
|||||||
self.stack.push(b);
|
self.stack.push(b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Thunk::UnaryPlus {
|
||||||
|
args: (a, rest), ..
|
||||||
|
} => {
|
||||||
|
let first: &[&Expression] = &[a];
|
||||||
|
for arg in first.iter().copied().chain(rest).rev() {
|
||||||
|
self.stack.push(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
Thunk::Binary { args, .. } => {
|
Thunk::Binary { args, .. } => {
|
||||||
for arg in args.iter().rev() {
|
for arg in args.iter().rev() {
|
||||||
self.stack.push(arg);
|
self.stack.push(arg);
|
||||||
|
@ -759,6 +759,47 @@ fn just_pid() {
|
|||||||
assert_eq!(stdout.parse::<u32>().unwrap(), pid);
|
assert_eq!(stdout.parse::<u32>().unwrap(), pid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shell_no_argument() {
|
||||||
|
Test::new()
|
||||||
|
.justfile("var := shell()")
|
||||||
|
.args(["--evaluate"])
|
||||||
|
.stderr(
|
||||||
|
"
|
||||||
|
error: Function `shell` called with 0 arguments but takes 1 or more
|
||||||
|
——▶ justfile:1:8
|
||||||
|
│
|
||||||
|
1 │ var := shell()
|
||||||
|
│ ^^^^^
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.status(EXIT_FAILURE)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shell_minimal() {
|
||||||
|
assert_eval_eq("shell('echo $0 $1', 'justice', 'legs')", "justice legs");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shell_error() {
|
||||||
|
Test::new()
|
||||||
|
.justfile("var := shell('exit 1')")
|
||||||
|
.args(["--evaluate"])
|
||||||
|
.stderr(
|
||||||
|
"
|
||||||
|
error: Call to function `shell` failed: Process exited with status code 1
|
||||||
|
——▶ justfile:1:8
|
||||||
|
│
|
||||||
|
1 │ var := shell('exit 1')
|
||||||
|
│ ^^^^^
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.status(EXIT_FAILURE)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn blake3() {
|
fn blake3() {
|
||||||
Test::new()
|
Test::new()
|
||||||
|
Loading…
Reference in New Issue
Block a user