Allow line continuations in plain recipes with '\' (#99)

Some ugly code, but not as bad as I thought.

Elected not to go with indentation based line continuations. Too many
weird edge cases and feels like a gratuitious incompatibility with make.

Fixes #9
This commit is contained in:
Casey Rodarmor 2016-11-12 15:45:12 -08:00 committed by GitHub
parent 5b2d671c1d
commit babe97bf0d
3 changed files with 85 additions and 12 deletions

View File

@ -1157,3 +1157,51 @@ a Z="\t z":
"error: Justfile does not contain recipe `hell`.\nDid you mean `hello`?\n", "error: Justfile does not contain recipe `hell`.\nDid you mean `hello`?\n",
); );
} }
#[test]
fn line_continuation_with_space() {
integration_test(
&[],
r#"
foo:
echo a\
b \
c
"#,
0,
"a b c\n",
"echo a b c\n",
);
}
#[test]
fn line_continuation_with_quoted_space() {
integration_test(
&[],
r#"
foo:
echo 'a\
b \
c'
"#,
0,
"a b c\n",
"echo 'a b c'\n",
);
}
#[test]
fn line_continuation_no_space() {
integration_test(
&[],
r#"
foo:
echo a\
b\
c
"#,
0,
"abc\n",
"echo abc\n",
);
}

View File

@ -98,6 +98,15 @@ enum Fragment<'a> {
Expression{expression: Expression<'a>}, Expression{expression: Expression<'a>},
} }
impl<'a> Fragment<'a> {
fn continuation(&self) -> bool {
match *self {
Fragment::Text{ref text} => text.lexeme.ends_with('\\'),
_ => false,
}
}
}
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
enum Expression<'a> { enum Expression<'a> {
Variable{name: &'a str, token: Token<'a>}, Variable{name: &'a str, token: Token<'a>},
@ -345,8 +354,24 @@ impl<'a> Recipe<'a> {
recipe: self.name, io_error: io_error}) recipe: self.name, io_error: io_error})
}; };
} else { } else {
for line in &self.lines { let mut lines = self.lines.iter().peekable();
let evaluated = &evaluator.evaluate_line(line, &argument_map)?; loop {
if lines.peek().is_none() {
break;
}
let mut evaluated = String::new();
loop {
if lines.peek().is_none() {
break;
}
let line = lines.next().unwrap();
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 mut command = evaluated.as_str();
let quiet_command = command.starts_with('@'); let quiet_command = command.starts_with('@');
if quiet_command { if quiet_command {
@ -1792,7 +1817,7 @@ impl<'a> Parser<'a> {
return Err(self.unexpected_token(&token, &[Name, Eol, Eof])); return Err(self.unexpected_token(&token, &[Name, Eol, Eof]));
} }
let mut lines = vec![]; let mut lines: Vec<Vec<Fragment>> = vec![];
let mut shebang = false; let mut shebang = false;
if self.accepted(Indent) { if self.accepted(Indent) {
@ -1805,25 +1830,27 @@ impl<'a> Parser<'a> {
message: format!("Expected a line but got {}", token.kind) message: format!("Expected a line but got {}", token.kind)
})) }))
} }
let mut pieces = vec![]; let mut fragments = vec![];
while !(self.accepted(Eol) || self.peek(Dedent)) { while !(self.accepted(Eol) || self.peek(Dedent)) {
if let Some(token) = self.accept(Text) { if let Some(token) = self.accept(Text) {
if pieces.is_empty() { if fragments.is_empty() {
if lines.is_empty() { if lines.is_empty() {
if token.lexeme.starts_with("#!") { if token.lexeme.starts_with("#!") {
shebang = true; shebang = true;
} }
} else if !shebang && (token.lexeme.starts_with(' ') || } else if !shebang
token.lexeme.starts_with('\t')) { && !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(ErrorKind::ExtraLeadingWhitespace)); return Err(token.error(ErrorKind::ExtraLeadingWhitespace));
} }
} }
pieces.push(Fragment::Text{text: token}); fragments.push(Fragment::Text{text: token});
} else if let Some(token) = self.expect(InterpolationStart) { } else if let Some(token) = self.expect(InterpolationStart) {
return Err(self.unexpected_token(&token, &[Text, InterpolationStart, Eol])); return Err(self.unexpected_token(&token, &[Text, InterpolationStart, Eol]));
} else { } else {
pieces.push(Fragment::Expression{ fragments.push(Fragment::Expression{
expression: self.expression(true)? expression: self.expression(true)?
}); });
if let Some(token) = self.expect(InterpolationEnd) { if let Some(token) = self.expect(InterpolationEnd) {
@ -1832,7 +1859,7 @@ impl<'a> Parser<'a> {
} }
} }
lines.push(pieces); lines.push(fragments);
} }
} }

View File

@ -721,8 +721,6 @@ fn unknown_recipes() {
#[test] #[test]
fn extra_whitespace() { fn extra_whitespace() {
// we might want to make extra leading whitespace a line continuation in the future,
// so make it a error for now
let text = "a:\n blah\n blarg"; let text = "a:\n blah\n blarg";
parse_error(text, CompileError { parse_error(text, CompileError {
text: text, text: text,