parent
ec82cc20d3
commit
3d67786aaf
@ -48,13 +48,13 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
|||||||
pub fn evaluate_line(
|
pub fn evaluate_line(
|
||||||
&mut self,
|
&mut self,
|
||||||
line: &[Fragment<'a>],
|
line: &[Fragment<'a>],
|
||||||
arguments: &Map<&str, Cow<str>>
|
arguments: &Map<&str, Cow<str>>,
|
||||||
) -> RunResult<'a, String> {
|
) -> RunResult<'a, String> {
|
||||||
let mut evaluated = String::new();
|
let mut evaluated = String::new();
|
||||||
for fragment in line {
|
for fragment in line {
|
||||||
match *fragment {
|
match *fragment {
|
||||||
Fragment::Text{ref text} => evaluated += text.lexeme,
|
Fragment::Text { ref text } => evaluated += text.lexeme,
|
||||||
Fragment::Expression{ref expression} => {
|
Fragment::Expression { ref expression } => {
|
||||||
evaluated += &self.evaluate_expression(expression, arguments)?;
|
evaluated += &self.evaluate_expression(expression, arguments)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -76,7 +76,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(RuntimeError::Internal {
|
return Err(RuntimeError::Internal {
|
||||||
message: format!("attempted to evaluated unknown assignment {}", name)
|
message: format!("attempted to evaluated unknown assignment {}", name),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,10 +86,10 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
|||||||
fn evaluate_expression(
|
fn evaluate_expression(
|
||||||
&mut self,
|
&mut self,
|
||||||
expression: &Expression<'a>,
|
expression: &Expression<'a>,
|
||||||
arguments: &Map<&str, Cow<str>>
|
arguments: &Map<&str, Cow<str>>,
|
||||||
) -> RunResult<'a, String> {
|
) -> RunResult<'a, String> {
|
||||||
match *expression {
|
match *expression {
|
||||||
Expression::Variable{name, ..} => {
|
Expression::Variable { name, .. } => {
|
||||||
if self.evaluated.contains_key(name) {
|
if self.evaluated.contains_key(name) {
|
||||||
Ok(self.evaluated[name].clone())
|
Ok(self.evaluated[name].clone())
|
||||||
} else if self.scope.contains_key(name) {
|
} else if self.scope.contains_key(name) {
|
||||||
@ -101,34 +101,35 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
|||||||
Ok(arguments[name].to_string())
|
Ok(arguments[name].to_string())
|
||||||
} else {
|
} else {
|
||||||
Err(RuntimeError::Internal {
|
Err(RuntimeError::Internal {
|
||||||
message: format!("attempted to evaluate undefined variable `{}`", name)
|
message: format!("attempted to evaluate undefined variable `{}`", name),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expression::Call{name, arguments: ref call_arguments, ref token} => {
|
Expression::Call {
|
||||||
let call_arguments = call_arguments.iter().map(|argument| {
|
name,
|
||||||
self.evaluate_expression(argument, arguments)
|
arguments: ref call_arguments,
|
||||||
}).collect::<Result<Vec<String>, RuntimeError>>()?;
|
ref token,
|
||||||
|
} => {
|
||||||
|
let call_arguments = call_arguments
|
||||||
|
.iter()
|
||||||
|
.map(|argument| self.evaluate_expression(argument, arguments))
|
||||||
|
.collect::<Result<Vec<String>, RuntimeError>>()?;
|
||||||
let context = FunctionContext {
|
let context = FunctionContext {
|
||||||
invocation_directory: &self.invocation_directory,
|
invocation_directory: &self.invocation_directory,
|
||||||
dotenv: self.dotenv,
|
dotenv: self.dotenv,
|
||||||
};
|
};
|
||||||
evaluate_function(token, name, &context, &call_arguments)
|
evaluate_function(token, name, &context, &call_arguments)
|
||||||
}
|
}
|
||||||
Expression::String{ref cooked_string} => Ok(cooked_string.cooked.clone()),
|
Expression::String { ref cooked_string } => Ok(cooked_string.cooked.clone()),
|
||||||
Expression::Backtick{raw, ref token} => {
|
Expression::Backtick { raw, ref token } => {
|
||||||
if self.dry_run {
|
if self.dry_run {
|
||||||
Ok(format!("`{}`", raw))
|
Ok(format!("`{}`", raw))
|
||||||
} else {
|
} else {
|
||||||
Ok(self.run_backtick(self.dotenv, raw, token)?)
|
Ok(self.run_backtick(self.dotenv, raw, token)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expression::Concatination{ref lhs, ref rhs} => {
|
Expression::Concatination { ref lhs, ref rhs } => {
|
||||||
Ok(
|
Ok(self.evaluate_expression(lhs, arguments)? + &self.evaluate_expression(rhs, arguments)?)
|
||||||
self.evaluate_expression(lhs, arguments)?
|
|
||||||
+
|
|
||||||
&self.evaluate_expression(rhs, arguments)?
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -143,8 +144,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
|||||||
|
|
||||||
cmd.export_environment_variables(self.scope, dotenv, self.exports)?;
|
cmd.export_environment_variables(self.scope, dotenv, self.exports)?;
|
||||||
|
|
||||||
cmd.arg("-cu")
|
cmd.arg("-cu").arg(raw);
|
||||||
.arg(raw);
|
|
||||||
|
|
||||||
cmd.stderr(if self.quiet {
|
cmd.stderr(if self.quiet {
|
||||||
process::Stdio::null()
|
process::Stdio::null()
|
||||||
@ -152,13 +152,15 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
|||||||
process::Stdio::inherit()
|
process::Stdio::inherit()
|
||||||
});
|
});
|
||||||
|
|
||||||
InterruptHandler::guard(|| brev::output(cmd)
|
InterruptHandler::guard(|| {
|
||||||
.map_err(|output_error| RuntimeError::Backtick{token: token.clone(), output_error})
|
brev::output(cmd).map_err(|output_error| RuntimeError::Backtick {
|
||||||
)
|
token: token.clone(),
|
||||||
|
output_error,
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -173,11 +175,16 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn backtick_code() {
|
fn backtick_code() {
|
||||||
match parse_success("a:\n echo {{`f() { return 100; }; f`}}")
|
match parse_success("a:\n echo {{`f() { return 100; }; f`}}")
|
||||||
.run(&no_cwd_err(), &["a"], &Default::default()).unwrap_err() {
|
.run(&no_cwd_err(), &["a"], &Default::default())
|
||||||
RuntimeError::Backtick{token, output_error: OutputError::Code(code)} => {
|
.unwrap_err()
|
||||||
|
{
|
||||||
|
RuntimeError::Backtick {
|
||||||
|
token,
|
||||||
|
output_error: OutputError::Code(code),
|
||||||
|
} => {
|
||||||
assert_eq!(code, 100);
|
assert_eq!(code, 100);
|
||||||
assert_eq!(token.lexeme, "`f() { return 100; }; f`");
|
assert_eq!(token.lexeme, "`f() { return 100; }; f`");
|
||||||
},
|
}
|
||||||
other => panic!("expected a code run error, but got: {}", other),
|
other => panic!("expected a code run error, but got: {}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -196,10 +203,16 @@ recipe:
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
match parse_success(text).run(&no_cwd_err(), &["recipe"], &configuration).unwrap_err() {
|
match parse_success(text)
|
||||||
RuntimeError::Backtick{token, output_error: OutputError::Code(_)} => {
|
.run(&no_cwd_err(), &["recipe"], &configuration)
|
||||||
|
.unwrap_err()
|
||||||
|
{
|
||||||
|
RuntimeError::Backtick {
|
||||||
|
token,
|
||||||
|
output_error: OutputError::Code(_),
|
||||||
|
} => {
|
||||||
assert_eq!(token.lexeme, "`echo $exported_variable`");
|
assert_eq!(token.lexeme, "`echo $exported_variable`");
|
||||||
},
|
}
|
||||||
other => panic!("expected a backtick code errror, but got: {}", other),
|
other => panic!("expected a backtick code errror, but got: {}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,6 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
|
|||||||
assignments: &Map<&'a str, Expression<'a>>,
|
assignments: &Map<&'a str, Expression<'a>>,
|
||||||
assignment_tokens: &Map<&'a str, Token<'a>>,
|
assignment_tokens: &Map<&'a str, Token<'a>>,
|
||||||
) -> CompilationResult<'a, ()> {
|
) -> CompilationResult<'a, ()> {
|
||||||
|
|
||||||
let mut resolver = AssignmentResolver {
|
let mut resolver = AssignmentResolver {
|
||||||
stack: empty(),
|
stack: empty(),
|
||||||
seen: empty(),
|
seen: empty(),
|
||||||
@ -50,16 +49,15 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
|
|||||||
line: 0,
|
line: 0,
|
||||||
column: 0,
|
column: 0,
|
||||||
width: None,
|
width: None,
|
||||||
kind: Internal{message}
|
kind: Internal { message },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_expression(
|
fn resolve_expression(&mut self, expression: &Expression<'a>) -> CompilationResult<'a, ()> {
|
||||||
&mut self, expression: &Expression<'a>) -> CompilationResult<'a, ()> {
|
|
||||||
match *expression {
|
match *expression {
|
||||||
Expression::Variable{name, ref token} => {
|
Expression::Variable { name, ref token } => {
|
||||||
if self.evaluated.contains(name) {
|
if self.evaluated.contains(name) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else if self.seen.contains(name) {
|
} else if self.seen.contains(name) {
|
||||||
@ -72,17 +70,19 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
|
|||||||
} else if self.assignments.contains_key(name) {
|
} else if self.assignments.contains_key(name) {
|
||||||
self.resolve_assignment(name)?;
|
self.resolve_assignment(name)?;
|
||||||
} else {
|
} else {
|
||||||
return Err(token.error(UndefinedVariable{variable: name}));
|
return Err(token.error(UndefinedVariable { variable: name }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expression::Call{ref token, ref arguments, ..} => {
|
Expression::Call {
|
||||||
resolve_function(token, arguments.len())?
|
ref token,
|
||||||
}
|
ref arguments,
|
||||||
Expression::Concatination{ref lhs, ref rhs} => {
|
..
|
||||||
|
} => resolve_function(token, arguments.len())?,
|
||||||
|
Expression::Concatination { ref lhs, ref rhs } => {
|
||||||
self.resolve_expression(lhs)?;
|
self.resolve_expression(lhs)?;
|
||||||
self.resolve_expression(rhs)?;
|
self.resolve_expression(rhs)?;
|
||||||
}
|
}
|
||||||
Expression::String{..} | Expression::Backtick{..} => {}
|
Expression::String { .. } | Expression::Backtick { .. } => {}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,8 @@ extern crate atty;
|
|||||||
|
|
||||||
use common::*;
|
use common::*;
|
||||||
|
|
||||||
use self::ansi_term::{Style, Prefix, Suffix, ANSIGenericString};
|
|
||||||
use self::ansi_term::Color::*;
|
use self::ansi_term::Color::*;
|
||||||
|
use self::ansi_term::{ANSIGenericString, Prefix, Style, Suffix};
|
||||||
use self::atty::is as is_atty;
|
use self::atty::is as is_atty;
|
||||||
use self::atty::Stream;
|
use self::atty::Stream;
|
||||||
|
|
||||||
@ -34,10 +34,7 @@ impl Default for Color {
|
|||||||
|
|
||||||
impl Color {
|
impl Color {
|
||||||
fn restyle(self, style: Style) -> Color {
|
fn restyle(self, style: Style) -> Color {
|
||||||
Color {
|
Color { style, ..self }
|
||||||
style,
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn redirect(self, stream: Stream) -> Color {
|
fn redirect(self, stream: Stream) -> Color {
|
||||||
|
@ -5,7 +5,7 @@ pub trait CommandExt {
|
|||||||
&mut self,
|
&mut self,
|
||||||
scope: &Map<&'a str, String>,
|
scope: &Map<&'a str, String>,
|
||||||
dotenv: &Map<String, String>,
|
dotenv: &Map<String, String>,
|
||||||
exports: &Set<&'a str>
|
exports: &Set<&'a str>,
|
||||||
) -> RunResult<'a, ()>;
|
) -> RunResult<'a, ()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ impl CommandExt for Command {
|
|||||||
&mut self,
|
&mut self,
|
||||||
scope: &Map<&'a str, String>,
|
scope: &Map<&'a str, String>,
|
||||||
dotenv: &Map<String, String>,
|
dotenv: &Map<String, String>,
|
||||||
exports: &Set<&'a str>
|
exports: &Set<&'a str>,
|
||||||
) -> RunResult<'a, ()> {
|
) -> RunResult<'a, ()> {
|
||||||
for (name, value) in dotenv {
|
for (name, value) in dotenv {
|
||||||
self.env(name, value);
|
self.env(name, value);
|
||||||
|
@ -6,7 +6,7 @@ pub use std::ops::Range;
|
|||||||
pub use std::path::{Path, PathBuf};
|
pub use std::path::{Path, PathBuf};
|
||||||
pub use std::process::Command;
|
pub use std::process::Command;
|
||||||
pub use std::sync::{Mutex, MutexGuard};
|
pub use std::sync::{Mutex, MutexGuard};
|
||||||
pub use std::{cmp, env, fs, fmt, io, iter, process, vec, usize};
|
pub use std::{cmp, env, fmt, fs, io, iter, process, usize, vec};
|
||||||
|
|
||||||
pub use color::Color;
|
pub use color::Color;
|
||||||
pub use libc::{EXIT_FAILURE, EXIT_SUCCESS};
|
pub use libc::{EXIT_FAILURE, EXIT_SUCCESS};
|
||||||
@ -32,7 +32,7 @@ pub use parser::Parser;
|
|||||||
pub use range_ext::RangeExt;
|
pub use range_ext::RangeExt;
|
||||||
pub use recipe::{Recipe, RecipeContext};
|
pub use recipe::{Recipe, RecipeContext};
|
||||||
pub use recipe_resolver::RecipeResolver;
|
pub use recipe_resolver::RecipeResolver;
|
||||||
pub use runtime_error::{RuntimeError, RunResult};
|
pub use runtime_error::{RunResult, RuntimeError};
|
||||||
pub use shebang::Shebang;
|
pub use shebang::Shebang;
|
||||||
pub use token::{Token, TokenKind};
|
pub use token::{Token, TokenKind};
|
||||||
pub use verbosity::Verbosity;
|
pub use verbosity::Verbosity;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use common::*;
|
use common::*;
|
||||||
|
|
||||||
use misc::{Or, write_error_context, show_whitespace, maybe_s};
|
use misc::{maybe_s, show_whitespace, write_error_context, Or};
|
||||||
|
|
||||||
pub type CompilationResult<'a, T> = Result<T, CompilationError<'a>>;
|
pub type CompilationResult<'a, T> = Result<T, CompilationError<'a>>;
|
||||||
|
|
||||||
@ -16,27 +16,76 @@ pub struct CompilationError<'a> {
|
|||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum CompilationErrorKind<'a> {
|
pub enum CompilationErrorKind<'a> {
|
||||||
CircularRecipeDependency{recipe: &'a str, circle: Vec<&'a str>},
|
CircularRecipeDependency {
|
||||||
CircularVariableDependency{variable: &'a str, circle: Vec<&'a str>},
|
recipe: &'a str,
|
||||||
DependencyHasParameters{recipe: &'a str, dependency: &'a str},
|
circle: Vec<&'a str>,
|
||||||
DuplicateDependency{recipe: &'a str, dependency: &'a str},
|
},
|
||||||
DuplicateParameter{recipe: &'a str, parameter: &'a str},
|
CircularVariableDependency {
|
||||||
DuplicateRecipe{recipe: &'a str, first: usize},
|
variable: &'a str,
|
||||||
DuplicateVariable{variable: &'a str},
|
circle: Vec<&'a str>,
|
||||||
|
},
|
||||||
|
DependencyHasParameters {
|
||||||
|
recipe: &'a str,
|
||||||
|
dependency: &'a str,
|
||||||
|
},
|
||||||
|
DuplicateDependency {
|
||||||
|
recipe: &'a str,
|
||||||
|
dependency: &'a str,
|
||||||
|
},
|
||||||
|
DuplicateParameter {
|
||||||
|
recipe: &'a str,
|
||||||
|
parameter: &'a str,
|
||||||
|
},
|
||||||
|
DuplicateRecipe {
|
||||||
|
recipe: &'a str,
|
||||||
|
first: usize,
|
||||||
|
},
|
||||||
|
DuplicateVariable {
|
||||||
|
variable: &'a str,
|
||||||
|
},
|
||||||
ExtraLeadingWhitespace,
|
ExtraLeadingWhitespace,
|
||||||
FunctionArgumentCountMismatch{function: &'a str, found: usize, expected: usize},
|
FunctionArgumentCountMismatch {
|
||||||
InconsistentLeadingWhitespace{expected: &'a str, found: &'a str},
|
function: &'a str,
|
||||||
Internal{message: String},
|
found: usize,
|
||||||
InvalidEscapeSequence{character: char},
|
expected: usize,
|
||||||
MixedLeadingWhitespace{whitespace: &'a str},
|
},
|
||||||
|
InconsistentLeadingWhitespace {
|
||||||
|
expected: &'a str,
|
||||||
|
found: &'a str,
|
||||||
|
},
|
||||||
|
Internal {
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
InvalidEscapeSequence {
|
||||||
|
character: char,
|
||||||
|
},
|
||||||
|
MixedLeadingWhitespace {
|
||||||
|
whitespace: &'a str,
|
||||||
|
},
|
||||||
OuterShebang,
|
OuterShebang,
|
||||||
ParameterFollowsVariadicParameter{parameter: &'a str},
|
ParameterFollowsVariadicParameter {
|
||||||
ParameterShadowsVariable{parameter: &'a str},
|
parameter: &'a str,
|
||||||
RequiredParameterFollowsDefaultParameter{parameter: &'a str},
|
},
|
||||||
UndefinedVariable{variable: &'a str},
|
ParameterShadowsVariable {
|
||||||
UnexpectedToken{expected: Vec<TokenKind>, found: TokenKind},
|
parameter: &'a str,
|
||||||
UnknownDependency{recipe: &'a str, unknown: &'a str},
|
},
|
||||||
UnknownFunction{function: &'a str},
|
RequiredParameterFollowsDefaultParameter {
|
||||||
|
parameter: &'a str,
|
||||||
|
},
|
||||||
|
UndefinedVariable {
|
||||||
|
variable: &'a str,
|
||||||
|
},
|
||||||
|
UnexpectedToken {
|
||||||
|
expected: Vec<TokenKind>,
|
||||||
|
found: TokenKind,
|
||||||
|
},
|
||||||
|
UnknownDependency {
|
||||||
|
recipe: &'a str,
|
||||||
|
unknown: &'a str,
|
||||||
|
},
|
||||||
|
UnknownFunction {
|
||||||
|
function: &'a str,
|
||||||
|
},
|
||||||
UnknownStartOfToken,
|
UnknownStartOfToken,
|
||||||
UnterminatedInterpolation,
|
UnterminatedInterpolation,
|
||||||
UnterminatedString,
|
UnterminatedString,
|
||||||
@ -51,24 +100,35 @@ impl<'a> Display for CompilationError<'a> {
|
|||||||
write!(f, "{} {}", error.paint("error:"), message.prefix())?;
|
write!(f, "{} {}", error.paint("error:"), message.prefix())?;
|
||||||
|
|
||||||
match self.kind {
|
match self.kind {
|
||||||
CircularRecipeDependency{recipe, ref circle} => {
|
CircularRecipeDependency { recipe, ref circle } => {
|
||||||
if circle.len() == 2 {
|
if circle.len() == 2 {
|
||||||
writeln!(f, "Recipe `{}` depends on itself", recipe)?;
|
writeln!(f, "Recipe `{}` depends on itself", recipe)?;
|
||||||
} else {
|
} else {
|
||||||
writeln!(f, "Recipe `{}` has circular dependency `{}`",
|
writeln!(
|
||||||
recipe, circle.join(" -> "))?;
|
f,
|
||||||
|
"Recipe `{}` has circular dependency `{}`",
|
||||||
|
recipe,
|
||||||
|
circle.join(" -> ")
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CircularVariableDependency{variable, ref circle} => {
|
CircularVariableDependency {
|
||||||
|
variable,
|
||||||
|
ref circle,
|
||||||
|
} => {
|
||||||
if circle.len() == 2 {
|
if circle.len() == 2 {
|
||||||
writeln!(f, "Variable `{}` is defined in terms of itself", variable)?;
|
writeln!(f, "Variable `{}` is defined in terms of itself", variable)?;
|
||||||
} else {
|
} else {
|
||||||
writeln!(f, "Variable `{}` depends on its own value: `{}`",
|
writeln!(
|
||||||
variable, circle.join(" -> "))?;
|
f,
|
||||||
|
"Variable `{}` depends on its own value: `{}`",
|
||||||
|
variable,
|
||||||
|
circle.join(" -> ")
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
InvalidEscapeSequence{character} => {
|
InvalidEscapeSequence { character } => {
|
||||||
let representation = match character {
|
let representation = match character {
|
||||||
'`' => r"\`".to_string(),
|
'`' => r"\`".to_string(),
|
||||||
'\\' => r"\".to_string(),
|
'\\' => r"\".to_string(),
|
||||||
@ -78,37 +138,66 @@ impl<'a> Display for CompilationError<'a> {
|
|||||||
};
|
};
|
||||||
writeln!(f, "`\\{}` is not a valid escape sequence", representation)?;
|
writeln!(f, "`\\{}` is not a valid escape sequence", representation)?;
|
||||||
}
|
}
|
||||||
DuplicateParameter{recipe, parameter} => {
|
DuplicateParameter { recipe, parameter } => {
|
||||||
writeln!(f, "Recipe `{}` has duplicate parameter `{}`", recipe, parameter)?;
|
writeln!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` has duplicate parameter `{}`",
|
||||||
|
recipe, parameter
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
DuplicateVariable{variable} => {
|
DuplicateVariable { variable } => {
|
||||||
writeln!(f, "Variable `{}` has multiple definitions", variable)?;
|
writeln!(f, "Variable `{}` has multiple definitions", variable)?;
|
||||||
}
|
}
|
||||||
UnexpectedToken{ref expected, found} => {
|
UnexpectedToken {
|
||||||
|
ref expected,
|
||||||
|
found,
|
||||||
|
} => {
|
||||||
writeln!(f, "Expected {}, but found {}", Or(expected), found)?;
|
writeln!(f, "Expected {}, but found {}", Or(expected), found)?;
|
||||||
}
|
}
|
||||||
DuplicateDependency{recipe, dependency} => {
|
DuplicateDependency { recipe, dependency } => {
|
||||||
writeln!(f, "Recipe `{}` has duplicate dependency `{}`", recipe, dependency)?;
|
writeln!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` has duplicate dependency `{}`",
|
||||||
|
recipe, dependency
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
DuplicateRecipe{recipe, first} => {
|
DuplicateRecipe { recipe, first } => {
|
||||||
writeln!(f, "Recipe `{}` first defined on line {} is redefined on line {}",
|
writeln!(
|
||||||
recipe, first + 1, self.line + 1)?;
|
f,
|
||||||
|
"Recipe `{}` first defined on line {} is redefined on line {}",
|
||||||
|
recipe,
|
||||||
|
first + 1,
|
||||||
|
self.line + 1
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
DependencyHasParameters{recipe, dependency} => {
|
DependencyHasParameters { recipe, dependency } => {
|
||||||
writeln!(f, "Recipe `{}` depends on `{}` which requires arguments. \
|
writeln!(
|
||||||
Dependencies may not require arguments", recipe, dependency)?;
|
f,
|
||||||
|
"Recipe `{}` depends on `{}` which requires arguments. \
|
||||||
|
Dependencies may not require arguments",
|
||||||
|
recipe, dependency
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
ParameterShadowsVariable{parameter} => {
|
ParameterShadowsVariable { parameter } => {
|
||||||
writeln!(f, "Parameter `{}` shadows variable of the same name", parameter)?;
|
writeln!(
|
||||||
|
f,
|
||||||
|
"Parameter `{}` shadows variable of the same name",
|
||||||
|
parameter
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
RequiredParameterFollowsDefaultParameter{parameter} => {
|
RequiredParameterFollowsDefaultParameter { parameter } => {
|
||||||
writeln!(f, "Non-default parameter `{}` follows default parameter", parameter)?;
|
writeln!(
|
||||||
|
f,
|
||||||
|
"Non-default parameter `{}` follows default parameter",
|
||||||
|
parameter
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
ParameterFollowsVariadicParameter{parameter} => {
|
ParameterFollowsVariadicParameter { parameter } => {
|
||||||
writeln!(f, "Parameter `{}` follows variadic parameter", parameter)?;
|
writeln!(f, "Parameter `{}` follows variadic parameter", parameter)?;
|
||||||
}
|
}
|
||||||
MixedLeadingWhitespace{whitespace} => {
|
MixedLeadingWhitespace { whitespace } => {
|
||||||
writeln!(f,
|
writeln!(
|
||||||
|
f,
|
||||||
"Found a mix of tabs and spaces in leading whitespace: `{}`\n\
|
"Found a mix of tabs and spaces in leading whitespace: `{}`\n\
|
||||||
Leading whitespace may consist of tabs or spaces, but not both",
|
Leading whitespace may consist of tabs or spaces, but not both",
|
||||||
show_whitespace(whitespace)
|
show_whitespace(whitespace)
|
||||||
@ -117,30 +206,43 @@ impl<'a> Display for CompilationError<'a> {
|
|||||||
ExtraLeadingWhitespace => {
|
ExtraLeadingWhitespace => {
|
||||||
writeln!(f, "Recipe line has extra leading whitespace")?;
|
writeln!(f, "Recipe line has extra leading whitespace")?;
|
||||||
}
|
}
|
||||||
FunctionArgumentCountMismatch{function, found, expected} => {
|
FunctionArgumentCountMismatch {
|
||||||
|
function,
|
||||||
|
found,
|
||||||
|
expected,
|
||||||
|
} => {
|
||||||
writeln!(
|
writeln!(
|
||||||
f,
|
f,
|
||||||
"Function `{}` called with {} argument{} but takes {}",
|
"Function `{}` called with {} argument{} but takes {}",
|
||||||
function, found, maybe_s(found), expected
|
function,
|
||||||
|
found,
|
||||||
|
maybe_s(found),
|
||||||
|
expected
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
InconsistentLeadingWhitespace{expected, found} => {
|
InconsistentLeadingWhitespace { expected, found } => {
|
||||||
writeln!(f,
|
writeln!(
|
||||||
|
f,
|
||||||
"Recipe line has inconsistent leading whitespace. \
|
"Recipe line has inconsistent leading whitespace. \
|
||||||
Recipe started with `{}` but found line with `{}`",
|
Recipe started with `{}` but found line with `{}`",
|
||||||
show_whitespace(expected), show_whitespace(found)
|
show_whitespace(expected),
|
||||||
|
show_whitespace(found)
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
OuterShebang => {
|
OuterShebang => {
|
||||||
writeln!(f, "`#!` is reserved syntax outside of recipes")?;
|
writeln!(f, "`#!` is reserved syntax outside of recipes")?;
|
||||||
}
|
}
|
||||||
UnknownDependency{recipe, unknown} => {
|
UnknownDependency { recipe, unknown } => {
|
||||||
writeln!(f, "Recipe `{}` has unknown dependency `{}`", recipe, unknown)?;
|
writeln!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` has unknown dependency `{}`",
|
||||||
|
recipe, unknown
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
UndefinedVariable{variable} => {
|
UndefinedVariable { variable } => {
|
||||||
writeln!(f, "Variable `{}` not defined", variable)?;
|
writeln!(f, "Variable `{}` not defined", variable)?;
|
||||||
}
|
}
|
||||||
UnknownFunction{function} => {
|
UnknownFunction { function } => {
|
||||||
writeln!(f, "Call to unknown function `{}`", function)?;
|
writeln!(f, "Call to unknown function `{}`", function)?;
|
||||||
}
|
}
|
||||||
UnknownStartOfToken => {
|
UnknownStartOfToken => {
|
||||||
@ -152,10 +254,13 @@ impl<'a> Display for CompilationError<'a> {
|
|||||||
UnterminatedString => {
|
UnterminatedString => {
|
||||||
writeln!(f, "Unterminated string")?;
|
writeln!(f, "Unterminated string")?;
|
||||||
}
|
}
|
||||||
Internal{ref message} => {
|
Internal { ref message } => {
|
||||||
writeln!(f, "Internal error, this may indicate a bug in just: {}\n\
|
writeln!(
|
||||||
|
f,
|
||||||
|
"Internal error, this may indicate a bug in just: {}\n\
|
||||||
consider filing an issue: https://github.com/casey/just/issues/new",
|
consider filing an issue: https://github.com/casey/just/issues/new",
|
||||||
message)?;
|
message
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,10 +8,13 @@ pub struct CookedString<'a> {
|
|||||||
|
|
||||||
impl<'a> CookedString<'a> {
|
impl<'a> CookedString<'a> {
|
||||||
pub fn new(token: &Token<'a>) -> CompilationResult<'a, CookedString<'a>> {
|
pub fn new(token: &Token<'a>) -> CompilationResult<'a, CookedString<'a>> {
|
||||||
let raw = &token.lexeme[1..token.lexeme.len()-1];
|
let raw = &token.lexeme[1..token.lexeme.len() - 1];
|
||||||
|
|
||||||
if let TokenKind::RawString = token.kind {
|
if let TokenKind::RawString = token.kind {
|
||||||
Ok(CookedString{cooked: raw.to_string(), raw})
|
Ok(CookedString {
|
||||||
|
cooked: raw.to_string(),
|
||||||
|
raw,
|
||||||
|
})
|
||||||
} else if let TokenKind::StringToken = token.kind {
|
} else if let TokenKind::StringToken = token.kind {
|
||||||
let mut cooked = String::new();
|
let mut cooked = String::new();
|
||||||
let mut escape = false;
|
let mut escape = false;
|
||||||
@ -23,9 +26,11 @@ impl<'a> CookedString<'a> {
|
|||||||
't' => cooked.push('\t'),
|
't' => cooked.push('\t'),
|
||||||
'\\' => cooked.push('\\'),
|
'\\' => cooked.push('\\'),
|
||||||
'"' => cooked.push('"'),
|
'"' => cooked.push('"'),
|
||||||
other => return Err(token.error(CompilationErrorKind::InvalidEscapeSequence {
|
other => {
|
||||||
character: other,
|
return Err(
|
||||||
})),
|
token.error(CompilationErrorKind::InvalidEscapeSequence { character: other }),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
escape = false;
|
escape = false;
|
||||||
continue;
|
continue;
|
||||||
@ -36,10 +41,10 @@ impl<'a> CookedString<'a> {
|
|||||||
}
|
}
|
||||||
cooked.push(c);
|
cooked.push(c);
|
||||||
}
|
}
|
||||||
Ok(CookedString{raw, cooked})
|
Ok(CookedString { raw, cooked })
|
||||||
} else {
|
} else {
|
||||||
Err(token.error(CompilationErrorKind::Internal {
|
Err(token.error(CompilationErrorKind::Internal {
|
||||||
message: "cook_string() called on non-string token".to_string()
|
message: "cook_string() called on non-string token".to_string(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,35 +2,50 @@ use common::*;
|
|||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
pub enum Expression<'a> {
|
pub enum Expression<'a> {
|
||||||
Backtick{raw: &'a str, token: Token<'a>},
|
Backtick {
|
||||||
Call{name: &'a str, token: Token<'a>, arguments: Vec<Expression<'a>>},
|
raw: &'a str,
|
||||||
Concatination{lhs: Box<Expression<'a>>, rhs: Box<Expression<'a>>},
|
token: Token<'a>,
|
||||||
String{cooked_string: CookedString<'a>},
|
},
|
||||||
Variable{name: &'a str, token: Token<'a>},
|
Call {
|
||||||
|
name: &'a str,
|
||||||
|
token: Token<'a>,
|
||||||
|
arguments: Vec<Expression<'a>>,
|
||||||
|
},
|
||||||
|
Concatination {
|
||||||
|
lhs: Box<Expression<'a>>,
|
||||||
|
rhs: Box<Expression<'a>>,
|
||||||
|
},
|
||||||
|
String {
|
||||||
|
cooked_string: CookedString<'a>,
|
||||||
|
},
|
||||||
|
Variable {
|
||||||
|
name: &'a str,
|
||||||
|
token: Token<'a>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Expression<'a> {
|
impl<'a> Expression<'a> {
|
||||||
pub fn variables(&'a self) -> Variables<'a> {
|
pub fn variables(&'a self) -> Variables<'a> {
|
||||||
Variables {
|
Variables { stack: vec![self] }
|
||||||
stack: vec![self],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn functions(&'a self) -> Functions<'a> {
|
pub fn functions(&'a self) -> Functions<'a> {
|
||||||
Functions {
|
Functions { stack: vec![self] }
|
||||||
stack: vec![self],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Display for Expression<'a> {
|
impl<'a> Display for Expression<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
match *self {
|
match *self {
|
||||||
Expression::Backtick {raw, .. } => write!(f, "`{}`", raw)?,
|
Expression::Backtick { raw, .. } => write!(f, "`{}`", raw)?,
|
||||||
Expression::Concatination{ref lhs, ref rhs } => write!(f, "{} + {}", lhs, rhs)?,
|
Expression::Concatination { ref lhs, ref rhs } => write!(f, "{} + {}", lhs, rhs)?,
|
||||||
Expression::String {ref cooked_string } => write!(f, "\"{}\"", cooked_string.raw)?,
|
Expression::String { ref cooked_string } => write!(f, "\"{}\"", cooked_string.raw)?,
|
||||||
Expression::Variable {name, .. } => write!(f, "{}", name)?,
|
Expression::Variable { name, .. } => write!(f, "{}", name)?,
|
||||||
Expression::Call {name, ref arguments, ..} => {
|
Expression::Call {
|
||||||
|
name,
|
||||||
|
ref arguments,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
write!(f, "{}(", name)?;
|
write!(f, "{}(", name)?;
|
||||||
for (i, argument) in arguments.iter().enumerate() {
|
for (i, argument) in arguments.iter().enumerate() {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
@ -56,11 +71,11 @@ impl<'a> Iterator for Variables<'a> {
|
|||||||
fn next(&mut self) -> Option<&'a Token<'a>> {
|
fn next(&mut self) -> Option<&'a Token<'a>> {
|
||||||
match self.stack.pop() {
|
match self.stack.pop() {
|
||||||
None
|
None
|
||||||
| Some(&Expression::String{..})
|
| Some(&Expression::String { .. })
|
||||||
| Some(&Expression::Backtick{..})
|
| Some(&Expression::Backtick { .. })
|
||||||
| Some(&Expression::Call{..}) => None,
|
| Some(&Expression::Call { .. }) => None,
|
||||||
Some(&Expression::Variable{ref token,..}) => Some(token),
|
Some(&Expression::Variable { ref token, .. }) => Some(token),
|
||||||
Some(&Expression::Concatination{ref lhs, ref rhs}) => {
|
Some(&Expression::Concatination { ref lhs, ref rhs }) => {
|
||||||
self.stack.push(lhs);
|
self.stack.push(lhs);
|
||||||
self.stack.push(rhs);
|
self.stack.push(rhs);
|
||||||
self.next()
|
self.next()
|
||||||
@ -79,11 +94,15 @@ impl<'a> Iterator for Functions<'a> {
|
|||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
match self.stack.pop() {
|
match self.stack.pop() {
|
||||||
None
|
None
|
||||||
| Some(&Expression::String{..})
|
| Some(&Expression::String { .. })
|
||||||
| Some(&Expression::Backtick{..})
|
| Some(&Expression::Backtick { .. })
|
||||||
| Some(&Expression::Variable{..}) => None,
|
| Some(&Expression::Variable { .. }) => None,
|
||||||
Some(&Expression::Call{ref token, ref arguments, ..}) => Some((token, arguments.len())),
|
Some(&Expression::Call {
|
||||||
Some(&Expression::Concatination{ref lhs, ref rhs}) => {
|
ref token,
|
||||||
|
ref arguments,
|
||||||
|
..
|
||||||
|
}) => Some((token, arguments.len())),
|
||||||
|
Some(&Expression::Concatination { ref lhs, ref rhs }) => {
|
||||||
self.stack.push(lhs);
|
self.stack.push(lhs);
|
||||||
self.stack.push(rhs);
|
self.stack.push(rhs);
|
||||||
self.next()
|
self.next()
|
||||||
|
@ -2,14 +2,14 @@ use common::*;
|
|||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
pub enum Fragment<'a> {
|
pub enum Fragment<'a> {
|
||||||
Text{text: Token<'a>},
|
Text { text: Token<'a> },
|
||||||
Expression{expression: Expression<'a>},
|
Expression { expression: Expression<'a> },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Fragment<'a> {
|
impl<'a> Fragment<'a> {
|
||||||
pub fn continuation(&self) -> bool {
|
pub fn continuation(&self) -> bool {
|
||||||
match *self {
|
match *self {
|
||||||
Fragment::Text{ref text} => text.lexeme.ends_with('\\'),
|
Fragment::Text { ref text } => text.lexeme.ends_with('\\'),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,19 +6,24 @@ use platform::{Platform, PlatformInterface};
|
|||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref FUNCTIONS: Map<&'static str, Function> = vec![
|
static ref FUNCTIONS: Map<&'static str, Function> = vec![
|
||||||
("arch", Function::Nullary(arch )),
|
("arch", Function::Nullary(arch)),
|
||||||
("os", Function::Nullary(os )),
|
("os", Function::Nullary(os)),
|
||||||
("os_family", Function::Nullary(os_family )),
|
("os_family", Function::Nullary(os_family)),
|
||||||
("env_var", Function::Unary (env_var )),
|
("env_var", Function::Unary(env_var)),
|
||||||
("env_var_or_default", Function::Binary (env_var_or_default)),
|
("env_var_or_default", Function::Binary(env_var_or_default)),
|
||||||
("invocation_directory", Function::Nullary(invocation_directory)),
|
(
|
||||||
].into_iter().collect();
|
"invocation_directory",
|
||||||
|
Function::Nullary(invocation_directory)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Function {
|
enum Function {
|
||||||
Nullary(fn(&FunctionContext, ) -> Result<String, String>),
|
Nullary(fn(&FunctionContext) -> Result<String, String>),
|
||||||
Unary (fn(&FunctionContext, &str ) -> Result<String, String>),
|
Unary(fn(&FunctionContext, &str) -> Result<String, String>),
|
||||||
Binary (fn(&FunctionContext, &str, &str) -> Result<String, String>),
|
Binary(fn(&FunctionContext, &str, &str) -> Result<String, String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Function {
|
impl Function {
|
||||||
@ -42,17 +47,19 @@ pub fn resolve_function<'a>(token: &Token<'a>, argc: usize) -> CompilationResult
|
|||||||
if let Some(function) = FUNCTIONS.get(&name) {
|
if let Some(function) = FUNCTIONS.get(&name) {
|
||||||
use self::Function::*;
|
use self::Function::*;
|
||||||
match (function, argc) {
|
match (function, argc) {
|
||||||
(&Nullary(_), 0) |
|
(&Nullary(_), 0) | (&Unary(_), 1) | (&Binary(_), 2) => Ok(()),
|
||||||
(&Unary(_), 1) |
|
_ => Err(
|
||||||
(&Binary(_), 2) => Ok(()),
|
token.error(CompilationErrorKind::FunctionArgumentCountMismatch {
|
||||||
_ => {
|
function: name,
|
||||||
Err(token.error(CompilationErrorKind::FunctionArgumentCountMismatch{
|
found: argc,
|
||||||
function: name, found: argc, expected: function.argc(),
|
expected: function.argc(),
|
||||||
}))
|
}),
|
||||||
}
|
),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(token.error(CompilationErrorKind::UnknownFunction{function: token.lexeme}))
|
Err(token.error(CompilationErrorKind::UnknownFunction {
|
||||||
|
function: token.lexeme,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,27 +67,36 @@ pub fn evaluate_function<'a>(
|
|||||||
token: &Token<'a>,
|
token: &Token<'a>,
|
||||||
name: &'a str,
|
name: &'a str,
|
||||||
context: &FunctionContext,
|
context: &FunctionContext,
|
||||||
arguments: &[String]
|
arguments: &[String],
|
||||||
) -> RunResult<'a, String> {
|
) -> RunResult<'a, String> {
|
||||||
if let Some(function) = FUNCTIONS.get(name) {
|
if let Some(function) = FUNCTIONS.get(name) {
|
||||||
use self::Function::*;
|
use self::Function::*;
|
||||||
let argc = arguments.len();
|
let argc = arguments.len();
|
||||||
match (function, argc) {
|
match (function, argc) {
|
||||||
(&Nullary(f), 0) => f(context)
|
(&Nullary(f), 0) => f(context).map_err(|message| RuntimeError::FunctionCall {
|
||||||
.map_err(|message| RuntimeError::FunctionCall{token: token.clone(), message}),
|
token: token.clone(),
|
||||||
(&Unary(f), 1) => f(context, &arguments[0])
|
message,
|
||||||
.map_err(|message| RuntimeError::FunctionCall{token: token.clone(), message}),
|
}),
|
||||||
(&Binary(f), 2) => f(context, &arguments[0], &arguments[1])
|
(&Unary(f), 1) => f(context, &arguments[0]).map_err(|message| RuntimeError::FunctionCall {
|
||||||
.map_err(|message| RuntimeError::FunctionCall{token: token.clone(), message}),
|
token: token.clone(),
|
||||||
_ => {
|
message,
|
||||||
Err(RuntimeError::Internal {
|
}),
|
||||||
message: format!("attempted to evaluate function `{}` with {} arguments", name, argc)
|
(&Binary(f), 2) => {
|
||||||
|
f(context, &arguments[0], &arguments[1]).map_err(|message| RuntimeError::FunctionCall {
|
||||||
|
token: token.clone(),
|
||||||
|
message,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
_ => Err(RuntimeError::Internal {
|
||||||
|
message: format!(
|
||||||
|
"attempted to evaluate function `{}` with {} arguments",
|
||||||
|
name, argc
|
||||||
|
),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(RuntimeError::Internal {
|
Err(RuntimeError::Internal {
|
||||||
message: format!("attempted to evaluate unknown function: `{}`", name)
|
message: format!("attempted to evaluate unknown function: `{}`", name),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,9 +114,9 @@ pub fn os_family(_context: &FunctionContext) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn invocation_directory(context: &FunctionContext) -> Result<String, String> {
|
pub fn invocation_directory(context: &FunctionContext) -> Result<String, String> {
|
||||||
context.invocation_directory.clone()
|
context.invocation_directory.clone().and_then(|s| {
|
||||||
.and_then(|s| Platform::to_shell_path(&s)
|
Platform::to_shell_path(&s).map_err(|e| format!("Error getting shell path: {}", e))
|
||||||
.map_err(|e| format!("Error getting shell path: {}", e)))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn env_var(context: &FunctionContext, key: &str) -> Result<String, String> {
|
pub fn env_var(context: &FunctionContext, key: &str) -> Result<String, String> {
|
||||||
@ -112,8 +128,10 @@ pub fn env_var(context: &FunctionContext, key: &str) -> Result<String, String> {
|
|||||||
|
|
||||||
match env::var(key) {
|
match env::var(key) {
|
||||||
Err(NotPresent) => Err(format!("environment variable `{}` not present", key)),
|
Err(NotPresent) => Err(format!("environment variable `{}` not present", key)),
|
||||||
Err(NotUnicode(os_string)) =>
|
Err(NotUnicode(os_string)) => Err(format!(
|
||||||
Err(format!("environment variable `{}` not unicode: {:?}", key, os_string)),
|
"environment variable `{}` not unicode: {:?}",
|
||||||
|
key, os_string
|
||||||
|
)),
|
||||||
Ok(value) => Ok(value),
|
Ok(value) => Ok(value),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -130,8 +148,10 @@ pub fn env_var_or_default(
|
|||||||
use std::env::VarError::*;
|
use std::env::VarError::*;
|
||||||
match env::var(key) {
|
match env::var(key) {
|
||||||
Err(NotPresent) => Ok(default.to_string()),
|
Err(NotPresent) => Ok(default.to_string()),
|
||||||
Err(NotUnicode(os_string)) =>
|
Err(NotUnicode(os_string)) => Err(format!(
|
||||||
Err(format!("environment variable `{}` not unicode: {:?}", key, os_string)),
|
"environment variable `{}` not unicode: {:?}",
|
||||||
|
key, os_string
|
||||||
|
)),
|
||||||
Ok(value) => Ok(value),
|
Ok(value) => Ok(value),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ use common::*;
|
|||||||
|
|
||||||
pub fn compile(text: &str) {
|
pub fn compile(text: &str) {
|
||||||
if let Err(error) = Parser::parse(text) {
|
if let Err(error) = Parser::parse(text) {
|
||||||
if let CompilationErrorKind::Internal{..} = error.kind {
|
if let CompilationErrorKind::Internal { .. } = error.kind {
|
||||||
panic!("{}", error)
|
panic!("{}", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -126,17 +126,15 @@ impl<'a> Justfile<'a> where {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let context = RecipeContext{invocation_directory, configuration, scope};
|
let context = RecipeContext {
|
||||||
|
invocation_directory,
|
||||||
|
configuration,
|
||||||
|
scope,
|
||||||
|
};
|
||||||
|
|
||||||
let mut ran = empty();
|
let mut ran = empty();
|
||||||
for (recipe, arguments) in grouped {
|
for (recipe, arguments) in grouped {
|
||||||
self.run_recipe(
|
self.run_recipe(&context, recipe, arguments, &dotenv, &mut ran)?
|
||||||
&context,
|
|
||||||
recipe,
|
|
||||||
arguments,
|
|
||||||
&dotenv,
|
|
||||||
&mut ran,
|
|
||||||
)?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -152,21 +150,10 @@ impl<'a> Justfile<'a> where {
|
|||||||
) -> RunResult<()> {
|
) -> RunResult<()> {
|
||||||
for dependency_name in &recipe.dependencies {
|
for dependency_name in &recipe.dependencies {
|
||||||
if !ran.contains(dependency_name) {
|
if !ran.contains(dependency_name) {
|
||||||
self.run_recipe(
|
self.run_recipe(context, &self.recipes[dependency_name], &[], dotenv, ran)?;
|
||||||
context,
|
|
||||||
&self.recipes[dependency_name],
|
|
||||||
&[],
|
|
||||||
dotenv,
|
|
||||||
ran,
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
recipe.run(
|
recipe.run(context, arguments, dotenv, &self.exports)?;
|
||||||
context,
|
|
||||||
arguments,
|
|
||||||
dotenv,
|
|
||||||
&self.exports,
|
|
||||||
)?;
|
|
||||||
ran.insert(recipe.name);
|
ran.insert(recipe.name);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -313,10 +300,7 @@ a return code:
|
|||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
} => {
|
} => {
|
||||||
let param_names = parameters
|
let param_names = parameters.iter().map(|p| p.name).collect::<Vec<&str>>();
|
||||||
.iter()
|
|
||||||
.map(|p| p.name)
|
|
||||||
.collect::<Vec<&str>>();
|
|
||||||
assert_eq!(recipe, "a");
|
assert_eq!(recipe, "a");
|
||||||
assert_eq!(param_names, ["b", "c", "d"]);
|
assert_eq!(param_names, ["b", "c", "d"]);
|
||||||
assert_eq!(found, 2);
|
assert_eq!(found, 2);
|
||||||
@ -340,10 +324,7 @@ a return code:
|
|||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
} => {
|
} => {
|
||||||
let param_names = parameters
|
let param_names = parameters.iter().map(|p| p.name).collect::<Vec<&str>>();
|
||||||
.iter()
|
|
||||||
.map(|p| p.name)
|
|
||||||
.collect::<Vec<&str>>();
|
|
||||||
assert_eq!(recipe, "a");
|
assert_eq!(recipe, "a");
|
||||||
assert_eq!(param_names, ["b", "c", "d"]);
|
assert_eq!(param_names, ["b", "c", "d"]);
|
||||||
assert_eq!(found, 2);
|
assert_eq!(found, 2);
|
||||||
@ -367,10 +348,7 @@ a return code:
|
|||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
} => {
|
} => {
|
||||||
let param_names = parameters
|
let param_names = parameters.iter().map(|p| p.name).collect::<Vec<&str>>();
|
||||||
.iter()
|
|
||||||
.map(|p| p.name)
|
|
||||||
.collect::<Vec<&str>>();
|
|
||||||
assert_eq!(recipe, "a");
|
assert_eq!(recipe, "a");
|
||||||
assert_eq!(param_names, ["b", "c", "d"]);
|
assert_eq!(param_names, ["b", "c", "d"]);
|
||||||
assert_eq!(found, 0);
|
assert_eq!(found, 0);
|
||||||
@ -394,10 +372,7 @@ a return code:
|
|||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
} => {
|
} => {
|
||||||
let param_names = parameters
|
let param_names = parameters.iter().map(|p| p.name).collect::<Vec<&str>>();
|
||||||
.iter()
|
|
||||||
.map(|p| p.name)
|
|
||||||
.collect::<Vec<&str>>();
|
|
||||||
assert_eq!(recipe, "a");
|
assert_eq!(recipe, "a");
|
||||||
assert_eq!(param_names, ["b", "c", "d"]);
|
assert_eq!(param_names, ["b", "c", "d"]);
|
||||||
assert_eq!(found, 1);
|
assert_eq!(found, 1);
|
||||||
@ -421,10 +396,7 @@ a return code:
|
|||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
} => {
|
} => {
|
||||||
let param_names = parameters
|
let param_names = parameters.iter().map(|p| p.name).collect::<Vec<&str>>();
|
||||||
.iter()
|
|
||||||
.map(|p| p.name)
|
|
||||||
.collect::<Vec<&str>>();
|
|
||||||
assert_eq!(recipe, "a");
|
assert_eq!(recipe, "a");
|
||||||
assert_eq!(param_names, ["b", "c", "d"]);
|
assert_eq!(param_names, ["b", "c", "d"]);
|
||||||
assert_eq!(found, 0);
|
assert_eq!(found, 0);
|
||||||
|
217
src/lexer.rs
217
src/lexer.rs
@ -1,7 +1,7 @@
|
|||||||
use common::*;
|
use common::*;
|
||||||
|
|
||||||
use TokenKind::*;
|
|
||||||
use CompilationErrorKind::*;
|
use CompilationErrorKind::*;
|
||||||
|
use TokenKind::*;
|
||||||
|
|
||||||
fn re(pattern: &str) -> Regex {
|
fn re(pattern: &str) -> Regex {
|
||||||
Regex::new(pattern).unwrap()
|
Regex::new(pattern).unwrap()
|
||||||
@ -39,7 +39,7 @@ enum State<'a> {
|
|||||||
|
|
||||||
impl<'a> Lexer<'a> {
|
impl<'a> Lexer<'a> {
|
||||||
pub fn lex(text: &'a str) -> CompilationResult<Vec<Token<'a>>> {
|
pub fn lex(text: &'a str) -> CompilationResult<Vec<Token<'a>>> {
|
||||||
let lexer = Lexer{
|
let lexer = Lexer {
|
||||||
tokens: vec![],
|
tokens: vec![],
|
||||||
rest: text,
|
rest: text,
|
||||||
index: 0,
|
index: 0,
|
||||||
@ -80,19 +80,21 @@ impl<'a> Lexer<'a> {
|
|||||||
static ref INDENT: Regex = re(r"^([ \t]*)[^ \t\n\r]");
|
static ref INDENT: Regex = re(r"^([ \t]*)[^ \t\n\r]");
|
||||||
}
|
}
|
||||||
|
|
||||||
let indentation = INDENT.captures(self.rest).map(|captures| captures.get(1).unwrap().as_str());
|
let indentation = INDENT
|
||||||
|
.captures(self.rest)
|
||||||
|
.map(|captures| captures.get(1).unwrap().as_str());
|
||||||
|
|
||||||
if self.column == 0 {
|
if self.column == 0 {
|
||||||
if let Some(kind) = match (self.state.last().unwrap(), indentation) {
|
if let Some(kind) = match (self.state.last().unwrap(), indentation) {
|
||||||
// ignore: was no indentation and there still isn't
|
// ignore: was no indentation and there still isn't
|
||||||
// or current line is blank
|
// or current line is blank
|
||||||
(&State::Start, Some("")) | (_, None) => {
|
(&State::Start, Some("")) | (_, None) => None,
|
||||||
None
|
|
||||||
}
|
|
||||||
// indent: was no indentation, now there is
|
// indent: was no indentation, now there is
|
||||||
(&State::Start, Some(current)) => {
|
(&State::Start, Some(current)) => {
|
||||||
if mixed_whitespace(current) {
|
if mixed_whitespace(current) {
|
||||||
return Err(self.error(MixedLeadingWhitespace{whitespace: current}));
|
return Err(self.error(MixedLeadingWhitespace {
|
||||||
|
whitespace: current,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
//indent = Some(current);
|
//indent = Some(current);
|
||||||
self.state.push(State::Indent(current));
|
self.state.push(State::Indent(current));
|
||||||
@ -107,9 +109,9 @@ impl<'a> Lexer<'a> {
|
|||||||
// was indentation and still is, check if the new indentation matches
|
// was indentation and still is, check if the new indentation matches
|
||||||
(&State::Indent(previous), Some(current)) => {
|
(&State::Indent(previous), Some(current)) => {
|
||||||
if !current.starts_with(previous) {
|
if !current.starts_with(previous) {
|
||||||
return Err(self.error(InconsistentLeadingWhitespace{
|
return Err(self.error(InconsistentLeadingWhitespace {
|
||||||
expected: previous,
|
expected: previous,
|
||||||
found: current
|
found: current,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@ -117,7 +119,7 @@ impl<'a> Lexer<'a> {
|
|||||||
// at column 0 in some other state: this should never happen
|
// at column 0 in some other state: this should never happen
|
||||||
(&State::Text, _) | (&State::Interpolation, _) => {
|
(&State::Text, _) | (&State::Interpolation, _) => {
|
||||||
return Err(self.error(Internal {
|
return Err(self.error(Internal {
|
||||||
message: "unexpected state at column 0".to_string()
|
message: "unexpected state at column 0".to_string(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} {
|
} {
|
||||||
@ -129,27 +131,27 @@ impl<'a> Lexer<'a> {
|
|||||||
|
|
||||||
pub fn inner(mut self) -> CompilationResult<'a, Vec<Token<'a>>> {
|
pub fn inner(mut self) -> CompilationResult<'a, Vec<Token<'a>>> {
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref AT: Regex = token(r"@" );
|
static ref AT: Regex = token(r"@");
|
||||||
static ref BACKTICK: Regex = token(r"`[^`\n\r]*`" );
|
static ref BACKTICK: Regex = token(r"`[^`\n\r]*`");
|
||||||
static ref COLON: Regex = token(r":" );
|
static ref COLON: Regex = token(r":");
|
||||||
static ref COMMA: Regex = token(r"," );
|
static ref COMMA: Regex = token(r",");
|
||||||
static ref COMMENT: Regex = token(r"#([^!\n\r][^\n\r]*)?\r?$" );
|
static ref COMMENT: Regex = token(r"#([^!\n\r][^\n\r]*)?\r?$");
|
||||||
static ref EOF: Regex = token(r"\z" );
|
static ref EOF: Regex = token(r"\z");
|
||||||
static ref EOL: Regex = token(r"\n|\r\n" );
|
static ref EOL: Regex = token(r"\n|\r\n");
|
||||||
static ref EQUALS: Regex = token(r"=" );
|
static ref EQUALS: Regex = token(r"=");
|
||||||
static ref INTERPOLATION_END: Regex = token(r"[}][}]" );
|
static ref INTERPOLATION_END: Regex = token(r"[}][}]");
|
||||||
static ref INTERPOLATION_START_TOKEN: Regex = token(r"[{][{]" );
|
static ref INTERPOLATION_START_TOKEN: Regex = token(r"[{][{]");
|
||||||
static ref NAME: Regex = token(r"([a-zA-Z_][a-zA-Z0-9_-]*)" );
|
static ref NAME: Regex = token(r"([a-zA-Z_][a-zA-Z0-9_-]*)");
|
||||||
static ref PAREN_L: Regex = token(r"[(]" );
|
static ref PAREN_L: Regex = token(r"[(]");
|
||||||
static ref PAREN_R: Regex = token(r"[)]" );
|
static ref PAREN_R: Regex = token(r"[)]");
|
||||||
static ref PLUS: Regex = token(r"[+]" );
|
static ref PLUS: Regex = token(r"[+]");
|
||||||
static ref RAW_STRING: Regex = token(r#"'[^']*'"# );
|
static ref RAW_STRING: Regex = token(r#"'[^']*'"#);
|
||||||
static ref STRING: Regex = token(r#"["]"# );
|
static ref STRING: Regex = token(r#"["]"#);
|
||||||
static ref UNTERMINATED_RAW_STRING: Regex = token(r#"'[^']*"# );
|
static ref UNTERMINATED_RAW_STRING: Regex = token(r#"'[^']*"#);
|
||||||
static ref INTERPOLATION_START: Regex = re(r"^[{][{]" );
|
static ref INTERPOLATION_START: Regex = re(r"^[{][{]");
|
||||||
static ref LEADING_TEXT: Regex = re(r"^(?m)(.+?)[{][{]" );
|
static ref LEADING_TEXT: Regex = re(r"^(?m)(.+?)[{][{]");
|
||||||
static ref LINE: Regex = re(r"^(?m)[ \t]+[^ \t\n\r].*$");
|
static ref LINE: Regex = re(r"^(?m)[ \t]+[^ \t\n\r].*$");
|
||||||
static ref TEXT: Regex = re(r"^(?m)(.+)" );
|
static ref TEXT: Regex = re(r"^(?m)(.+)");
|
||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@ -163,17 +165,25 @@ impl<'a> Lexer<'a> {
|
|||||||
self.tokens.push(token);
|
self.tokens.push(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (prefix, lexeme, kind) =
|
let (prefix, lexeme, kind) = if let (0, &State::Indent(indent), Some(captures)) = (
|
||||||
if let (0, &State::Indent(indent), Some(captures)) =
|
self.column,
|
||||||
(self.column, self.state.last().unwrap(), LINE.captures(self.rest)) {
|
self.state.last().unwrap(),
|
||||||
|
LINE.captures(self.rest),
|
||||||
|
) {
|
||||||
let line = captures.get(0).unwrap().as_str();
|
let line = captures.get(0).unwrap().as_str();
|
||||||
if !line.starts_with(indent) {
|
if !line.starts_with(indent) {
|
||||||
return Err(self.error(Internal{message: "unexpected indent".to_string()}));
|
return Err(self.error(Internal {
|
||||||
|
message: "unexpected indent".to_string(),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
self.state.push(State::Text);
|
self.state.push(State::Text);
|
||||||
(&line[0..indent.len()], "", Line)
|
(&line[0..indent.len()], "", Line)
|
||||||
} else if let Some(captures) = EOF.captures(self.rest) {
|
} else if let Some(captures) = EOF.captures(self.rest) {
|
||||||
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Eof)
|
(
|
||||||
|
captures.get(1).unwrap().as_str(),
|
||||||
|
captures.get(2).unwrap().as_str(),
|
||||||
|
Eof,
|
||||||
|
)
|
||||||
} else if let State::Text = *self.state.last().unwrap() {
|
} else if let State::Text = *self.state.last().unwrap() {
|
||||||
if let Some(captures) = INTERPOLATION_START.captures(self.rest) {
|
if let Some(captures) = INTERPOLATION_START.captures(self.rest) {
|
||||||
self.state.push(State::Interpolation);
|
self.state.push(State::Interpolation);
|
||||||
@ -184,51 +194,111 @@ impl<'a> Lexer<'a> {
|
|||||||
("", captures.get(1).unwrap().as_str(), Text)
|
("", captures.get(1).unwrap().as_str(), Text)
|
||||||
} else if let Some(captures) = EOL.captures(self.rest) {
|
} else if let Some(captures) = EOL.captures(self.rest) {
|
||||||
self.state.pop();
|
self.state.pop();
|
||||||
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Eol)
|
(
|
||||||
|
captures.get(1).unwrap().as_str(),
|
||||||
|
captures.get(2).unwrap().as_str(),
|
||||||
|
Eol,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return Err(self.error(Internal {
|
return Err(self.error(Internal {
|
||||||
message: format!("Could not match token in text state: \"{}\"", self.rest)
|
message: format!("Could not match token in text state: \"{}\"", self.rest),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} else if let Some(captures) = INTERPOLATION_START_TOKEN.captures(self.rest) {
|
} else if let Some(captures) = INTERPOLATION_START_TOKEN.captures(self.rest) {
|
||||||
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), InterpolationStart)
|
(
|
||||||
|
captures.get(1).unwrap().as_str(),
|
||||||
|
captures.get(2).unwrap().as_str(),
|
||||||
|
InterpolationStart,
|
||||||
|
)
|
||||||
} else if let Some(captures) = INTERPOLATION_END.captures(self.rest) {
|
} else if let Some(captures) = INTERPOLATION_END.captures(self.rest) {
|
||||||
if self.state.last().unwrap() == &State::Interpolation {
|
if self.state.last().unwrap() == &State::Interpolation {
|
||||||
self.state.pop();
|
self.state.pop();
|
||||||
}
|
}
|
||||||
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), InterpolationEnd)
|
(
|
||||||
|
captures.get(1).unwrap().as_str(),
|
||||||
|
captures.get(2).unwrap().as_str(),
|
||||||
|
InterpolationEnd,
|
||||||
|
)
|
||||||
} else if let Some(captures) = NAME.captures(self.rest) {
|
} else if let Some(captures) = NAME.captures(self.rest) {
|
||||||
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Name)
|
(
|
||||||
|
captures.get(1).unwrap().as_str(),
|
||||||
|
captures.get(2).unwrap().as_str(),
|
||||||
|
Name,
|
||||||
|
)
|
||||||
} else if let Some(captures) = EOL.captures(self.rest) {
|
} else if let Some(captures) = EOL.captures(self.rest) {
|
||||||
if self.state.last().unwrap() == &State::Interpolation {
|
if self.state.last().unwrap() == &State::Interpolation {
|
||||||
return Err(self.error(UnterminatedInterpolation));
|
return Err(self.error(UnterminatedInterpolation));
|
||||||
}
|
}
|
||||||
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Eol)
|
(
|
||||||
|
captures.get(1).unwrap().as_str(),
|
||||||
|
captures.get(2).unwrap().as_str(),
|
||||||
|
Eol,
|
||||||
|
)
|
||||||
} else if let Some(captures) = BACKTICK.captures(self.rest) {
|
} else if let Some(captures) = BACKTICK.captures(self.rest) {
|
||||||
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Backtick)
|
(
|
||||||
|
captures.get(1).unwrap().as_str(),
|
||||||
|
captures.get(2).unwrap().as_str(),
|
||||||
|
Backtick,
|
||||||
|
)
|
||||||
} else if let Some(captures) = COLON.captures(self.rest) {
|
} else if let Some(captures) = COLON.captures(self.rest) {
|
||||||
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Colon)
|
(
|
||||||
|
captures.get(1).unwrap().as_str(),
|
||||||
|
captures.get(2).unwrap().as_str(),
|
||||||
|
Colon,
|
||||||
|
)
|
||||||
} else if let Some(captures) = AT.captures(self.rest) {
|
} else if let Some(captures) = AT.captures(self.rest) {
|
||||||
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), At)
|
(
|
||||||
|
captures.get(1).unwrap().as_str(),
|
||||||
|
captures.get(2).unwrap().as_str(),
|
||||||
|
At,
|
||||||
|
)
|
||||||
} else if let Some(captures) = COMMA.captures(self.rest) {
|
} else if let Some(captures) = COMMA.captures(self.rest) {
|
||||||
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Comma)
|
(
|
||||||
|
captures.get(1).unwrap().as_str(),
|
||||||
|
captures.get(2).unwrap().as_str(),
|
||||||
|
Comma,
|
||||||
|
)
|
||||||
} else if let Some(captures) = PAREN_L.captures(self.rest) {
|
} else if let Some(captures) = PAREN_L.captures(self.rest) {
|
||||||
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), ParenL)
|
(
|
||||||
|
captures.get(1).unwrap().as_str(),
|
||||||
|
captures.get(2).unwrap().as_str(),
|
||||||
|
ParenL,
|
||||||
|
)
|
||||||
} else if let Some(captures) = PAREN_R.captures(self.rest) {
|
} else if let Some(captures) = PAREN_R.captures(self.rest) {
|
||||||
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), ParenR)
|
(
|
||||||
|
captures.get(1).unwrap().as_str(),
|
||||||
|
captures.get(2).unwrap().as_str(),
|
||||||
|
ParenR,
|
||||||
|
)
|
||||||
} else if let Some(captures) = PLUS.captures(self.rest) {
|
} else if let Some(captures) = PLUS.captures(self.rest) {
|
||||||
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Plus)
|
(
|
||||||
|
captures.get(1).unwrap().as_str(),
|
||||||
|
captures.get(2).unwrap().as_str(),
|
||||||
|
Plus,
|
||||||
|
)
|
||||||
} else if let Some(captures) = EQUALS.captures(self.rest) {
|
} else if let Some(captures) = EQUALS.captures(self.rest) {
|
||||||
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Equals)
|
(
|
||||||
|
captures.get(1).unwrap().as_str(),
|
||||||
|
captures.get(2).unwrap().as_str(),
|
||||||
|
Equals,
|
||||||
|
)
|
||||||
} else if let Some(captures) = COMMENT.captures(self.rest) {
|
} else if let Some(captures) = COMMENT.captures(self.rest) {
|
||||||
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Comment)
|
(
|
||||||
|
captures.get(1).unwrap().as_str(),
|
||||||
|
captures.get(2).unwrap().as_str(),
|
||||||
|
Comment,
|
||||||
|
)
|
||||||
} else if let Some(captures) = RAW_STRING.captures(self.rest) {
|
} else if let Some(captures) = RAW_STRING.captures(self.rest) {
|
||||||
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), RawString)
|
(
|
||||||
|
captures.get(1).unwrap().as_str(),
|
||||||
|
captures.get(2).unwrap().as_str(),
|
||||||
|
RawString,
|
||||||
|
)
|
||||||
} else if UNTERMINATED_RAW_STRING.is_match(self.rest) {
|
} else if UNTERMINATED_RAW_STRING.is_match(self.rest) {
|
||||||
return Err(self.error(UnterminatedString));
|
return Err(self.error(UnterminatedString));
|
||||||
} else if let Some(captures) = STRING.captures(self.rest) {
|
} else if let Some(captures) = STRING.captures(self.rest) {
|
||||||
let prefix = captures.get(1).unwrap().as_str();
|
let prefix = captures.get(1).unwrap().as_str();
|
||||||
let contents = &self.rest[prefix.len()+1..];
|
let contents = &self.rest[prefix.len() + 1..];
|
||||||
if contents.is_empty() {
|
if contents.is_empty() {
|
||||||
return Err(self.error(UnterminatedString));
|
return Err(self.error(UnterminatedString));
|
||||||
}
|
}
|
||||||
@ -266,10 +336,12 @@ impl<'a> Lexer<'a> {
|
|||||||
if len == 0 {
|
if len == 0 {
|
||||||
let last = self.tokens.last().unwrap();
|
let last = self.tokens.last().unwrap();
|
||||||
match last.kind {
|
match last.kind {
|
||||||
Eof => {},
|
Eof => {}
|
||||||
_ => return Err(last.error(Internal {
|
_ => {
|
||||||
message: format!("zero length token: {:?}", last)
|
return Err(last.error(Internal {
|
||||||
})),
|
message: format!("zero length token: {:?}", last),
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,37 +386,45 @@ mod test {
|
|||||||
let input = $input;
|
let input = $input;
|
||||||
let expected = $expected;
|
let expected = $expected;
|
||||||
let tokens = ::Lexer::lex(input).unwrap();
|
let tokens = ::Lexer::lex(input).unwrap();
|
||||||
let roundtrip = tokens.iter().map(|t| {
|
let roundtrip = tokens
|
||||||
|
.iter()
|
||||||
|
.map(|t| {
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
s += t.prefix;
|
s += t.prefix;
|
||||||
s += t.lexeme;
|
s += t.lexeme;
|
||||||
s
|
s
|
||||||
}).collect::<Vec<_>>().join("");
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("");
|
||||||
let actual = token_summary(&tokens);
|
let actual = token_summary(&tokens);
|
||||||
if actual != expected {
|
if actual != expected {
|
||||||
panic!("token summary mismatch:\nexpected: {}\ngot: {}\n", expected, actual);
|
panic!(
|
||||||
|
"token summary mismatch:\nexpected: {}\ngot: {}\n",
|
||||||
|
expected, actual
|
||||||
|
);
|
||||||
}
|
}
|
||||||
assert_eq!(input, roundtrip);
|
assert_eq!(input, roundtrip);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn token_summary(tokens: &[Token]) -> String {
|
fn token_summary(tokens: &[Token]) -> String {
|
||||||
tokens.iter().map(|t| {
|
tokens
|
||||||
match t.kind {
|
.iter()
|
||||||
|
.map(|t| match t.kind {
|
||||||
At => "@",
|
At => "@",
|
||||||
Backtick => "`",
|
Backtick => "`",
|
||||||
Colon => ":",
|
Colon => ":",
|
||||||
Comma => ",",
|
Comma => ",",
|
||||||
Comment{..} => "#",
|
Comment { .. } => "#",
|
||||||
Dedent => "<",
|
Dedent => "<",
|
||||||
Eof => ".",
|
Eof => ".",
|
||||||
Eol => "$",
|
Eol => "$",
|
||||||
Equals => "=",
|
Equals => "=",
|
||||||
Indent{..} => ">",
|
Indent { .. } => ">",
|
||||||
InterpolationEnd => "}",
|
InterpolationEnd => "}",
|
||||||
InterpolationStart => "{",
|
InterpolationStart => "{",
|
||||||
Line{..} => "^",
|
Line { .. } => "^",
|
||||||
Name => "N",
|
Name => "N",
|
||||||
ParenL => "(",
|
ParenL => "(",
|
||||||
ParenR => ")",
|
ParenR => ")",
|
||||||
@ -352,8 +432,9 @@ mod test {
|
|||||||
RawString => "'",
|
RawString => "'",
|
||||||
StringToken => "\"",
|
StringToken => "\"",
|
||||||
Text => "_",
|
Text => "_",
|
||||||
}
|
})
|
||||||
}).collect::<Vec<_>>().join("")
|
.collect::<Vec<_>>()
|
||||||
|
.join("")
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! error_test {
|
macro_rules! error_test {
|
||||||
@ -390,7 +471,7 @@ mod test {
|
|||||||
panic!("tokenize succeeded but expected: {}\n{}", expected, input);
|
panic!("tokenize succeeded but expected: {}\n{}", expected, input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
summary_test! {
|
summary_test! {
|
||||||
|
@ -6,12 +6,14 @@ pub fn load_dotenv() -> RunResult<'static, Map<String, String>> {
|
|||||||
match dotenv::dotenv_iter() {
|
match dotenv::dotenv_iter() {
|
||||||
Ok(iter) => {
|
Ok(iter) => {
|
||||||
let result: dotenv::Result<Map<String, String>> = iter.collect();
|
let result: dotenv::Result<Map<String, String>> = iter.collect();
|
||||||
result.map_err(|dotenv_error| RuntimeError::Dotenv{dotenv_error})
|
result.map_err(|dotenv_error| RuntimeError::Dotenv { dotenv_error })
|
||||||
}
|
}
|
||||||
Err(dotenv_error) => if dotenv_error.not_found() {
|
Err(dotenv_error) => {
|
||||||
|
if dotenv_error.not_found() {
|
||||||
Ok(Map::new())
|
Ok(Map::new())
|
||||||
} else {
|
} else {
|
||||||
Err(RuntimeError::Dotenv{dotenv_error})
|
Err(RuntimeError::Dotenv { dotenv_error })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
71
src/misc.rs
71
src/misc.rs
@ -3,7 +3,14 @@ use common::*;
|
|||||||
use unicode_width::UnicodeWidthChar;
|
use unicode_width::UnicodeWidthChar;
|
||||||
|
|
||||||
pub fn show_whitespace(text: &str) -> String {
|
pub fn show_whitespace(text: &str) -> String {
|
||||||
text.chars().map(|c| match c { '\t' => '␉', ' ' => '␠', _ => c }).collect()
|
text
|
||||||
|
.chars()
|
||||||
|
.map(|c| match c {
|
||||||
|
'\t' => '␉',
|
||||||
|
' ' => '␠',
|
||||||
|
_ => c,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default<T: Default>() -> T {
|
pub fn default<T: Default>() -> T {
|
||||||
@ -32,10 +39,11 @@ pub fn conjoin<T: Display>(
|
|||||||
conjunction: &str,
|
conjunction: &str,
|
||||||
) -> Result<(), fmt::Error> {
|
) -> Result<(), fmt::Error> {
|
||||||
match values.len() {
|
match values.len() {
|
||||||
0 => {},
|
0 => {}
|
||||||
1 => write!(f, "{}", values[0])?,
|
1 => write!(f, "{}", values[0])?,
|
||||||
2 => write!(f, "{} {} {}", values[0], conjunction, values[1])?,
|
2 => write!(f, "{} {} {}", values[0], conjunction, values[1])?,
|
||||||
_ => for (i, item) in values.iter().enumerate() {
|
_ => {
|
||||||
|
for (i, item) in values.iter().enumerate() {
|
||||||
write!(f, "{}", item)?;
|
write!(f, "{}", item)?;
|
||||||
if i == values.len() - 1 {
|
if i == values.len() - 1 {
|
||||||
} else if i == values.len() - 2 {
|
} else if i == values.len() - 2 {
|
||||||
@ -43,7 +51,8 @@ pub fn conjoin<T: Display>(
|
|||||||
} else {
|
} else {
|
||||||
write!(f, ", ")?
|
write!(f, ", ")?
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -76,7 +85,6 @@ pub fn write_error_context(
|
|||||||
} else {
|
} else {
|
||||||
if i < column {
|
if i < column {
|
||||||
space_column += UnicodeWidthChar::width(c).unwrap_or(0);
|
space_column += UnicodeWidthChar::width(c).unwrap_or(0);
|
||||||
|
|
||||||
}
|
}
|
||||||
if i >= column && i < column + width.unwrap_or(1) {
|
if i >= column && i < column + width.unwrap_or(1) {
|
||||||
space_width += UnicodeWidthChar::width(c).unwrap_or(0);
|
space_width += UnicodeWidthChar::width(c).unwrap_or(0);
|
||||||
@ -90,15 +98,36 @@ pub fn write_error_context(
|
|||||||
writeln!(f, "{} | {}", line_number, space_line)?;
|
writeln!(f, "{} | {}", line_number, space_line)?;
|
||||||
write!(f, "{0:1$} |", "", line_number_width)?;
|
write!(f, "{0:1$} |", "", line_number_width)?;
|
||||||
if width == None {
|
if width == None {
|
||||||
write!(f, " {0:1$}{2}^{3}", "", space_column, red.prefix(), red.suffix())?;
|
write!(
|
||||||
|
f,
|
||||||
|
" {0:1$}{2}^{3}",
|
||||||
|
"",
|
||||||
|
space_column,
|
||||||
|
red.prefix(),
|
||||||
|
red.suffix()
|
||||||
|
)?;
|
||||||
} else {
|
} else {
|
||||||
write!(f, " {0:1$}{2}{3:^<4$}{5}", "", space_column,
|
write!(
|
||||||
red.prefix(), "", space_width, red.suffix())?;
|
f,
|
||||||
|
" {0:1$}{2}{3:^<4$}{5}",
|
||||||
|
"",
|
||||||
|
space_column,
|
||||||
|
red.prefix(),
|
||||||
|
"",
|
||||||
|
space_width,
|
||||||
|
red.suffix()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if index != text.len() {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"internal error: Error has invalid line number: {}",
|
||||||
|
line_number
|
||||||
|
)?
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
None => if index != text.len() {
|
|
||||||
write!(f, "internal error: Error has invalid line number: {}", line_number)?
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -111,7 +140,7 @@ impl<'a, T: Display> Display for And<'a, T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Or <'a, T: 'a + Display>(pub &'a [T]);
|
pub struct Or<'a, T: 'a + Display>(pub &'a [T]);
|
||||||
|
|
||||||
impl<'a, T: Display> Display for Or<'a, T> {
|
impl<'a, T: Display> Display for Or<'a, T> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
@ -133,17 +162,17 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn conjoin_or() {
|
fn conjoin_or() {
|
||||||
assert_eq!("1", Or(&[1 ]).to_string());
|
assert_eq!("1", Or(&[1]).to_string());
|
||||||
assert_eq!("1 or 2", Or(&[1,2 ]).to_string());
|
assert_eq!("1 or 2", Or(&[1, 2]).to_string());
|
||||||
assert_eq!("1, 2, or 3", Or(&[1,2,3 ]).to_string());
|
assert_eq!("1, 2, or 3", Or(&[1, 2, 3]).to_string());
|
||||||
assert_eq!("1, 2, 3, or 4", Or(&[1,2,3,4]).to_string());
|
assert_eq!("1, 2, 3, or 4", Or(&[1, 2, 3, 4]).to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn conjoin_and() {
|
fn conjoin_and() {
|
||||||
assert_eq!("1", And(&[1 ]).to_string());
|
assert_eq!("1", And(&[1]).to_string());
|
||||||
assert_eq!("1 and 2", And(&[1,2 ]).to_string());
|
assert_eq!("1 and 2", And(&[1, 2]).to_string());
|
||||||
assert_eq!("1, 2, and 3", And(&[1,2,3 ]).to_string());
|
assert_eq!("1, 2, and 3", And(&[1, 2, 3]).to_string());
|
||||||
assert_eq!("1, 2, 3, and 4", And(&[1,2,3,4]).to_string());
|
assert_eq!("1, 2, 3, and 4", And(&[1, 2, 3, 4]).to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,10 @@ impl<'a> Display for Parameter<'a> {
|
|||||||
}
|
}
|
||||||
write!(f, "{}", color.parameter().paint(self.name))?;
|
write!(f, "{}", color.parameter().paint(self.name))?;
|
||||||
if let Some(ref default) = self.default {
|
if let Some(ref default) = self.default {
|
||||||
let escaped = default.chars().flat_map(char::escape_default).collect::<String>();;
|
let escaped = default
|
||||||
|
.chars()
|
||||||
|
.flat_map(char::escape_default)
|
||||||
|
.collect::<String>();;
|
||||||
write!(f, r#"='{}'"#, color.string().paint(&escaped))?;
|
write!(f, r#"='{}'"#, color.string().paint(&escaped))?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
109
src/parser.rs
109
src/parser.rs
@ -1,8 +1,8 @@
|
|||||||
use common::*;
|
use common::*;
|
||||||
|
|
||||||
use itertools;
|
use itertools;
|
||||||
use TokenKind::*;
|
|
||||||
use CompilationErrorKind::*;
|
use CompilationErrorKind::*;
|
||||||
|
use TokenKind::*;
|
||||||
|
|
||||||
pub struct Parser<'a> {
|
pub struct Parser<'a> {
|
||||||
text: &'a str,
|
text: &'a str,
|
||||||
@ -96,7 +96,7 @@ impl<'a> Parser<'a> {
|
|||||||
if let Some(recipe) = self.recipes.get(name.lexeme) {
|
if let Some(recipe) = self.recipes.get(name.lexeme) {
|
||||||
return Err(name.error(DuplicateRecipe {
|
return Err(name.error(DuplicateRecipe {
|
||||||
recipe: recipe.name,
|
recipe: recipe.name,
|
||||||
first: recipe.line_number
|
first: recipe.line_number,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,11 +108,13 @@ impl<'a> Parser<'a> {
|
|||||||
|
|
||||||
let parameter = match self.accept(Name) {
|
let parameter = match self.accept(Name) {
|
||||||
Some(parameter) => parameter,
|
Some(parameter) => parameter,
|
||||||
None => if let Some(plus) = plus {
|
None => {
|
||||||
|
if let Some(plus) = plus {
|
||||||
return Err(self.unexpected_token(&plus, &[Name]));
|
return Err(self.unexpected_token(&plus, &[Name]));
|
||||||
} else {
|
} else {
|
||||||
break
|
break;
|
||||||
},
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let variadic = plus.is_some();
|
let variadic = plus.is_some();
|
||||||
@ -125,7 +127,8 @@ impl<'a> Parser<'a> {
|
|||||||
|
|
||||||
if parameters.iter().any(|p| p.name == parameter.lexeme) {
|
if parameters.iter().any(|p| p.name == parameter.lexeme) {
|
||||||
return Err(parameter.error(DuplicateParameter {
|
return Err(parameter.error(DuplicateParameter {
|
||||||
recipe: name.lexeme, parameter: parameter.lexeme
|
recipe: name.lexeme,
|
||||||
|
parameter: parameter.lexeme,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,7 +145,7 @@ impl<'a> Parser<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if parsed_parameter_with_default && default.is_none() {
|
if parsed_parameter_with_default && default.is_none() {
|
||||||
return Err(parameter.error(RequiredParameterFollowsDefaultParameter{
|
return Err(parameter.error(RequiredParameterFollowsDefaultParameter {
|
||||||
parameter: parameter.lexeme,
|
parameter: parameter.lexeme,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -174,7 +177,7 @@ impl<'a> Parser<'a> {
|
|||||||
if dependencies.contains(&dependency.lexeme) {
|
if dependencies.contains(&dependency.lexeme) {
|
||||||
return Err(dependency.error(DuplicateDependency {
|
return Err(dependency.error(DuplicateDependency {
|
||||||
recipe: name.lexeme,
|
recipe: name.lexeme,
|
||||||
dependency: dependency.lexeme
|
dependency: dependency.lexeme,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
dependencies.push(dependency.lexeme);
|
dependencies.push(dependency.lexeme);
|
||||||
@ -195,9 +198,9 @@ impl<'a> Parser<'a> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(token) = self.expect(Line) {
|
if let Some(token) = self.expect(Line) {
|
||||||
return Err(token.error(Internal{
|
return Err(token.error(Internal {
|
||||||
message: format!("Expected a line but got {}", token.kind)
|
message: format!("Expected a line but got {}", token.kind),
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
let mut fragments = vec![];
|
let mut fragments = vec![];
|
||||||
|
|
||||||
@ -209,18 +212,22 @@ impl<'a> Parser<'a> {
|
|||||||
shebang = true;
|
shebang = true;
|
||||||
}
|
}
|
||||||
} else if !shebang
|
} else if !shebang
|
||||||
&& !lines.last().and_then(|line| line.last())
|
&& !lines
|
||||||
.map(Fragment::continuation).unwrap_or(false)
|
.last()
|
||||||
&& (token.lexeme.starts_with(' ') || token.lexeme.starts_with('\t')) {
|
.and_then(|line| line.last())
|
||||||
|
.map(Fragment::continuation)
|
||||||
|
.unwrap_or(false)
|
||||||
|
&& (token.lexeme.starts_with(' ') || token.lexeme.starts_with('\t'))
|
||||||
|
{
|
||||||
return Err(token.error(ExtraLeadingWhitespace));
|
return Err(token.error(ExtraLeadingWhitespace));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fragments.push(Fragment::Text{text: token});
|
fragments.push(Fragment::Text { text: token });
|
||||||
} else if let Some(token) = self.expect(InterpolationStart) {
|
} else if let Some(token) = self.expect(InterpolationStart) {
|
||||||
return Err(self.unexpected_token(&token, &[Text, InterpolationStart, Eol]));
|
return Err(self.unexpected_token(&token, &[Text, InterpolationStart, Eol]));
|
||||||
} else {
|
} else {
|
||||||
fragments.push(Fragment::Expression{
|
fragments.push(Fragment::Expression {
|
||||||
expression: self.expression()?
|
expression: self.expression()?,
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(token) = self.expect(InterpolationEnd) {
|
if let Some(token) = self.expect(InterpolationEnd) {
|
||||||
@ -233,7 +240,9 @@ impl<'a> Parser<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.recipes.insert(name.lexeme, Recipe {
|
self.recipes.insert(
|
||||||
|
name.lexeme,
|
||||||
|
Recipe {
|
||||||
line_number: name.line,
|
line_number: name.line,
|
||||||
name: name.lexeme,
|
name: name.lexeme,
|
||||||
doc: doc.map(|t| t.lexeme[1..].trim()),
|
doc: doc.map(|t| t.lexeme[1..].trim()),
|
||||||
@ -244,7 +253,8 @@ impl<'a> Parser<'a> {
|
|||||||
parameters,
|
parameters,
|
||||||
quiet,
|
quiet,
|
||||||
shebang,
|
shebang,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -261,24 +271,34 @@ impl<'a> Parser<'a> {
|
|||||||
if let Some(token) = self.expect(ParenR) {
|
if let Some(token) = self.expect(ParenR) {
|
||||||
return Err(self.unexpected_token(&token, &[Name, StringToken, ParenR]));
|
return Err(self.unexpected_token(&token, &[Name, StringToken, ParenR]));
|
||||||
}
|
}
|
||||||
Expression::Call {name: first.lexeme, token: first, arguments}
|
Expression::Call {
|
||||||
|
name: first.lexeme,
|
||||||
|
token: first,
|
||||||
|
arguments,
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Expression::Variable {name: first.lexeme, token: first}
|
Expression::Variable {
|
||||||
|
name: first.lexeme,
|
||||||
|
token: first,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Backtick => Expression::Backtick {
|
Backtick => Expression::Backtick {
|
||||||
raw: &first.lexeme[1..first.lexeme.len()-1],
|
raw: &first.lexeme[1..first.lexeme.len() - 1],
|
||||||
token: first
|
token: first,
|
||||||
|
},
|
||||||
|
RawString | StringToken => Expression::String {
|
||||||
|
cooked_string: CookedString::new(&first)?,
|
||||||
},
|
},
|
||||||
RawString | StringToken => {
|
|
||||||
Expression::String{cooked_string: CookedString::new(&first)?}
|
|
||||||
}
|
|
||||||
_ => return Err(self.unexpected_token(&first, &[Name, StringToken])),
|
_ => return Err(self.unexpected_token(&first, &[Name, StringToken])),
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.accepted(Plus) {
|
if self.accepted(Plus) {
|
||||||
let rhs = self.expression()?;
|
let rhs = self.expression()?;
|
||||||
Ok(Expression::Concatination{lhs: Box::new(lhs), rhs: Box::new(rhs)})
|
Ok(Expression::Concatination {
|
||||||
|
lhs: Box::new(lhs),
|
||||||
|
rhs: Box::new(rhs),
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Ok(lhs)
|
Ok(lhs)
|
||||||
}
|
}
|
||||||
@ -304,7 +324,9 @@ impl<'a> Parser<'a> {
|
|||||||
|
|
||||||
fn assignment(&mut self, name: Token<'a>, export: bool) -> CompilationResult<'a, ()> {
|
fn assignment(&mut self, name: Token<'a>, export: bool) -> CompilationResult<'a, ()> {
|
||||||
if self.assignments.contains_key(name.lexeme) {
|
if self.assignments.contains_key(name.lexeme) {
|
||||||
return Err(name.error(DuplicateVariable {variable: name.lexeme}));
|
return Err(name.error(DuplicateVariable {
|
||||||
|
variable: name.lexeme,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
if export {
|
if export {
|
||||||
self.exports.insert(name.lexeme);
|
self.exports.insert(name.lexeme);
|
||||||
@ -338,14 +360,17 @@ impl<'a> Parser<'a> {
|
|||||||
}
|
}
|
||||||
doc = Some(token);
|
doc = Some(token);
|
||||||
}
|
}
|
||||||
At => if let Some(name) = self.accept(Name) {
|
At => {
|
||||||
|
if let Some(name) = self.accept(Name) {
|
||||||
self.recipe(&name, doc, true)?;
|
self.recipe(&name, doc, true)?;
|
||||||
doc = None;
|
doc = None;
|
||||||
} else {
|
} else {
|
||||||
let unexpected = &self.tokens.next().unwrap();
|
let unexpected = &self.tokens.next().unwrap();
|
||||||
return Err(self.unexpected_token(unexpected, &[Name]));
|
return Err(self.unexpected_token(unexpected, &[Name]));
|
||||||
},
|
}
|
||||||
Name => if token.lexeme == "export" {
|
}
|
||||||
|
Name => {
|
||||||
|
if token.lexeme == "export" {
|
||||||
let next = self.tokens.next().unwrap();
|
let next = self.tokens.next().unwrap();
|
||||||
if next.kind == Name && self.accepted(Equals) {
|
if next.kind == Name && self.accepted(Equals) {
|
||||||
self.assignment(next, true)?;
|
self.assignment(next, true)?;
|
||||||
@ -361,26 +386,32 @@ impl<'a> Parser<'a> {
|
|||||||
} else {
|
} else {
|
||||||
self.recipe(&token, doc, false)?;
|
self.recipe(&token, doc, false)?;
|
||||||
doc = None;
|
doc = None;
|
||||||
},
|
}
|
||||||
|
}
|
||||||
_ => return Err(self.unexpected_token(&token, &[Name, At])),
|
_ => return Err(self.unexpected_token(&token, &[Name, At])),
|
||||||
},
|
},
|
||||||
None => return Err(CompilationError {
|
None => {
|
||||||
|
return Err(CompilationError {
|
||||||
text: self.text,
|
text: self.text,
|
||||||
index: 0,
|
index: 0,
|
||||||
line: 0,
|
line: 0,
|
||||||
column: 0,
|
column: 0,
|
||||||
width: None,
|
width: None,
|
||||||
kind: Internal {
|
kind: Internal {
|
||||||
message: "unexpected end of token stream".to_string()
|
message: "unexpected end of token stream".to_string(),
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(token) = self.tokens.next() {
|
if let Some(token) = self.tokens.next() {
|
||||||
return Err(token.error(Internal {
|
return Err(token.error(Internal {
|
||||||
message: format!("unexpected token remaining after parsing completed: {:?}", token.kind)
|
message: format!(
|
||||||
}))
|
"unexpected token remaining after parsing completed: {:?}",
|
||||||
|
token.kind
|
||||||
|
),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
RecipeResolver::resolve_recipes(&self.recipes, &self.assignments, self.text)?;
|
RecipeResolver::resolve_recipes(&self.recipes, &self.assignments, self.text)?;
|
||||||
@ -389,7 +420,7 @@ impl<'a> Parser<'a> {
|
|||||||
for parameter in &recipe.parameters {
|
for parameter in &recipe.parameters {
|
||||||
if self.assignments.contains_key(parameter.token.lexeme) {
|
if self.assignments.contains_key(parameter.token.lexeme) {
|
||||||
return Err(parameter.token.error(ParameterShadowsVariable {
|
return Err(parameter.token.error(ParameterShadowsVariable {
|
||||||
parameter: parameter.token.lexeme
|
parameter: parameter.token.lexeme,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -434,7 +465,7 @@ mod test {
|
|||||||
assert_eq!(actual, expected);
|
assert_eq!(actual, expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
summary_test! {
|
summary_test! {
|
||||||
|
@ -7,8 +7,11 @@ pub struct Platform;
|
|||||||
pub trait PlatformInterface {
|
pub trait PlatformInterface {
|
||||||
/// Construct a command equivelant to running the script at `path` with the
|
/// Construct a command equivelant to running the script at `path` with the
|
||||||
/// shebang line `shebang`
|
/// shebang line `shebang`
|
||||||
fn make_shebang_command(path: &Path, command: &str, argument: Option<&str>)
|
fn make_shebang_command(
|
||||||
-> Result<Command, brev::OutputError>;
|
path: &Path,
|
||||||
|
command: &str,
|
||||||
|
argument: Option<&str>,
|
||||||
|
) -> Result<Command, brev::OutputError>;
|
||||||
|
|
||||||
/// Set the execute permission on the file pointed to by `path`
|
/// Set the execute permission on the file pointed to by `path`
|
||||||
fn set_execute_permission(path: &Path) -> Result<(), io::Error>;
|
fn set_execute_permission(path: &Path) -> Result<(), io::Error>;
|
||||||
@ -20,12 +23,13 @@ pub trait PlatformInterface {
|
|||||||
fn to_shell_path(path: &Path) -> Result<String, String>;
|
fn to_shell_path(path: &Path) -> Result<String, String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
impl PlatformInterface for Platform {
|
impl PlatformInterface for Platform {
|
||||||
fn make_shebang_command(path: &Path, _command: &str, _argument: Option<&str>)
|
fn make_shebang_command(
|
||||||
-> Result<Command, brev::OutputError>
|
path: &Path,
|
||||||
{
|
_command: &str,
|
||||||
|
_argument: Option<&str>,
|
||||||
|
) -> Result<Command, brev::OutputError> {
|
||||||
// shebang scripts can be executed directly on unix
|
// shebang scripts can be executed directly on unix
|
||||||
Ok(Command::new(path))
|
Ok(Command::new(path))
|
||||||
}
|
}
|
||||||
@ -50,17 +54,20 @@ impl PlatformInterface for Platform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn to_shell_path(path: &Path) -> Result<String, String> {
|
fn to_shell_path(path: &Path) -> Result<String, String> {
|
||||||
path.to_str().map(str::to_string)
|
path
|
||||||
.ok_or_else(|| String::from(
|
.to_str()
|
||||||
"Error getting current directory: unicode decode error"))
|
.map(str::to_string)
|
||||||
|
.ok_or_else(|| String::from("Error getting current directory: unicode decode error"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
impl PlatformInterface for Platform {
|
impl PlatformInterface for Platform {
|
||||||
fn make_shebang_command(path: &Path, command: &str, argument: Option<&str>)
|
fn make_shebang_command(
|
||||||
-> Result<Command, brev::OutputError>
|
path: &Path,
|
||||||
{
|
command: &str,
|
||||||
|
argument: Option<&str>,
|
||||||
|
) -> Result<Command, brev::OutputError> {
|
||||||
// Translate path to the interpreter from unix style to windows style
|
// Translate path to the interpreter from unix style to windows style
|
||||||
let mut cygpath = Command::new("cygpath");
|
let mut cygpath = Command::new("cygpath");
|
||||||
cygpath.arg("--windows");
|
cygpath.arg("--windows");
|
||||||
|
@ -4,7 +4,10 @@ pub trait RangeExt<T> {
|
|||||||
fn range_contains(&self, i: T) -> bool;
|
fn range_contains(&self, i: T) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> RangeExt<T> for Range<T> where T: PartialOrd + Copy {
|
impl<T> RangeExt<T> for Range<T>
|
||||||
|
where
|
||||||
|
T: PartialOrd + Copy,
|
||||||
|
{
|
||||||
fn range_contains(&self, i: T) -> bool {
|
fn range_contains(&self, i: T) -> bool {
|
||||||
i >= self.start && i < self.end
|
i >= self.start && i < self.end
|
||||||
}
|
}
|
||||||
@ -16,10 +19,10 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn range() {
|
fn range() {
|
||||||
assert!( ( 0.. 1).range_contains( 0));
|
assert!((0..1).range_contains(0));
|
||||||
assert!( (10..20).range_contains(15));
|
assert!((10..20).range_contains(15));
|
||||||
assert!(!( 0.. 0).range_contains( 0));
|
assert!(!(0..0).range_contains(0));
|
||||||
assert!(!( 1..10).range_contains( 0));
|
assert!(!(1..10).range_contains(0));
|
||||||
assert!(!( 1..10).range_contains(10));
|
assert!(!(1..10).range_contains(10));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
127
src/recipe.rs
127
src/recipe.rs
@ -1,6 +1,6 @@
|
|||||||
use common::*;
|
use common::*;
|
||||||
|
|
||||||
use std::process::{ExitStatus, Command, Stdio};
|
use std::process::{Command, ExitStatus, Stdio};
|
||||||
|
|
||||||
use platform::{Platform, PlatformInterface};
|
use platform::{Platform, PlatformInterface};
|
||||||
|
|
||||||
@ -9,11 +9,18 @@ use platform::{Platform, PlatformInterface};
|
|||||||
fn error_from_signal(
|
fn error_from_signal(
|
||||||
recipe: &str,
|
recipe: &str,
|
||||||
line_number: Option<usize>,
|
line_number: Option<usize>,
|
||||||
exit_status: ExitStatus
|
exit_status: ExitStatus,
|
||||||
) -> RuntimeError {
|
) -> RuntimeError {
|
||||||
match Platform::signal_from_exit_status(exit_status) {
|
match Platform::signal_from_exit_status(exit_status) {
|
||||||
Some(signal) => RuntimeError::Signal{recipe, line_number, signal},
|
Some(signal) => RuntimeError::Signal {
|
||||||
None => RuntimeError::Unknown{recipe, line_number},
|
recipe,
|
||||||
|
line_number,
|
||||||
|
signal,
|
||||||
|
},
|
||||||
|
None => RuntimeError::Unknown {
|
||||||
|
recipe,
|
||||||
|
line_number,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,7 +50,11 @@ impl<'a> Recipe<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn min_arguments(&self) -> usize {
|
pub fn min_arguments(&self) -> usize {
|
||||||
self.parameters.iter().filter(|p| p.default.is_none()).count()
|
self
|
||||||
|
.parameters
|
||||||
|
.iter()
|
||||||
|
.filter(|p| p.default.is_none())
|
||||||
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn max_arguments(&self) -> usize {
|
pub fn max_arguments(&self) -> usize {
|
||||||
@ -65,7 +76,12 @@ impl<'a> Recipe<'a> {
|
|||||||
|
|
||||||
if configuration.verbosity.loquacious() {
|
if configuration.verbosity.loquacious() {
|
||||||
let color = configuration.color.stderr().banner();
|
let color = configuration.color.stderr().banner();
|
||||||
eprintln!("{}===> Running recipe `{}`...{}", color.prefix(), self.name, color.suffix());
|
eprintln!(
|
||||||
|
"{}===> Running recipe `{}`...{}",
|
||||||
|
color.prefix(),
|
||||||
|
self.name,
|
||||||
|
color.suffix()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut argument_map = Map::new();
|
let mut argument_map = Map::new();
|
||||||
@ -75,9 +91,11 @@ impl<'a> Recipe<'a> {
|
|||||||
let value = if rest.is_empty() {
|
let value = if rest.is_empty() {
|
||||||
match parameter.default {
|
match parameter.default {
|
||||||
Some(ref default) => Cow::Borrowed(default.as_str()),
|
Some(ref default) => Cow::Borrowed(default.as_str()),
|
||||||
None => return Err(RuntimeError::Internal{
|
None => {
|
||||||
message: "missing parameter without default".to_string()
|
return Err(RuntimeError::Internal {
|
||||||
}),
|
message: "missing parameter without default".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if parameter.variadic {
|
} else if parameter.variadic {
|
||||||
let value = Cow::Owned(rest.to_vec().join(" "));
|
let value = Cow::Owned(rest.to_vec().join(" "));
|
||||||
@ -120,13 +138,17 @@ impl<'a> Recipe<'a> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let tmp = TempDir::new("just")
|
let tmp = TempDir::new("just").map_err(|error| RuntimeError::TmpdirIoError {
|
||||||
.map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?;
|
recipe: self.name,
|
||||||
|
io_error: error,
|
||||||
|
})?;
|
||||||
let mut path = tmp.path().to_path_buf();
|
let mut path = tmp.path().to_path_buf();
|
||||||
path.push(self.name);
|
path.push(self.name);
|
||||||
{
|
{
|
||||||
let mut f = fs::File::create(&path)
|
let mut f = fs::File::create(&path).map_err(|error| RuntimeError::TmpdirIoError {
|
||||||
.map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?;
|
recipe: self.name,
|
||||||
|
io_error: error,
|
||||||
|
})?;
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
// add the shebang
|
// add the shebang
|
||||||
text += &evaluated_lines[0];
|
text += &evaluated_lines[0];
|
||||||
@ -147,44 +169,65 @@ impl<'a> Recipe<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
f.write_all(text.as_bytes())
|
f.write_all(text.as_bytes())
|
||||||
.map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?;
|
.map_err(|error| RuntimeError::TmpdirIoError {
|
||||||
|
recipe: self.name,
|
||||||
|
io_error: error,
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// make the script executable
|
// make the script executable
|
||||||
Platform::set_execute_permission(&path)
|
Platform::set_execute_permission(&path).map_err(|error| RuntimeError::TmpdirIoError {
|
||||||
.map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?;
|
recipe: self.name,
|
||||||
|
io_error: error,
|
||||||
let shebang_line = evaluated_lines.first()
|
|
||||||
.ok_or_else(|| RuntimeError::Internal {
|
|
||||||
message: "evaluated_lines was empty".to_string()
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let Shebang{interpreter, argument} = Shebang::new(shebang_line)
|
let shebang_line = evaluated_lines
|
||||||
|
.first()
|
||||||
.ok_or_else(|| RuntimeError::Internal {
|
.ok_or_else(|| RuntimeError::Internal {
|
||||||
message: format!("bad shebang line: {}", shebang_line)
|
message: "evaluated_lines was empty".to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let Shebang {
|
||||||
|
interpreter,
|
||||||
|
argument,
|
||||||
|
} = Shebang::new(shebang_line).ok_or_else(|| RuntimeError::Internal {
|
||||||
|
message: format!("bad shebang line: {}", shebang_line),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// create a command to run the script
|
// create a command to run the script
|
||||||
let mut command = Platform::make_shebang_command(&path, interpreter, argument)
|
let mut command =
|
||||||
.map_err(|output_error| RuntimeError::Cygpath{recipe: self.name, output_error})?;
|
Platform::make_shebang_command(&path, interpreter, argument).map_err(|output_error| {
|
||||||
|
RuntimeError::Cygpath {
|
||||||
|
recipe: self.name,
|
||||||
|
output_error,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
command.export_environment_variables(&context.scope, dotenv, exports)?;
|
command.export_environment_variables(&context.scope, dotenv, exports)?;
|
||||||
|
|
||||||
// run it!
|
// run it!
|
||||||
match InterruptHandler::guard(|| command.status()) {
|
match InterruptHandler::guard(|| command.status()) {
|
||||||
Ok(exit_status) => if let Some(code) = exit_status.code() {
|
Ok(exit_status) => {
|
||||||
|
if let Some(code) = exit_status.code() {
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
return Err(RuntimeError::Code{recipe: self.name, line_number: None, code})
|
return Err(RuntimeError::Code {
|
||||||
|
recipe: self.name,
|
||||||
|
line_number: None,
|
||||||
|
code,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(error_from_signal(self.name, None, exit_status))
|
return Err(error_from_signal(self.name, None, exit_status));
|
||||||
},
|
}
|
||||||
Err(io_error) => return Err(RuntimeError::Shebang {
|
}
|
||||||
|
Err(io_error) => {
|
||||||
|
return Err(RuntimeError::Shebang {
|
||||||
recipe: self.name,
|
recipe: self.name,
|
||||||
command: interpreter.to_string(),
|
command: interpreter.to_string(),
|
||||||
argument: argument.map(String::from),
|
argument: argument.map(String::from),
|
||||||
io_error,
|
io_error,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
let mut lines = self.lines.iter().peekable();
|
let mut lines = self.lines.iter().peekable();
|
||||||
@ -219,8 +262,7 @@ impl<'a> Recipe<'a> {
|
|||||||
|
|
||||||
if configuration.dry_run
|
if configuration.dry_run
|
||||||
|| configuration.verbosity.loquacious()
|
|| configuration.verbosity.loquacious()
|
||||||
|| !((quiet_command ^ self.quiet)
|
|| !((quiet_command ^ self.quiet) || configuration.quiet)
|
||||||
|| configuration.quiet)
|
|
||||||
{
|
{
|
||||||
let color = if configuration.highlight {
|
let color = if configuration.highlight {
|
||||||
configuration.color.command()
|
configuration.color.command()
|
||||||
@ -246,19 +288,25 @@ impl<'a> Recipe<'a> {
|
|||||||
cmd.export_environment_variables(&context.scope, dotenv, exports)?;
|
cmd.export_environment_variables(&context.scope, dotenv, exports)?;
|
||||||
|
|
||||||
match InterruptHandler::guard(|| cmd.status()) {
|
match InterruptHandler::guard(|| cmd.status()) {
|
||||||
Ok(exit_status) => if let Some(code) = exit_status.code() {
|
Ok(exit_status) => {
|
||||||
|
if let Some(code) = exit_status.code() {
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
return Err(RuntimeError::Code{
|
return Err(RuntimeError::Code {
|
||||||
recipe: self.name, line_number: Some(line_number), code,
|
recipe: self.name,
|
||||||
|
line_number: Some(line_number),
|
||||||
|
code,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(error_from_signal(self.name, Some(line_number), exit_status));
|
return Err(error_from_signal(self.name, Some(line_number), exit_status));
|
||||||
},
|
}
|
||||||
Err(io_error) => return Err(RuntimeError::IoError{
|
}
|
||||||
|
Err(io_error) => {
|
||||||
|
return Err(RuntimeError::IoError {
|
||||||
recipe: self.name,
|
recipe: self.name,
|
||||||
io_error,
|
io_error,
|
||||||
}),
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -289,9 +337,8 @@ impl<'a> Display for Recipe<'a> {
|
|||||||
write!(f, " ")?;
|
write!(f, " ")?;
|
||||||
}
|
}
|
||||||
match *piece {
|
match *piece {
|
||||||
Fragment::Text{ref text} => write!(f, "{}", text.lexeme)?,
|
Fragment::Text { ref text } => write!(f, "{}", text.lexeme)?,
|
||||||
Fragment::Expression{ref expression, ..} =>
|
Fragment::Expression { ref expression, .. } => write!(f, "{{{{{}}}}}", expression)?,
|
||||||
write!(f, "{{{{{}}}}}", expression)?,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if i + 1 < self.lines.len() {
|
if i + 1 < self.lines.len() {
|
||||||
|
@ -40,7 +40,7 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
|
|||||||
for recipe in recipes.values() {
|
for recipe in recipes.values() {
|
||||||
for line in &recipe.lines {
|
for line in &recipe.lines {
|
||||||
for fragment in line {
|
for fragment in line {
|
||||||
if let Fragment::Expression{ref expression, ..} = *fragment {
|
if let Fragment::Expression { ref expression, .. } = *fragment {
|
||||||
for (function, argc) in expression.functions() {
|
for (function, argc) in expression.functions() {
|
||||||
if let Err(error) = resolve_function(function, argc) {
|
if let Err(error) = resolve_function(function, argc) {
|
||||||
return Err(CompilationError {
|
return Err(CompilationError {
|
||||||
@ -60,7 +60,7 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
|
|||||||
let undefined = !assignments.contains_key(name)
|
let undefined = !assignments.contains_key(name)
|
||||||
&& !recipe.parameters.iter().any(|p| p.name == name);
|
&& !recipe.parameters.iter().any(|p| p.name == name);
|
||||||
if undefined {
|
if undefined {
|
||||||
let error = variable.error(UndefinedVariable{variable: name});
|
let error = variable.error(UndefinedVariable { variable: name });
|
||||||
return Err(CompilationError {
|
return Err(CompilationError {
|
||||||
index: error.index,
|
index: error.index,
|
||||||
line: error.line,
|
line: error.line,
|
||||||
@ -83,29 +83,38 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
|
|||||||
|
|
||||||
fn resolve_recipe(&mut self, recipe: &Recipe<'a>) -> CompilationResult<'a, ()> {
|
fn resolve_recipe(&mut self, recipe: &Recipe<'a>) -> CompilationResult<'a, ()> {
|
||||||
if self.resolved.contains(recipe.name) {
|
if self.resolved.contains(recipe.name) {
|
||||||
return Ok(())
|
return Ok(());
|
||||||
}
|
}
|
||||||
self.stack.push(recipe.name);
|
self.stack.push(recipe.name);
|
||||||
self.seen.insert(recipe.name);
|
self.seen.insert(recipe.name);
|
||||||
for dependency_token in &recipe.dependency_tokens {
|
for dependency_token in &recipe.dependency_tokens {
|
||||||
match self.recipes.get(dependency_token.lexeme) {
|
match self.recipes.get(dependency_token.lexeme) {
|
||||||
Some(dependency) => if !self.resolved.contains(dependency.name) {
|
Some(dependency) => {
|
||||||
|
if !self.resolved.contains(dependency.name) {
|
||||||
if self.seen.contains(dependency.name) {
|
if self.seen.contains(dependency.name) {
|
||||||
let first = self.stack[0];
|
let first = self.stack[0];
|
||||||
self.stack.push(first);
|
self.stack.push(first);
|
||||||
return Err(dependency_token.error(CircularRecipeDependency {
|
return Err(
|
||||||
|
dependency_token.error(CircularRecipeDependency {
|
||||||
recipe: recipe.name,
|
recipe: recipe.name,
|
||||||
circle: self.stack.iter()
|
circle: self
|
||||||
|
.stack
|
||||||
|
.iter()
|
||||||
.skip_while(|name| **name != dependency.name)
|
.skip_while(|name| **name != dependency.name)
|
||||||
.cloned().collect()
|
.cloned()
|
||||||
}));
|
.collect(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
self.resolve_recipe(dependency)?;
|
self.resolve_recipe(dependency)?;
|
||||||
},
|
}
|
||||||
None => return Err(dependency_token.error(UnknownDependency {
|
}
|
||||||
|
None => {
|
||||||
|
return Err(dependency_token.error(UnknownDependency {
|
||||||
recipe: recipe.name,
|
recipe: recipe.name,
|
||||||
unknown: dependency_token.lexeme
|
unknown: dependency_token.lexeme,
|
||||||
})),
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.resolved.insert(recipe.name);
|
self.resolved.insert(recipe.name);
|
||||||
|
207
src/run.rs
207
src/run.rs
@ -1,23 +1,21 @@
|
|||||||
use common::*;
|
use common::*;
|
||||||
|
|
||||||
use std::{convert, ffi};
|
use clap::{App, AppSettings, Arg, ArgGroup};
|
||||||
use clap::{App, Arg, ArgGroup, AppSettings};
|
|
||||||
use configuration::DEFAULT_SHELL;
|
use configuration::DEFAULT_SHELL;
|
||||||
use misc::maybe_s;
|
|
||||||
use unicode_width::UnicodeWidthStr;
|
|
||||||
use env_logger;
|
use env_logger;
|
||||||
use interrupt_handler::InterruptHandler;
|
use interrupt_handler::InterruptHandler;
|
||||||
|
use misc::maybe_s;
|
||||||
|
use std::{convert, ffi};
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use ansi_term::enable_ansi_support;
|
use ansi_term::enable_ansi_support;
|
||||||
|
|
||||||
fn edit<P: convert::AsRef<ffi::OsStr>>(path: P) -> ! {
|
fn edit<P: convert::AsRef<ffi::OsStr>>(path: P) -> ! {
|
||||||
let editor = env::var_os("EDITOR")
|
let editor =
|
||||||
.unwrap_or_else(|| die!("Error getting EDITOR environment variable"));
|
env::var_os("EDITOR").unwrap_or_else(|| die!("Error getting EDITOR environment variable"));
|
||||||
|
|
||||||
let error = Command::new(editor)
|
let error = Command::new(editor).arg(path).status();
|
||||||
.arg(path)
|
|
||||||
.status();
|
|
||||||
|
|
||||||
match error {
|
match error {
|
||||||
Ok(status) => process::exit(status.code().unwrap_or(EXIT_FAILURE)),
|
Ok(status) => process::exit(status.code().unwrap_or(EXIT_FAILURE)),
|
||||||
@ -42,102 +40,151 @@ pub fn run() {
|
|||||||
enable_ansi_support().ok();
|
enable_ansi_support().ok();
|
||||||
|
|
||||||
env_logger::Builder::from_env(
|
env_logger::Builder::from_env(
|
||||||
env_logger::Env::new().filter("JUST_LOG").write_style("JUST_LOG_STYLE")
|
env_logger::Env::new()
|
||||||
).init();
|
.filter("JUST_LOG")
|
||||||
|
.write_style("JUST_LOG_STYLE"),
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
|
||||||
let invocation_directory = env::current_dir()
|
let invocation_directory =
|
||||||
.map_err(|e| format!("Error getting current directory: {}", e));
|
env::current_dir().map_err(|e| format!("Error getting current directory: {}", e));
|
||||||
|
|
||||||
let matches = App::new(env!("CARGO_PKG_NAME"))
|
let matches = App::new(env!("CARGO_PKG_NAME"))
|
||||||
.version(concat!("v", env!("CARGO_PKG_VERSION")))
|
.version(concat!("v", env!("CARGO_PKG_VERSION")))
|
||||||
.author(env!("CARGO_PKG_AUTHORS"))
|
.author(env!("CARGO_PKG_AUTHORS"))
|
||||||
.about(concat!(env!("CARGO_PKG_DESCRIPTION"), " - ", env!("CARGO_PKG_HOMEPAGE")))
|
.about(concat!(
|
||||||
|
env!("CARGO_PKG_DESCRIPTION"),
|
||||||
|
" - ",
|
||||||
|
env!("CARGO_PKG_HOMEPAGE")
|
||||||
|
))
|
||||||
.help_message("Print help information")
|
.help_message("Print help information")
|
||||||
.version_message("Print version information")
|
.version_message("Print version information")
|
||||||
.setting(AppSettings::ColoredHelp)
|
.setting(AppSettings::ColoredHelp)
|
||||||
.setting(AppSettings::TrailingVarArg)
|
.setting(AppSettings::TrailingVarArg)
|
||||||
.arg(Arg::with_name("ARGUMENTS")
|
.arg(
|
||||||
|
Arg::with_name("ARGUMENTS")
|
||||||
.multiple(true)
|
.multiple(true)
|
||||||
.help("The recipe(s) to run, defaults to the first recipe in the justfile"))
|
.help("The recipe(s) to run, defaults to the first recipe in the justfile"),
|
||||||
.arg(Arg::with_name("COLOR")
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("COLOR")
|
||||||
.long("color")
|
.long("color")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.possible_values(&["auto", "always", "never"])
|
.possible_values(&["auto", "always", "never"])
|
||||||
.default_value("auto")
|
.default_value("auto")
|
||||||
.help("Print colorful output"))
|
.help("Print colorful output"),
|
||||||
.arg(Arg::with_name("DRY-RUN")
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("DRY-RUN")
|
||||||
.long("dry-run")
|
.long("dry-run")
|
||||||
.help("Print what just would do without doing it")
|
.help("Print what just would do without doing it")
|
||||||
.conflicts_with("QUIET"))
|
.conflicts_with("QUIET"),
|
||||||
.arg(Arg::with_name("DUMP")
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("DUMP")
|
||||||
.long("dump")
|
.long("dump")
|
||||||
.help("Print entire justfile"))
|
.help("Print entire justfile"),
|
||||||
.arg(Arg::with_name("EDIT")
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("EDIT")
|
||||||
.short("e")
|
.short("e")
|
||||||
.long("edit")
|
.long("edit")
|
||||||
.help("Open justfile with $EDITOR"))
|
.help("Open justfile with $EDITOR"),
|
||||||
.arg(Arg::with_name("EVALUATE")
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("EVALUATE")
|
||||||
.long("evaluate")
|
.long("evaluate")
|
||||||
.help("Print evaluated variables"))
|
.help("Print evaluated variables"),
|
||||||
.arg(Arg::with_name("HIGHLIGHT")
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("HIGHLIGHT")
|
||||||
.long("highlight")
|
.long("highlight")
|
||||||
.help("Highlight echoed recipe lines in bold"))
|
.help("Highlight echoed recipe lines in bold"),
|
||||||
.arg(Arg::with_name("JUSTFILE")
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("JUSTFILE")
|
||||||
.short("f")
|
.short("f")
|
||||||
.long("justfile")
|
.long("justfile")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.help("Use <JUSTFILE> as justfile. --working-directory must also be set")
|
.help("Use <JUSTFILE> as justfile. --working-directory must also be set")
|
||||||
.requires("WORKING-DIRECTORY"))
|
.requires("WORKING-DIRECTORY"),
|
||||||
.arg(Arg::with_name("LIST")
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("LIST")
|
||||||
.short("l")
|
.short("l")
|
||||||
.long("list")
|
.long("list")
|
||||||
.help("List available recipes and their arguments"))
|
.help("List available recipes and their arguments"),
|
||||||
.arg(Arg::with_name("QUIET")
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("QUIET")
|
||||||
.short("q")
|
.short("q")
|
||||||
.long("quiet")
|
.long("quiet")
|
||||||
.help("Suppress all output")
|
.help("Suppress all output")
|
||||||
.conflicts_with("DRY-RUN"))
|
.conflicts_with("DRY-RUN"),
|
||||||
.arg(Arg::with_name("SET")
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("SET")
|
||||||
.long("set")
|
.long("set")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.number_of_values(2)
|
.number_of_values(2)
|
||||||
.value_names(&["VARIABLE", "VALUE"])
|
.value_names(&["VARIABLE", "VALUE"])
|
||||||
.multiple(true)
|
.multiple(true)
|
||||||
.help("Set <VARIABLE> to <VALUE>"))
|
.help("Set <VARIABLE> to <VALUE>"),
|
||||||
.arg(Arg::with_name("SHELL")
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("SHELL")
|
||||||
.long("shell")
|
.long("shell")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.default_value(DEFAULT_SHELL)
|
.default_value(DEFAULT_SHELL)
|
||||||
.help("Invoke <SHELL> to run recipes"))
|
.help("Invoke <SHELL> to run recipes"),
|
||||||
.arg(Arg::with_name("SHOW")
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("SHOW")
|
||||||
.short("s")
|
.short("s")
|
||||||
.long("show")
|
.long("show")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.value_name("RECIPE")
|
.value_name("RECIPE")
|
||||||
.help("Show information about <RECIPE>"))
|
.help("Show information about <RECIPE>"),
|
||||||
.arg(Arg::with_name("SUMMARY")
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("SUMMARY")
|
||||||
.long("summary")
|
.long("summary")
|
||||||
.help("List names of available recipes"))
|
.help("List names of available recipes"),
|
||||||
.arg(Arg::with_name("VERBOSE")
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("VERBOSE")
|
||||||
.short("v")
|
.short("v")
|
||||||
.long("verbose")
|
.long("verbose")
|
||||||
.multiple(true)
|
.multiple(true)
|
||||||
.help("Use verbose output"))
|
.help("Use verbose output"),
|
||||||
.arg(Arg::with_name("WORKING-DIRECTORY")
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("WORKING-DIRECTORY")
|
||||||
.short("d")
|
.short("d")
|
||||||
.long("working-directory")
|
.long("working-directory")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.help("Use <WORKING-DIRECTORY> as working directory. --justfile must also be set")
|
.help("Use <WORKING-DIRECTORY> as working directory. --justfile must also be set")
|
||||||
.requires("JUSTFILE"))
|
.requires("JUSTFILE"),
|
||||||
.group(ArgGroup::with_name("EARLY-EXIT")
|
)
|
||||||
.args(&["DUMP", "EDIT", "LIST", "SHOW", "SUMMARY", "ARGUMENTS", "EVALUATE"]))
|
.group(ArgGroup::with_name("EARLY-EXIT").args(&[
|
||||||
|
"DUMP",
|
||||||
|
"EDIT",
|
||||||
|
"LIST",
|
||||||
|
"SHOW",
|
||||||
|
"SUMMARY",
|
||||||
|
"ARGUMENTS",
|
||||||
|
"EVALUATE",
|
||||||
|
]))
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
|
||||||
let color = match matches.value_of("COLOR").expect("`--color` had no value") {
|
let color = match matches.value_of("COLOR").expect("`--color` had no value") {
|
||||||
"auto" => Color::auto(),
|
"auto" => Color::auto(),
|
||||||
"always" => Color::always(),
|
"always" => Color::always(),
|
||||||
"never" => Color::never(),
|
"never" => Color::never(),
|
||||||
other => die!("Invalid argument `{}` to --color. This is a bug in just.", other),
|
other => die!(
|
||||||
|
"Invalid argument `{}` to --color. This is a bug in just.",
|
||||||
|
other
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let set_count = matches.occurrences_of("SET");
|
let set_count = matches.occurrences_of("SET");
|
||||||
@ -151,15 +198,25 @@ pub fn run() {
|
|||||||
|
|
||||||
let override_re = Regex::new("^([^=]+)=(.*)$").unwrap();
|
let override_re = Regex::new("^([^=]+)=(.*)$").unwrap();
|
||||||
|
|
||||||
let raw_arguments = matches.values_of("ARGUMENTS").map(|values| values.collect::<Vec<_>>())
|
let raw_arguments = matches
|
||||||
|
.values_of("ARGUMENTS")
|
||||||
|
.map(|values| values.collect::<Vec<_>>())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
for argument in raw_arguments.iter().take_while(|arg| override_re.is_match(arg)) {
|
for argument in raw_arguments
|
||||||
|
.iter()
|
||||||
|
.take_while(|arg| override_re.is_match(arg))
|
||||||
|
{
|
||||||
let captures = override_re.captures(argument).unwrap();
|
let captures = override_re.captures(argument).unwrap();
|
||||||
overrides.insert(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str());
|
overrides.insert(
|
||||||
|
captures.get(1).unwrap().as_str(),
|
||||||
|
captures.get(2).unwrap().as_str(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let rest = raw_arguments.iter().skip_while(|arg| override_re.is_match(arg))
|
let rest = raw_arguments
|
||||||
|
.iter()
|
||||||
|
.skip_while(|arg| override_re.is_match(arg))
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.flat_map(|(i, argument)| {
|
.flat_map(|(i, argument)| {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
@ -208,10 +265,12 @@ pub fn run() {
|
|||||||
'outer: loop {
|
'outer: loop {
|
||||||
for candidate in &["justfile", "Justfile"] {
|
for candidate in &["justfile", "Justfile"] {
|
||||||
match fs::metadata(candidate) {
|
match fs::metadata(candidate) {
|
||||||
Ok(metadata) => if metadata.is_file() {
|
Ok(metadata) => {
|
||||||
|
if metadata.is_file() {
|
||||||
name = *candidate;
|
name = *candidate;
|
||||||
break 'outer;
|
break 'outer;
|
||||||
},
|
}
|
||||||
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
if error.kind() != io::ErrorKind::NotFound {
|
if error.kind() != io::ErrorKind::NotFound {
|
||||||
die!("Error fetching justfile metadata: {}", error)
|
die!("Error fetching justfile metadata: {}", error)
|
||||||
@ -221,7 +280,11 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match env::current_dir() {
|
match env::current_dir() {
|
||||||
Ok(pathbuf) => if pathbuf.as_os_str() == "/" { die!("No justfile found."); },
|
Ok(pathbuf) => {
|
||||||
|
if pathbuf.as_os_str() == "/" {
|
||||||
|
die!("No justfile found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(error) => die!("Error getting current dir: {}", error),
|
Err(error) => die!("Error getting current dir: {}", error),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,19 +303,21 @@ pub fn run() {
|
|||||||
.unwrap_or_else(|error| die!("Error reading justfile: {}", error));
|
.unwrap_or_else(|error| die!("Error reading justfile: {}", error));
|
||||||
}
|
}
|
||||||
|
|
||||||
let justfile = Parser::parse(&text).unwrap_or_else(|error|
|
let justfile = Parser::parse(&text).unwrap_or_else(|error| {
|
||||||
if color.stderr().active() {
|
if color.stderr().active() {
|
||||||
die!("{:#}", error);
|
die!("{:#}", error);
|
||||||
} else {
|
} else {
|
||||||
die!("{}", error);
|
die!("{}", error);
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
if matches.is_present("SUMMARY") {
|
if matches.is_present("SUMMARY") {
|
||||||
if justfile.count() == 0 {
|
if justfile.count() == 0 {
|
||||||
eprintln!("Justfile contains no recipes.");
|
eprintln!("Justfile contains no recipes.");
|
||||||
} else {
|
} else {
|
||||||
let summary = justfile.recipes.iter()
|
let summary = justfile
|
||||||
|
.recipes
|
||||||
|
.iter()
|
||||||
.filter(|&(_, recipe)| !recipe.private)
|
.filter(|&(_, recipe)| !recipe.private)
|
||||||
.map(|(name, _)| name)
|
.map(|(name, _)| name)
|
||||||
.cloned()
|
.cloned()
|
||||||
@ -305,10 +370,12 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
if let Some(doc) = recipe.doc {
|
if let Some(doc) = recipe.doc {
|
||||||
print!(
|
print!(
|
||||||
" {:padding$}{} {}", "", doc_color.paint("#"), doc_color.paint(doc),
|
" {:padding$}{} {}",
|
||||||
padding = max_line_width.saturating_sub(
|
"",
|
||||||
line_widths.get(name).cloned().unwrap_or(max_line_width)
|
doc_color.paint("#"),
|
||||||
)
|
doc_color.paint(doc),
|
||||||
|
padding =
|
||||||
|
max_line_width.saturating_sub(line_widths.get(name).cloned().unwrap_or(max_line_width))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
@ -337,8 +404,12 @@ pub fn run() {
|
|||||||
} else if let Some(recipe) = justfile.first() {
|
} else if let Some(recipe) = justfile.first() {
|
||||||
let min_arguments = recipe.min_arguments();
|
let min_arguments = recipe.min_arguments();
|
||||||
if min_arguments > 0 {
|
if min_arguments > 0 {
|
||||||
die!("Recipe `{}` cannot be used as default recipe since it requires at least {} argument{}.",
|
die!(
|
||||||
recipe.name, min_arguments, maybe_s(min_arguments));
|
"Recipe `{}` cannot be used as default recipe since it requires at least {} argument{}.",
|
||||||
|
recipe.name,
|
||||||
|
min_arguments,
|
||||||
|
maybe_s(min_arguments)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
vec![recipe.name]
|
vec![recipe.name]
|
||||||
} else {
|
} else {
|
||||||
@ -362,11 +433,7 @@ pub fn run() {
|
|||||||
warn!("Failed to set CTRL-C handler: {}", error)
|
warn!("Failed to set CTRL-C handler: {}", error)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(run_error) = justfile.run(
|
if let Err(run_error) = justfile.run(&invocation_directory, &arguments, &configuration) {
|
||||||
&invocation_directory,
|
|
||||||
&arguments,
|
|
||||||
&configuration)
|
|
||||||
{
|
|
||||||
if !configuration.quiet {
|
if !configuration.quiet {
|
||||||
if color.stderr().active() {
|
if color.stderr().active() {
|
||||||
eprintln!("{:#}", run_error);
|
eprintln!("{:#}", run_error);
|
||||||
|
@ -4,7 +4,7 @@ use dotenv;
|
|||||||
|
|
||||||
use brev::OutputError;
|
use brev::OutputError;
|
||||||
|
|
||||||
use misc::{And, Or, maybe_s, Tick, ticks, write_error_context};
|
use misc::{maybe_s, ticks, write_error_context, And, Or, Tick};
|
||||||
|
|
||||||
use self::RuntimeError::*;
|
use self::RuntimeError::*;
|
||||||
|
|
||||||
@ -17,38 +17,82 @@ fn write_token_error_context(f: &mut fmt::Formatter, token: &Token) -> Result<()
|
|||||||
token.index,
|
token.index,
|
||||||
token.line,
|
token.line,
|
||||||
token.column + token.prefix.len(),
|
token.column + token.prefix.len(),
|
||||||
Some(token.lexeme.len())
|
Some(token.lexeme.len()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum RuntimeError<'a> {
|
pub enum RuntimeError<'a> {
|
||||||
ArgumentCountMismatch{
|
ArgumentCountMismatch {
|
||||||
recipe: &'a str,
|
recipe: &'a str,
|
||||||
parameters: Vec<&'a Parameter<'a>>,
|
parameters: Vec<&'a Parameter<'a>>,
|
||||||
found: usize,
|
found: usize,
|
||||||
min: usize,
|
min: usize,
|
||||||
max: usize,
|
max: usize,
|
||||||
},
|
},
|
||||||
Backtick{token: Token<'a>, output_error: OutputError},
|
Backtick {
|
||||||
Code{recipe: &'a str, line_number: Option<usize>, code: i32},
|
token: Token<'a>,
|
||||||
Cygpath{recipe: &'a str, output_error: OutputError},
|
output_error: OutputError,
|
||||||
Dotenv{dotenv_error: dotenv::Error},
|
},
|
||||||
FunctionCall{token: Token<'a>, message: String},
|
Code {
|
||||||
Internal{message: String},
|
recipe: &'a str,
|
||||||
IoError{recipe: &'a str, io_error: io::Error},
|
line_number: Option<usize>,
|
||||||
Shebang{recipe: &'a str, command: String, argument: Option<String>, io_error: io::Error},
|
code: i32,
|
||||||
Signal{recipe: &'a str, line_number: Option<usize>, signal: i32},
|
},
|
||||||
TmpdirIoError{recipe: &'a str, io_error: io::Error},
|
Cygpath {
|
||||||
UnknownOverrides{overrides: Vec<&'a str>},
|
recipe: &'a str,
|
||||||
UnknownRecipes{recipes: Vec<&'a str>, suggestion: Option<&'a str>},
|
output_error: OutputError,
|
||||||
Unknown{recipe: &'a str, line_number: Option<usize>},
|
},
|
||||||
|
Dotenv {
|
||||||
|
dotenv_error: dotenv::Error,
|
||||||
|
},
|
||||||
|
FunctionCall {
|
||||||
|
token: Token<'a>,
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
Internal {
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
IoError {
|
||||||
|
recipe: &'a str,
|
||||||
|
io_error: io::Error,
|
||||||
|
},
|
||||||
|
Shebang {
|
||||||
|
recipe: &'a str,
|
||||||
|
command: String,
|
||||||
|
argument: Option<String>,
|
||||||
|
io_error: io::Error,
|
||||||
|
},
|
||||||
|
Signal {
|
||||||
|
recipe: &'a str,
|
||||||
|
line_number: Option<usize>,
|
||||||
|
signal: i32,
|
||||||
|
},
|
||||||
|
TmpdirIoError {
|
||||||
|
recipe: &'a str,
|
||||||
|
io_error: io::Error,
|
||||||
|
},
|
||||||
|
UnknownOverrides {
|
||||||
|
overrides: Vec<&'a str>,
|
||||||
|
},
|
||||||
|
UnknownRecipes {
|
||||||
|
recipes: Vec<&'a str>,
|
||||||
|
suggestion: Option<&'a str>,
|
||||||
|
},
|
||||||
|
Unknown {
|
||||||
|
recipe: &'a str,
|
||||||
|
line_number: Option<usize>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> RuntimeError<'a> {
|
impl<'a> RuntimeError<'a> {
|
||||||
pub fn code(&self) -> Option<i32> {
|
pub fn code(&self) -> Option<i32> {
|
||||||
match *self {
|
match *self {
|
||||||
Code{code, ..} | Backtick{output_error: OutputError::Code(code), ..} => Some(code),
|
Code { code, .. }
|
||||||
|
| Backtick {
|
||||||
|
output_error: OutputError::Code(code),
|
||||||
|
..
|
||||||
|
} => Some(code),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -57,7 +101,11 @@ impl<'a> RuntimeError<'a> {
|
|||||||
impl<'a> Display for RuntimeError<'a> {
|
impl<'a> Display for RuntimeError<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
use RuntimeError::*;
|
use RuntimeError::*;
|
||||||
let color = if f.alternate() { Color::always() } else { Color::never() };
|
let color = if f.alternate() {
|
||||||
|
Color::always()
|
||||||
|
} else {
|
||||||
|
Color::never()
|
||||||
|
};
|
||||||
let error = color.error();
|
let error = color.error();
|
||||||
let message = color.message();
|
let message = color.message();
|
||||||
write!(f, "{} {}", error.paint("error:"), message.prefix())?;
|
write!(f, "{} {}", error.paint("error:"), message.prefix())?;
|
||||||
@ -65,30 +113,64 @@ impl<'a> Display for RuntimeError<'a> {
|
|||||||
let mut error_token = None;
|
let mut error_token = None;
|
||||||
|
|
||||||
match *self {
|
match *self {
|
||||||
UnknownRecipes{ref recipes, ref suggestion} => {
|
UnknownRecipes {
|
||||||
write!(f, "Justfile does not contain recipe{} {}.",
|
ref recipes,
|
||||||
maybe_s(recipes.len()), Or(&ticks(recipes)))?;
|
ref suggestion,
|
||||||
|
} => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Justfile does not contain recipe{} {}.",
|
||||||
|
maybe_s(recipes.len()),
|
||||||
|
Or(&ticks(recipes))
|
||||||
|
)?;
|
||||||
if let Some(suggestion) = *suggestion {
|
if let Some(suggestion) = *suggestion {
|
||||||
write!(f, "\nDid you mean `{}`?", suggestion)?;
|
write!(f, "\nDid you mean `{}`?", suggestion)?;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
UnknownOverrides{ref overrides} => {
|
UnknownOverrides { ref overrides } => {
|
||||||
write!(f, "Variable{} {} overridden on the command line but not present in justfile",
|
write!(
|
||||||
|
f,
|
||||||
|
"Variable{} {} overridden on the command line but not present in justfile",
|
||||||
maybe_s(overrides.len()),
|
maybe_s(overrides.len()),
|
||||||
And(&overrides.iter().map(Tick).collect::<Vec<_>>()))?;
|
And(&overrides.iter().map(Tick).collect::<Vec<_>>())
|
||||||
},
|
)?;
|
||||||
ArgumentCountMismatch{recipe, ref parameters, found, min, max} => {
|
}
|
||||||
|
ArgumentCountMismatch {
|
||||||
|
recipe,
|
||||||
|
ref parameters,
|
||||||
|
found,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
} => {
|
||||||
if min == max {
|
if min == max {
|
||||||
let expected = min;
|
let expected = min;
|
||||||
write!(f, "Recipe `{}` got {} argument{} but {}takes {}",
|
write!(
|
||||||
recipe, found, maybe_s(found),
|
f,
|
||||||
if expected < found { "only " } else { "" }, expected)?;
|
"Recipe `{}` got {} argument{} but {}takes {}",
|
||||||
|
recipe,
|
||||||
|
found,
|
||||||
|
maybe_s(found),
|
||||||
|
if expected < found { "only " } else { "" },
|
||||||
|
expected
|
||||||
|
)?;
|
||||||
} else if found < min {
|
} else if found < min {
|
||||||
write!(f, "Recipe `{}` got {} argument{} but takes at least {}",
|
write!(
|
||||||
recipe, found, maybe_s(found), min)?;
|
f,
|
||||||
|
"Recipe `{}` got {} argument{} but takes at least {}",
|
||||||
|
recipe,
|
||||||
|
found,
|
||||||
|
maybe_s(found),
|
||||||
|
min
|
||||||
|
)?;
|
||||||
} else if found > max {
|
} else if found > max {
|
||||||
write!(f, "Recipe `{}` got {} argument{} but takes at most {}",
|
write!(
|
||||||
recipe, found, maybe_s(found), max)?;
|
f,
|
||||||
|
"Recipe `{}` got {} argument{} but takes at most {}",
|
||||||
|
recipe,
|
||||||
|
found,
|
||||||
|
maybe_s(found),
|
||||||
|
max
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
write!(f, "\nusage:\n just {}", recipe)?;
|
write!(f, "\nusage:\n just {}", recipe)?;
|
||||||
for param in parameters {
|
for param in parameters {
|
||||||
@ -98,89 +180,170 @@ impl<'a> Display for RuntimeError<'a> {
|
|||||||
write!(f, " {}", param)?;
|
write!(f, " {}", param)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Code{recipe, line_number, code} => {
|
Code {
|
||||||
|
recipe,
|
||||||
|
line_number,
|
||||||
|
code,
|
||||||
|
} => {
|
||||||
if let Some(n) = line_number {
|
if let Some(n) = line_number {
|
||||||
write!(f, "Recipe `{}` failed on line {} with exit code {}", recipe, n, code)?;
|
write!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` failed on line {} with exit code {}",
|
||||||
|
recipe, n, code
|
||||||
|
)?;
|
||||||
} else {
|
} else {
|
||||||
write!(f, "Recipe `{}` failed with exit code {}", recipe, code)?;
|
write!(f, "Recipe `{}` failed with exit code {}", recipe, code)?;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Cygpath{recipe, ref output_error} => match *output_error {
|
Cygpath {
|
||||||
|
recipe,
|
||||||
|
ref output_error,
|
||||||
|
} => match *output_error {
|
||||||
OutputError::Code(code) => {
|
OutputError::Code(code) => {
|
||||||
write!(f, "Cygpath failed with exit code {} while translating recipe `{}` \
|
write!(
|
||||||
shebang interpreter path", code, recipe)?;
|
f,
|
||||||
|
"Cygpath failed with exit code {} while translating recipe `{}` \
|
||||||
|
shebang interpreter path",
|
||||||
|
code, recipe
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
OutputError::Signal(signal) => {
|
OutputError::Signal(signal) => {
|
||||||
write!(f, "Cygpath terminated by signal {} while translating recipe `{}` \
|
write!(
|
||||||
shebang interpreter path", signal, recipe)?;
|
f,
|
||||||
|
"Cygpath terminated by signal {} while translating recipe `{}` \
|
||||||
|
shebang interpreter path",
|
||||||
|
signal, recipe
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
OutputError::Unknown => {
|
OutputError::Unknown => {
|
||||||
write!(f, "Cygpath experienced an unknown failure while translating recipe `{}` \
|
write!(
|
||||||
shebang interpreter path", recipe)?;
|
f,
|
||||||
|
"Cygpath experienced an unknown failure while translating recipe `{}` \
|
||||||
|
shebang interpreter path",
|
||||||
|
recipe
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
OutputError::Io(ref io_error) => {
|
OutputError::Io(ref io_error) => {
|
||||||
match io_error.kind() {
|
match io_error.kind() {
|
||||||
io::ErrorKind::NotFound => write!(
|
io::ErrorKind::NotFound => write!(
|
||||||
f, "Could not find `cygpath` executable to translate recipe `{}` \
|
f,
|
||||||
shebang interpreter path:\n{}", recipe, io_error),
|
"Could not find `cygpath` executable to translate recipe `{}` \
|
||||||
|
shebang interpreter path:\n{}",
|
||||||
|
recipe, io_error
|
||||||
|
),
|
||||||
io::ErrorKind::PermissionDenied => write!(
|
io::ErrorKind::PermissionDenied => write!(
|
||||||
f, "Could not run `cygpath` executable to translate recipe `{}` \
|
f,
|
||||||
shebang interpreter path:\n{}", recipe, io_error),
|
"Could not run `cygpath` executable to translate recipe `{}` \
|
||||||
|
shebang interpreter path:\n{}",
|
||||||
|
recipe, io_error
|
||||||
|
),
|
||||||
_ => write!(f, "Could not run `cygpath` executable:\n{}", io_error),
|
_ => write!(f, "Could not run `cygpath` executable:\n{}", io_error),
|
||||||
}?;
|
}?;
|
||||||
}
|
}
|
||||||
OutputError::Utf8(ref utf8_error) => {
|
OutputError::Utf8(ref utf8_error) => {
|
||||||
write!(f, "Cygpath successfully translated recipe `{}` shebang interpreter path, \
|
write!(
|
||||||
but output was not utf8: {}", recipe, utf8_error)?;
|
f,
|
||||||
|
"Cygpath successfully translated recipe `{}` shebang interpreter path, \
|
||||||
|
but output was not utf8: {}",
|
||||||
|
recipe, utf8_error
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Dotenv{ref dotenv_error} => {
|
Dotenv { ref dotenv_error } => {
|
||||||
writeln!(f, "Failed to load .env: {}", dotenv_error)?;
|
writeln!(f, "Failed to load .env: {}", dotenv_error)?;
|
||||||
}
|
}
|
||||||
FunctionCall{ref token, ref message} => {
|
FunctionCall {
|
||||||
|
ref token,
|
||||||
|
ref message,
|
||||||
|
} => {
|
||||||
writeln!(f, "Call to function `{}` failed: {}", token.lexeme, message)?;
|
writeln!(f, "Call to function `{}` failed: {}", token.lexeme, message)?;
|
||||||
error_token = Some(token);
|
error_token = Some(token);
|
||||||
}
|
}
|
||||||
Shebang{recipe, ref command, ref argument, ref io_error} => {
|
Shebang {
|
||||||
|
recipe,
|
||||||
|
ref command,
|
||||||
|
ref argument,
|
||||||
|
ref io_error,
|
||||||
|
} => {
|
||||||
if let Some(ref argument) = *argument {
|
if let Some(ref argument) = *argument {
|
||||||
write!(f, "Recipe `{}` with shebang `#!{} {}` execution error: {}",
|
write!(
|
||||||
recipe, command, argument, io_error)?;
|
f,
|
||||||
|
"Recipe `{}` with shebang `#!{} {}` execution error: {}",
|
||||||
|
recipe, command, argument, io_error
|
||||||
|
)?;
|
||||||
} else {
|
} else {
|
||||||
write!(f, "Recipe `{}` with shebang `#!{}` execution error: {}",
|
write!(
|
||||||
recipe, command, io_error)?;
|
f,
|
||||||
|
"Recipe `{}` with shebang `#!{}` execution error: {}",
|
||||||
|
recipe, command, io_error
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Signal{recipe, line_number, signal} => {
|
Signal {
|
||||||
|
recipe,
|
||||||
|
line_number,
|
||||||
|
signal,
|
||||||
|
} => {
|
||||||
if let Some(n) = line_number {
|
if let Some(n) = line_number {
|
||||||
write!(f, "Recipe `{}` was terminated on line {} by signal {}", recipe, n, signal)?;
|
write!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` was terminated on line {} by signal {}",
|
||||||
|
recipe, n, signal
|
||||||
|
)?;
|
||||||
} else {
|
} else {
|
||||||
write!(f, "Recipe `{}` was terminated by signal {}", recipe, signal)?;
|
write!(f, "Recipe `{}` was terminated by signal {}", recipe, signal)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Unknown{recipe, line_number} => {
|
Unknown {
|
||||||
|
recipe,
|
||||||
|
line_number,
|
||||||
|
} => {
|
||||||
if let Some(n) = line_number {
|
if let Some(n) = line_number {
|
||||||
write!(f, "Recipe `{}` failed on line {} for an unknown reason", recipe, n)?;
|
write!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` failed on line {} for an unknown reason",
|
||||||
|
recipe, n
|
||||||
|
)?;
|
||||||
} else {
|
} else {
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
IoError{recipe, ref io_error} => {
|
IoError {
|
||||||
|
recipe,
|
||||||
|
ref io_error,
|
||||||
|
} => {
|
||||||
match io_error.kind() {
|
match io_error.kind() {
|
||||||
io::ErrorKind::NotFound => writeln!(f,
|
io::ErrorKind::NotFound => writeln!(
|
||||||
|
f,
|
||||||
"Recipe `{}` could not be run because just could not find `sh`:{}",
|
"Recipe `{}` could not be run because just could not find `sh`:{}",
|
||||||
recipe, io_error),
|
recipe, io_error
|
||||||
|
),
|
||||||
io::ErrorKind::PermissionDenied => writeln!(
|
io::ErrorKind::PermissionDenied => writeln!(
|
||||||
f, "Recipe `{}` could not be run because just could not run `sh`:{}",
|
f,
|
||||||
recipe, io_error),
|
"Recipe `{}` could not be run because just could not run `sh`:{}",
|
||||||
_ => writeln!(f, "Recipe `{}` could not be run because of an IO error while \
|
recipe, io_error
|
||||||
launching `sh`:{}", recipe, io_error),
|
),
|
||||||
|
_ => writeln!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` could not be run because of an IO error while \
|
||||||
|
launching `sh`:{}",
|
||||||
|
recipe, io_error
|
||||||
|
),
|
||||||
}?;
|
}?;
|
||||||
},
|
}
|
||||||
TmpdirIoError{recipe, ref io_error} =>
|
TmpdirIoError {
|
||||||
writeln!(f, "Recipe `{}` could not be run because of an IO error while trying \
|
recipe,
|
||||||
|
ref io_error,
|
||||||
|
} => writeln!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` could not be run because of an IO error while trying \
|
||||||
to create a temporary directory or write a file to that directory`:{}",
|
to create a temporary directory or write a file to that directory`:{}",
|
||||||
recipe, io_error)?,
|
recipe, io_error
|
||||||
Backtick{ref token, ref output_error} => match *output_error {
|
)?,
|
||||||
|
Backtick {
|
||||||
|
ref token,
|
||||||
|
ref output_error,
|
||||||
|
} => match *output_error {
|
||||||
OutputError::Code(code) => {
|
OutputError::Code(code) => {
|
||||||
writeln!(f, "Backtick failed with exit code {}", code)?;
|
writeln!(f, "Backtick failed with exit code {}", code)?;
|
||||||
error_token = Some(token);
|
error_token = Some(token);
|
||||||
@ -196,24 +359,40 @@ impl<'a> Display for RuntimeError<'a> {
|
|||||||
OutputError::Io(ref io_error) => {
|
OutputError::Io(ref io_error) => {
|
||||||
match io_error.kind() {
|
match io_error.kind() {
|
||||||
io::ErrorKind::NotFound => write!(
|
io::ErrorKind::NotFound => write!(
|
||||||
f, "Backtick could not be run because just could not find `sh`:\n{}",
|
f,
|
||||||
io_error),
|
"Backtick could not be run because just could not find `sh`:\n{}",
|
||||||
|
io_error
|
||||||
|
),
|
||||||
io::ErrorKind::PermissionDenied => write!(
|
io::ErrorKind::PermissionDenied => write!(
|
||||||
f, "Backtick could not be run because just could not run `sh`:\n{}", io_error),
|
f,
|
||||||
_ => write!(f, "Backtick could not be run because of an IO \
|
"Backtick could not be run because just could not run `sh`:\n{}",
|
||||||
error while launching `sh`:\n{}", io_error),
|
io_error
|
||||||
|
),
|
||||||
|
_ => write!(
|
||||||
|
f,
|
||||||
|
"Backtick could not be run because of an IO \
|
||||||
|
error while launching `sh`:\n{}",
|
||||||
|
io_error
|
||||||
|
),
|
||||||
}?;
|
}?;
|
||||||
error_token = Some(token);
|
error_token = Some(token);
|
||||||
}
|
}
|
||||||
OutputError::Utf8(ref utf8_error) => {
|
OutputError::Utf8(ref utf8_error) => {
|
||||||
writeln!(f, "Backtick succeeded but stdout was not utf8: {}", utf8_error)?;
|
writeln!(
|
||||||
|
f,
|
||||||
|
"Backtick succeeded but stdout was not utf8: {}",
|
||||||
|
utf8_error
|
||||||
|
)?;
|
||||||
error_token = Some(token);
|
error_token = Some(token);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Internal{ref message} => {
|
Internal { ref message } => {
|
||||||
write!(f, "Internal error, this may indicate a bug in just: {} \
|
write!(
|
||||||
|
f,
|
||||||
|
"Internal error, this may indicate a bug in just: {} \
|
||||||
consider filing an issue: https://github.com/casey/just/issues/new",
|
consider filing an issue: https://github.com/casey/just/issues/new",
|
||||||
message)?;
|
message
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +23,10 @@ impl<'a> Shebang<'a> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(Shebang{interpreter, argument})
|
Some(Shebang {
|
||||||
|
interpreter,
|
||||||
|
argument,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,26 +38,59 @@ mod test {
|
|||||||
fn split_shebang() {
|
fn split_shebang() {
|
||||||
fn check(text: &str, expected_split: Option<(&str, Option<&str>)>) {
|
fn check(text: &str, expected_split: Option<(&str, Option<&str>)>) {
|
||||||
let shebang = Shebang::new(text);
|
let shebang = Shebang::new(text);
|
||||||
assert_eq!(shebang.map(|shebang| (shebang.interpreter, shebang.argument)), expected_split);
|
assert_eq!(
|
||||||
|
shebang.map(|shebang| (shebang.interpreter, shebang.argument)),
|
||||||
|
expected_split
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
check("#! ", None );
|
check("#! ", None);
|
||||||
check("#!", None );
|
check("#!", None);
|
||||||
check("#!/bin/bash", Some(("/bin/bash", None )));
|
check("#!/bin/bash", Some(("/bin/bash", None)));
|
||||||
check("#!/bin/bash ", Some(("/bin/bash", None )));
|
check("#!/bin/bash ", Some(("/bin/bash", None)));
|
||||||
check("#!/usr/bin/env python", Some(("/usr/bin/env", Some("python" ))));
|
check(
|
||||||
check("#!/usr/bin/env python ", Some(("/usr/bin/env", Some("python" ))));
|
"#!/usr/bin/env python",
|
||||||
check("#!/usr/bin/env python -x", Some(("/usr/bin/env", Some("python -x" ))));
|
Some(("/usr/bin/env", Some("python"))),
|
||||||
check("#!/usr/bin/env python -x", Some(("/usr/bin/env", Some("python -x"))));
|
);
|
||||||
check("#!/usr/bin/env python \t-x\t", Some(("/usr/bin/env", Some("python \t-x"))));
|
check(
|
||||||
check("#/usr/bin/env python \t-x\t", None );
|
"#!/usr/bin/env python ",
|
||||||
check("#! /bin/bash", Some(("/bin/bash", None )));
|
Some(("/usr/bin/env", Some("python"))),
|
||||||
check("#!\t\t/bin/bash ", Some(("/bin/bash", None )));
|
);
|
||||||
check("#! \t\t/usr/bin/env python", Some(("/usr/bin/env", Some("python" ))));
|
check(
|
||||||
check("#! /usr/bin/env python ", Some(("/usr/bin/env", Some("python" ))));
|
"#!/usr/bin/env python -x",
|
||||||
check("#! /usr/bin/env python -x", Some(("/usr/bin/env", Some("python -x" ))));
|
Some(("/usr/bin/env", Some("python -x"))),
|
||||||
check("#! /usr/bin/env python -x", Some(("/usr/bin/env", Some("python -x"))));
|
);
|
||||||
check("#! /usr/bin/env python \t-x\t", Some(("/usr/bin/env", Some("python \t-x"))));
|
check(
|
||||||
check("# /usr/bin/env python \t-x\t", None );
|
"#!/usr/bin/env python -x",
|
||||||
|
Some(("/usr/bin/env", Some("python -x"))),
|
||||||
|
);
|
||||||
|
check(
|
||||||
|
"#!/usr/bin/env python \t-x\t",
|
||||||
|
Some(("/usr/bin/env", Some("python \t-x"))),
|
||||||
|
);
|
||||||
|
check("#/usr/bin/env python \t-x\t", None);
|
||||||
|
check("#! /bin/bash", Some(("/bin/bash", None)));
|
||||||
|
check("#!\t\t/bin/bash ", Some(("/bin/bash", None)));
|
||||||
|
check(
|
||||||
|
"#! \t\t/usr/bin/env python",
|
||||||
|
Some(("/usr/bin/env", Some("python"))),
|
||||||
|
);
|
||||||
|
check(
|
||||||
|
"#! /usr/bin/env python ",
|
||||||
|
Some(("/usr/bin/env", Some("python"))),
|
||||||
|
);
|
||||||
|
check(
|
||||||
|
"#! /usr/bin/env python -x",
|
||||||
|
Some(("/usr/bin/env", Some("python -x"))),
|
||||||
|
);
|
||||||
|
check(
|
||||||
|
"#! /usr/bin/env python -x",
|
||||||
|
Some(("/usr/bin/env", Some("python -x"))),
|
||||||
|
);
|
||||||
|
check(
|
||||||
|
"#! /usr/bin/env python \t-x\t",
|
||||||
|
Some(("/usr/bin/env", Some("python \t-x"))),
|
||||||
|
);
|
||||||
|
check("# /usr/bin/env python \t-x\t", None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,5 +45,5 @@ macro_rules! compilation_error_test {
|
|||||||
panic!("parse succeeded but expected: {}\n{}", expected, input);
|
panic!("parse succeeded but expected: {}\n{}", expected, input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,10 @@ pub enum TokenKind {
|
|||||||
impl Display for TokenKind {
|
impl Display for TokenKind {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
use TokenKind::*;
|
use TokenKind::*;
|
||||||
write!(f, "{}", match *self {
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match *self {
|
||||||
Backtick => "backtick",
|
Backtick => "backtick",
|
||||||
Colon => "':'",
|
Colon => "':'",
|
||||||
Comma => "','",
|
Comma => "','",
|
||||||
@ -72,6 +75,7 @@ impl Display for TokenKind {
|
|||||||
StringToken => "string",
|
StringToken => "string",
|
||||||
RawString => "raw string",
|
RawString => "raw string",
|
||||||
Text => "command text",
|
Text => "command text",
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,9 +48,12 @@ fn integration_test(
|
|||||||
expected_stderr: &str,
|
expected_stderr: &str,
|
||||||
expected_status: i32,
|
expected_status: i32,
|
||||||
) {
|
) {
|
||||||
let tmp = TempDir::new("just-integration")
|
let tmp = TempDir::new("just-integration").unwrap_or_else(|err| {
|
||||||
.unwrap_or_else(
|
panic!(
|
||||||
|err| panic!("integration test: failed to create temporary directory: {}", err));
|
"integration test: failed to create temporary directory: {}",
|
||||||
|
err
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
let mut justfile_path = tmp.path().to_path_buf();
|
let mut justfile_path = tmp.path().to_path_buf();
|
||||||
justfile_path.push("justfile");
|
justfile_path.push("justfile");
|
||||||
@ -77,13 +80,19 @@ fn integration_test(
|
|||||||
|
|
||||||
let stdout = str::from_utf8(&output.stdout).unwrap();
|
let stdout = str::from_utf8(&output.stdout).unwrap();
|
||||||
if stdout != expected_stdout {
|
if stdout != expected_stdout {
|
||||||
println!("bad stdout:\ngot:\n{}\n\nexpected:\n{}", stdout, expected_stdout);
|
println!(
|
||||||
|
"bad stdout:\ngot:\n{}\n\nexpected:\n{}",
|
||||||
|
stdout, expected_stdout
|
||||||
|
);
|
||||||
failure = true;
|
failure = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let stderr = str::from_utf8(&output.stderr).unwrap();
|
let stderr = str::from_utf8(&output.stderr).unwrap();
|
||||||
if stderr != expected_stderr {
|
if stderr != expected_stderr {
|
||||||
println!("bad stderr:\ngot:\n{}\n\nexpected:\n{}", stderr, expected_stderr);
|
println!(
|
||||||
|
"bad stderr:\ngot:\n{}\n\nexpected:\n{}",
|
||||||
|
stderr, expected_stderr
|
||||||
|
);
|
||||||
failure = true;
|
failure = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -591,7 +600,6 @@ wut:
|
|||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
integration_test! {
|
integration_test! {
|
||||||
name: export_shebang,
|
name: export_shebang,
|
||||||
justfile: r#"
|
justfile: r#"
|
||||||
@ -929,7 +937,6 @@ integration_test! {
|
|||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
integration_test! {
|
integration_test! {
|
||||||
name: required_after_default,
|
name: required_after_default,
|
||||||
justfile: "bar:\nhello baz arg='foo' bar:",
|
justfile: "bar:\nhello baz arg='foo' bar:",
|
||||||
@ -969,7 +976,6 @@ hello baz arg="XYZ\t\" ":
|
|||||||
status: EXIT_SUCCESS,
|
status: EXIT_SUCCESS,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
integration_test! {
|
integration_test! {
|
||||||
name: use_raw_string_default,
|
name: use_raw_string_default,
|
||||||
justfile: r#"
|
justfile: r#"
|
||||||
@ -1228,7 +1234,6 @@ foo:
|
|||||||
status: EXIT_SUCCESS,
|
status: EXIT_SUCCESS,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
integration_test! {
|
integration_test! {
|
||||||
name: env_var_failure,
|
name: env_var_failure,
|
||||||
justfile: "a:\n echo {{env_var('ZADDY')}}",
|
justfile: "a:\n echo {{env_var('ZADDY')}}",
|
||||||
@ -1500,7 +1505,6 @@ a:"#,
|
|||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
integration_test! {
|
integration_test! {
|
||||||
name: multiline_raw_string,
|
name: multiline_raw_string,
|
||||||
justfile: "
|
justfile: "
|
||||||
|
@ -4,8 +4,12 @@ extern crate libc;
|
|||||||
extern crate tempdir;
|
extern crate tempdir;
|
||||||
|
|
||||||
use executable_path::executable_path;
|
use executable_path::executable_path;
|
||||||
|
use std::{
|
||||||
|
process::Command,
|
||||||
|
thread,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
use tempdir::TempDir;
|
use tempdir::TempDir;
|
||||||
use std::{process::Command, time::{Duration, Instant}, thread};
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn kill(process_id: u32) {
|
fn kill(process_id: u32) {
|
||||||
@ -16,9 +20,12 @@ fn kill(process_id: u32) {
|
|||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn interrupt_test(justfile: &str) {
|
fn interrupt_test(justfile: &str) {
|
||||||
let tmp = TempDir::new("just-interrupts")
|
let tmp = TempDir::new("just-interrupts").unwrap_or_else(|err| {
|
||||||
.unwrap_or_else(
|
panic!(
|
||||||
|err| panic!("integration test: failed to create temporary directory: {}", err));
|
"integration test: failed to create temporary directory: {}",
|
||||||
|
err
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
let mut justfile_path = tmp.path().to_path_buf();
|
let mut justfile_path = tmp.path().to_path_buf();
|
||||||
justfile_path.push("justfile");
|
justfile_path.push("justfile");
|
||||||
@ -53,29 +60,35 @@ fn interrupt_test(justfile: &str) {
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn interrupt_shebang() {
|
fn interrupt_shebang() {
|
||||||
interrupt_test("
|
interrupt_test(
|
||||||
|
"
|
||||||
default:
|
default:
|
||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
sleep 2
|
sleep 2
|
||||||
");
|
",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn interrupt_line() {
|
fn interrupt_line() {
|
||||||
interrupt_test("
|
interrupt_test(
|
||||||
|
"
|
||||||
default:
|
default:
|
||||||
@sleep 2
|
@sleep 2
|
||||||
");
|
",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn interrupt_backtick() {
|
fn interrupt_backtick() {
|
||||||
interrupt_test("
|
interrupt_test(
|
||||||
|
"
|
||||||
foo = `sleep 2`
|
foo = `sleep 2`
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@echo hello
|
@echo hello
|
||||||
");
|
",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,16 +4,19 @@ extern crate target;
|
|||||||
extern crate tempdir;
|
extern crate tempdir;
|
||||||
|
|
||||||
use executable_path::executable_path;
|
use executable_path::executable_path;
|
||||||
|
use std::path::Path;
|
||||||
use std::process;
|
use std::process;
|
||||||
use std::str;
|
use std::str;
|
||||||
use std::path::Path;
|
|
||||||
use tempdir::TempDir;
|
use tempdir::TempDir;
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn to_shell_path(path: &Path) -> String {
|
fn to_shell_path(path: &Path) -> String {
|
||||||
use std::fs;
|
use std::fs;
|
||||||
fs::canonicalize(path).expect("canonicalize failed")
|
fs::canonicalize(path)
|
||||||
.to_str().map(str::to_string).expect("unicode decode failed")
|
.expect("canonicalize failed")
|
||||||
|
.to_str()
|
||||||
|
.map(str::to_string)
|
||||||
|
.expect("unicode decode failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
@ -27,13 +30,19 @@ fn to_shell_path(path: &Path) -> String {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_invocation_directory() {
|
fn test_invocation_directory() {
|
||||||
let tmp = TempDir::new("just-integration")
|
let tmp = TempDir::new("just-integration").unwrap_or_else(|err| {
|
||||||
.unwrap_or_else(
|
panic!(
|
||||||
|err| panic!("integration test: failed to create temporary directory: {}", err));
|
"integration test: failed to create temporary directory: {}",
|
||||||
|
err
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
let mut justfile_path = tmp.path().to_path_buf();
|
let mut justfile_path = tmp.path().to_path_buf();
|
||||||
justfile_path.push("justfile");
|
justfile_path.push("justfile");
|
||||||
brev::dump(justfile_path, "default:\n @cd {{invocation_directory()}}\n @echo {{invocation_directory()}}");
|
brev::dump(
|
||||||
|
justfile_path,
|
||||||
|
"default:\n @cd {{invocation_directory()}}\n @echo {{invocation_directory()}}",
|
||||||
|
);
|
||||||
|
|
||||||
let mut subdir = tmp.path().to_path_buf();
|
let mut subdir = tmp.path().to_path_buf();
|
||||||
subdir.push("subdir");
|
subdir.push("subdir");
|
||||||
@ -48,8 +57,7 @@ fn test_invocation_directory() {
|
|||||||
let mut failure = false;
|
let mut failure = false;
|
||||||
|
|
||||||
let expected_status = 0;
|
let expected_status = 0;
|
||||||
let expected_stdout =
|
let expected_stdout = to_shell_path(&subdir) + "\n";
|
||||||
to_shell_path(&subdir) + "\n";
|
|
||||||
let expected_stderr = "";
|
let expected_stderr = "";
|
||||||
|
|
||||||
let status = output.status.code().unwrap();
|
let status = output.status.code().unwrap();
|
||||||
@ -60,13 +68,19 @@ fn test_invocation_directory() {
|
|||||||
|
|
||||||
let stdout = str::from_utf8(&output.stdout).unwrap();
|
let stdout = str::from_utf8(&output.stdout).unwrap();
|
||||||
if stdout != expected_stdout {
|
if stdout != expected_stdout {
|
||||||
println!("bad stdout:\ngot:\n{:?}\n\nexpected:\n{:?}", stdout, expected_stdout);
|
println!(
|
||||||
|
"bad stdout:\ngot:\n{:?}\n\nexpected:\n{:?}",
|
||||||
|
stdout, expected_stdout
|
||||||
|
);
|
||||||
failure = true;
|
failure = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let stderr = str::from_utf8(&output.stderr).unwrap();
|
let stderr = str::from_utf8(&output.stderr).unwrap();
|
||||||
if stderr != expected_stderr {
|
if stderr != expected_stderr {
|
||||||
println!("bad stderr:\ngot:\n{:?}\n\nexpected:\n{:?}", stderr, expected_stderr);
|
println!(
|
||||||
|
"bad stderr:\ngot:\n{:?}\n\nexpected:\n{:?}",
|
||||||
|
stderr, expected_stderr
|
||||||
|
);
|
||||||
failure = true;
|
failure = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,9 +2,9 @@ extern crate brev;
|
|||||||
extern crate executable_path;
|
extern crate executable_path;
|
||||||
extern crate tempdir;
|
extern crate tempdir;
|
||||||
|
|
||||||
use tempdir::TempDir;
|
|
||||||
use std::{path, str, fs, process};
|
|
||||||
use executable_path::executable_path;
|
use executable_path::executable_path;
|
||||||
|
use std::{fs, path, process, str};
|
||||||
|
use tempdir::TempDir;
|
||||||
|
|
||||||
fn search_test<P: AsRef<path::Path>>(path: P, args: &[&str]) {
|
fn search_test<P: AsRef<path::Path>>(path: P, args: &[&str]) {
|
||||||
let binary = executable_path("just");
|
let binary = executable_path("just");
|
||||||
|
Loading…
Reference in New Issue
Block a user