1
0
mirror of https://github.com/casey/just synced 2024-07-08 20:16:14 +00:00

Unify string lexing (#790)

Unify lexing of backticks, cooked strings, and raw strings. Also allow
newlines in backticks and cooked strings, since I can't think of a reason
not to.
This commit is contained in:
Casey Rodarmor 2021-04-04 16:41:02 -07:00 committed by GitHub
parent 78b67f6cae
commit dd578d141c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 385 additions and 302 deletions

View File

@ -9,7 +9,7 @@ tokens
------
```
BACKTICK = `[^`\n\r]*`
BACKTICK = `[^`]*`
COMMENT = #([^!].*)?$
DEDENT = emitted when indentation decreases
EOF = emitted at the end of the file
@ -17,7 +17,7 @@ INDENT = emitted when indentation increases
LINE = emitted before a recipe line
NAME = [a-zA-Z_][a-zA-Z0-9_-]*
NEWLINE = \n|\r\n
RAW_STRING = '[^'\r\n]*'
RAW_STRING = '[^']*'
STRING = "[^"]*" # also processes \n \r \t \" \\ escapes
TEXT = recipe text, only matches in a recipe body
```

View File

@ -557,31 +557,27 @@ string-with-slash := "\"
string-with-tab := " "
```
Single-quoted strings do not recognize escape sequences and may contain line breaks:
Strings may contain line breaks:
```make
single := '
hello
'
double := "
goodbye
"
```
Single-quoted strings do not recognize escape sequences:
```make
escapes := '\t\n\r\"\\'
line-breaks := 'hello
this
is
a
raw
string!
'
```
```sh
$ just --evaluate
escapes := "\t\n\r\"\\"
line-breaks := "hello
this
is
a
raw
string!
"
```
=== Ignoring Errors

View File

@ -55,10 +55,10 @@ pub(crate) use crate::{
recipe_context::RecipeContext, recipe_resolver::RecipeResolver, runtime_error::RuntimeError,
scope::Scope, search::Search, search_config::SearchConfig, search_error::SearchError, set::Set,
setting::Setting, settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace,
string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table,
thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables,
verbosity::Verbosity, warning::Warning,
string_kind::StringKind, string_literal::StringLiteral, subcommand::Subcommand,
suggestion::Suggestion, table::Table, thunk::Thunk, token::Token, token_kind::TokenKind,
unresolved_dependency::UnresolvedDependency, unresolved_recipe::UnresolvedRecipe,
use_color::UseColor, variables::Variables, verbosity::Verbosity, warning::Warning,
};
// type aliases

View File

@ -242,10 +242,10 @@ impl Display for CompilationError<'_> {
UnterminatedInterpolation => {
writeln!(f, "Unterminated interpolation")?;
},
UnterminatedString => {
UnterminatedString(StringKind::Cooked) | UnterminatedString(StringKind::Raw) => {
writeln!(f, "Unterminated string")?;
},
UnterminatedBacktick => {
UnterminatedString(StringKind::Backtick) => {
writeln!(f, "Unterminated backtick")?;
},
Internal { ref message } => {

View File

@ -107,6 +107,5 @@ pub(crate) enum CompilationErrorKind<'src> {
open_line: usize,
},
UnterminatedInterpolation,
UnterminatedString,
UnterminatedBacktick,
UnterminatedString(StringKind),
}

View File

@ -30,8 +30,8 @@ pub(crate) struct Lexer<'src> {
recipe_body: bool,
/// Indentation stack
indentation: Vec<&'src str>,
/// Current interpolation start token
interpolation_start: Option<Token<'src>>,
/// Interpolation token start stack
interpolation_stack: Vec<Token<'src>>,
/// Current open delimiters
open_delimiters: Vec<(Delimiter, usize)>,
}
@ -60,7 +60,7 @@ impl<'src> Lexer<'src> {
token_end: start,
recipe_body_pending: false,
recipe_body: false,
interpolation_start: None,
interpolation_stack: Vec::new(),
open_delimiters: Vec::new(),
chars,
next,
@ -210,10 +210,8 @@ impl<'src> Lexer<'src> {
// The width of the error site to highlight depends on the kind of error:
let length = match kind {
// highlight ' or "
UnterminatedString => 1,
// highlight `
UnterminatedBacktick => 1,
// highlight ', ", or `
UnterminatedString(_) => 1,
// highlight the full token
_ => self.lexeme().len(),
};
@ -280,7 +278,7 @@ impl<'src> Lexer<'src> {
match self.next {
Some(first) => {
if let Some(interpolation_start) = self.interpolation_start {
if let Some(&interpolation_start) = self.interpolation_stack.last() {
self.lex_interpolation(interpolation_start, first)?
} else if self.recipe_body {
self.lex_body()?
@ -292,7 +290,7 @@ impl<'src> Lexer<'src> {
}
}
if let Some(interpolation_start) = self.interpolation_start {
if let Some(&interpolation_start) = self.interpolation_stack.last() {
return Err(Self::unterminated_interpolation_error(interpolation_start));
}
@ -477,10 +475,10 @@ impl<'src> Lexer<'src> {
'}' => self.lex_delimiter(BraceR),
'+' => self.lex_single(Plus),
'#' => self.lex_comment(),
'`' => self.lex_backtick(),
' ' => self.lex_whitespace(),
'"' => self.lex_cooked_string(),
'\'' => self.lex_raw_string(),
'`' => self.lex_string(StringKind::Backtick),
'"' => self.lex_string(StringKind::Cooked),
'\'' => self.lex_string(StringKind::Raw),
'\n' => self.lex_eol(),
'\r' => self.lex_eol(),
'\t' => self.lex_whitespace(),
@ -500,7 +498,13 @@ impl<'src> Lexer<'src> {
) -> CompilationResult<'src, ()> {
if self.rest_starts_with("}}") {
// end current interpolation
self.interpolation_start = None;
if self.interpolation_stack.pop().is_none() {
self.advance()?;
self.advance()?;
return Err(self.internal_error(
"Lexer::lex_interpolation found `}}` but was called with empty interpolation stack.",
));
}
// Emit interpolation end token
self.lex_double(InterpolationEnd)
} else if self.at_eol_or_eof() {
@ -530,7 +534,7 @@ impl<'src> Lexer<'src> {
continue;
}
if let Some('\n') = self.next {
if self.rest_starts_with("\n") {
break Newline;
}
@ -542,7 +546,7 @@ impl<'src> Lexer<'src> {
break Interpolation;
}
if self.next.is_none() {
if self.rest().is_empty() {
break EndOfFile;
}
@ -559,7 +563,9 @@ impl<'src> Lexer<'src> {
NewlineCarriageReturn => self.lex_double(Eol),
Interpolation => {
self.lex_double(InterpolationStart)?;
self.interpolation_start = Some(self.tokens[self.tokens.len() - 1]);
self
.interpolation_stack
.push(self.tokens[self.tokens.len() - 1]);
Ok(())
},
EndOfFile => Ok(()),
@ -738,25 +744,6 @@ impl<'src> Lexer<'src> {
Ok(())
}
/// Lex backtick: `[^\r\n]*`
fn lex_backtick(&mut self) -> CompilationResult<'src, ()> {
// advance over initial `
self.advance()?;
while !self.next_is('`') {
if self.at_eol_or_eof() {
return Err(self.error(UnterminatedBacktick));
}
self.advance()?;
}
self.advance()?;
self.token(Backtick);
Ok(())
}
/// Lex whitespace: [ \t]+
fn lex_whitespace(&mut self) -> CompilationResult<'src, ()> {
while self.next_is_whitespace() {
@ -768,48 +755,29 @@ impl<'src> Lexer<'src> {
Ok(())
}
/// Lex raw string: '[^']*'
fn lex_raw_string(&mut self) -> CompilationResult<'src, ()> {
self.presume('\'')?;
loop {
match self.next {
Some('\'') => break,
None => return Err(self.error(UnterminatedString)),
Some(_) => {},
}
self.advance()?;
}
self.presume('\'')?;
self.token(StringRaw);
Ok(())
}
/// Lex cooked string: "[^"\n\r]*" (also processes escape sequences)
fn lex_cooked_string(&mut self) -> CompilationResult<'src, ()> {
self.presume('"')?;
/// Lex a backtick, cooked string, or raw string.
///
/// Backtick: `[^`]*`
/// Cooked string: "[^"]*" # also processes escape sequences
/// Raw string: '[^']*'
fn lex_string(&mut self, kind: StringKind) -> CompilationResult<'src, ()> {
self.presume(kind.delimiter())?;
let mut escape = false;
loop {
match self.next {
Some('\r') | Some('\n') | None => return Err(self.error(UnterminatedString)),
Some('"') if !escape => break,
Some('\\') if !escape => escape = true,
_ => escape = false,
Some(c) if c == kind.delimiter() && !escape => break,
Some('\\') if kind.processes_escape_sequences() && !escape => escape = true,
Some(_) => escape = false,
None => return Err(self.error(kind.unterminated_error_kind())),
}
self.advance()?;
}
// advance over closing "
self.advance()?;
self.token(StringCooked);
self.presume(kind.delimiter())?;
self.token(kind.token_kind());
Ok(())
}
@ -821,6 +789,10 @@ mod tests {
use pretty_assertions::assert_eq;
const STRING_BACKTICK: TokenKind = StringToken(StringKind::Backtick);
const STRING_RAW: TokenKind = StringToken(StringKind::Raw);
const STRING_COOKED: TokenKind = StringToken(StringKind::Cooked);
macro_rules! test {
{
name: $name:ident,
@ -929,7 +901,7 @@ mod tests {
Dedent | Eof => "",
// Variable lexemes
Text | StringCooked | StringRaw | Identifier | Comment | Backtick | Unspecified =>
Text | StringToken(_) | Identifier | Comment | Unspecified =>
panic!("Token {:?} has no default lexeme", kind),
}
}
@ -993,19 +965,37 @@ mod tests {
test! {
name: backtick,
text: "`echo`",
tokens: (Backtick:"`echo`"),
tokens: (STRING_BACKTICK:"`echo`"),
}
test! {
name: backtick_multi_line,
text: "`echo\necho`",
tokens: (STRING_BACKTICK:"`echo\necho`"),
}
test! {
name: raw_string,
text: "'hello'",
tokens: (StringRaw:"'hello'"),
tokens: (STRING_RAW:"'hello'"),
}
test! {
name: raw_string_multi_line,
text: "'hello\ngoodbye'",
tokens: (STRING_RAW:"'hello\ngoodbye'"),
}
test! {
name: cooked_string,
text: "\"hello\"",
tokens: (StringCooked:"\"hello\""),
tokens: (STRING_COOKED:"\"hello\""),
}
test! {
name: cooked_string_multi_line,
text: "\"hello\ngoodbye\"",
tokens: (STRING_COOKED:"\"hello\ngoodbye\""),
}
test! {
@ -1066,11 +1056,11 @@ mod tests {
Whitespace,
Equals,
Whitespace,
StringRaw:"'foo'",
STRING_RAW:"'foo'",
Whitespace,
Plus,
Whitespace,
StringRaw:"'bar'",
STRING_RAW:"'bar'",
)
}
@ -1085,16 +1075,16 @@ mod tests {
Equals,
Whitespace,
ParenL,
StringRaw:"'foo'",
STRING_RAW:"'foo'",
Whitespace,
Plus,
Whitespace,
StringRaw:"'bar'",
STRING_RAW:"'bar'",
ParenR,
Whitespace,
Plus,
Whitespace,
Backtick:"`baz`",
STRING_BACKTICK:"`baz`",
),
}
@ -1421,11 +1411,11 @@ mod tests {
Indent:" ",
Text:"echo ",
InterpolationStart,
Backtick:"`echo hello`",
STRING_BACKTICK:"`echo hello`",
Whitespace,
Plus,
Whitespace,
Backtick:"`echo goodbye`",
STRING_BACKTICK:"`echo goodbye`",
InterpolationEnd,
Dedent,
),
@ -1441,7 +1431,7 @@ mod tests {
Indent:" ",
Text:"echo ",
InterpolationStart,
StringRaw:"'\n'",
STRING_RAW:"'\n'",
InterpolationEnd,
Dedent,
),
@ -1513,19 +1503,19 @@ mod tests {
Whitespace,
Equals,
Whitespace,
StringCooked:"\"'a'\"",
STRING_COOKED:"\"'a'\"",
Whitespace,
Plus,
Whitespace,
StringRaw:"'\"b\"'",
STRING_RAW:"'\"b\"'",
Whitespace,
Plus,
Whitespace,
StringCooked:"\"'c'\"",
STRING_COOKED:"\"'c'\"",
Whitespace,
Plus,
Whitespace,
StringRaw:"'\"d\"'",
STRING_RAW:"'\"d\"'",
Comment:"#echo hello",
)
}
@ -1593,7 +1583,7 @@ mod tests {
Whitespace,
Plus,
Whitespace,
StringCooked:"\"z\"",
STRING_COOKED:"\"z\"",
Whitespace,
Plus,
Whitespace,
@ -1717,7 +1707,7 @@ mod tests {
Eol,
Identifier:"A",
Equals,
StringRaw:"'1'",
STRING_RAW:"'1'",
Eol,
Identifier:"echo",
Colon,
@ -1742,11 +1732,11 @@ mod tests {
Indent:" ",
Text:"echo ",
InterpolationStart,
Backtick:"`echo hello`",
STRING_BACKTICK:"`echo hello`",
Whitespace,
Plus,
Whitespace,
Backtick:"`echo goodbye`",
STRING_BACKTICK:"`echo goodbye`",
InterpolationEnd,
Dedent
),
@ -1775,11 +1765,11 @@ mod tests {
Whitespace,
Equals,
Whitespace,
Backtick:"`echo hello`",
STRING_BACKTICK:"`echo hello`",
Whitespace,
Plus,
Whitespace,
Backtick:"`echo goodbye`",
STRING_BACKTICK:"`echo goodbye`",
),
}
@ -2021,7 +2011,7 @@ mod tests {
line: 0,
column: 4,
width: 1,
kind: UnterminatedString,
kind: UnterminatedString(StringKind::Cooked),
}
error! {
@ -2031,7 +2021,7 @@ mod tests {
line: 0,
column: 4,
width: 1,
kind: UnterminatedString,
kind: UnterminatedString(StringKind::Raw),
}
error! {
@ -2052,7 +2042,7 @@ mod tests {
line: 0,
column: 0,
width: 1,
kind: UnterminatedBacktick,
kind: UnterminatedString(StringKind::Backtick),
}
error! {
@ -2112,7 +2102,7 @@ mod tests {
line: 0,
column: 4,
width: 1,
kind: UnterminatedString,
kind: UnterminatedString(StringKind::Cooked),
}
error! {

View File

@ -117,6 +117,7 @@ mod setting;
mod settings;
mod shebang;
mod show_whitespace;
mod string_kind;
mod string_literal;
mod subcommand;
mod suggestion;

View File

@ -453,11 +453,11 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
/// Parse a value, e.g. `(bar)`
fn parse_value(&mut self) -> CompilationResult<'src, Expression<'src>> {
if self.next_is(StringCooked) || self.next_is(StringRaw) {
if self.next_is(StringToken(StringKind::Cooked)) || self.next_is(StringToken(StringKind::Raw)) {
Ok(Expression::StringLiteral {
string_literal: self.parse_string_literal()?,
})
} else if self.next_is(Backtick) {
} else if self.next_is(StringToken(StringKind::Backtick)) {
let next = self.next()?;
let contents = &next.lexeme()[1..next.lexeme().len() - 1];
@ -486,16 +486,19 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
/// Parse a string literal, e.g. `"FOO"`
fn parse_string_literal(&mut self) -> CompilationResult<'src, StringLiteral<'src>> {
let token = self.expect_any(&[StringRaw, StringCooked])?;
let token = self.expect_any(&[
StringToken(StringKind::Raw),
StringToken(StringKind::Cooked),
])?;
let raw = &token.lexeme()[1..token.lexeme().len() - 1];
match token.kind {
StringRaw => Ok(StringLiteral {
StringToken(StringKind::Raw) => Ok(StringLiteral {
raw,
cooked: Cow::Borrowed(raw),
}),
StringCooked => {
StringToken(StringKind::Cooked) => {
let mut cooked = String::new();
let mut escape = false;
for c in raw.chars() {
@ -1720,7 +1723,13 @@ mod tests {
column: 10,
width: 1,
kind: UnexpectedToken {
expected: vec![Backtick, Identifier, ParenL, StringCooked, StringRaw],
expected: vec![
Identifier,
ParenL,
StringToken(StringKind::Backtick),
StringToken(StringKind::Cooked),
StringToken(StringKind::Raw)
],
found: Eol
},
}
@ -1733,7 +1742,13 @@ mod tests {
column: 10,
width: 0,
kind: UnexpectedToken {
expected: vec![Backtick, Identifier, ParenL, StringCooked, StringRaw],
expected: vec![
Identifier,
ParenL,
StringToken(StringKind::Backtick),
StringToken(StringKind::Cooked),
StringToken(StringKind::Raw)
],
found: Eof,
},
}
@ -1769,7 +1784,14 @@ mod tests {
column: 9,
width: 0,
kind: UnexpectedToken{
expected: vec![Backtick, Identifier, ParenL, ParenR, StringCooked, StringRaw],
expected: vec![
Identifier,
ParenL,
ParenR,
StringToken(StringKind::Backtick),
StringToken(StringKind::Cooked),
StringToken(StringKind::Raw)
],
found: Eof,
},
}
@ -1782,7 +1804,14 @@ mod tests {
column: 12,
width: 2,
kind: UnexpectedToken{
expected: vec![Backtick, Identifier, ParenL, ParenR, StringCooked, StringRaw],
expected: vec![
Identifier,
ParenL,
ParenR,
StringToken(StringKind::Backtick),
StringToken(StringKind::Cooked),
StringToken(StringKind::Raw)
],
found: InterpolationEnd,
},
}
@ -1858,7 +1887,7 @@ mod tests {
column: 14,
width: 1,
kind: UnexpectedToken {
expected: vec![StringCooked, StringRaw],
expected: vec![StringToken(StringKind::Cooked), StringToken(StringKind::Raw)],
found: BracketR,
},
}
@ -1897,7 +1926,7 @@ mod tests {
column: 21,
width: 0,
kind: UnexpectedToken {
expected: vec![BracketR, StringCooked, StringRaw],
expected: vec![BracketR, StringToken(StringKind::Cooked), StringToken(StringKind::Raw)],
found: Eof,
},
}

33
src/string_kind.rs Normal file
View File

@ -0,0 +1,33 @@
use crate::common::*;
#[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)]
pub(crate) enum StringKind {
Backtick,
Cooked,
Raw,
}
impl StringKind {
pub(crate) fn delimiter(self) -> char {
match self {
Self::Backtick => '`',
Self::Cooked => '"',
Self::Raw => '\'',
}
}
pub(crate) fn token_kind(self) -> TokenKind {
TokenKind::StringToken(self)
}
pub(crate) fn unterminated_error_kind(self) -> CompilationErrorKind<'static> {
CompilationErrorKind::UnterminatedString(self)
}
pub(crate) fn processes_escape_sequences(self) -> bool {
match self {
Self::Backtick | Self::Raw => false,
Self::Cooked => true,
}
}
}

View File

@ -4,7 +4,6 @@ use crate::common::*;
pub(crate) enum TokenKind {
Asterisk,
At,
Backtick,
BangEquals,
BraceL,
BraceR,
@ -27,8 +26,7 @@ pub(crate) enum TokenKind {
ParenL,
ParenR,
Plus,
StringCooked,
StringRaw,
StringToken(StringKind),
Text,
Unspecified,
Whitespace,
@ -40,7 +38,6 @@ impl Display for TokenKind {
write!(f, "{}", match *self {
Asterisk => "'*'",
At => "'@'",
Backtick => "backtick",
BangEquals => "'!='",
BraceL => "'{'",
BraceR => "'}'",
@ -63,8 +60,9 @@ impl Display for TokenKind {
ParenL => "'('",
ParenR => "')'",
Plus => "'+'",
StringCooked => "cooked string",
StringRaw => "raw string",
StringToken(StringKind::Backtick) => "backtick",
StringToken(StringKind::Cooked) => "cooked string",
StringToken(StringKind::Raw) => "raw string",
Text => "command text",
Whitespace => "whitespace",
Unspecified => "unspecified",

View File

@ -20,4 +20,5 @@ mod quiet;
mod readme;
mod search;
mod shell;
mod string;
mod working_directory;

View File

@ -588,18 +588,6 @@ hello := "c"
"#,
}
test! {
name: raw_string,
justfile: r#"
export EXPORTED_VARIABLE := '\z'
recipe:
printf "$EXPORTED_VARIABLE"
"#,
stdout: "\\z",
stderr: "printf \"$EXPORTED_VARIABLE\"\n",
}
test! {
name: line_error_spacing,
justfile: r#"
@ -1508,145 +1496,6 @@ test! {
status: EXIT_FAILURE,
}
test! {
name: invalid_escape_sequence,
justfile: r#"x := "\q"
a:"#,
args: ("a"),
stdout: "",
stderr: "error: `\\q` is not a valid escape sequence
|
1 | x := \"\\q\"
| ^^^^
",
status: EXIT_FAILURE,
}
test! {
name: multiline_raw_string,
justfile: "
string := 'hello
whatever'
a:
echo '{{string}}'
",
args: ("a"),
stdout: "hello
whatever
",
stderr: "echo 'hello
whatever'
",
}
test! {
name: error_line_after_multiline_raw_string,
justfile: "
string := 'hello
whatever' + 'yo'
a:
echo '{{foo}}'
",
args: ("a"),
stdout: "",
stderr: "error: Variable `foo` not defined
|
7 | echo '{{foo}}'
| ^^^
",
status: EXIT_FAILURE,
}
test! {
name: error_column_after_multiline_raw_string,
justfile: "
string := 'hello
whatever' + bar
a:
echo '{{string}}'
",
args: ("a"),
stdout: "",
stderr: "error: Variable `bar` not defined
|
4 | whatever' + bar
| ^^^
",
status: EXIT_FAILURE,
}
test! {
name: multiline_raw_string_in_interpolation,
justfile: r#"
a:
echo '{{"a" + '
' + "b"}}'
"#,
args: ("a"),
stdout: "
a
b
",
stderr: "
echo 'a
b'
",
}
test! {
name: error_line_after_multiline_raw_string_in_interpolation,
justfile: r#"
a:
echo '{{"a" + '
' + "b"}}'
echo {{b}}
"#,
args: ("a"),
stdout: "",
stderr: "error: Variable `b` not defined
|
6 | echo {{b}}
| ^
",
status: EXIT_FAILURE,
}
test! {
name: unterminated_raw_string,
justfile: "
a b= ':
",
args: ("a"),
stdout: "",
stderr: "error: Unterminated string
|
2 | a b= ':
| ^
",
status: EXIT_FAILURE,
}
test! {
name: unterminated_string,
justfile: r#"
a b= ":
"#,
args: ("a"),
stdout: "",
stderr: r#"error: Unterminated string
|
2 | a b= ":
| ^
"#,
status: EXIT_FAILURE,
}
test! {
name: plus_variadic_recipe,
justfile: "
@ -2059,20 +1908,6 @@ foo:
status: EXIT_FAILURE,
}
test! {
name: unterminated_backtick,
justfile: "
foo a=\t`echo blaaaaaah:
echo {{a}}",
stderr: r#"
error: Unterminated backtick
|
2 | foo a= `echo blaaaaaah:
| ^
"#,
status: EXIT_FAILURE,
}
test! {
name: unknown_start_of_token,
justfile: "

201
tests/string.rs Normal file
View File

@ -0,0 +1,201 @@
use crate::common::*;
test! {
name: raw_string,
justfile: r#"
export EXPORTED_VARIABLE := '\z'
recipe:
printf "$EXPORTED_VARIABLE"
"#,
stdout: "\\z",
stderr: "printf \"$EXPORTED_VARIABLE\"\n",
}
test! {
name: multiline_raw_string,
justfile: "
string := 'hello
whatever'
a:
echo '{{string}}'
",
args: ("a"),
stdout: "hello
whatever
",
stderr: "echo 'hello
whatever'
",
}
test! {
name: multiline_backtick,
justfile: "
string := `echo hello
echo goodbye
`
a:
echo '{{string}}'
",
args: ("a"),
stdout: "hello\ngoodbye\n",
stderr: "echo 'hello
goodbye'
",
}
test! {
name: multiline_cooked_string,
justfile: r#"
string := "hello
whatever"
a:
echo '{{string}}'
"#,
args: ("a"),
stdout: "hello
whatever
",
stderr: "echo 'hello
whatever'
",
}
test! {
name: invalid_escape_sequence,
justfile: r#"x := "\q"
a:"#,
args: ("a"),
stdout: "",
stderr: "error: `\\q` is not a valid escape sequence
|
1 | x := \"\\q\"
| ^^^^
",
status: EXIT_FAILURE,
}
test! {
name: error_line_after_multiline_raw_string,
justfile: "
string := 'hello
whatever' + 'yo'
a:
echo '{{foo}}'
",
args: ("a"),
stdout: "",
stderr: "error: Variable `foo` not defined
|
7 | echo '{{foo}}'
| ^^^
",
status: EXIT_FAILURE,
}
test! {
name: error_column_after_multiline_raw_string,
justfile: "
string := 'hello
whatever' + bar
a:
echo '{{string}}'
",
args: ("a"),
stdout: "",
stderr: "error: Variable `bar` not defined
|
4 | whatever' + bar
| ^^^
",
status: EXIT_FAILURE,
}
test! {
name: multiline_raw_string_in_interpolation,
justfile: r#"
a:
echo '{{"a" + '
' + "b"}}'
"#,
args: ("a"),
stdout: "
a
b
",
stderr: "
echo 'a
b'
",
}
test! {
name: error_line_after_multiline_raw_string_in_interpolation,
justfile: r#"
a:
echo '{{"a" + '
' + "b"}}'
echo {{b}}
"#,
args: ("a"),
stdout: "",
stderr: "error: Variable `b` not defined
|
6 | echo {{b}}
| ^
",
status: EXIT_FAILURE,
}
test! {
name: unterminated_raw_string,
justfile: "
a b= ':
",
args: ("a"),
stdout: "",
stderr: "error: Unterminated string
|
2 | a b= ':
| ^
",
status: EXIT_FAILURE,
}
test! {
name: unterminated_string,
justfile: r#"
a b= ":
"#,
args: ("a"),
stdout: "",
stderr: r#"error: Unterminated string
|
2 | a b= ":
| ^
"#,
status: EXIT_FAILURE,
}
test! {
name: unterminated_backtick,
justfile: "
foo a=\t`echo blaaaaaah:
echo {{a}}",
stderr: r#"
error: Unterminated backtick
|
2 | foo a= `echo blaaaaaah:
| ^
"#,
status: EXIT_FAILURE,
}