Refactor Everything (#250)
This commit is contained in:
parent
86dc82f548
commit
2b6b715528
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -13,6 +13,7 @@ dependencies = [
|
|||||||
"regex 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"regex 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"utilities 0.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -247,6 +248,10 @@ name = "utf8-ranges"
|
|||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utilities"
|
||||||
|
version = "0.0.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vec_map"
|
name = "vec_map"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
@ -19,3 +19,6 @@ libc = "^0.2.21"
|
|||||||
regex = "^0.2.2"
|
regex = "^0.2.2"
|
||||||
tempdir = "^0.3.5"
|
tempdir = "^0.3.5"
|
||||||
unicode-width = "^0.1.3"
|
unicode-width = "^0.1.3"
|
||||||
|
|
||||||
|
[dev-dependencies.utilities]
|
||||||
|
path = "utilities"
|
||||||
|
19
justfile
19
justfile
@ -1,13 +1,24 @@
|
|||||||
|
bt='0'
|
||||||
|
|
||||||
|
export RUST_BACKTRACE=bt
|
||||||
|
|
||||||
test: build
|
test: build
|
||||||
cargo test --lib
|
cargo test
|
||||||
|
|
||||||
|
@spam:
|
||||||
|
{ \
|
||||||
|
figlet test; \
|
||||||
|
cargo build --color always 2>&1; \
|
||||||
|
cargo test --color always -- --color always 2>&1; \
|
||||||
|
} | less
|
||||||
|
|
||||||
# only run tests matching PATTERN
|
# only run tests matching PATTERN
|
||||||
filter PATTERN: build
|
filter PATTERN: build
|
||||||
cargo test --lib {{PATTERN}}
|
cargo test {{PATTERN}}
|
||||||
|
|
||||||
# test with backtrace
|
# test with backtrace
|
||||||
backtrace:
|
backtrace:
|
||||||
RUST_BACKTRACE=1 cargo test --lib
|
RUST_BACKTRACE=1 cargo test
|
||||||
|
|
||||||
build:
|
build:
|
||||||
cargo build
|
cargo build
|
||||||
@ -63,7 +74,7 @@ sloc:
|
|||||||
echo Checking for long lines...
|
echo Checking for long lines...
|
||||||
! grep --color -En '.{101}' src/*.rs
|
! grep --color -En '.{101}' src/*.rs
|
||||||
|
|
||||||
rename FROM TO:
|
replace FROM TO:
|
||||||
find src -name '*.rs' | xargs sed -i '' -E 's/{{FROM}}/{{TO}}/g'
|
find src -name '*.rs' | xargs sed -i '' -E 's/{{FROM}}/{{TO}}/g'
|
||||||
|
|
||||||
nop:
|
nop:
|
||||||
|
179
src/assignment_evaluator.rs
Normal file
179
src/assignment_evaluator.rs
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
use common::*;
|
||||||
|
|
||||||
|
use brev;
|
||||||
|
|
||||||
|
pub fn evaluate_assignments<'a>(
|
||||||
|
assignments: &Map<&'a str, Expression<'a>>,
|
||||||
|
overrides: &Map<&str, &str>,
|
||||||
|
quiet: bool,
|
||||||
|
shell: &'a str,
|
||||||
|
) -> Result<Map<&'a str, String>, RuntimeError<'a>> {
|
||||||
|
let mut evaluator = AssignmentEvaluator {
|
||||||
|
assignments: assignments,
|
||||||
|
evaluated: empty(),
|
||||||
|
exports: &empty(),
|
||||||
|
overrides: overrides,
|
||||||
|
quiet: quiet,
|
||||||
|
scope: &empty(),
|
||||||
|
shell: shell,
|
||||||
|
};
|
||||||
|
|
||||||
|
for name in assignments.keys() {
|
||||||
|
evaluator.evaluate_assignment(name)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(evaluator.evaluated)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_backtick<'a, 'b>(
|
||||||
|
raw: &str,
|
||||||
|
token: &Token<'a>,
|
||||||
|
scope: &Map<&'a str, String>,
|
||||||
|
exports: &Set<&'a str>,
|
||||||
|
quiet: bool,
|
||||||
|
shell: &'b str,
|
||||||
|
) -> Result<String, RuntimeError<'a>> {
|
||||||
|
let mut cmd = Command::new(shell);
|
||||||
|
|
||||||
|
cmd.export_environment_variables(scope, exports)?;
|
||||||
|
|
||||||
|
cmd.arg("-cu")
|
||||||
|
.arg(raw);
|
||||||
|
|
||||||
|
cmd.stderr(if quiet {
|
||||||
|
process::Stdio::null()
|
||||||
|
} else {
|
||||||
|
process::Stdio::inherit()
|
||||||
|
});
|
||||||
|
|
||||||
|
brev::output(cmd).map_err(|output_error| RuntimeError::Backtick{token: token.clone(), output_error})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AssignmentEvaluator<'a: 'b, 'b> {
|
||||||
|
pub assignments: &'b Map<&'a str, Expression<'a>>,
|
||||||
|
pub evaluated: Map<&'a str, String>,
|
||||||
|
pub exports: &'b Set<&'a str>,
|
||||||
|
pub overrides: &'b Map<&'b str, &'b str>,
|
||||||
|
pub quiet: bool,
|
||||||
|
pub scope: &'b Map<&'a str, String>,
|
||||||
|
pub shell: &'b str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
||||||
|
pub fn evaluate_line(
|
||||||
|
&mut self,
|
||||||
|
line: &[Fragment<'a>],
|
||||||
|
arguments: &Map<&str, Cow<str>>
|
||||||
|
) -> Result<String, RuntimeError<'a>> {
|
||||||
|
let mut evaluated = String::new();
|
||||||
|
for fragment in line {
|
||||||
|
match *fragment {
|
||||||
|
Fragment::Text{ref text} => evaluated += text.lexeme,
|
||||||
|
Fragment::Expression{ref expression} => {
|
||||||
|
evaluated += &self.evaluate_expression(expression, arguments)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(evaluated)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_assignment(&mut self, name: &'a str) -> Result<(), RuntimeError<'a>> {
|
||||||
|
if self.evaluated.contains_key(name) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(expression) = self.assignments.get(name) {
|
||||||
|
if let Some(value) = self.overrides.get(name) {
|
||||||
|
self.evaluated.insert(name, value.to_string());
|
||||||
|
} else {
|
||||||
|
let value = self.evaluate_expression(expression, &empty())?;
|
||||||
|
self.evaluated.insert(name, value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(RuntimeError::Internal {
|
||||||
|
message: format!("attempted to evaluated unknown assignment {}", name)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_expression(
|
||||||
|
&mut self,
|
||||||
|
expression: &Expression<'a>,
|
||||||
|
arguments: &Map<&str, Cow<str>>
|
||||||
|
) -> Result<String, RuntimeError<'a>> {
|
||||||
|
Ok(match *expression {
|
||||||
|
Expression::Variable{name, ..} => {
|
||||||
|
if self.evaluated.contains_key(name) {
|
||||||
|
self.evaluated[name].clone()
|
||||||
|
} else if self.scope.contains_key(name) {
|
||||||
|
self.scope[name].clone()
|
||||||
|
} else if self.assignments.contains_key(name) {
|
||||||
|
self.evaluate_assignment(name)?;
|
||||||
|
self.evaluated[name].clone()
|
||||||
|
} else if arguments.contains_key(name) {
|
||||||
|
arguments[name].to_string()
|
||||||
|
} else {
|
||||||
|
return Err(RuntimeError::Internal {
|
||||||
|
message: format!("attempted to evaluate undefined variable `{}`", name)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expression::String{ref cooked_string} => cooked_string.cooked.clone(),
|
||||||
|
Expression::Backtick{raw, ref token} => {
|
||||||
|
run_backtick(raw, token, self.scope, self.exports, self.quiet, self.shell)?
|
||||||
|
}
|
||||||
|
Expression::Concatination{ref lhs, ref rhs} => {
|
||||||
|
self.evaluate_expression(lhs, arguments)?
|
||||||
|
+
|
||||||
|
&self.evaluate_expression(rhs, arguments)?
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use brev::OutputError;
|
||||||
|
use testing::parse_success;
|
||||||
|
use Configuration;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn backtick_code() {
|
||||||
|
match parse_success("a:\n echo {{`f() { return 100; }; f`}}")
|
||||||
|
.run(&["a"], &Default::default()).unwrap_err() {
|
||||||
|
RuntimeError::Backtick{token, output_error: OutputError::Code(code)} => {
|
||||||
|
assert_eq!(code, 100);
|
||||||
|
assert_eq!(token.lexeme, "`f() { return 100; }; f`");
|
||||||
|
},
|
||||||
|
other => panic!("expected a code run error, but got: {}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn export_assignment_backtick() {
|
||||||
|
let text = r#"
|
||||||
|
export exported_variable = "A"
|
||||||
|
b = `echo $exported_variable`
|
||||||
|
|
||||||
|
recipe:
|
||||||
|
echo {{b}}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let options = Configuration {
|
||||||
|
quiet: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
match parse_success(text).run(&["recipe"], &options).unwrap_err() {
|
||||||
|
RuntimeError::Backtick{token, output_error: OutputError::Code(_)} => {
|
||||||
|
assert_eq!(token.lexeme, "`echo $exported_variable`");
|
||||||
|
},
|
||||||
|
other => panic!("expected a backtick code errror, but got: {}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
129
src/assignment_resolver.rs
Normal file
129
src/assignment_resolver.rs
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
use common::*;
|
||||||
|
|
||||||
|
pub fn resolve_assignments<'a>(
|
||||||
|
assignments: &Map<&'a str, Expression<'a>>,
|
||||||
|
assignment_tokens: &Map<&'a str, Token<'a>>,
|
||||||
|
) -> Result<(), CompilationError<'a>> {
|
||||||
|
|
||||||
|
let mut resolver = AssignmentResolver {
|
||||||
|
assignments: assignments,
|
||||||
|
assignment_tokens: assignment_tokens,
|
||||||
|
stack: empty(),
|
||||||
|
seen: empty(),
|
||||||
|
evaluated: empty(),
|
||||||
|
};
|
||||||
|
|
||||||
|
for name in assignments.keys() {
|
||||||
|
resolver.resolve_assignment(name)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AssignmentResolver<'a: 'b, 'b> {
|
||||||
|
assignments: &'b Map<&'a str, Expression<'a>>,
|
||||||
|
assignment_tokens: &'b Map<&'a str, Token<'a>>,
|
||||||
|
stack: Vec<&'a str>,
|
||||||
|
seen: Set<&'a str>,
|
||||||
|
evaluated: Set<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
|
||||||
|
fn resolve_assignment(&mut self, name: &'a str) -> Result<(), CompilationError<'a>> {
|
||||||
|
if self.evaluated.contains(name) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.seen.insert(name);
|
||||||
|
self.stack.push(name);
|
||||||
|
|
||||||
|
if let Some(expression) = self.assignments.get(name) {
|
||||||
|
self.resolve_expression(expression)?;
|
||||||
|
self.evaluated.insert(name);
|
||||||
|
} else {
|
||||||
|
let message = format!("attempted to resolve unknown assignment `{}`", name);
|
||||||
|
return Err(CompilationError {
|
||||||
|
text: "",
|
||||||
|
index: 0,
|
||||||
|
line: 0,
|
||||||
|
column: 0,
|
||||||
|
width: None,
|
||||||
|
kind: CompilationErrorKind::Internal{message}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_expression(&mut self, expression: &Expression<'a>) -> Result<(), CompilationError<'a>> {
|
||||||
|
match *expression {
|
||||||
|
Expression::Variable{name, ref token} => {
|
||||||
|
if self.evaluated.contains(name) {
|
||||||
|
return Ok(());
|
||||||
|
} else if self.seen.contains(name) {
|
||||||
|
let token = &self.assignment_tokens[name];
|
||||||
|
self.stack.push(name);
|
||||||
|
return Err(token.error(CompilationErrorKind::CircularVariableDependency {
|
||||||
|
variable: name,
|
||||||
|
circle: self.stack.clone(),
|
||||||
|
}));
|
||||||
|
} else if self.assignments.contains_key(name) {
|
||||||
|
self.resolve_assignment(name)?;
|
||||||
|
} else {
|
||||||
|
return Err(token.error(CompilationErrorKind::UndefinedVariable{variable: name}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expression::Concatination{ref lhs, ref rhs} => {
|
||||||
|
self.resolve_expression(lhs)?;
|
||||||
|
self.resolve_expression(rhs)?;
|
||||||
|
}
|
||||||
|
Expression::String{..} | Expression::Backtick{..} => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use testing::parse_error;
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn circular_variable_dependency() {
|
||||||
|
let text = "a = b\nb = a";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 0,
|
||||||
|
line: 0,
|
||||||
|
column: 0,
|
||||||
|
width: Some(1),
|
||||||
|
kind: CompilationErrorKind::CircularVariableDependency{variable: "a", circle: vec!["a", "b", "a"]}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn self_variable_dependency() {
|
||||||
|
let text = "a = a";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 0,
|
||||||
|
line: 0,
|
||||||
|
column: 0,
|
||||||
|
width: Some(1),
|
||||||
|
kind: CompilationErrorKind::CircularVariableDependency{variable: "a", circle: vec!["a", "a"]}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn unknown_expression_variable() {
|
||||||
|
let text = "x = yy";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 4,
|
||||||
|
line: 0,
|
||||||
|
column: 4,
|
||||||
|
width: Some(2),
|
||||||
|
kind: CompilationErrorKind::UndefinedVariable{variable: "yy"},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
extern crate ansi_term;
|
extern crate ansi_term;
|
||||||
extern crate atty;
|
extern crate atty;
|
||||||
|
|
||||||
use prelude::*;
|
use common::*;
|
||||||
|
|
||||||
use self::ansi_term::{Style, Prefix, Suffix, ANSIGenericString};
|
use self::ansi_term::{Style, Prefix, Suffix, ANSIGenericString};
|
||||||
use self::ansi_term::Color::*;
|
use self::ansi_term::Color::*;
|
||||||
use self::atty::is as is_atty;
|
use self::atty::is as is_atty;
|
||||||
|
28
src/command_ext.rs
Normal file
28
src/command_ext.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
use common::*;
|
||||||
|
|
||||||
|
pub trait CommandExt {
|
||||||
|
fn export_environment_variables<'a>(
|
||||||
|
&mut self,
|
||||||
|
scope: &Map<&'a str, String>,
|
||||||
|
exports: &Set<&'a str>
|
||||||
|
) -> Result<(), RuntimeError<'a>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandExt for Command {
|
||||||
|
fn export_environment_variables<'a>(
|
||||||
|
&mut self,
|
||||||
|
scope: &Map<&'a str, String>,
|
||||||
|
exports: &Set<&'a str>
|
||||||
|
) -> Result<(), RuntimeError<'a>> {
|
||||||
|
for name in exports {
|
||||||
|
if let Some(value) = scope.get(name) {
|
||||||
|
self.env(name, value);
|
||||||
|
} else {
|
||||||
|
return Err(RuntimeError::Internal {
|
||||||
|
message: format!("scope does not contain exported variable `{}`", name),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
141
src/compilation_error.rs
Normal file
141
src/compilation_error.rs
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
use common::*;
|
||||||
|
|
||||||
|
use misc::{Or, write_error_context, show_whitespace};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct CompilationError<'a> {
|
||||||
|
pub text: &'a str,
|
||||||
|
pub index: usize,
|
||||||
|
pub line: usize,
|
||||||
|
pub column: usize,
|
||||||
|
pub width: Option<usize>,
|
||||||
|
pub kind: CompilationErrorKind<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum CompilationErrorKind<'a> {
|
||||||
|
CircularRecipeDependency{recipe: &'a str, circle: Vec<&'a str>},
|
||||||
|
CircularVariableDependency{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,
|
||||||
|
InconsistentLeadingWhitespace{expected: &'a str, found: &'a str},
|
||||||
|
Internal{message: String},
|
||||||
|
InvalidEscapeSequence{character: char},
|
||||||
|
MixedLeadingWhitespace{whitespace: &'a str},
|
||||||
|
OuterShebang,
|
||||||
|
ParameterShadowsVariable{parameter: &'a str},
|
||||||
|
RequiredParameterFollowsDefaultParameter{parameter: &'a str},
|
||||||
|
ParameterFollowsVariadicParameter{parameter: &'a str},
|
||||||
|
UndefinedVariable{variable: &'a str},
|
||||||
|
UnexpectedToken{expected: Vec<TokenKind>, found: TokenKind},
|
||||||
|
UnknownDependency{recipe: &'a str, unknown: &'a str},
|
||||||
|
UnknownStartOfToken,
|
||||||
|
UnterminatedString,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Display for CompilationError<'a> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
use CompilationErrorKind::*;
|
||||||
|
let error = Color::fmt(f).error();
|
||||||
|
let message = Color::fmt(f).message();
|
||||||
|
|
||||||
|
write!(f, "{} {}", error.paint("error:"), message.prefix())?;
|
||||||
|
|
||||||
|
match self.kind {
|
||||||
|
CircularRecipeDependency{recipe, ref circle} => {
|
||||||
|
if circle.len() == 2 {
|
||||||
|
writeln!(f, "Recipe `{}` depends on itself", recipe)?;
|
||||||
|
} else {
|
||||||
|
writeln!(f, "Recipe `{}` has circular dependency `{}`",
|
||||||
|
recipe, circle.join(" -> "))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CircularVariableDependency{variable, ref circle} => {
|
||||||
|
if circle.len() == 2 {
|
||||||
|
writeln!(f, "Variable `{}` is defined in terms of itself", variable)?;
|
||||||
|
} else {
|
||||||
|
writeln!(f, "Variable `{}` depends on its own value: `{}`",
|
||||||
|
variable, circle.join(" -> "))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InvalidEscapeSequence{character} => {
|
||||||
|
writeln!(f, "`\\{}` is not a valid escape sequence",
|
||||||
|
character.escape_default().collect::<String>())?;
|
||||||
|
}
|
||||||
|
DuplicateParameter{recipe, parameter} => {
|
||||||
|
writeln!(f, "Recipe `{}` has duplicate parameter `{}`", recipe, parameter)?;
|
||||||
|
}
|
||||||
|
DuplicateVariable{variable} => {
|
||||||
|
writeln!(f, "Variable `{}` has multiple definitions", variable)?;
|
||||||
|
}
|
||||||
|
UnexpectedToken{ref expected, found} => {
|
||||||
|
writeln!(f, "Expected {}, but found {}", Or(expected), found)?;
|
||||||
|
}
|
||||||
|
DuplicateDependency{recipe, dependency} => {
|
||||||
|
writeln!(f, "Recipe `{}` has duplicate dependency `{}`", recipe, dependency)?;
|
||||||
|
}
|
||||||
|
DuplicateRecipe{recipe, first} => {
|
||||||
|
writeln!(f, "Recipe `{}` first defined on line {} is redefined on line {}",
|
||||||
|
recipe, first + 1, self.line + 1)?;
|
||||||
|
}
|
||||||
|
DependencyHasParameters{recipe, dependency} => {
|
||||||
|
writeln!(f, "Recipe `{}` depends on `{}` which requires arguments. \
|
||||||
|
Dependencies may not require arguments", recipe, dependency)?;
|
||||||
|
}
|
||||||
|
ParameterShadowsVariable{parameter} => {
|
||||||
|
writeln!(f, "Parameter `{}` shadows variable of the same name", parameter)?;
|
||||||
|
}
|
||||||
|
RequiredParameterFollowsDefaultParameter{parameter} => {
|
||||||
|
writeln!(f, "Non-default parameter `{}` follows default parameter", parameter)?;
|
||||||
|
}
|
||||||
|
ParameterFollowsVariadicParameter{parameter} => {
|
||||||
|
writeln!(f, "Parameter `{}` follows variadic parameter", parameter)?;
|
||||||
|
}
|
||||||
|
MixedLeadingWhitespace{whitespace} => {
|
||||||
|
writeln!(f,
|
||||||
|
"Found a mix of tabs and spaces in leading whitespace: `{}`\n\
|
||||||
|
Leading whitespace may consist of tabs or spaces, but not both",
|
||||||
|
show_whitespace(whitespace)
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
ExtraLeadingWhitespace => {
|
||||||
|
writeln!(f, "Recipe line has extra leading whitespace")?;
|
||||||
|
}
|
||||||
|
InconsistentLeadingWhitespace{expected, found} => {
|
||||||
|
writeln!(f,
|
||||||
|
"Recipe line has inconsistent leading whitespace. \
|
||||||
|
Recipe started with `{}` but found line with `{}`",
|
||||||
|
show_whitespace(expected), show_whitespace(found)
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
OuterShebang => {
|
||||||
|
writeln!(f, "`#!` is reserved syntax outside of recipes")?;
|
||||||
|
}
|
||||||
|
UnknownDependency{recipe, unknown} => {
|
||||||
|
writeln!(f, "Recipe `{}` has unknown dependency `{}`", recipe, unknown)?;
|
||||||
|
}
|
||||||
|
UndefinedVariable{variable} => {
|
||||||
|
writeln!(f, "Variable `{}` not defined", variable)?;
|
||||||
|
}
|
||||||
|
UnknownStartOfToken => {
|
||||||
|
writeln!(f, "Unknown start of token:")?;
|
||||||
|
}
|
||||||
|
UnterminatedString => {
|
||||||
|
writeln!(f, "Unterminated string")?;
|
||||||
|
}
|
||||||
|
Internal{ref message} => {
|
||||||
|
writeln!(f, "Internal error, this may indicate a bug in just: {}\n\
|
||||||
|
consider filing an issue: https://github.com/casey/just/issues/new",
|
||||||
|
message)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(f, "{}", message.suffix())?;
|
||||||
|
|
||||||
|
write_error_context(f, self.text, self.index, self.line, self.column, self.width)
|
||||||
|
}
|
||||||
|
}
|
29
src/configuration.rs
Normal file
29
src/configuration.rs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
use common::*;
|
||||||
|
|
||||||
|
pub const DEFAULT_SHELL: &'static str = "sh";
|
||||||
|
|
||||||
|
pub struct Configuration<'a> {
|
||||||
|
pub dry_run: bool,
|
||||||
|
pub evaluate: bool,
|
||||||
|
pub highlight: bool,
|
||||||
|
pub overrides: Map<&'a str, &'a str>,
|
||||||
|
pub quiet: bool,
|
||||||
|
pub shell: &'a str,
|
||||||
|
pub color: Color,
|
||||||
|
pub verbose: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Default for Configuration<'a> {
|
||||||
|
fn default() -> Configuration<'static> {
|
||||||
|
Configuration {
|
||||||
|
dry_run: false,
|
||||||
|
evaluate: false,
|
||||||
|
highlight: false,
|
||||||
|
overrides: empty(),
|
||||||
|
quiet: false,
|
||||||
|
shell: DEFAULT_SHELL,
|
||||||
|
color: default(),
|
||||||
|
verbose: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
48
src/cooked_string.rs
Normal file
48
src/cooked_string.rs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
use common::*;
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
pub struct CookedString<'a> {
|
||||||
|
pub raw: &'a str,
|
||||||
|
pub cooked: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> CookedString<'a> {
|
||||||
|
pub fn new(token: &Token<'a>) -> Result<CookedString<'a>, CompilationError<'a>> {
|
||||||
|
let raw = &token.lexeme[1..token.lexeme.len()-1];
|
||||||
|
|
||||||
|
if let TokenKind::RawString = token.kind {
|
||||||
|
Ok(CookedString{raw: raw, cooked: raw.to_string()})
|
||||||
|
} else if let TokenKind::StringToken = token.kind {
|
||||||
|
let mut cooked = String::new();
|
||||||
|
let mut escape = false;
|
||||||
|
for c in raw.chars() {
|
||||||
|
if escape {
|
||||||
|
match c {
|
||||||
|
'n' => cooked.push('\n'),
|
||||||
|
'r' => cooked.push('\r'),
|
||||||
|
't' => cooked.push('\t'),
|
||||||
|
'\\' => cooked.push('\\'),
|
||||||
|
'"' => cooked.push('"'),
|
||||||
|
other => return Err(token.error(CompilationErrorKind::InvalidEscapeSequence {
|
||||||
|
character: other,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
escape = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if c == '\\' {
|
||||||
|
escape = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cooked.push(c);
|
||||||
|
}
|
||||||
|
Ok(CookedString{raw: raw, cooked: cooked})
|
||||||
|
} else {
|
||||||
|
Err(token.error(CompilationErrorKind::Internal {
|
||||||
|
message: "cook_string() called on non-string token".to_string()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
49
src/expression.rs
Normal file
49
src/expression.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
use common::*;
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
pub enum Expression<'a> {
|
||||||
|
Variable{name: &'a str, token: Token<'a>},
|
||||||
|
String{cooked_string: CookedString<'a>},
|
||||||
|
Backtick{raw: &'a str, token: Token<'a>},
|
||||||
|
Concatination{lhs: Box<Expression<'a>>, rhs: Box<Expression<'a>>},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Expression<'a> {
|
||||||
|
pub fn variables(&'a self) -> Variables<'a> {
|
||||||
|
Variables {
|
||||||
|
stack: vec![self],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Display for Expression<'a> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
match *self {
|
||||||
|
Expression::Backtick {raw, .. } => write!(f, "`{}`", raw)?,
|
||||||
|
Expression::Concatination{ref lhs, ref rhs } => write!(f, "{} + {}", lhs, rhs)?,
|
||||||
|
Expression::String {ref cooked_string} => write!(f, "\"{}\"", cooked_string.raw)?,
|
||||||
|
Expression::Variable {name, .. } => write!(f, "{}", name)?,
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Variables<'a> {
|
||||||
|
stack: Vec<&'a Expression<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for Variables<'a> {
|
||||||
|
type Item = &'a Token<'a>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<&'a Token<'a>> {
|
||||||
|
match self.stack.pop() {
|
||||||
|
None | Some(&Expression::String{..}) | Some(&Expression::Backtick{..}) => None,
|
||||||
|
Some(&Expression::Variable{ref token,..}) => Some(token),
|
||||||
|
Some(&Expression::Concatination{ref lhs, ref rhs}) => {
|
||||||
|
self.stack.push(lhs);
|
||||||
|
self.stack.push(rhs);
|
||||||
|
self.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
src/fragment.rs
Normal file
17
src/fragment.rs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
use common::*;
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
pub enum Fragment<'a> {
|
||||||
|
Text{text: Token<'a>},
|
||||||
|
Expression{expression: Expression<'a>},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Fragment<'a> {
|
||||||
|
pub fn continuation(&self) -> bool {
|
||||||
|
match *self {
|
||||||
|
Fragment::Text{ref text} => text.lexeme.ends_with('\\'),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
345
src/justfile.rs
Normal file
345
src/justfile.rs
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
use common::*;
|
||||||
|
|
||||||
|
use edit_distance::edit_distance;
|
||||||
|
use assignment_evaluator::evaluate_assignments;
|
||||||
|
use range_ext::RangeExt;
|
||||||
|
|
||||||
|
pub struct Justfile<'a> {
|
||||||
|
pub recipes: Map<&'a str, Recipe<'a>>,
|
||||||
|
pub assignments: Map<&'a str, Expression<'a>>,
|
||||||
|
pub exports: Set<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> Justfile<'a> where 'a: 'b {
|
||||||
|
pub fn first(&self) -> Option<&Recipe> {
|
||||||
|
let mut first: Option<&Recipe> = None;
|
||||||
|
for recipe in self.recipes.values() {
|
||||||
|
if let Some(first_recipe) = first {
|
||||||
|
if recipe.line_number < first_recipe.line_number {
|
||||||
|
first = Some(recipe)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
first = Some(recipe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
first
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn count(&self) -> usize {
|
||||||
|
self.recipes.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn suggest(&self, name: &str) -> Option<&'a str> {
|
||||||
|
let mut suggestions = self.recipes.keys()
|
||||||
|
.map(|suggestion| (edit_distance(suggestion, name), suggestion))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
suggestions.sort();
|
||||||
|
if let Some(&(distance, suggestion)) = suggestions.first() {
|
||||||
|
if distance < 3 {
|
||||||
|
return Some(suggestion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(
|
||||||
|
&'a self,
|
||||||
|
arguments: &[&'a str],
|
||||||
|
options: &Configuration<'a>,
|
||||||
|
) -> Result<(), RuntimeError<'a>> {
|
||||||
|
let unknown_overrides = options.overrides.keys().cloned()
|
||||||
|
.filter(|name| !self.assignments.contains_key(name))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if !unknown_overrides.is_empty() {
|
||||||
|
return Err(RuntimeError::UnknownOverrides{overrides: unknown_overrides});
|
||||||
|
}
|
||||||
|
|
||||||
|
let scope = evaluate_assignments(
|
||||||
|
&self.assignments,
|
||||||
|
&options.overrides,
|
||||||
|
options.quiet,
|
||||||
|
options.shell,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if options.evaluate {
|
||||||
|
let mut width = 0;
|
||||||
|
for name in scope.keys() {
|
||||||
|
width = cmp::max(name.len(), width);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (name, value) in scope {
|
||||||
|
println!("{0:1$} = \"{2}\"", name, width, value);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut missing = vec![];
|
||||||
|
let mut grouped = vec![];
|
||||||
|
let mut rest = arguments;
|
||||||
|
|
||||||
|
while let Some((argument, mut tail)) = rest.split_first() {
|
||||||
|
if let Some(recipe) = self.recipes.get(argument) {
|
||||||
|
if recipe.parameters.is_empty() {
|
||||||
|
grouped.push((recipe, &tail[0..0]));
|
||||||
|
} else {
|
||||||
|
let argument_range = recipe.argument_range();
|
||||||
|
let argument_count = cmp::min(tail.len(), recipe.max_arguments());
|
||||||
|
if !argument_range.range_contains(argument_count) {
|
||||||
|
return Err(RuntimeError::ArgumentCountMismatch {
|
||||||
|
recipe: recipe.name,
|
||||||
|
found: tail.len(),
|
||||||
|
min: recipe.min_arguments(),
|
||||||
|
max: recipe.max_arguments(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
grouped.push((recipe, &tail[0..argument_count]));
|
||||||
|
tail = &tail[argument_count..];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
missing.push(*argument);
|
||||||
|
}
|
||||||
|
rest = tail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !missing.is_empty() {
|
||||||
|
let suggestion = if missing.len() == 1 {
|
||||||
|
self.suggest(missing.first().unwrap())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
return Err(RuntimeError::UnknownRecipes{recipes: missing, suggestion: suggestion});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ran = empty();
|
||||||
|
for (recipe, arguments) in grouped {
|
||||||
|
self.run_recipe(recipe, arguments, &scope, &mut ran, options)?
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_recipe<'c>(
|
||||||
|
&'c self,
|
||||||
|
recipe: &Recipe<'a>,
|
||||||
|
arguments: &[&'a str],
|
||||||
|
scope: &Map<&'c str, String>,
|
||||||
|
ran: &mut Set<&'a str>,
|
||||||
|
options: &Configuration<'a>,
|
||||||
|
) -> Result<(), RuntimeError> {
|
||||||
|
for dependency_name in &recipe.dependencies {
|
||||||
|
if !ran.contains(dependency_name) {
|
||||||
|
self.run_recipe(&self.recipes[dependency_name], &[], scope, ran, options)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recipe.run(arguments, scope, &self.exports, options)?;
|
||||||
|
ran.insert(recipe.name);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Display for Justfile<'a> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
let mut items = self.recipes.len() + self.assignments.len();
|
||||||
|
for (name, expression) in &self.assignments {
|
||||||
|
if self.exports.contains(name) {
|
||||||
|
write!(f, "export ")?;
|
||||||
|
}
|
||||||
|
write!(f, "{} = {}", name, expression)?;
|
||||||
|
items -= 1;
|
||||||
|
if items != 0 {
|
||||||
|
write!(f, "\n\n")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for recipe in self.recipes.values() {
|
||||||
|
write!(f, "{}", recipe)?;
|
||||||
|
items -= 1;
|
||||||
|
if items != 0 {
|
||||||
|
write!(f, "\n\n")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use testing::parse_success;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_recipes() {
|
||||||
|
match parse_success("a:\nb:\nc:").run(&["a", "x", "y", "z"], &Default::default()).unwrap_err() {
|
||||||
|
RuntimeError::UnknownRecipes{recipes, suggestion} => {
|
||||||
|
assert_eq!(recipes, &["x", "y", "z"]);
|
||||||
|
assert_eq!(suggestion, None);
|
||||||
|
}
|
||||||
|
other => panic!("expected an unknown recipe error, but got: {}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn run_shebang() {
|
||||||
|
// this test exists to make sure that shebang recipes
|
||||||
|
// run correctly. although this script is still
|
||||||
|
// executed by a shell its behavior depends on the value of a
|
||||||
|
// variable and continuing even though a command fails,
|
||||||
|
// whereas in plain recipes variables are not available
|
||||||
|
// in subsequent lines and execution stops when a line
|
||||||
|
// fails
|
||||||
|
let text = "
|
||||||
|
a:
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
code=200
|
||||||
|
x() { return $code; }
|
||||||
|
x
|
||||||
|
x
|
||||||
|
";
|
||||||
|
|
||||||
|
match parse_success(text).run(&["a"], &Default::default()).unwrap_err() {
|
||||||
|
RuntimeError::Code{recipe, line_number, code} => {
|
||||||
|
assert_eq!(recipe, "a");
|
||||||
|
assert_eq!(code, 200);
|
||||||
|
assert_eq!(line_number, None);
|
||||||
|
},
|
||||||
|
other => panic!("expected a code run error, but got: {}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn code_error() {
|
||||||
|
match parse_success("fail:\n @exit 100")
|
||||||
|
.run(&["fail"], &Default::default()).unwrap_err() {
|
||||||
|
RuntimeError::Code{recipe, line_number, code} => {
|
||||||
|
assert_eq!(recipe, "fail");
|
||||||
|
assert_eq!(code, 100);
|
||||||
|
assert_eq!(line_number, Some(2));
|
||||||
|
},
|
||||||
|
other => panic!("expected a code run error, but got: {}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn run_args() {
|
||||||
|
let text = r#"
|
||||||
|
a return code:
|
||||||
|
@x() { {{return}} {{code + "0"}}; }; x"#;
|
||||||
|
|
||||||
|
match parse_success(text).run(&["a", "return", "15"], &Default::default()).unwrap_err() {
|
||||||
|
RuntimeError::Code{recipe, line_number, code} => {
|
||||||
|
assert_eq!(recipe, "a");
|
||||||
|
assert_eq!(code, 150);
|
||||||
|
assert_eq!(line_number, Some(3));
|
||||||
|
},
|
||||||
|
other => panic!("expected a code run error, but got: {}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_some_arguments() {
|
||||||
|
match parse_success("a b c d:").run(&["a", "b", "c"], &Default::default()).unwrap_err() {
|
||||||
|
RuntimeError::ArgumentCountMismatch{recipe, found, min, max} => {
|
||||||
|
assert_eq!(recipe, "a");
|
||||||
|
assert_eq!(found, 2);
|
||||||
|
assert_eq!(min, 3);
|
||||||
|
assert_eq!(max, 3);
|
||||||
|
},
|
||||||
|
other => panic!("expected a code run error, but got: {}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_some_arguments_variadic() {
|
||||||
|
match parse_success("a b c +d:").run(&["a", "B", "C"], &Default::default()).unwrap_err() {
|
||||||
|
RuntimeError::ArgumentCountMismatch{recipe, found, min, max} => {
|
||||||
|
assert_eq!(recipe, "a");
|
||||||
|
assert_eq!(found, 2);
|
||||||
|
assert_eq!(min, 3);
|
||||||
|
assert_eq!(max, usize::MAX - 1);
|
||||||
|
},
|
||||||
|
other => panic!("expected a code run error, but got: {}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_all_arguments() {
|
||||||
|
match parse_success("a b c d:\n echo {{b}}{{c}}{{d}}")
|
||||||
|
.run(&["a"], &Default::default()).unwrap_err() {
|
||||||
|
RuntimeError::ArgumentCountMismatch{recipe, found, min, max} => {
|
||||||
|
assert_eq!(recipe, "a");
|
||||||
|
assert_eq!(found, 0);
|
||||||
|
assert_eq!(min, 3);
|
||||||
|
assert_eq!(max, 3);
|
||||||
|
},
|
||||||
|
other => panic!("expected a code run error, but got: {}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_some_defaults() {
|
||||||
|
match parse_success("a b c d='hello':").run(&["a", "b"], &Default::default()).unwrap_err() {
|
||||||
|
RuntimeError::ArgumentCountMismatch{recipe, found, min, max} => {
|
||||||
|
assert_eq!(recipe, "a");
|
||||||
|
assert_eq!(found, 1);
|
||||||
|
assert_eq!(min, 2);
|
||||||
|
assert_eq!(max, 3);
|
||||||
|
},
|
||||||
|
other => panic!("expected a code run error, but got: {}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_all_defaults() {
|
||||||
|
match parse_success("a b c='r' d='h':").run(&["a"], &Default::default()).unwrap_err() {
|
||||||
|
RuntimeError::ArgumentCountMismatch{recipe, found, min, max} => {
|
||||||
|
assert_eq!(recipe, "a");
|
||||||
|
assert_eq!(found, 0);
|
||||||
|
assert_eq!(min, 1);
|
||||||
|
assert_eq!(max, 3);
|
||||||
|
},
|
||||||
|
other => panic!("expected a code run error, but got: {}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_overrides() {
|
||||||
|
let mut options: Configuration = Default::default();
|
||||||
|
options.overrides.insert("foo", "bar");
|
||||||
|
options.overrides.insert("baz", "bob");
|
||||||
|
match parse_success("a:\n echo {{`f() { return 100; }; f`}}")
|
||||||
|
.run(&["a"], &options).unwrap_err() {
|
||||||
|
RuntimeError::UnknownOverrides{overrides} => {
|
||||||
|
assert_eq!(overrides, &["baz", "foo"]);
|
||||||
|
},
|
||||||
|
other => panic!("expected a code run error, but got: {}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn export_failure() {
|
||||||
|
let text = r#"
|
||||||
|
export foo = "a"
|
||||||
|
baz = "c"
|
||||||
|
export bar = "b"
|
||||||
|
export abc = foo + bar + baz
|
||||||
|
|
||||||
|
wut:
|
||||||
|
echo $foo $bar $baz
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let options = Configuration {
|
||||||
|
quiet: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
match parse_success(text).run(&["wut"], &options).unwrap_err() {
|
||||||
|
RuntimeError::Code{code: _, line_number, recipe} => {
|
||||||
|
assert_eq!(recipe, "wut");
|
||||||
|
assert_eq!(line_number, Some(8));
|
||||||
|
},
|
||||||
|
other => panic!("expected a recipe code errror, but got: {}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
2163
src/lib.rs
2163
src/lib.rs
File diff suppressed because it is too large
Load Diff
81
src/main.rs
81
src/main.rs
@ -1,5 +1,82 @@
|
|||||||
extern crate just;
|
#[macro_use]
|
||||||
|
extern crate lazy_static;
|
||||||
|
extern crate ansi_term;
|
||||||
|
extern crate brev;
|
||||||
|
extern crate clap;
|
||||||
|
extern crate edit_distance;
|
||||||
|
extern crate itertools;
|
||||||
|
extern crate libc;
|
||||||
|
extern crate regex;
|
||||||
|
extern crate tempdir;
|
||||||
|
extern crate unicode_width;
|
||||||
|
|
||||||
|
mod platform;
|
||||||
|
mod run;
|
||||||
|
mod color;
|
||||||
|
mod compilation_error;
|
||||||
|
mod runtime_error;
|
||||||
|
mod misc;
|
||||||
|
mod justfile;
|
||||||
|
mod recipe;
|
||||||
|
mod token;
|
||||||
|
mod parser;
|
||||||
|
mod tokenizer;
|
||||||
|
mod cooked_string;
|
||||||
|
mod recipe_resolver;
|
||||||
|
mod assignment_resolver;
|
||||||
|
mod assignment_evaluator;
|
||||||
|
mod configuration;
|
||||||
|
mod parameter;
|
||||||
|
mod expression;
|
||||||
|
mod fragment;
|
||||||
|
mod shebang;
|
||||||
|
mod command_ext;
|
||||||
|
mod range_ext;
|
||||||
|
|
||||||
|
#[cfg(test)] mod testing;
|
||||||
|
|
||||||
|
use tokenizer::tokenize;
|
||||||
|
|
||||||
|
mod common {
|
||||||
|
pub use std::borrow::Cow;
|
||||||
|
pub use std::collections::{BTreeMap as Map, BTreeSet as Set};
|
||||||
|
pub use std::fmt::Display;
|
||||||
|
pub use std::io::prelude::*;
|
||||||
|
pub use std::ops::Range;
|
||||||
|
pub use std::path::{Path, PathBuf};
|
||||||
|
pub use std::process::Command;
|
||||||
|
pub use std::{cmp, env, fs, fmt, io, iter, process, vec, usize};
|
||||||
|
|
||||||
|
pub use color::Color;
|
||||||
|
pub use libc::{EXIT_FAILURE, EXIT_SUCCESS};
|
||||||
|
pub use regex::Regex;
|
||||||
|
pub use tempdir::TempDir;
|
||||||
|
|
||||||
|
pub use assignment_evaluator::AssignmentEvaluator;
|
||||||
|
pub use command_ext::CommandExt;
|
||||||
|
pub use compilation_error::{CompilationError, CompilationErrorKind};
|
||||||
|
pub use configuration::Configuration;
|
||||||
|
pub use cooked_string::CookedString;
|
||||||
|
pub use expression::Expression;
|
||||||
|
pub use fragment::Fragment;
|
||||||
|
pub use justfile::Justfile;
|
||||||
|
pub use misc::{default, empty};
|
||||||
|
pub use parameter::Parameter;
|
||||||
|
pub use parser::Parser;
|
||||||
|
pub use recipe::Recipe;
|
||||||
|
pub use runtime_error::RuntimeError;
|
||||||
|
pub use shebang::Shebang;
|
||||||
|
pub use token::{Token, TokenKind};
|
||||||
|
}
|
||||||
|
|
||||||
|
use common::*;
|
||||||
|
|
||||||
|
fn compile(text: &str) -> Result<Justfile, CompilationError> {
|
||||||
|
let tokens = tokenize(text)?;
|
||||||
|
let parser = Parser::new(text, tokens);
|
||||||
|
parser.justfile()
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
just::app();
|
run::run();
|
||||||
}
|
}
|
||||||
|
149
src/misc.rs
Normal file
149
src/misc.rs
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
use common::*;
|
||||||
|
|
||||||
|
use unicode_width::UnicodeWidthChar;
|
||||||
|
|
||||||
|
pub fn show_whitespace(text: &str) -> String {
|
||||||
|
text.chars().map(|c| match c { '\t' => '␉', ' ' => '␠', _ => c }).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default<T: Default>() -> T {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn empty<T, C: iter::FromIterator<T>>() -> C {
|
||||||
|
iter::empty().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ticks<T: Display>(ts: &[T]) -> Vec<Tick<T>> {
|
||||||
|
ts.iter().map(Tick).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn maybe_s(n: usize) -> &'static str {
|
||||||
|
if n == 1 {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
"s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn conjoin<T: Display>(
|
||||||
|
f: &mut fmt::Formatter,
|
||||||
|
values: &[T],
|
||||||
|
conjunction: &str,
|
||||||
|
) -> Result<(), fmt::Error> {
|
||||||
|
match values.len() {
|
||||||
|
0 => {},
|
||||||
|
1 => write!(f, "{}", values[0])?,
|
||||||
|
2 => write!(f, "{} {} {}", values[0], conjunction, values[1])?,
|
||||||
|
_ => for (i, item) in values.iter().enumerate() {
|
||||||
|
write!(f, "{}", item)?;
|
||||||
|
if i == values.len() - 1 {
|
||||||
|
} else if i == values.len() - 2 {
|
||||||
|
write!(f, ", {} ", conjunction)?;
|
||||||
|
} else {
|
||||||
|
write!(f, ", ")?
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_error_context(
|
||||||
|
f: &mut fmt::Formatter,
|
||||||
|
text: &str,
|
||||||
|
index: usize,
|
||||||
|
line: usize,
|
||||||
|
column: usize,
|
||||||
|
width: Option<usize>,
|
||||||
|
) -> Result<(), fmt::Error> {
|
||||||
|
let line_number = line + 1;
|
||||||
|
let red = Color::fmt(f).error();
|
||||||
|
match text.lines().nth(line) {
|
||||||
|
Some(line) => {
|
||||||
|
let mut i = 0;
|
||||||
|
let mut space_column = 0;
|
||||||
|
let mut space_line = String::new();
|
||||||
|
let mut space_width = 0;
|
||||||
|
for c in line.chars() {
|
||||||
|
if c == '\t' {
|
||||||
|
space_line.push_str(" ");
|
||||||
|
if i < column {
|
||||||
|
space_column += 4;
|
||||||
|
}
|
||||||
|
if i >= column && i < column + width.unwrap_or(1) {
|
||||||
|
space_width += 4;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if i < column {
|
||||||
|
space_column += UnicodeWidthChar::width(c).unwrap_or(0);
|
||||||
|
|
||||||
|
}
|
||||||
|
if i >= column && i < column + width.unwrap_or(1) {
|
||||||
|
space_width += UnicodeWidthChar::width(c).unwrap_or(0);
|
||||||
|
}
|
||||||
|
space_line.push(c);
|
||||||
|
}
|
||||||
|
i += c.len_utf8();
|
||||||
|
}
|
||||||
|
let line_number_width = line_number.to_string().len();
|
||||||
|
write!(f, "{0:1$} |\n", "", line_number_width)?;
|
||||||
|
write!(f, "{} | {}\n", line_number, space_line)?;
|
||||||
|
write!(f, "{0:1$} |", "", line_number_width)?;
|
||||||
|
if width == None {
|
||||||
|
write!(f, " {0:1$}{2}^{3}", "", space_column, red.prefix(), red.suffix())?;
|
||||||
|
} else {
|
||||||
|
write!(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)?
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct And<'a, T: 'a + Display>(pub &'a [T]);
|
||||||
|
|
||||||
|
impl<'a, T: Display> Display for And<'a, T> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
conjoin(f, self.0, "and")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Or <'a, T: 'a + Display>(pub &'a [T]);
|
||||||
|
|
||||||
|
impl<'a, T: Display> Display for Or<'a, T> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
conjoin(f, self.0, "or")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Tick<'a, T: 'a + Display>(pub &'a T);
|
||||||
|
|
||||||
|
impl<'a, T: Display> Display for Tick<'a, T> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
write!(f, "`{}`", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn conjoin_or() {
|
||||||
|
assert_eq!("1", Or(&[1 ]).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, 3, or 4", Or(&[1,2,3,4]).to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn conjoin_and() {
|
||||||
|
assert_eq!("1", And(&[1 ]).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, 3, and 4", And(&[1,2,3,4]).to_string());
|
||||||
|
}
|
||||||
|
}
|
25
src/parameter.rs
Normal file
25
src/parameter.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
use common::*;
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
pub struct Parameter<'a> {
|
||||||
|
pub default: Option<String>,
|
||||||
|
pub name: &'a str,
|
||||||
|
pub token: Token<'a>,
|
||||||
|
pub variadic: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Display for Parameter<'a> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
let color = Color::fmt(f);
|
||||||
|
if self.variadic {
|
||||||
|
write!(f, "{}", color.annotation().paint("+"))?;
|
||||||
|
}
|
||||||
|
write!(f, "{}", color.parameter().paint(self.name))?;
|
||||||
|
if let Some(ref default) = self.default {
|
||||||
|
let escaped = default.chars().flat_map(char::escape_default).collect::<String>();;
|
||||||
|
write!(f, r#"='{}'"#, color.string().paint(&escaped))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
848
src/parser.rs
Normal file
848
src/parser.rs
Normal file
@ -0,0 +1,848 @@
|
|||||||
|
use common::*;
|
||||||
|
|
||||||
|
use itertools;
|
||||||
|
use token::TokenKind::*;
|
||||||
|
use recipe_resolver::resolve_recipes;
|
||||||
|
use assignment_resolver::resolve_assignments;
|
||||||
|
|
||||||
|
pub struct Parser<'a> {
|
||||||
|
text: &'a str,
|
||||||
|
tokens: itertools::PutBack<vec::IntoIter<Token<'a>>>,
|
||||||
|
recipes: Map<&'a str, Recipe<'a>>,
|
||||||
|
assignments: Map<&'a str, Expression<'a>>,
|
||||||
|
assignment_tokens: Map<&'a str, Token<'a>>,
|
||||||
|
exports: Set<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Parser<'a> {
|
||||||
|
pub fn new(text: &'a str, tokens: Vec<Token<'a>>) -> Parser<'a> {
|
||||||
|
Parser {
|
||||||
|
text: text,
|
||||||
|
tokens: itertools::put_back(tokens),
|
||||||
|
recipes: empty(),
|
||||||
|
assignments: empty(),
|
||||||
|
assignment_tokens: empty(),
|
||||||
|
exports: empty(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peek(&mut self, kind: TokenKind) -> bool {
|
||||||
|
let next = self.tokens.next().unwrap();
|
||||||
|
let result = next.kind == kind;
|
||||||
|
self.tokens.put_back(next);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accept(&mut self, kind: TokenKind) -> Option<Token<'a>> {
|
||||||
|
if self.peek(kind) {
|
||||||
|
self.tokens.next()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accept_any(&mut self, kinds: &[TokenKind]) -> Option<Token<'a>> {
|
||||||
|
for kind in kinds {
|
||||||
|
if self.peek(*kind) {
|
||||||
|
return self.tokens.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accepted(&mut self, kind: TokenKind) -> bool {
|
||||||
|
self.accept(kind).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect(&mut self, kind: TokenKind) -> Option<Token<'a>> {
|
||||||
|
if self.peek(kind) {
|
||||||
|
self.tokens.next();
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.tokens.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect_eol(&mut self) -> Option<Token<'a>> {
|
||||||
|
self.accepted(Comment);
|
||||||
|
if self.peek(Eol) {
|
||||||
|
self.accept(Eol);
|
||||||
|
None
|
||||||
|
} else if self.peek(Eof) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.tokens.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unexpected_token(&self, found: &Token<'a>, expected: &[TokenKind]) -> CompilationError<'a> {
|
||||||
|
found.error(CompilationErrorKind::UnexpectedToken {
|
||||||
|
expected: expected.to_vec(),
|
||||||
|
found: found.kind,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recipe(
|
||||||
|
&mut self,
|
||||||
|
name: Token<'a>,
|
||||||
|
doc: Option<Token<'a>>,
|
||||||
|
quiet: bool,
|
||||||
|
) -> Result<(), CompilationError<'a>> {
|
||||||
|
if let Some(recipe) = self.recipes.get(name.lexeme) {
|
||||||
|
return Err(name.error(CompilationErrorKind::DuplicateRecipe {
|
||||||
|
recipe: recipe.name,
|
||||||
|
first: recipe.line_number
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut parsed_parameter_with_default = false;
|
||||||
|
let mut parsed_variadic_parameter = false;
|
||||||
|
let mut parameters: Vec<Parameter> = vec![];
|
||||||
|
loop {
|
||||||
|
let plus = self.accept(Plus);
|
||||||
|
|
||||||
|
let parameter = match self.accept(Name) {
|
||||||
|
Some(parameter) => parameter,
|
||||||
|
None => if let Some(plus) = plus {
|
||||||
|
return Err(self.unexpected_token(&plus, &[Name]));
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let variadic = plus.is_some();
|
||||||
|
|
||||||
|
if parsed_variadic_parameter {
|
||||||
|
return Err(parameter.error(CompilationErrorKind::ParameterFollowsVariadicParameter {
|
||||||
|
parameter: parameter.lexeme,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if parameters.iter().any(|p| p.name == parameter.lexeme) {
|
||||||
|
return Err(parameter.error(CompilationErrorKind::DuplicateParameter {
|
||||||
|
recipe: name.lexeme, parameter: parameter.lexeme
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let default;
|
||||||
|
if self.accepted(Equals) {
|
||||||
|
if let Some(string) = self.accept_any(&[StringToken, RawString]) {
|
||||||
|
default = Some(CookedString::new(&string)?.cooked);
|
||||||
|
} else {
|
||||||
|
let unexpected = self.tokens.next().unwrap();
|
||||||
|
return Err(self.unexpected_token(&unexpected, &[StringToken, RawString]));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
default = None
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsed_parameter_with_default && default.is_none() {
|
||||||
|
return Err(parameter.error(CompilationErrorKind::RequiredParameterFollowsDefaultParameter{
|
||||||
|
parameter: parameter.lexeme,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed_parameter_with_default |= default.is_some();
|
||||||
|
parsed_variadic_parameter = variadic;
|
||||||
|
|
||||||
|
parameters.push(Parameter {
|
||||||
|
default: default,
|
||||||
|
name: parameter.lexeme,
|
||||||
|
token: parameter,
|
||||||
|
variadic: variadic,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(token) = self.expect(Colon) {
|
||||||
|
// if we haven't accepted any parameters, an equals
|
||||||
|
// would have been fine as part of an assignment
|
||||||
|
if parameters.is_empty() {
|
||||||
|
return Err(self.unexpected_token(&token, &[Name, Plus, Colon, Equals]));
|
||||||
|
} else {
|
||||||
|
return Err(self.unexpected_token(&token, &[Name, Plus, Colon]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut dependencies = vec![];
|
||||||
|
let mut dependency_tokens = vec![];
|
||||||
|
while let Some(dependency) = self.accept(Name) {
|
||||||
|
if dependencies.contains(&dependency.lexeme) {
|
||||||
|
return Err(dependency.error(CompilationErrorKind::DuplicateDependency {
|
||||||
|
recipe: name.lexeme,
|
||||||
|
dependency: dependency.lexeme
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
dependencies.push(dependency.lexeme);
|
||||||
|
dependency_tokens.push(dependency);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(token) = self.expect_eol() {
|
||||||
|
return Err(self.unexpected_token(&token, &[Name, Eol, Eof]));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut lines: Vec<Vec<Fragment>> = vec![];
|
||||||
|
let mut shebang = false;
|
||||||
|
|
||||||
|
if self.accepted(Indent) {
|
||||||
|
while !self.accepted(Dedent) {
|
||||||
|
if self.accepted(Eol) {
|
||||||
|
lines.push(vec![]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(token) = self.expect(Line) {
|
||||||
|
return Err(token.error(CompilationErrorKind::Internal{
|
||||||
|
message: format!("Expected a line but got {}", token.kind)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
let mut fragments = vec![];
|
||||||
|
|
||||||
|
while !(self.accepted(Eol) || self.peek(Dedent)) {
|
||||||
|
if let Some(token) = self.accept(Text) {
|
||||||
|
if fragments.is_empty() {
|
||||||
|
if lines.is_empty() {
|
||||||
|
if token.lexeme.starts_with("#!") {
|
||||||
|
shebang = true;
|
||||||
|
}
|
||||||
|
} else if !shebang
|
||||||
|
&& !lines.last().and_then(|line| line.last())
|
||||||
|
.map(Fragment::continuation).unwrap_or(false)
|
||||||
|
&& (token.lexeme.starts_with(' ') || token.lexeme.starts_with('\t')) {
|
||||||
|
return Err(token.error(CompilationErrorKind::ExtraLeadingWhitespace));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fragments.push(Fragment::Text{text: token});
|
||||||
|
} else if let Some(token) = self.expect(InterpolationStart) {
|
||||||
|
return Err(self.unexpected_token(&token, &[Text, InterpolationStart, Eol]));
|
||||||
|
} else {
|
||||||
|
fragments.push(Fragment::Expression{
|
||||||
|
expression: self.expression(true)?
|
||||||
|
});
|
||||||
|
if let Some(token) = self.expect(InterpolationEnd) {
|
||||||
|
return Err(self.unexpected_token(&token, &[InterpolationEnd]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(fragments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.recipes.insert(name.lexeme, Recipe {
|
||||||
|
line_number: name.line,
|
||||||
|
name: name.lexeme,
|
||||||
|
doc: doc.map(|t| t.lexeme[1..].trim()),
|
||||||
|
dependencies: dependencies,
|
||||||
|
dependency_tokens: dependency_tokens,
|
||||||
|
parameters: parameters,
|
||||||
|
private: &name.lexeme[0..1] == "_",
|
||||||
|
lines: lines,
|
||||||
|
shebang: shebang,
|
||||||
|
quiet: quiet,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expression(&mut self, interpolation: bool) -> Result<Expression<'a>, CompilationError<'a>> {
|
||||||
|
let first = self.tokens.next().unwrap();
|
||||||
|
let lhs = match first.kind {
|
||||||
|
Name => Expression::Variable {name: first.lexeme, token: first},
|
||||||
|
Backtick => Expression::Backtick {
|
||||||
|
raw: &first.lexeme[1..first.lexeme.len()-1],
|
||||||
|
token: first
|
||||||
|
},
|
||||||
|
RawString | StringToken => {
|
||||||
|
Expression::String{cooked_string: CookedString::new(&first)?}
|
||||||
|
}
|
||||||
|
_ => return Err(self.unexpected_token(&first, &[Name, StringToken])),
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.accepted(Plus) {
|
||||||
|
let rhs = self.expression(interpolation)?;
|
||||||
|
Ok(Expression::Concatination{lhs: Box::new(lhs), rhs: Box::new(rhs)})
|
||||||
|
} else if interpolation && self.peek(InterpolationEnd) {
|
||||||
|
Ok(lhs)
|
||||||
|
} else if let Some(token) = self.expect_eol() {
|
||||||
|
if interpolation {
|
||||||
|
return Err(self.unexpected_token(&token, &[Plus, Eol, InterpolationEnd]))
|
||||||
|
} else {
|
||||||
|
Err(self.unexpected_token(&token, &[Plus, Eol]))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(lhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assignment(&mut self, name: Token<'a>, export: bool) -> Result<(), CompilationError<'a>> {
|
||||||
|
if self.assignments.contains_key(name.lexeme) {
|
||||||
|
return Err(name.error(CompilationErrorKind::DuplicateVariable {variable: name.lexeme}));
|
||||||
|
}
|
||||||
|
if export {
|
||||||
|
self.exports.insert(name.lexeme);
|
||||||
|
}
|
||||||
|
let expression = self.expression(false)?;
|
||||||
|
self.assignments.insert(name.lexeme, expression);
|
||||||
|
self.assignment_tokens.insert(name.lexeme, name);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn justfile(mut self) -> Result<Justfile<'a>, CompilationError<'a>> {
|
||||||
|
let mut doc = None;
|
||||||
|
loop {
|
||||||
|
match self.tokens.next() {
|
||||||
|
Some(token) => match token.kind {
|
||||||
|
Eof => break,
|
||||||
|
Eol => {
|
||||||
|
doc = None;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Comment => {
|
||||||
|
if let Some(token) = self.expect_eol() {
|
||||||
|
return Err(token.error(CompilationErrorKind::Internal {
|
||||||
|
message: format!("found comment followed by {}", token.kind),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
doc = Some(token);
|
||||||
|
}
|
||||||
|
At => if let Some(name) = self.accept(Name) {
|
||||||
|
self.recipe(name, doc, true)?;
|
||||||
|
doc = None;
|
||||||
|
} else {
|
||||||
|
let unexpected = &self.tokens.next().unwrap();
|
||||||
|
return Err(self.unexpected_token(unexpected, &[Name]));
|
||||||
|
},
|
||||||
|
Name => if token.lexeme == "export" {
|
||||||
|
let next = self.tokens.next().unwrap();
|
||||||
|
if next.kind == Name && self.accepted(Equals) {
|
||||||
|
self.assignment(next, true)?;
|
||||||
|
doc = None;
|
||||||
|
} else {
|
||||||
|
self.tokens.put_back(next);
|
||||||
|
self.recipe(token, doc, false)?;
|
||||||
|
doc = None;
|
||||||
|
}
|
||||||
|
} else if self.accepted(Equals) {
|
||||||
|
self.assignment(token, false)?;
|
||||||
|
doc = None;
|
||||||
|
} else {
|
||||||
|
self.recipe(token, doc, false)?;
|
||||||
|
doc = None;
|
||||||
|
},
|
||||||
|
_ => return Err(self.unexpected_token(&token, &[Name, At])),
|
||||||
|
},
|
||||||
|
None => return Err(CompilationError {
|
||||||
|
text: self.text,
|
||||||
|
index: 0,
|
||||||
|
line: 0,
|
||||||
|
column: 0,
|
||||||
|
width: None,
|
||||||
|
kind: CompilationErrorKind::Internal {
|
||||||
|
message: "unexpected end of token stream".to_string()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(token) = self.tokens.next() {
|
||||||
|
return Err(token.error(CompilationErrorKind::Internal {
|
||||||
|
message: format!("unexpected token remaining after parsing completed: {:?}", token.kind)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_recipes(&self.recipes, &self.assignments, self.text)?;
|
||||||
|
|
||||||
|
for recipe in self.recipes.values() {
|
||||||
|
for parameter in &recipe.parameters {
|
||||||
|
if self.assignments.contains_key(parameter.token.lexeme) {
|
||||||
|
return Err(parameter.token.error(CompilationErrorKind::ParameterShadowsVariable {
|
||||||
|
parameter: parameter.token.lexeme
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for dependency in &recipe.dependency_tokens {
|
||||||
|
if !self.recipes[dependency.lexeme].parameters.is_empty() {
|
||||||
|
return Err(dependency.error(CompilationErrorKind::DependencyHasParameters {
|
||||||
|
recipe: recipe.name,
|
||||||
|
dependency: dependency.lexeme,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_assignments(&self.assignments, &self.assignment_tokens)?;
|
||||||
|
|
||||||
|
Ok(Justfile {
|
||||||
|
recipes: self.recipes,
|
||||||
|
assignments: self.assignments,
|
||||||
|
exports: self.exports,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use brev;
|
||||||
|
use testing::parse_success;
|
||||||
|
use testing::parse_error;
|
||||||
|
|
||||||
|
fn parse_summary(input: &str, output: &str) {
|
||||||
|
let justfile = parse_success(input);
|
||||||
|
let s = format!("{:#}", justfile);
|
||||||
|
if s != output {
|
||||||
|
println!("got:\n\"{}\"\n", s);
|
||||||
|
println!("\texpected:\n\"{}\"", output);
|
||||||
|
assert_eq!(s, output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_empty() {
|
||||||
|
parse_summary("
|
||||||
|
|
||||||
|
# hello
|
||||||
|
|
||||||
|
|
||||||
|
", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_string_default() {
|
||||||
|
parse_summary(r#"
|
||||||
|
|
||||||
|
foo a="b\t":
|
||||||
|
|
||||||
|
|
||||||
|
"#, r#"foo a='b\t':"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_variadic() {
|
||||||
|
parse_summary(r#"
|
||||||
|
|
||||||
|
foo +a:
|
||||||
|
|
||||||
|
|
||||||
|
"#, r#"foo +a:"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_variadic_string_default() {
|
||||||
|
parse_summary(r#"
|
||||||
|
|
||||||
|
foo +a="Hello":
|
||||||
|
|
||||||
|
|
||||||
|
"#, r#"foo +a='Hello':"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_raw_string_default() {
|
||||||
|
parse_summary(r#"
|
||||||
|
|
||||||
|
foo a='b\t':
|
||||||
|
|
||||||
|
|
||||||
|
"#, r#"foo a='b\\t':"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_export() {
|
||||||
|
parse_summary(r#"
|
||||||
|
export a = "hello"
|
||||||
|
|
||||||
|
"#, r#"export a = "hello""#);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_complex() {
|
||||||
|
parse_summary("
|
||||||
|
x:
|
||||||
|
y:
|
||||||
|
z:
|
||||||
|
foo = \"xx\"
|
||||||
|
bar = foo
|
||||||
|
goodbye = \"y\"
|
||||||
|
hello a b c : x y z #hello
|
||||||
|
#! blah
|
||||||
|
#blarg
|
||||||
|
{{ foo + bar}}abc{{ goodbye\t + \"x\" }}xyz
|
||||||
|
1
|
||||||
|
2
|
||||||
|
3
|
||||||
|
", "bar = foo
|
||||||
|
|
||||||
|
foo = \"xx\"
|
||||||
|
|
||||||
|
goodbye = \"y\"
|
||||||
|
|
||||||
|
hello a b c: x y z
|
||||||
|
#! blah
|
||||||
|
#blarg
|
||||||
|
{{foo + bar}}abc{{goodbye + \"x\"}}xyz
|
||||||
|
1
|
||||||
|
2
|
||||||
|
3
|
||||||
|
|
||||||
|
x:
|
||||||
|
|
||||||
|
y:
|
||||||
|
|
||||||
|
z:");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_shebang() {
|
||||||
|
parse_summary("
|
||||||
|
practicum = 'hello'
|
||||||
|
install:
|
||||||
|
\t#!/bin/sh
|
||||||
|
\tif [[ -f {{practicum}} ]]; then
|
||||||
|
\t\treturn
|
||||||
|
\tfi
|
||||||
|
", "practicum = \"hello\"
|
||||||
|
|
||||||
|
install:
|
||||||
|
#!/bin/sh
|
||||||
|
if [[ -f {{practicum}} ]]; then
|
||||||
|
\treturn
|
||||||
|
fi"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_assignments() {
|
||||||
|
parse_summary(
|
||||||
|
r#"a = "0"
|
||||||
|
c = a + b + a + b
|
||||||
|
b = "1"
|
||||||
|
"#,
|
||||||
|
|
||||||
|
r#"a = "0"
|
||||||
|
|
||||||
|
b = "1"
|
||||||
|
|
||||||
|
c = a + b + a + b"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_assignment_backticks() {
|
||||||
|
parse_summary(
|
||||||
|
"a = `echo hello`
|
||||||
|
c = a + b + a + b
|
||||||
|
b = `echo goodbye`",
|
||||||
|
|
||||||
|
"a = `echo hello`
|
||||||
|
|
||||||
|
b = `echo goodbye`
|
||||||
|
|
||||||
|
c = a + b + a + b");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_interpolation_backticks() {
|
||||||
|
parse_summary(
|
||||||
|
r#"a:
|
||||||
|
echo {{ `echo hello` + "blarg" }} {{ `echo bob` }}"#,
|
||||||
|
r#"a:
|
||||||
|
echo {{`echo hello` + "blarg"}} {{`echo bob`}}"#,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_colon() {
|
||||||
|
let text = "a b c\nd e f";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 5,
|
||||||
|
line: 0,
|
||||||
|
column: 5,
|
||||||
|
width: Some(1),
|
||||||
|
kind: CompilationErrorKind::UnexpectedToken{expected: vec![Name, Plus, Colon], found: Eol},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_default_eol() {
|
||||||
|
let text = "hello arg=\n";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 10,
|
||||||
|
line: 0,
|
||||||
|
column: 10,
|
||||||
|
width: Some(1),
|
||||||
|
kind: CompilationErrorKind::UnexpectedToken{expected: vec![StringToken, RawString], found: Eol},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_default_eof() {
|
||||||
|
let text = "hello arg=";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 10,
|
||||||
|
line: 0,
|
||||||
|
column: 10,
|
||||||
|
width: Some(0),
|
||||||
|
kind: CompilationErrorKind::UnexpectedToken{expected: vec![StringToken, RawString], found: Eof},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_default_colon() {
|
||||||
|
let text = "hello arg=:";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 10,
|
||||||
|
line: 0,
|
||||||
|
column: 10,
|
||||||
|
width: Some(1),
|
||||||
|
kind: CompilationErrorKind::UnexpectedToken{expected: vec![StringToken, RawString], found: Colon},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_default_backtick() {
|
||||||
|
let text = "hello arg=`hello`";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 10,
|
||||||
|
line: 0,
|
||||||
|
column: 10,
|
||||||
|
width: Some(7),
|
||||||
|
kind: CompilationErrorKind::UnexpectedToken{expected: vec![StringToken, RawString], found: Backtick},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parameter_after_variadic() {
|
||||||
|
let text = "foo +a bbb:";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 7,
|
||||||
|
line: 0,
|
||||||
|
column: 7,
|
||||||
|
width: Some(3),
|
||||||
|
kind: CompilationErrorKind::ParameterFollowsVariadicParameter{parameter: "bbb"}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn required_after_default() {
|
||||||
|
let text = "hello arg='foo' bar:";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 16,
|
||||||
|
line: 0,
|
||||||
|
column: 16,
|
||||||
|
width: Some(3),
|
||||||
|
kind: CompilationErrorKind::RequiredParameterFollowsDefaultParameter{parameter: "bar"},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_eol() {
|
||||||
|
let text = "a b c: z =";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 9,
|
||||||
|
line: 0,
|
||||||
|
column: 9,
|
||||||
|
width: Some(1),
|
||||||
|
kind: CompilationErrorKind::UnexpectedToken{expected: vec![Name, Eol, Eof], found: Equals},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn eof_test() {
|
||||||
|
parse_summary("x:\ny:\nz:\na b c: x y z", "a b c: x y z\n\nx:\n\ny:\n\nz:");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duplicate_parameter() {
|
||||||
|
let text = "a b b:";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 4,
|
||||||
|
line: 0,
|
||||||
|
column: 4,
|
||||||
|
width: Some(1),
|
||||||
|
kind: CompilationErrorKind::DuplicateParameter{recipe: "a", parameter: "b"}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parameter_shadows_varible() {
|
||||||
|
let text = "foo = \"h\"\na foo:";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 12,
|
||||||
|
line: 1,
|
||||||
|
column: 2,
|
||||||
|
width: Some(3),
|
||||||
|
kind: CompilationErrorKind::ParameterShadowsVariable{parameter: "foo"}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dependency_has_parameters() {
|
||||||
|
let text = "foo arg:\nb: foo";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 12,
|
||||||
|
line: 1,
|
||||||
|
column: 3,
|
||||||
|
width: Some(3),
|
||||||
|
kind: CompilationErrorKind::DependencyHasParameters{recipe: "b", dependency: "foo"}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duplicate_dependency() {
|
||||||
|
let text = "a b c: b c z z";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 13,
|
||||||
|
line: 0,
|
||||||
|
column: 13,
|
||||||
|
width: Some(1),
|
||||||
|
kind: CompilationErrorKind::DuplicateDependency{recipe: "a", dependency: "z"}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duplicate_recipe() {
|
||||||
|
let text = "a:\nb:\na:";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 6,
|
||||||
|
line: 2,
|
||||||
|
column: 0,
|
||||||
|
width: Some(1),
|
||||||
|
kind: CompilationErrorKind::DuplicateRecipe{recipe: "a", first: 0}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duplicate_variable() {
|
||||||
|
let text = "a = \"0\"\na = \"0\"";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 8,
|
||||||
|
line: 1,
|
||||||
|
column: 0,
|
||||||
|
width: Some(1),
|
||||||
|
kind: CompilationErrorKind::DuplicateVariable{variable: "a"}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_quote_escape() {
|
||||||
|
parse_summary(
|
||||||
|
r#"a = "hello\"""#,
|
||||||
|
r#"a = "hello\"""#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_escapes() {
|
||||||
|
parse_summary(
|
||||||
|
r#"a = "\n\t\r\"\\""#,
|
||||||
|
r#"a = "\n\t\r\"\\""#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parameters() {
|
||||||
|
parse_summary(
|
||||||
|
"a b c:
|
||||||
|
{{b}} {{c}}",
|
||||||
|
"a b c:
|
||||||
|
{{b}} {{c}}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extra_whitespace() {
|
||||||
|
let text = "a:\n blah\n blarg";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 10,
|
||||||
|
line: 2,
|
||||||
|
column: 1,
|
||||||
|
width: Some(6),
|
||||||
|
kind: CompilationErrorKind::ExtraLeadingWhitespace
|
||||||
|
});
|
||||||
|
|
||||||
|
// extra leading whitespace is okay in a shebang recipe
|
||||||
|
parse_success("a:\n #!\n print(1)");
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn interpolation_outside_of_recipe() {
|
||||||
|
let text = "{{";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 0,
|
||||||
|
line: 0,
|
||||||
|
column: 0,
|
||||||
|
width: Some(2),
|
||||||
|
kind: CompilationErrorKind::UnexpectedToken{expected: vec![Name, At], found: InterpolationStart},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn unclosed_interpolation_delimiter() {
|
||||||
|
let text = "a:\n echo {{ foo";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 15,
|
||||||
|
line: 1,
|
||||||
|
column: 12,
|
||||||
|
width: Some(0),
|
||||||
|
kind: CompilationErrorKind::UnexpectedToken{expected: vec![Plus, Eol, InterpolationEnd], found: Dedent},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plus_following_parameter() {
|
||||||
|
let text = "a b c+:";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 5,
|
||||||
|
line: 0,
|
||||||
|
column: 5,
|
||||||
|
width: Some(1),
|
||||||
|
kind: CompilationErrorKind::UnexpectedToken{expected: vec![Name], found: Plus},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn readme_test() {
|
||||||
|
let mut justfiles = vec![];
|
||||||
|
let mut current = None;
|
||||||
|
|
||||||
|
for line in brev::slurp("README.asc").lines() {
|
||||||
|
if let Some(mut justfile) = current {
|
||||||
|
if line == "```" {
|
||||||
|
justfiles.push(justfile);
|
||||||
|
current = None;
|
||||||
|
} else {
|
||||||
|
justfile += line;
|
||||||
|
justfile += "\n";
|
||||||
|
current = Some(justfile);
|
||||||
|
}
|
||||||
|
} else if line == "```make" {
|
||||||
|
current = Some(String::new());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for justfile in justfiles {
|
||||||
|
parse_success(&justfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
use ::prelude::*;
|
use common::*;
|
||||||
|
|
||||||
|
use brev;
|
||||||
|
|
||||||
pub struct Platform;
|
pub struct Platform;
|
||||||
|
|
||||||
@ -6,7 +8,7 @@ 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(path: &Path, command: &str, argument: Option<&str>)
|
||||||
-> Result<process::Command, super::OutputError>;
|
-> 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>;
|
||||||
@ -18,10 +20,10 @@ pub trait PlatformInterface {
|
|||||||
#[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(path: &Path, _command: &str, _argument: Option<&str>)
|
||||||
-> Result<process::Command, super::OutputError>
|
-> Result<Command, brev::OutputError>
|
||||||
{
|
{
|
||||||
// shebang scripts can be executed directly on unix
|
// shebang scripts can be executed directly on unix
|
||||||
Ok(process::Command::new(path))
|
Ok(Command::new(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_execute_permission(path: &Path) -> Result<(), io::Error> {
|
fn set_execute_permission(path: &Path) -> Result<(), io::Error> {
|
||||||
@ -47,14 +49,14 @@ impl PlatformInterface for Platform {
|
|||||||
#[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(path: &Path, command: &str, argument: Option<&str>)
|
||||||
-> Result<process::Command, super::OutputError>
|
-> 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 = process::Command::new("cygpath");
|
let mut cygpath = Command::new("cygpath");
|
||||||
cygpath.arg("--windows");
|
cygpath.arg("--windows");
|
||||||
cygpath.arg(command);
|
cygpath.arg(command);
|
||||||
|
|
||||||
let mut cmd = process::Command::new(super::output(cygpath)?);
|
let mut cmd = Command::new(brev::output(cygpath)?);
|
||||||
if let Some(argument) = argument {
|
if let Some(argument) = argument {
|
||||||
cmd.arg(argument);
|
cmd.arg(argument);
|
||||||
}
|
}
|
||||||
|
25
src/range_ext.rs
Normal file
25
src/range_ext.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
use common::*;
|
||||||
|
|
||||||
|
pub trait RangeExt<T> {
|
||||||
|
fn range_contains(&self, i: T) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> RangeExt<T> for Range<T> where T: PartialOrd + Copy {
|
||||||
|
fn range_contains(&self, i: T) -> bool {
|
||||||
|
i >= self.start && i < self.end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn range() {
|
||||||
|
assert!( ( 0.. 1).range_contains( 0));
|
||||||
|
assert!( (10..20).range_contains(15));
|
||||||
|
assert!(!( 0.. 0).range_contains( 0));
|
||||||
|
assert!(!( 1..10).range_contains( 0));
|
||||||
|
assert!(!( 1..10).range_contains(10));
|
||||||
|
}
|
||||||
|
}
|
281
src/recipe.rs
Normal file
281
src/recipe.rs
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
use common::*;
|
||||||
|
|
||||||
|
use std::process::{ExitStatus, Command, Stdio};
|
||||||
|
|
||||||
|
use platform::{Platform, PlatformInterface};
|
||||||
|
|
||||||
|
/// Return a `RuntimeError::Signal` if the process was terminated by a signal,
|
||||||
|
/// otherwise return an `RuntimeError::UnknownFailure`
|
||||||
|
fn error_from_signal(
|
||||||
|
recipe: &str,
|
||||||
|
line_number: Option<usize>,
|
||||||
|
exit_status: ExitStatus
|
||||||
|
) -> RuntimeError {
|
||||||
|
match Platform::signal_from_exit_status(exit_status) {
|
||||||
|
Some(signal) => RuntimeError::Signal{recipe: recipe, line_number: line_number, signal: signal},
|
||||||
|
None => RuntimeError::Unknown{recipe: recipe, line_number: line_number},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
pub struct Recipe<'a> {
|
||||||
|
pub dependencies: Vec<&'a str>,
|
||||||
|
pub dependency_tokens: Vec<Token<'a>>,
|
||||||
|
pub doc: Option<&'a str>,
|
||||||
|
pub line_number: usize,
|
||||||
|
pub lines: Vec<Vec<Fragment<'a>>>,
|
||||||
|
pub name: &'a str,
|
||||||
|
pub parameters: Vec<Parameter<'a>>,
|
||||||
|
pub private: bool,
|
||||||
|
pub quiet: bool,
|
||||||
|
pub shebang: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Recipe<'a> {
|
||||||
|
pub fn argument_range(&self) -> Range<usize> {
|
||||||
|
self.min_arguments()..self.max_arguments() + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn min_arguments(&self) -> usize {
|
||||||
|
self.parameters.iter().filter(|p| !p.default.is_some()).count()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max_arguments(&self) -> usize {
|
||||||
|
if self.parameters.iter().any(|p| p.variadic) {
|
||||||
|
usize::MAX - 1
|
||||||
|
} else {
|
||||||
|
self.parameters.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(
|
||||||
|
&self,
|
||||||
|
arguments: &[&'a str],
|
||||||
|
scope: &Map<&'a str, String>,
|
||||||
|
exports: &Set<&'a str>,
|
||||||
|
options: &Configuration,
|
||||||
|
) -> Result<(), RuntimeError<'a>> {
|
||||||
|
if options.verbose {
|
||||||
|
let color = options.color.stderr().banner();
|
||||||
|
eprintln!("{}===> Running recipe `{}`...{}", color.prefix(), self.name, color.suffix());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut argument_map = Map::new();
|
||||||
|
|
||||||
|
let mut rest = arguments;
|
||||||
|
for parameter in &self.parameters {
|
||||||
|
let value = if rest.is_empty() {
|
||||||
|
match parameter.default {
|
||||||
|
Some(ref default) => Cow::Borrowed(default.as_str()),
|
||||||
|
None => return Err(RuntimeError::Internal{
|
||||||
|
message: "missing parameter without default".to_string()
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
} else if parameter.variadic {
|
||||||
|
let value = Cow::Owned(rest.to_vec().join(" "));
|
||||||
|
rest = &[];
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
let value = Cow::Borrowed(rest[0]);
|
||||||
|
rest = &rest[1..];
|
||||||
|
value
|
||||||
|
};
|
||||||
|
argument_map.insert(parameter.name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut evaluator = AssignmentEvaluator {
|
||||||
|
evaluated: empty(),
|
||||||
|
scope: scope,
|
||||||
|
exports: exports,
|
||||||
|
assignments: &empty(),
|
||||||
|
overrides: &empty(),
|
||||||
|
quiet: options.quiet,
|
||||||
|
shell: options.shell,
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.shebang {
|
||||||
|
let mut evaluated_lines = vec![];
|
||||||
|
for line in &self.lines {
|
||||||
|
evaluated_lines.push(evaluator.evaluate_line(line, &argument_map)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.dry_run || self.quiet {
|
||||||
|
for line in &evaluated_lines {
|
||||||
|
eprintln!("{}", line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.dry_run {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let tmp = TempDir::new("just")
|
||||||
|
.map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?;
|
||||||
|
let mut path = tmp.path().to_path_buf();
|
||||||
|
path.push(self.name);
|
||||||
|
{
|
||||||
|
let mut f = fs::File::create(&path)
|
||||||
|
.map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?;
|
||||||
|
let mut text = String::new();
|
||||||
|
// add the shebang
|
||||||
|
text += &evaluated_lines[0];
|
||||||
|
text += "\n";
|
||||||
|
// add blank lines so that lines in the generated script
|
||||||
|
// have the same line number as the corresponding lines
|
||||||
|
// in the justfile
|
||||||
|
for _ in 1..(self.line_number + 2) {
|
||||||
|
text += "\n"
|
||||||
|
}
|
||||||
|
for line in &evaluated_lines[1..] {
|
||||||
|
text += line;
|
||||||
|
text += "\n";
|
||||||
|
}
|
||||||
|
f.write_all(text.as_bytes())
|
||||||
|
.map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make the script executable
|
||||||
|
Platform::set_execute_permission(&path)
|
||||||
|
.map_err(|error| RuntimeError::TmpdirIoError{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)
|
||||||
|
.ok_or_else(|| RuntimeError::Internal {
|
||||||
|
message: format!("bad shebang line: {}", shebang_line)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// create a command to run the script
|
||||||
|
let mut command = Platform::make_shebang_command(&path, interpreter, argument)
|
||||||
|
.map_err(|output_error| RuntimeError::Cygpath{recipe: self.name, output_error: output_error})?;
|
||||||
|
|
||||||
|
command.export_environment_variables(scope, exports)?;
|
||||||
|
|
||||||
|
// run it!
|
||||||
|
match command.status() {
|
||||||
|
Ok(exit_status) => if let Some(code) = exit_status.code() {
|
||||||
|
if code != 0 {
|
||||||
|
return Err(RuntimeError::Code{recipe: self.name, line_number: None, code: code})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(error_from_signal(self.name, None, exit_status))
|
||||||
|
},
|
||||||
|
Err(io_error) => return Err(RuntimeError::Shebang {
|
||||||
|
recipe: self.name,
|
||||||
|
command: interpreter.to_string(),
|
||||||
|
argument: argument.map(String::from),
|
||||||
|
io_error: io_error
|
||||||
|
})
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
let mut lines = self.lines.iter().peekable();
|
||||||
|
let mut line_number = self.line_number + 1;
|
||||||
|
loop {
|
||||||
|
if lines.peek().is_none() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let mut evaluated = String::new();
|
||||||
|
loop {
|
||||||
|
if lines.peek().is_none() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let line = lines.next().unwrap();
|
||||||
|
line_number += 1;
|
||||||
|
evaluated += &evaluator.evaluate_line(line, &argument_map)?;
|
||||||
|
if line.last().map(Fragment::continuation).unwrap_or(false) {
|
||||||
|
evaluated.pop();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut command = evaluated.as_str();
|
||||||
|
let quiet_command = command.starts_with('@');
|
||||||
|
if quiet_command {
|
||||||
|
command = &command[1..];
|
||||||
|
}
|
||||||
|
|
||||||
|
if command == "" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.dry_run || options.verbose || !((quiet_command ^ self.quiet) || options.quiet) {
|
||||||
|
let color = if options.highlight {
|
||||||
|
options.color.command()
|
||||||
|
} else {
|
||||||
|
options.color
|
||||||
|
};
|
||||||
|
eprintln!("{}", color.stderr().paint(command));
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.dry_run {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cmd = Command::new(options.shell);
|
||||||
|
|
||||||
|
cmd.arg("-cu").arg(command);
|
||||||
|
|
||||||
|
if options.quiet {
|
||||||
|
cmd.stderr(Stdio::null());
|
||||||
|
cmd.stdout(Stdio::null());
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.export_environment_variables(scope, exports)?;
|
||||||
|
|
||||||
|
match cmd.status() {
|
||||||
|
Ok(exit_status) => if let Some(code) = exit_status.code() {
|
||||||
|
if code != 0 {
|
||||||
|
return Err(RuntimeError::Code{
|
||||||
|
recipe: self.name, line_number: Some(line_number), code: code
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(error_from_signal(self.name, Some(line_number), exit_status));
|
||||||
|
},
|
||||||
|
Err(io_error) => return Err(RuntimeError::IoError{
|
||||||
|
recipe: self.name, io_error: io_error}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Display for Recipe<'a> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
if let Some(doc) = self.doc {
|
||||||
|
writeln!(f, "# {}", doc)?;
|
||||||
|
}
|
||||||
|
write!(f, "{}", self.name)?;
|
||||||
|
for parameter in &self.parameters {
|
||||||
|
write!(f, " {}", parameter)?;
|
||||||
|
}
|
||||||
|
write!(f, ":")?;
|
||||||
|
for dependency in &self.dependencies {
|
||||||
|
write!(f, " {}", dependency)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, pieces) in self.lines.iter().enumerate() {
|
||||||
|
if i == 0 {
|
||||||
|
writeln!(f, "")?;
|
||||||
|
}
|
||||||
|
for (j, piece) in pieces.iter().enumerate() {
|
||||||
|
if j == 0 {
|
||||||
|
write!(f, " ")?;
|
||||||
|
}
|
||||||
|
match *piece {
|
||||||
|
Fragment::Text{ref text} => write!(f, "{}", text.lexeme)?,
|
||||||
|
Fragment::Expression{ref expression, ..} =>
|
||||||
|
write!(f, "{}{}{}", "{{", expression, "}}")?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i + 1 < self.lines.len() {
|
||||||
|
write!(f, "\n")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
171
src/recipe_resolver.rs
Normal file
171
src/recipe_resolver.rs
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
use common::*;
|
||||||
|
|
||||||
|
pub fn resolve_recipes<'a>(
|
||||||
|
recipes: &Map<&'a str, Recipe<'a>>,
|
||||||
|
assignments: &Map<&'a str, Expression<'a>>,
|
||||||
|
text: &'a str,
|
||||||
|
) -> Result<(), CompilationError<'a>> {
|
||||||
|
let mut resolver = RecipeResolver {
|
||||||
|
seen: empty(),
|
||||||
|
stack: empty(),
|
||||||
|
resolved: empty(),
|
||||||
|
recipes: recipes,
|
||||||
|
};
|
||||||
|
|
||||||
|
for recipe in recipes.values() {
|
||||||
|
resolver.resolve(recipe)?;
|
||||||
|
resolver.seen = empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
for recipe in recipes.values() {
|
||||||
|
for line in &recipe.lines {
|
||||||
|
for fragment in line {
|
||||||
|
if let Fragment::Expression{ref expression, ..} = *fragment {
|
||||||
|
for variable in expression.variables() {
|
||||||
|
let name = variable.lexeme;
|
||||||
|
let undefined = !assignments.contains_key(name)
|
||||||
|
&& !recipe.parameters.iter().any(|p| p.name == name);
|
||||||
|
if undefined {
|
||||||
|
// There's a borrow issue here that seems too difficult to solve.
|
||||||
|
// The error derived from the variable token has too short a lifetime,
|
||||||
|
// so we create a new error from its contents, which do live long
|
||||||
|
// enough.
|
||||||
|
//
|
||||||
|
// I suspect the solution here is to give recipes, pieces, and expressions
|
||||||
|
// two lifetime parameters instead of one, with one being the lifetime
|
||||||
|
// of the struct, and the second being the lifetime of the tokens
|
||||||
|
// that it contains
|
||||||
|
let error = variable.error(CompilationErrorKind::UndefinedVariable{variable: name});
|
||||||
|
return Err(CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: error.index,
|
||||||
|
line: error.line,
|
||||||
|
column: error.column,
|
||||||
|
width: error.width,
|
||||||
|
kind: CompilationErrorKind::UndefinedVariable {
|
||||||
|
variable: &text[error.index..error.index + error.width.unwrap()],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RecipeResolver<'a: 'b, 'b> {
|
||||||
|
stack: Vec<&'a str>,
|
||||||
|
seen: Set<&'a str>,
|
||||||
|
resolved: Set<&'a str>,
|
||||||
|
recipes: &'b Map<&'a str, Recipe<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> RecipeResolver<'a, 'b> {
|
||||||
|
fn resolve(&mut self, recipe: &Recipe<'a>) -> Result<(), CompilationError<'a>> {
|
||||||
|
if self.resolved.contains(recipe.name) {
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
self.stack.push(recipe.name);
|
||||||
|
self.seen.insert(recipe.name);
|
||||||
|
for dependency_token in &recipe.dependency_tokens {
|
||||||
|
match self.recipes.get(dependency_token.lexeme) {
|
||||||
|
Some(dependency) => if !self.resolved.contains(dependency.name) {
|
||||||
|
if self.seen.contains(dependency.name) {
|
||||||
|
let first = self.stack[0];
|
||||||
|
self.stack.push(first);
|
||||||
|
return Err(dependency_token.error(CompilationErrorKind::CircularRecipeDependency {
|
||||||
|
recipe: recipe.name,
|
||||||
|
circle: self.stack.iter()
|
||||||
|
.skip_while(|name| **name != dependency.name)
|
||||||
|
.cloned().collect()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
self.resolve(dependency)?;
|
||||||
|
},
|
||||||
|
None => return Err(dependency_token.error(CompilationErrorKind::UnknownDependency {
|
||||||
|
recipe: recipe.name,
|
||||||
|
unknown: dependency_token.lexeme
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.resolved.insert(recipe.name);
|
||||||
|
self.stack.pop();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use testing::parse_error;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn circular_recipe_dependency() {
|
||||||
|
let text = "a: b\nb: a";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 8,
|
||||||
|
line: 1,
|
||||||
|
column: 3,
|
||||||
|
width: Some(1),
|
||||||
|
kind: CompilationErrorKind::CircularRecipeDependency{recipe: "b", circle: vec!["a", "b", "a"]}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn self_recipe_dependency() {
|
||||||
|
let text = "a: a";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 3,
|
||||||
|
line: 0,
|
||||||
|
column: 3,
|
||||||
|
width: Some(1),
|
||||||
|
kind: CompilationErrorKind::CircularRecipeDependency{recipe: "a", circle: vec!["a", "a"]}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_dependency() {
|
||||||
|
let text = "a: b";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 3,
|
||||||
|
line: 0,
|
||||||
|
column: 3,
|
||||||
|
width: Some(1),
|
||||||
|
kind: CompilationErrorKind::UnknownDependency{recipe: "a", unknown: "b"}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_interpolation_variable() {
|
||||||
|
let text = "x:\n {{ hello}}";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 9,
|
||||||
|
line: 1,
|
||||||
|
column: 6,
|
||||||
|
width: Some(5),
|
||||||
|
kind: CompilationErrorKind::UndefinedVariable{variable: "hello"},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_second_interpolation_variable() {
|
||||||
|
let text = "wtf=\"x\"\nx:\n echo\n foo {{wtf}} {{ lol }}";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 33,
|
||||||
|
line: 3,
|
||||||
|
column: 16,
|
||||||
|
width: Some(3),
|
||||||
|
kind: CompilationErrorKind::UndefinedVariable{variable: "lol"},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,12 +1,10 @@
|
|||||||
extern crate clap;
|
use common::*;
|
||||||
extern crate libc;
|
|
||||||
|
|
||||||
use color::Color;
|
|
||||||
use prelude::*;
|
|
||||||
use std::{convert, ffi};
|
use std::{convert, ffi};
|
||||||
use std::collections::BTreeMap;
|
use clap::{App, Arg, ArgGroup, AppSettings};
|
||||||
use self::clap::{App, Arg, ArgGroup, AppSettings};
|
use compile;
|
||||||
use super::{Slurp, RunOptions, compile, DEFAULT_SHELL, maybe_s};
|
use misc::maybe_s;
|
||||||
|
use configuration::DEFAULT_SHELL;
|
||||||
|
|
||||||
macro_rules! die {
|
macro_rules! die {
|
||||||
($($arg:tt)*) => {{
|
($($arg:tt)*) => {{
|
||||||
@ -20,7 +18,7 @@ fn edit<P: convert::AsRef<ffi::OsStr>>(path: P) -> ! {
|
|||||||
let editor = env::var_os("EDITOR")
|
let editor = env::var_os("EDITOR")
|
||||||
.unwrap_or_else(|| die!("Error getting EDITOR environment variable"));
|
.unwrap_or_else(|| die!("Error getting EDITOR environment variable"));
|
||||||
|
|
||||||
let error = process::Command::new(editor)
|
let error = Command::new(editor)
|
||||||
.arg(path)
|
.arg(path)
|
||||||
.status();
|
.status();
|
||||||
|
|
||||||
@ -30,7 +28,19 @@ fn edit<P: convert::AsRef<ffi::OsStr>>(path: P) -> ! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn app() {
|
trait Slurp {
|
||||||
|
fn slurp(&mut self) -> Result<String, io::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Slurp for fs::File {
|
||||||
|
fn slurp(&mut self) -> io::Result<String> {
|
||||||
|
let mut destination = String::new();
|
||||||
|
self.read_to_string(&mut destination)?;
|
||||||
|
Ok(destination)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run() {
|
||||||
let matches = App::new("just")
|
let matches = App::new("just")
|
||||||
.version(concat!("v", env!("CARGO_PKG_VERSION")))
|
.version(concat!("v", env!("CARGO_PKG_VERSION")))
|
||||||
.author(env!("CARGO_PKG_AUTHORS"))
|
.author(env!("CARGO_PKG_AUTHORS"))
|
||||||
@ -121,7 +131,7 @@ pub fn app() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let set_count = matches.occurrences_of("SET");
|
let set_count = matches.occurrences_of("SET");
|
||||||
let mut overrides = BTreeMap::new();
|
let mut overrides = Map::new();
|
||||||
if set_count > 0 {
|
if set_count > 0 {
|
||||||
let mut values = matches.values_of("SET").unwrap();
|
let mut values = matches.values_of("SET").unwrap();
|
||||||
for _ in 0..set_count {
|
for _ in 0..set_count {
|
||||||
@ -300,13 +310,13 @@ pub fn app() {
|
|||||||
die!("Justfile contains no recipes.");
|
die!("Justfile contains no recipes.");
|
||||||
};
|
};
|
||||||
|
|
||||||
let options = RunOptions {
|
let options = Configuration {
|
||||||
dry_run: matches.is_present("DRY-RUN"),
|
dry_run: matches.is_present("DRY-RUN"),
|
||||||
evaluate: matches.is_present("EVALUATE"),
|
evaluate: matches.is_present("EVALUATE"),
|
||||||
highlight: matches.is_present("HIGHLIGHT"),
|
highlight: matches.is_present("HIGHLIGHT"),
|
||||||
overrides: overrides,
|
overrides: overrides,
|
||||||
quiet: matches.is_present("QUIET"),
|
quiet: matches.is_present("QUIET"),
|
||||||
shell: matches.value_of("SHELL"),
|
shell: matches.value_of("SHELL").unwrap(),
|
||||||
color: color,
|
color: color,
|
||||||
verbose: matches.is_present("VERBOSE"),
|
verbose: matches.is_present("VERBOSE"),
|
||||||
};
|
};
|
||||||
@ -320,6 +330,6 @@ pub fn app() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
process::exit(run_error.code().unwrap_or(libc::EXIT_FAILURE));
|
process::exit(run_error.code().unwrap_or(EXIT_FAILURE));
|
||||||
}
|
}
|
||||||
}
|
}
|
201
src/runtime_error.rs
Normal file
201
src/runtime_error.rs
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
use common::*;
|
||||||
|
|
||||||
|
use brev::OutputError;
|
||||||
|
|
||||||
|
use misc::{And, Or, maybe_s, Tick, ticks, write_error_context};
|
||||||
|
|
||||||
|
use self::RuntimeError::*;
|
||||||
|
|
||||||
|
fn write_token_error_context(f: &mut fmt::Formatter, token: &Token) -> Result<(), fmt::Error> {
|
||||||
|
write_error_context(
|
||||||
|
f,
|
||||||
|
token.text,
|
||||||
|
token.index,
|
||||||
|
token.line,
|
||||||
|
token.column + token.prefix.len(),
|
||||||
|
Some(token.lexeme.len())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum RuntimeError<'a> {
|
||||||
|
ArgumentCountMismatch{recipe: &'a str, found: usize, min: usize, max: usize},
|
||||||
|
Backtick{token: Token<'a>, output_error: OutputError},
|
||||||
|
Code{recipe: &'a str, line_number: Option<usize>, code: i32},
|
||||||
|
Cygpath{recipe: &'a str, output_error: OutputError},
|
||||||
|
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},
|
||||||
|
Unknown{recipe: &'a str, line_number: Option<usize>},
|
||||||
|
UnknownOverrides{overrides: Vec<&'a str>},
|
||||||
|
UnknownRecipes{recipes: Vec<&'a str>, suggestion: Option<&'a str>},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> RuntimeError<'a> {
|
||||||
|
pub fn code(&self) -> Option<i32> {
|
||||||
|
match *self {
|
||||||
|
Code{code, ..} | Backtick{output_error: OutputError::Code(code), ..} => Some(code),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Display for RuntimeError<'a> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
use RuntimeError::*;
|
||||||
|
let color = if f.alternate() { Color::always() } else { Color::never() };
|
||||||
|
let error = color.error();
|
||||||
|
let message = color.message();
|
||||||
|
write!(f, "{} {}", error.paint("error:"), message.prefix())?;
|
||||||
|
|
||||||
|
let mut error_token = None;
|
||||||
|
|
||||||
|
match *self {
|
||||||
|
UnknownRecipes{ref recipes, ref suggestion} => {
|
||||||
|
write!(f, "Justfile does not contain recipe{} {}.",
|
||||||
|
maybe_s(recipes.len()), Or(&ticks(recipes)))?;
|
||||||
|
if let Some(suggestion) = *suggestion {
|
||||||
|
write!(f, "\nDid you mean `{}`?", suggestion)?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
UnknownOverrides{ref overrides} => {
|
||||||
|
write!(f, "Variable{} {} overridden on the command line but not present in justfile",
|
||||||
|
maybe_s(overrides.len()),
|
||||||
|
And(&overrides.iter().map(Tick).collect::<Vec<_>>()))?;
|
||||||
|
},
|
||||||
|
ArgumentCountMismatch{recipe, found, min, max} => {
|
||||||
|
if min == max {
|
||||||
|
let expected = min;
|
||||||
|
write!(f, "Recipe `{}` got {} argument{} but {}takes {}",
|
||||||
|
recipe, found, maybe_s(found),
|
||||||
|
if expected < found { "only " } else { "" }, expected)?;
|
||||||
|
} else if found < min {
|
||||||
|
write!(f, "Recipe `{}` got {} argument{} but takes at least {}",
|
||||||
|
recipe, found, maybe_s(found), min)?;
|
||||||
|
} else if found > max {
|
||||||
|
write!(f, "Recipe `{}` got {} argument{} but takes at most {}",
|
||||||
|
recipe, found, maybe_s(found), max)?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Code{recipe, line_number, code} => {
|
||||||
|
if let Some(n) = line_number {
|
||||||
|
write!(f, "Recipe `{}` failed on line {} with exit code {}", recipe, n, code)?;
|
||||||
|
} else {
|
||||||
|
write!(f, "Recipe `{}` failed with exit code {}", recipe, code)?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Cygpath{recipe, ref output_error} => match *output_error {
|
||||||
|
OutputError::Code(code) => {
|
||||||
|
write!(f, "Cygpath failed with exit code {} while translating recipe `{}` \
|
||||||
|
shebang interpreter path", code, recipe)?;
|
||||||
|
}
|
||||||
|
OutputError::Signal(signal) => {
|
||||||
|
write!(f, "Cygpath terminated by signal {} while translating recipe `{}` \
|
||||||
|
shebang interpreter path", signal, recipe)?;
|
||||||
|
}
|
||||||
|
OutputError::Unknown => {
|
||||||
|
write!(f, "Cygpath experienced an unknown failure while translating recipe `{}` \
|
||||||
|
shebang interpreter path", recipe)?;
|
||||||
|
}
|
||||||
|
OutputError::Io(ref io_error) => {
|
||||||
|
match io_error.kind() {
|
||||||
|
io::ErrorKind::NotFound => write!(
|
||||||
|
f, "Could not find `cygpath` executable to translate recipe `{}` \
|
||||||
|
shebang interpreter path:\n{}", recipe, io_error),
|
||||||
|
io::ErrorKind::PermissionDenied => write!(
|
||||||
|
f, "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),
|
||||||
|
}?;
|
||||||
|
}
|
||||||
|
OutputError::Utf8(ref utf8_error) => {
|
||||||
|
write!(f, "Cygpath successfully translated recipe `{}` shebang interpreter path, \
|
||||||
|
but output was not utf8: {}", recipe, utf8_error)?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Shebang{recipe, ref command, ref argument, ref io_error} => {
|
||||||
|
if let Some(ref argument) = *argument {
|
||||||
|
write!(f, "Recipe `{}` with shebang `#!{} {}` execution error: {}",
|
||||||
|
recipe, command, argument, io_error)?;
|
||||||
|
} else {
|
||||||
|
write!(f, "Recipe `{}` with shebang `#!{}` execution error: {}",
|
||||||
|
recipe, command, io_error)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Signal{recipe, line_number, signal} => {
|
||||||
|
if let Some(n) = line_number {
|
||||||
|
write!(f, "Recipe `{}` was terminated on line {} by signal {}", recipe, n, signal)?;
|
||||||
|
} else {
|
||||||
|
write!(f, "Recipe `{}` was terminated by signal {}", recipe, signal)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Unknown{recipe, line_number} => {
|
||||||
|
if let Some(n) = line_number {
|
||||||
|
write!(f, "Recipe `{}` failed on line {} for an unknown reason", recipe, n)?;
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
IoError{recipe, ref io_error} => {
|
||||||
|
match io_error.kind() {
|
||||||
|
io::ErrorKind::NotFound => write!(f,
|
||||||
|
"Recipe `{}` could not be run because just could not find `sh`:\n{}",
|
||||||
|
recipe, io_error),
|
||||||
|
io::ErrorKind::PermissionDenied => write!(
|
||||||
|
f, "Recipe `{}` could not be run because just could not run `sh`:\n{}",
|
||||||
|
recipe, io_error),
|
||||||
|
_ => write!(f, "Recipe `{}` could not be run because of an IO error while \
|
||||||
|
launching `sh`:\n{}", recipe, io_error),
|
||||||
|
}?;
|
||||||
|
},
|
||||||
|
TmpdirIoError{recipe, ref io_error} =>
|
||||||
|
write!(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`:\n{}",
|
||||||
|
recipe, io_error)?,
|
||||||
|
Backtick{ref token, ref output_error} => match *output_error {
|
||||||
|
OutputError::Code(code) => {
|
||||||
|
write!(f, "Backtick failed with exit code {}\n", code)?;
|
||||||
|
error_token = Some(token);
|
||||||
|
}
|
||||||
|
OutputError::Signal(signal) => {
|
||||||
|
write!(f, "Backtick was terminated by signal {}", signal)?;
|
||||||
|
error_token = Some(token);
|
||||||
|
}
|
||||||
|
OutputError::Unknown => {
|
||||||
|
write!(f, "Backtick failed for an unknown reason")?;
|
||||||
|
error_token = Some(token);
|
||||||
|
}
|
||||||
|
OutputError::Io(ref io_error) => {
|
||||||
|
match io_error.kind() {
|
||||||
|
io::ErrorKind::NotFound => write!(
|
||||||
|
f, "Backtick could not be run because just could not find `sh`:\n{}",
|
||||||
|
io_error),
|
||||||
|
io::ErrorKind::PermissionDenied => write!(
|
||||||
|
f, "Backtick could not be run because just could not run `sh`:\n{}", io_error),
|
||||||
|
_ => write!(f, "Backtick could not be run because of an IO \
|
||||||
|
error while launching `sh`:\n{}", io_error),
|
||||||
|
}?;
|
||||||
|
error_token = Some(token);
|
||||||
|
}
|
||||||
|
OutputError::Utf8(ref utf8_error) => {
|
||||||
|
write!(f, "Backtick succeeded but stdout was not utf8: {}", utf8_error)?;
|
||||||
|
error_token = Some(token);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Internal{ref message} => {
|
||||||
|
write!(f, "Internal error, this may indicate a bug in just: {} \
|
||||||
|
consider filing an issue: https://github.com/casey/just/issues/new",
|
||||||
|
message)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(f, "{}", message.suffix())?;
|
||||||
|
|
||||||
|
if let Some(token) = error_token {
|
||||||
|
write_token_error_context(f, token)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
60
src/shebang.rs
Normal file
60
src/shebang.rs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
pub struct Shebang<'a> {
|
||||||
|
pub interpreter: &'a str,
|
||||||
|
pub argument: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Shebang<'a> {
|
||||||
|
pub fn new(text: &'a str) -> Option<Shebang<'a>> {
|
||||||
|
if !text.starts_with("#!") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pieces = text[2..]
|
||||||
|
.lines()
|
||||||
|
.nth(0)
|
||||||
|
.unwrap_or("")
|
||||||
|
.trim()
|
||||||
|
.splitn(2, |c| c == ' ' || c == '\t');
|
||||||
|
|
||||||
|
let interpreter = pieces.next().unwrap_or("");
|
||||||
|
let argument = pieces.next();
|
||||||
|
|
||||||
|
if interpreter == "" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Shebang{interpreter, argument})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::Shebang;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn split_shebang() {
|
||||||
|
fn check(text: &str, expected_split: Option<(&str, Option<&str>)>) {
|
||||||
|
let shebang = Shebang::new(text);
|
||||||
|
assert_eq!(shebang.map(|shebang| (shebang.interpreter, shebang.argument)), expected_split);
|
||||||
|
}
|
||||||
|
|
||||||
|
check("#! ", None );
|
||||||
|
check("#!", 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("#!/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 );
|
||||||
|
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 );
|
||||||
|
}
|
||||||
|
}
|
25
src/testing.rs
Normal file
25
src/testing.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
use common::*;
|
||||||
|
|
||||||
|
use compile;
|
||||||
|
|
||||||
|
pub fn parse_success(text: &str) -> Justfile {
|
||||||
|
match compile(text) {
|
||||||
|
Ok(justfile) => justfile,
|
||||||
|
Err(error) => panic!("Expected successful parse but got error:\n{}", error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_error(text: &str, expected: CompilationError) {
|
||||||
|
if let Err(error) = compile(text) {
|
||||||
|
assert_eq!(error.text, expected.text);
|
||||||
|
assert_eq!(error.index, expected.index);
|
||||||
|
assert_eq!(error.line, expected.line);
|
||||||
|
assert_eq!(error.column, expected.column);
|
||||||
|
assert_eq!(error.kind, expected.kind);
|
||||||
|
assert_eq!(error.width, expected.width);
|
||||||
|
assert_eq!(error, expected);
|
||||||
|
} else {
|
||||||
|
panic!("Expected {:?} but parse succeeded", expected.kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
71
src/token.rs
Normal file
71
src/token.rs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
use common::*;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub struct Token<'a> {
|
||||||
|
pub index: usize,
|
||||||
|
pub line: usize,
|
||||||
|
pub column: usize,
|
||||||
|
pub text: &'a str,
|
||||||
|
pub prefix: &'a str,
|
||||||
|
pub lexeme: &'a str,
|
||||||
|
pub kind: TokenKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Token<'a> {
|
||||||
|
pub fn error(&self, kind: CompilationErrorKind<'a>) -> CompilationError<'a> {
|
||||||
|
CompilationError {
|
||||||
|
text: self.text,
|
||||||
|
index: self.index + self.prefix.len(),
|
||||||
|
line: self.line,
|
||||||
|
column: self.column + self.prefix.len(),
|
||||||
|
width: Some(self.lexeme.len()),
|
||||||
|
kind: kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||||
|
pub enum TokenKind {
|
||||||
|
At,
|
||||||
|
Backtick,
|
||||||
|
Colon,
|
||||||
|
Comment,
|
||||||
|
Dedent,
|
||||||
|
Eof,
|
||||||
|
Eol,
|
||||||
|
Equals,
|
||||||
|
Indent,
|
||||||
|
InterpolationEnd,
|
||||||
|
InterpolationStart,
|
||||||
|
Line,
|
||||||
|
Name,
|
||||||
|
Plus,
|
||||||
|
RawString,
|
||||||
|
StringToken,
|
||||||
|
Text,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for TokenKind {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
use TokenKind::*;
|
||||||
|
write!(f, "{}", match *self {
|
||||||
|
Backtick => "backtick",
|
||||||
|
Colon => "':'",
|
||||||
|
Comment => "comment",
|
||||||
|
Dedent => "dedent",
|
||||||
|
Eof => "end of file",
|
||||||
|
Eol => "end of line",
|
||||||
|
Equals => "'='",
|
||||||
|
Indent => "indent",
|
||||||
|
InterpolationEnd => "'}}'",
|
||||||
|
InterpolationStart => "'{{'",
|
||||||
|
Line => "command",
|
||||||
|
Name => "name",
|
||||||
|
Plus => "'+'",
|
||||||
|
At => "'@'",
|
||||||
|
StringToken => "string",
|
||||||
|
RawString => "raw string",
|
||||||
|
Text => "command text",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
585
src/tokenizer.rs
Normal file
585
src/tokenizer.rs
Normal file
@ -0,0 +1,585 @@
|
|||||||
|
use common::*;
|
||||||
|
|
||||||
|
use TokenKind::*;
|
||||||
|
|
||||||
|
fn re(pattern: &str) -> Regex {
|
||||||
|
Regex::new(pattern).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn token(pattern: &str) -> Regex {
|
||||||
|
let mut s = String::new();
|
||||||
|
s += r"^(?m)([ \t]*)(";
|
||||||
|
s += pattern;
|
||||||
|
s += ")";
|
||||||
|
re(&s)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mixed_whitespace(text: &str) -> bool {
|
||||||
|
!(text.chars().all(|c| c == ' ') || text.chars().all(|c| c == '\t'))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tokenize(text: &str) -> Result<Vec<Token>, CompilationError> {
|
||||||
|
lazy_static! {
|
||||||
|
static ref BACKTICK: Regex = token(r"`[^`\n\r]*`" );
|
||||||
|
static ref COLON: Regex = token(r":" );
|
||||||
|
static ref AT: Regex = token(r"@" );
|
||||||
|
static ref COMMENT: Regex = token(r"#([^!\n\r].*)?$" );
|
||||||
|
static ref EOF: Regex = token(r"(?-m)$" );
|
||||||
|
static ref EOL: Regex = token(r"\n|\r\n" );
|
||||||
|
static ref EQUALS: Regex = token(r"=" );
|
||||||
|
static ref INTERPOLATION_END: 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 PLUS: Regex = token(r"[+]" );
|
||||||
|
static ref STRING: Regex = token("\"" );
|
||||||
|
static ref RAW_STRING: Regex = token(r#"'[^']*'"# );
|
||||||
|
static ref UNTERMINATED_RAW_STRING: Regex = token(r#"'[^']*"# );
|
||||||
|
static ref INDENT: Regex = re(r"^([ \t]*)[^ \t\n\r]" );
|
||||||
|
static ref INTERPOLATION_START: Regex = re(r"^[{][{]" );
|
||||||
|
static ref LEADING_TEXT: Regex = re(r"^(?m)(.+?)[{][{]" );
|
||||||
|
static ref LINE: Regex = re(r"^(?m)[ \t]+[^ \t\n\r].*$");
|
||||||
|
static ref TEXT: Regex = re(r"^(?m)(.+)" );
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
enum State<'a> {
|
||||||
|
Start,
|
||||||
|
Indent(&'a str),
|
||||||
|
Text,
|
||||||
|
Interpolation,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn indentation(text: &str) -> Option<&str> {
|
||||||
|
INDENT.captures(text).map(|captures| captures.get(1).unwrap().as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tokens = vec![];
|
||||||
|
let mut rest = text;
|
||||||
|
let mut index = 0;
|
||||||
|
let mut line = 0;
|
||||||
|
let mut column = 0;
|
||||||
|
let mut state = vec![State::Start];
|
||||||
|
|
||||||
|
macro_rules! error {
|
||||||
|
($kind:expr) => {{
|
||||||
|
Err(CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: index,
|
||||||
|
line: line,
|
||||||
|
column: column,
|
||||||
|
width: None,
|
||||||
|
kind: $kind,
|
||||||
|
})
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if column == 0 {
|
||||||
|
if let Some(kind) = match (state.last().unwrap(), indentation(rest)) {
|
||||||
|
// ignore: was no indentation and there still isn't
|
||||||
|
// or current line is blank
|
||||||
|
(&State::Start, Some("")) | (_, None) => {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
// indent: was no indentation, now there is
|
||||||
|
(&State::Start, Some(current)) => {
|
||||||
|
if mixed_whitespace(current) {
|
||||||
|
return error!(CompilationErrorKind::MixedLeadingWhitespace{whitespace: current})
|
||||||
|
}
|
||||||
|
//indent = Some(current);
|
||||||
|
state.push(State::Indent(current));
|
||||||
|
Some(Indent)
|
||||||
|
}
|
||||||
|
// dedent: there was indentation and now there isn't
|
||||||
|
(&State::Indent(_), Some("")) => {
|
||||||
|
// indent = None;
|
||||||
|
state.pop();
|
||||||
|
Some(Dedent)
|
||||||
|
}
|
||||||
|
// was indentation and still is, check if the new indentation matches
|
||||||
|
(&State::Indent(previous), Some(current)) => {
|
||||||
|
if !current.starts_with(previous) {
|
||||||
|
return error!(CompilationErrorKind::InconsistentLeadingWhitespace{
|
||||||
|
expected: previous,
|
||||||
|
found: current
|
||||||
|
});
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
// at column 0 in some other state: this should never happen
|
||||||
|
(&State::Text, _) | (&State::Interpolation, _) => {
|
||||||
|
return error!(CompilationErrorKind::Internal {
|
||||||
|
message: "unexpected state at column 0".to_string()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} {
|
||||||
|
tokens.push(Token {
|
||||||
|
index: index,
|
||||||
|
line: line,
|
||||||
|
column: column,
|
||||||
|
text: text,
|
||||||
|
prefix: "",
|
||||||
|
lexeme: "",
|
||||||
|
kind: kind,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert a dedent if we're indented and we hit the end of the file
|
||||||
|
if &State::Start != state.last().unwrap() && EOF.is_match(rest) {
|
||||||
|
tokens.push(Token {
|
||||||
|
index: index,
|
||||||
|
line: line,
|
||||||
|
column: column,
|
||||||
|
text: text,
|
||||||
|
prefix: "",
|
||||||
|
lexeme: "",
|
||||||
|
kind: Dedent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let (prefix, lexeme, kind) =
|
||||||
|
if let (0, &State::Indent(indent), Some(captures)) =
|
||||||
|
(column, state.last().unwrap(), LINE.captures(rest)) {
|
||||||
|
let line = captures.get(0).unwrap().as_str();
|
||||||
|
if !line.starts_with(indent) {
|
||||||
|
return error!(CompilationErrorKind::Internal{message: "unexpected indent".to_string()});
|
||||||
|
}
|
||||||
|
state.push(State::Text);
|
||||||
|
(&line[0..indent.len()], "", Line)
|
||||||
|
} else if let Some(captures) = EOF.captures(rest) {
|
||||||
|
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Eof)
|
||||||
|
} else if let State::Text = *state.last().unwrap() {
|
||||||
|
if let Some(captures) = INTERPOLATION_START.captures(rest) {
|
||||||
|
state.push(State::Interpolation);
|
||||||
|
("", captures.get(0).unwrap().as_str(), InterpolationStart)
|
||||||
|
} else if let Some(captures) = LEADING_TEXT.captures(rest) {
|
||||||
|
("", captures.get(1).unwrap().as_str(), Text)
|
||||||
|
} else if let Some(captures) = TEXT.captures(rest) {
|
||||||
|
("", captures.get(1).unwrap().as_str(), Text)
|
||||||
|
} else if let Some(captures) = EOL.captures(rest) {
|
||||||
|
state.pop();
|
||||||
|
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Eol)
|
||||||
|
} else {
|
||||||
|
return error!(CompilationErrorKind::Internal {
|
||||||
|
message: format!("Could not match token in text state: \"{}\"", rest)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if let Some(captures) = INTERPOLATION_START_TOKEN.captures(rest) {
|
||||||
|
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), InterpolationStart)
|
||||||
|
} else if let Some(captures) = INTERPOLATION_END.captures(rest) {
|
||||||
|
if state.last().unwrap() == &State::Interpolation {
|
||||||
|
state.pop();
|
||||||
|
}
|
||||||
|
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), InterpolationEnd)
|
||||||
|
} else if let Some(captures) = NAME.captures(rest) {
|
||||||
|
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Name)
|
||||||
|
} else if let Some(captures) = EOL.captures(rest) {
|
||||||
|
if state.last().unwrap() == &State::Interpolation {
|
||||||
|
return error!(CompilationErrorKind::Internal {
|
||||||
|
message: "hit EOL while still in interpolation state".to_string()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Eol)
|
||||||
|
} else if let Some(captures) = BACKTICK.captures(rest) {
|
||||||
|
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Backtick)
|
||||||
|
} else if let Some(captures) = COLON.captures(rest) {
|
||||||
|
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Colon)
|
||||||
|
} else if let Some(captures) = AT.captures(rest) {
|
||||||
|
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), At)
|
||||||
|
} else if let Some(captures) = PLUS.captures(rest) {
|
||||||
|
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Plus)
|
||||||
|
} else if let Some(captures) = EQUALS.captures(rest) {
|
||||||
|
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Equals)
|
||||||
|
} else if let Some(captures) = COMMENT.captures(rest) {
|
||||||
|
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Comment)
|
||||||
|
} else if let Some(captures) = RAW_STRING.captures(rest) {
|
||||||
|
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), RawString)
|
||||||
|
} else if UNTERMINATED_RAW_STRING.is_match(rest) {
|
||||||
|
return error!(CompilationErrorKind::UnterminatedString);
|
||||||
|
} else if let Some(captures) = STRING.captures(rest) {
|
||||||
|
let prefix = captures.get(1).unwrap().as_str();
|
||||||
|
let contents = &rest[prefix.len()+1..];
|
||||||
|
if contents.is_empty() {
|
||||||
|
return error!(CompilationErrorKind::UnterminatedString);
|
||||||
|
}
|
||||||
|
let mut len = 0;
|
||||||
|
let mut escape = false;
|
||||||
|
for c in contents.chars() {
|
||||||
|
if c == '\n' || c == '\r' {
|
||||||
|
return error!(CompilationErrorKind::UnterminatedString);
|
||||||
|
} else if !escape && c == '"' {
|
||||||
|
break;
|
||||||
|
} else if !escape && c == '\\' {
|
||||||
|
escape = true;
|
||||||
|
} else if escape {
|
||||||
|
escape = false;
|
||||||
|
}
|
||||||
|
len += c.len_utf8();
|
||||||
|
}
|
||||||
|
let start = prefix.len();
|
||||||
|
let content_end = start + len + 1;
|
||||||
|
if escape || content_end >= rest.len() {
|
||||||
|
return error!(CompilationErrorKind::UnterminatedString);
|
||||||
|
}
|
||||||
|
(prefix, &rest[start..content_end + 1], StringToken)
|
||||||
|
} else if rest.starts_with("#!") {
|
||||||
|
return error!(CompilationErrorKind::OuterShebang)
|
||||||
|
} else {
|
||||||
|
return error!(CompilationErrorKind::UnknownStartOfToken)
|
||||||
|
};
|
||||||
|
|
||||||
|
tokens.push(Token {
|
||||||
|
index: index,
|
||||||
|
line: line,
|
||||||
|
column: column,
|
||||||
|
prefix: prefix,
|
||||||
|
text: text,
|
||||||
|
lexeme: lexeme,
|
||||||
|
kind: kind,
|
||||||
|
});
|
||||||
|
|
||||||
|
let len = prefix.len() + lexeme.len();
|
||||||
|
|
||||||
|
if len == 0 {
|
||||||
|
let last = tokens.last().unwrap();
|
||||||
|
match last.kind {
|
||||||
|
Eof => {},
|
||||||
|
_ => return Err(last.error(CompilationErrorKind::Internal {
|
||||||
|
message: format!("zero length token: {:?}", last)
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match tokens.last().unwrap().kind {
|
||||||
|
Eol => {
|
||||||
|
line += 1;
|
||||||
|
column = 0;
|
||||||
|
}
|
||||||
|
Eof => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
RawString => {
|
||||||
|
let lexeme_lines = lexeme.lines().count();
|
||||||
|
line += lexeme_lines - 1;
|
||||||
|
if lexeme_lines == 1 {
|
||||||
|
column += len;
|
||||||
|
} else {
|
||||||
|
column = lexeme.lines().last().unwrap().len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
column += len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rest = &rest[len..];
|
||||||
|
index += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use testing::parse_error;
|
||||||
|
|
||||||
|
fn tokenize_success(text: &str, expected_summary: &str) {
|
||||||
|
let tokens = tokenize(text).unwrap();
|
||||||
|
let roundtrip = tokens.iter().map(|t| {
|
||||||
|
let mut s = String::new();
|
||||||
|
s += t.prefix;
|
||||||
|
s += t.lexeme;
|
||||||
|
s
|
||||||
|
}).collect::<Vec<_>>().join("");
|
||||||
|
let summary = token_summary(&tokens);
|
||||||
|
if summary != expected_summary {
|
||||||
|
panic!("token summary mismatch:\nexpected: {}\ngot: {}\n", expected_summary, summary);
|
||||||
|
}
|
||||||
|
assert_eq!(text, roundtrip);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tokenize_error(text: &str, expected: CompilationError) {
|
||||||
|
if let Err(error) = tokenize(text) {
|
||||||
|
assert_eq!(error.text, expected.text);
|
||||||
|
assert_eq!(error.index, expected.index);
|
||||||
|
assert_eq!(error.line, expected.line);
|
||||||
|
assert_eq!(error.column, expected.column);
|
||||||
|
assert_eq!(error.kind, expected.kind);
|
||||||
|
assert_eq!(error, expected);
|
||||||
|
} else {
|
||||||
|
panic!("tokenize() succeeded but expected: {}\n{}", expected, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn token_summary(tokens: &[Token]) -> String {
|
||||||
|
tokens.iter().map(|t| {
|
||||||
|
match t.kind {
|
||||||
|
At => "@",
|
||||||
|
Backtick => "`",
|
||||||
|
Colon => ":",
|
||||||
|
Comment{..} => "#",
|
||||||
|
Dedent => "<",
|
||||||
|
Eof => ".",
|
||||||
|
Eol => "$",
|
||||||
|
Equals => "=",
|
||||||
|
Indent{..} => ">",
|
||||||
|
InterpolationEnd => "}",
|
||||||
|
InterpolationStart => "{",
|
||||||
|
Line{..} => "^",
|
||||||
|
Name => "N",
|
||||||
|
Plus => "+",
|
||||||
|
RawString => "'",
|
||||||
|
StringToken => "\"",
|
||||||
|
Text => "_",
|
||||||
|
}
|
||||||
|
}).collect::<Vec<_>>().join("")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tokanize_strings() {
|
||||||
|
tokenize_success(
|
||||||
|
r#"a = "'a'" + '"b"' + "'c'" + '"d"'#echo hello"#,
|
||||||
|
r#"N="+'+"+'#."#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tokenize_recipe_interpolation_eol() {
|
||||||
|
let text = "foo: # some comment
|
||||||
|
{{hello}}
|
||||||
|
";
|
||||||
|
tokenize_success(text, "N:#$>^{N}$<.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tokenize_recipe_interpolation_eof() {
|
||||||
|
let text = "foo: # more comments
|
||||||
|
{{hello}}
|
||||||
|
# another comment
|
||||||
|
";
|
||||||
|
tokenize_success(text, "N:#$>^{N}$<#$.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tokenize_recipe_complex_interpolation_expression() {
|
||||||
|
let text = "foo: #lol\n {{a + b + \"z\" + blarg}}";
|
||||||
|
tokenize_success(text, "N:#$>^{N+N+\"+N}<.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tokenize_recipe_multiple_interpolations() {
|
||||||
|
let text = "foo:#ok\n {{a}}0{{b}}1{{c}}";
|
||||||
|
tokenize_success(text, "N:#$>^{N}_{N}_{N}<.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tokenize_junk() {
|
||||||
|
let text = "bob
|
||||||
|
|
||||||
|
hello blah blah blah : a b c #whatever
|
||||||
|
";
|
||||||
|
tokenize_success(text, "N$$NNNN:NNN#$.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tokenize_empty_lines() {
|
||||||
|
let text = "
|
||||||
|
# this does something
|
||||||
|
hello:
|
||||||
|
asdf
|
||||||
|
bsdf
|
||||||
|
|
||||||
|
csdf
|
||||||
|
|
||||||
|
dsdf # whatever
|
||||||
|
|
||||||
|
# yolo
|
||||||
|
";
|
||||||
|
|
||||||
|
tokenize_success(text, "$#$N:$>^_$^_$$^_$$^_$$<#$.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tokenize_comment_before_variable() {
|
||||||
|
let text = "
|
||||||
|
#
|
||||||
|
A='1'
|
||||||
|
echo:
|
||||||
|
echo {{A}}
|
||||||
|
";
|
||||||
|
tokenize_success(text, "$#$N='$N:$>^_{N}$<.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tokenize_interpolation_backticks() {
|
||||||
|
tokenize_success(
|
||||||
|
"hello:\n echo {{`echo hello` + `echo goodbye`}}",
|
||||||
|
"N:$>^_{`+`}<."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tokenize_assignment_backticks() {
|
||||||
|
tokenize_success(
|
||||||
|
"a = `echo hello` + `echo goodbye`",
|
||||||
|
"N=`+`."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tokenize_multiple() {
|
||||||
|
let text = "
|
||||||
|
hello:
|
||||||
|
a
|
||||||
|
b
|
||||||
|
|
||||||
|
c
|
||||||
|
|
||||||
|
d
|
||||||
|
|
||||||
|
# hello
|
||||||
|
bob:
|
||||||
|
frank
|
||||||
|
";
|
||||||
|
|
||||||
|
tokenize_success(text, "$N:$>^_$^_$$^_$$^_$$<#$N:$>^_$<.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tokenize_comment() {
|
||||||
|
tokenize_success("a:=#", "N:=#.")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tokenize_space_then_tab() {
|
||||||
|
let text = "a:
|
||||||
|
0
|
||||||
|
1
|
||||||
|
\t2
|
||||||
|
";
|
||||||
|
tokenize_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 9,
|
||||||
|
line: 3,
|
||||||
|
column: 0,
|
||||||
|
width: None,
|
||||||
|
kind: CompilationErrorKind::InconsistentLeadingWhitespace{expected: " ", found: "\t"},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tokenize_tabs_then_tab_space() {
|
||||||
|
let text = "a:
|
||||||
|
\t\t0
|
||||||
|
\t\t 1
|
||||||
|
\t 2
|
||||||
|
";
|
||||||
|
tokenize_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 12,
|
||||||
|
line: 3,
|
||||||
|
column: 0,
|
||||||
|
width: None,
|
||||||
|
kind: CompilationErrorKind::InconsistentLeadingWhitespace{expected: "\t\t", found: "\t "},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tokenize_outer_shebang() {
|
||||||
|
let text = "#!/usr/bin/env bash";
|
||||||
|
tokenize_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 0,
|
||||||
|
line: 0,
|
||||||
|
column: 0,
|
||||||
|
width: None,
|
||||||
|
kind: CompilationErrorKind::OuterShebang
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tokenize_unknown() {
|
||||||
|
let text = "~";
|
||||||
|
tokenize_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 0,
|
||||||
|
line: 0,
|
||||||
|
column: 0,
|
||||||
|
width: None,
|
||||||
|
kind: CompilationErrorKind::UnknownStartOfToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn tokenize_order() {
|
||||||
|
let text = r"
|
||||||
|
b: a
|
||||||
|
@mv a b
|
||||||
|
|
||||||
|
a:
|
||||||
|
@touch F
|
||||||
|
@touch a
|
||||||
|
|
||||||
|
d: c
|
||||||
|
@rm c
|
||||||
|
|
||||||
|
c: b
|
||||||
|
@mv b c";
|
||||||
|
tokenize_success(text, "$N:N$>^_$$<N:$>^_$^_$$<N:N$>^_$$<N:N$>^_<.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unterminated_string() {
|
||||||
|
let text = r#"a = ""#;
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 3,
|
||||||
|
line: 0,
|
||||||
|
column: 3,
|
||||||
|
width: None,
|
||||||
|
kind: CompilationErrorKind::UnterminatedString,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unterminated_string_with_escapes() {
|
||||||
|
let text = r#"a = "\n\t\r\"\\"#;
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 3,
|
||||||
|
line: 0,
|
||||||
|
column: 3,
|
||||||
|
width: None,
|
||||||
|
kind: CompilationErrorKind::UnterminatedString,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn unterminated_raw_string() {
|
||||||
|
let text = "r a='asdf";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 4,
|
||||||
|
line: 0,
|
||||||
|
column: 4,
|
||||||
|
width: None,
|
||||||
|
kind: CompilationErrorKind::UnterminatedString,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mixed_leading_whitespace() {
|
||||||
|
let text = "a:\n\t echo hello";
|
||||||
|
parse_error(text, CompilationError {
|
||||||
|
text: text,
|
||||||
|
index: 3,
|
||||||
|
line: 1,
|
||||||
|
column: 0,
|
||||||
|
width: None,
|
||||||
|
kind: CompilationErrorKind::MixedLeadingWhitespace{whitespace: "\t "}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
1144
src/unit.rs
1144
src/unit.rs
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,14 @@
|
|||||||
extern crate tempdir;
|
extern crate tempdir;
|
||||||
extern crate brev;
|
extern crate brev;
|
||||||
|
extern crate libc;
|
||||||
|
extern crate utilities;
|
||||||
|
|
||||||
use ::prelude::*;
|
use libc::{EXIT_FAILURE, EXIT_SUCCESS};
|
||||||
use tempdir::TempDir;
|
use std::env;
|
||||||
|
use std::process;
|
||||||
use std::str;
|
use std::str;
|
||||||
|
use tempdir::TempDir;
|
||||||
|
use utilities::just_binary_path;
|
||||||
|
|
||||||
/// Instantiate integration tests for a given test case using
|
/// Instantiate integration tests for a given test case using
|
||||||
/// sh, dash, and bash.
|
/// sh, dash, and bash.
|
||||||
@ -22,8 +27,7 @@ macro_rules! integration_test {
|
|||||||
status: $status:expr,
|
status: $status:expr,
|
||||||
) => {
|
) => {
|
||||||
mod $name {
|
mod $name {
|
||||||
use ::prelude::*;
|
use super::*;
|
||||||
use super::integration_test;
|
|
||||||
|
|
||||||
// silence unused import warnings
|
// silence unused import warnings
|
||||||
const __: i32 = EXIT_SUCCESS;
|
const __: i32 = EXIT_SUCCESS;
|
||||||
@ -50,7 +54,7 @@ fn integration_test(
|
|||||||
path.push("justfile");
|
path.push("justfile");
|
||||||
brev::dump(path, justfile);
|
brev::dump(path, justfile);
|
||||||
|
|
||||||
let output = process::Command::new(&super::test_utils::just_binary_path())
|
let output = process::Command::new(&just_binary_path())
|
||||||
.current_dir(tmp.path())
|
.current_dir(tmp.path())
|
||||||
.args(&["--shell", shell])
|
.args(&["--shell", shell])
|
||||||
.args(args)
|
.args(args)
|
@ -1,10 +1,13 @@
|
|||||||
use ::prelude::*;
|
extern crate utilities;
|
||||||
|
extern crate brev;
|
||||||
|
extern crate tempdir;
|
||||||
|
|
||||||
|
use utilities::just_binary_path;
|
||||||
use tempdir::TempDir;
|
use tempdir::TempDir;
|
||||||
use std::{path, str};
|
use std::{path, str, fs, process};
|
||||||
use super::brev;
|
|
||||||
|
|
||||||
fn search_test<P: AsRef<path::Path>>(path: P, args: &[&str]) {
|
fn search_test<P: AsRef<path::Path>>(path: P, args: &[&str]) {
|
||||||
let binary = super::test_utils::just_binary_path();
|
let binary = just_binary_path();
|
||||||
|
|
||||||
let output = process::Command::new(binary)
|
let output = process::Command::new(binary)
|
||||||
.current_dir(path)
|
.current_dir(path)
|
5
utilities/Cargo.toml
Normal file
5
utilities/Cargo.toml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[package]
|
||||||
|
name = "utilities"
|
||||||
|
version = "0.0.0"
|
||||||
|
authors = ["Casey Rodarmor <casey@rodarmor.com>"]
|
||||||
|
publish = false
|
@ -1,4 +1,5 @@
|
|||||||
use ::prelude::*;
|
use std::env;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub fn just_binary_path() -> PathBuf {
|
pub fn just_binary_path() -> PathBuf {
|
||||||
let mut path = env::current_exe().unwrap();
|
let mut path = env::current_exe().unwrap();
|
||||||
@ -10,3 +11,4 @@ pub fn just_binary_path() -> PathBuf {
|
|||||||
path.push(exe);
|
path.push(exe);
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user