mirror of
https://github.com/casey/just.git
synced 2024-11-23 02:44:56 +03:00
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)
|
||||
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
|
||||
|
||||
- `env_var(key)` — Retrieves the environment variable with name `key`, aborting
|
||||
|
@ -76,6 +76,15 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Thunk::UnaryPlus {
|
||||
args: (a, rest), ..
|
||||
} => {
|
||||
self.resolve_expression(a)?;
|
||||
for arg in rest {
|
||||
self.resolve_expression(arg)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Thunk::Binary { args: [a, b], .. } => {
|
||||
self.resolve_expression(a)?;
|
||||
self.resolve_expression(b)
|
||||
|
@ -102,6 +102,22 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
||||
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 {
|
||||
name,
|
||||
function,
|
||||
@ -127,7 +143,6 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
||||
for arg in rest {
|
||||
rest_evaluated.push(self.evaluate_expression(arg)?);
|
||||
}
|
||||
|
||||
function(self, &a, &b, &rest_evaluated).map_err(|message| Error::FunctionCall {
|
||||
function: *name,
|
||||
message,
|
||||
@ -203,28 +218,27 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
cmd.arg(raw);
|
||||
|
||||
cmd.arg(command);
|
||||
cmd.args(args);
|
||||
cmd.current_dir(&self.search.working_directory);
|
||||
|
||||
cmd.export(self.settings, self.dotenv, &self.scope);
|
||||
|
||||
cmd.stdin(Stdio::inherit());
|
||||
|
||||
cmd.stderr(if self.config.verbosity.quiet() {
|
||||
Stdio::null()
|
||||
} else {
|
||||
Stdio::inherit()
|
||||
});
|
||||
|
||||
InterruptHandler::guard(|| {
|
||||
output(cmd).map_err(|output_error| Error::Backtick {
|
||||
token: *token,
|
||||
output_error,
|
||||
})
|
||||
})
|
||||
InterruptHandler::guard(|| output(cmd))
|
||||
}
|
||||
|
||||
pub(crate) fn evaluate_line(
|
||||
|
@ -14,6 +14,7 @@ pub(crate) enum Function {
|
||||
Nullary(fn(&Evaluator) -> Result<String, String>),
|
||||
Unary(fn(&Evaluator, &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>),
|
||||
BinaryPlus(fn(&Evaluator, &str, &str, &[String]) -> 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),
|
||||
"sha256" => Unary(sha256),
|
||||
"sha256_file" => Unary(sha256_file),
|
||||
"shell" => UnaryPlus(shell),
|
||||
"shoutykebabcase" => Unary(shoutykebabcase),
|
||||
"shoutysnakecase" => Unary(shoutysnakecase),
|
||||
"snakecase" => Unary(snakecase),
|
||||
@ -93,6 +95,7 @@ impl Function {
|
||||
Nullary(_) => 0..0,
|
||||
Unary(_) => 1..1,
|
||||
UnaryOpt(_) => 1..2,
|
||||
UnaryPlus(_) => 1..usize::MAX,
|
||||
Binary(_) => 2..2,
|
||||
BinaryPlus(_) => 2..usize::MAX,
|
||||
Ternary(_) => 3..3,
|
||||
@ -456,6 +459,12 @@ fn sha256_file(evaluator: &Evaluator, path: &str) -> Result<String, String> {
|
||||
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> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
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 {
|
||||
name, args: [a, b], ..
|
||||
} => {
|
||||
|
@ -261,6 +261,20 @@ impl Expression {
|
||||
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 {
|
||||
name, args: [a, b], ..
|
||||
} => 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>,
|
||||
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 {
|
||||
name: Name<'src>,
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
@ -46,6 +52,7 @@ impl<'src> Thunk<'src> {
|
||||
Self::Nullary { name, .. }
|
||||
| Self::Unary { name, .. }
|
||||
| Self::UnaryOpt { name, .. }
|
||||
| Self::UnaryPlus { name, .. }
|
||||
| Self::Binary { name, .. }
|
||||
| Self::BinaryPlus { name, .. }
|
||||
| Self::Ternary { name, .. } => name,
|
||||
@ -79,6 +86,15 @@ impl<'src> Thunk<'src> {
|
||||
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) => {
|
||||
let b = arguments.pop().unwrap().into();
|
||||
let a = arguments.pop().unwrap().into();
|
||||
@ -133,6 +149,17 @@ impl Display for Thunk<'_> {
|
||||
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 {
|
||||
name, args: [a, b], ..
|
||||
} => write!(f, "{}({a}, {b})", name.lexeme()),
|
||||
@ -175,6 +202,14 @@ impl<'src> Serialize for Thunk<'src> {
|
||||
seq.serialize_element(b)?;
|
||||
}
|
||||
}
|
||||
Self::UnaryPlus {
|
||||
args: (a, rest), ..
|
||||
} => {
|
||||
seq.serialize_element(a)?;
|
||||
for arg in rest {
|
||||
seq.serialize_element(arg)?;
|
||||
}
|
||||
}
|
||||
Self::Binary { args, .. } => {
|
||||
for arg in args {
|
||||
seq.serialize_element(arg)?;
|
||||
|
@ -28,6 +28,14 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> {
|
||||
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, .. } => {
|
||||
for arg in args.iter().rev() {
|
||||
self.stack.push(arg);
|
||||
|
@ -759,6 +759,47 @@ fn just_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]
|
||||
fn blake3() {
|
||||
Test::new()
|
||||
|
Loading…
Reference in New Issue
Block a user