Reform and improve string literals (#793)
- Combine and simplify string and backtick lexing. - Allow newlines in strings and backticks. - Add triple-delimited indented strings and backticks. Common indented literal non-blank line leading whitespace is stripped. - If a literal newline is escaped, it will be suppressed. - Backticks starting with `#!` are reserved for a future upgrade.
This commit is contained in:
parent
da97f8d7dd
commit
fec979c2c6
@ -10,6 +10,7 @@ tokens
|
|||||||
|
|
||||||
```
|
```
|
||||||
BACKTICK = `[^`]*`
|
BACKTICK = `[^`]*`
|
||||||
|
INDENTED_BACKTICK = ```[^(```)]*```
|
||||||
COMMENT = #([^!].*)?$
|
COMMENT = #([^!].*)?$
|
||||||
DEDENT = emitted when indentation decreases
|
DEDENT = emitted when indentation decreases
|
||||||
EOF = emitted at the end of the file
|
EOF = emitted at the end of the file
|
||||||
@ -18,7 +19,9 @@ LINE = emitted before a recipe line
|
|||||||
NAME = [a-zA-Z_][a-zA-Z0-9_-]*
|
NAME = [a-zA-Z_][a-zA-Z0-9_-]*
|
||||||
NEWLINE = \n|\r\n
|
NEWLINE = \n|\r\n
|
||||||
RAW_STRING = '[^']*'
|
RAW_STRING = '[^']*'
|
||||||
|
INDENTED_RAW_STRING = '''[^(''')]*'''
|
||||||
STRING = "[^"]*" # also processes \n \r \t \" \\ escapes
|
STRING = "[^"]*" # also processes \n \r \t \" \\ escapes
|
||||||
|
INDENTED_STRING = """[^("""]*""" # also processes \n \r \t \" \\ escapes
|
||||||
TEXT = recipe text, only matches in a recipe body
|
TEXT = recipe text, only matches in a recipe body
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -69,14 +72,16 @@ condition : expression '==' expression
|
|||||||
| expression '!=' expression
|
| expression '!=' expression
|
||||||
|
|
||||||
value : NAME '(' sequence? ')'
|
value : NAME '(' sequence? ')'
|
||||||
| STRING
|
|
||||||
| RAW_STRING
|
|
||||||
| BACKTICK
|
| BACKTICK
|
||||||
|
| INDENTED_BACKTICK
|
||||||
| NAME
|
| NAME
|
||||||
|
| string
|
||||||
| '(' expression ')'
|
| '(' expression ')'
|
||||||
|
|
||||||
string : STRING
|
string : STRING
|
||||||
|
| INDENTED_STRING
|
||||||
| RAW_STRING
|
| RAW_STRING
|
||||||
|
| INDENTED_RAW_STRING
|
||||||
|
|
||||||
sequence : expression ',' sequence
|
sequence : expression ',' sequence
|
||||||
| expression ','?
|
| expression ','?
|
||||||
|
36
README.adoc
36
README.adoc
@ -545,6 +545,8 @@ string-with-newline := "\n"
|
|||||||
string-with-carriage-return := "\r"
|
string-with-carriage-return := "\r"
|
||||||
string-with-double-quote := "\""
|
string-with-double-quote := "\""
|
||||||
string-with-slash := "\\"
|
string-with-slash := "\\"
|
||||||
|
string-with-no-newline := "\
|
||||||
|
"
|
||||||
```
|
```
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@ -553,6 +555,7 @@ $ just --evaluate
|
|||||||
string-with-double-quote := """
|
string-with-double-quote := """
|
||||||
string-with-newline := "
|
string-with-newline := "
|
||||||
"
|
"
|
||||||
|
string-with-no-newline := ""
|
||||||
string-with-slash := "\"
|
string-with-slash := "\"
|
||||||
string-with-tab := " "
|
string-with-tab := " "
|
||||||
```
|
```
|
||||||
@ -580,6 +583,25 @@ $ just --evaluate
|
|||||||
escapes := "\t\n\r\"\\"
|
escapes := "\t\n\r\"\\"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Indented versions of both single- and double-quoted strings, delimited by triple single- or triple double-quotes, are supported. Indented string lines are stripped of leading whitespace common to all non-blank lines:
|
||||||
|
|
||||||
|
```make
|
||||||
|
# this string will evaluate to `foo\nbar\n`
|
||||||
|
x := '''
|
||||||
|
foo
|
||||||
|
bar
|
||||||
|
'''
|
||||||
|
|
||||||
|
# this string will evaluate to `abc\n wuv\nbar\n`
|
||||||
|
y := """
|
||||||
|
abc
|
||||||
|
wuv
|
||||||
|
xyz
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
Similar to unindented strings, indented double-quoted strings process escape sequences, and indented single-quoted strings ignore escape sequences. Escape sequence processing takes place after unindentation. The unindention algorithm does not take escape-sequence produced whitespace or newlines into account.
|
||||||
|
|
||||||
=== Ignoring Errors
|
=== Ignoring Errors
|
||||||
|
|
||||||
Normally, if a command returns a nonzero exit status, execution will stop. To
|
Normally, if a command returns a nonzero exit status, execution will stop. To
|
||||||
@ -716,6 +738,20 @@ serve:
|
|||||||
./serve {{localhost}} 8080
|
./serve {{localhost}} 8080
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Indented backticks, delimited by three backticks, are de-indented in the same manner as indented strings:
|
||||||
|
|
||||||
|
```make
|
||||||
|
# This backtick evaluates the command `echo foo\necho bar\n`, which produces the value `foo\nbar\n`.
|
||||||
|
stuff := ```
|
||||||
|
echo foo
|
||||||
|
echo bar
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
See the <<Strings>> section for details on unindenting.
|
||||||
|
|
||||||
|
Backticks may not start with `#!`. This syntax is reserved for a future upgrade.
|
||||||
|
|
||||||
=== Conditional Expressions
|
=== Conditional Expressions
|
||||||
|
|
||||||
`if`/`else` expressions evaluate different branches depending on if two expressions evaluate to the same value:
|
`if`/`else` expressions evaluate different branches depending on if two expressions evaluate to the same value:
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
// stdlib
|
// stdlib
|
||||||
pub(crate) use std::{
|
pub(crate) use std::{
|
||||||
borrow::Cow,
|
|
||||||
cmp,
|
cmp,
|
||||||
collections::{BTreeMap, BTreeSet},
|
collections::{BTreeMap, BTreeSet},
|
||||||
env,
|
env,
|
||||||
@ -31,7 +30,9 @@ pub(crate) use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
|||||||
pub(crate) use crate::{config_error, setting};
|
pub(crate) use crate::{config_error, setting};
|
||||||
|
|
||||||
// functions
|
// functions
|
||||||
pub(crate) use crate::{default::default, empty::empty, load_dotenv::load_dotenv, output::output};
|
pub(crate) use crate::{
|
||||||
|
default::default, empty::empty, load_dotenv::load_dotenv, output::output, unindent::unindent,
|
||||||
|
};
|
||||||
|
|
||||||
// traits
|
// traits
|
||||||
pub(crate) use crate::{
|
pub(crate) use crate::{
|
||||||
|
@ -26,6 +26,9 @@ impl Display for CompilationError<'_> {
|
|||||||
recipe_line.ordinal(),
|
recipe_line.ordinal(),
|
||||||
)?;
|
)?;
|
||||||
},
|
},
|
||||||
|
BacktickShebang => {
|
||||||
|
writeln!(f, "Backticks may not start with `#!`")?;
|
||||||
|
},
|
||||||
CircularRecipeDependency { recipe, ref circle } =>
|
CircularRecipeDependency { recipe, ref circle } =>
|
||||||
if circle.len() == 2 {
|
if circle.len() == 2 {
|
||||||
writeln!(f, "Recipe `{}` depends on itself", recipe)?;
|
writeln!(f, "Recipe `{}` depends on itself", recipe)?;
|
||||||
@ -242,10 +245,10 @@ impl Display for CompilationError<'_> {
|
|||||||
UnterminatedInterpolation => {
|
UnterminatedInterpolation => {
|
||||||
writeln!(f, "Unterminated interpolation")?;
|
writeln!(f, "Unterminated interpolation")?;
|
||||||
},
|
},
|
||||||
UnterminatedString(StringKind::Cooked) | UnterminatedString(StringKind::Raw) => {
|
UnterminatedString => {
|
||||||
writeln!(f, "Unterminated string")?;
|
writeln!(f, "Unterminated string")?;
|
||||||
},
|
},
|
||||||
UnterminatedString(StringKind::Backtick) => {
|
UnterminatedBacktick => {
|
||||||
writeln!(f, "Unterminated backtick")?;
|
writeln!(f, "Unterminated backtick")?;
|
||||||
},
|
},
|
||||||
Internal { ref message } => {
|
Internal { ref message } => {
|
||||||
|
@ -6,6 +6,7 @@ pub(crate) enum CompilationErrorKind<'src> {
|
|||||||
alias: &'src str,
|
alias: &'src str,
|
||||||
recipe_line: usize,
|
recipe_line: usize,
|
||||||
},
|
},
|
||||||
|
BacktickShebang,
|
||||||
CircularRecipeDependency {
|
CircularRecipeDependency {
|
||||||
recipe: &'src str,
|
recipe: &'src str,
|
||||||
circle: Vec<&'src str>,
|
circle: Vec<&'src str>,
|
||||||
@ -107,5 +108,6 @@ pub(crate) enum CompilationErrorKind<'src> {
|
|||||||
open_line: usize,
|
open_line: usize,
|
||||||
},
|
},
|
||||||
UnterminatedInterpolation,
|
UnterminatedInterpolation,
|
||||||
UnterminatedString(StringKind),
|
UnterminatedString,
|
||||||
|
UnterminatedBacktick,
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Expression::StringLiteral { string_literal } => Ok(string_literal.cooked.to_string()),
|
Expression::StringLiteral { string_literal } => Ok(string_literal.cooked.clone()),
|
||||||
Expression::Backtick { contents, token } =>
|
Expression::Backtick { contents, token } =>
|
||||||
if self.config.dry_run {
|
if self.config.dry_run {
|
||||||
Ok(format!("`{}`", contents))
|
Ok(format!("`{}`", contents))
|
||||||
|
@ -10,7 +10,7 @@ use crate::common::*;
|
|||||||
pub(crate) enum Expression<'src> {
|
pub(crate) enum Expression<'src> {
|
||||||
/// `contents`
|
/// `contents`
|
||||||
Backtick {
|
Backtick {
|
||||||
contents: &'src str,
|
contents: String,
|
||||||
token: Token<'src>,
|
token: Token<'src>,
|
||||||
},
|
},
|
||||||
/// `name(arguments)`
|
/// `name(arguments)`
|
||||||
|
147
src/lexer.rs
147
src/lexer.rs
@ -129,6 +129,14 @@ impl<'src> Lexer<'src> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn presume_str(&mut self, s: &str) -> CompilationResult<'src, ()> {
|
||||||
|
for c in s.chars() {
|
||||||
|
self.presume(c)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Is next character c?
|
/// Is next character c?
|
||||||
fn next_is(&self, c: char) -> bool {
|
fn next_is(&self, c: char) -> bool {
|
||||||
self.next == Some(c)
|
self.next == Some(c)
|
||||||
@ -210,8 +218,14 @@ impl<'src> Lexer<'src> {
|
|||||||
|
|
||||||
// The width of the error site to highlight depends on the kind of error:
|
// The width of the error site to highlight depends on the kind of error:
|
||||||
let length = match kind {
|
let length = match kind {
|
||||||
// highlight ', ", or `
|
UnterminatedString | UnterminatedBacktick => {
|
||||||
UnterminatedString(_) => 1,
|
let kind = match StringKind::from_token_start(self.lexeme()) {
|
||||||
|
Some(kind) => kind,
|
||||||
|
None =>
|
||||||
|
return self.internal_error("Lexer::error: expected string or backtick token start"),
|
||||||
|
};
|
||||||
|
kind.delimiter().len()
|
||||||
|
},
|
||||||
// highlight the full token
|
// highlight the full token
|
||||||
_ => self.lexeme().len(),
|
_ => self.lexeme().len(),
|
||||||
};
|
};
|
||||||
@ -476,9 +490,7 @@ impl<'src> Lexer<'src> {
|
|||||||
'+' => self.lex_single(Plus),
|
'+' => self.lex_single(Plus),
|
||||||
'#' => self.lex_comment(),
|
'#' => self.lex_comment(),
|
||||||
' ' => self.lex_whitespace(),
|
' ' => self.lex_whitespace(),
|
||||||
'`' => self.lex_string(StringKind::Backtick),
|
'`' | '"' | '\'' => self.lex_string(),
|
||||||
'"' => self.lex_string(StringKind::Cooked),
|
|
||||||
'\'' => self.lex_string(StringKind::Raw),
|
|
||||||
'\n' => self.lex_eol(),
|
'\n' => self.lex_eol(),
|
||||||
'\r' => self.lex_eol(),
|
'\r' => self.lex_eol(),
|
||||||
'\t' => self.lex_whitespace(),
|
'\t' => self.lex_whitespace(),
|
||||||
@ -760,23 +772,33 @@ impl<'src> Lexer<'src> {
|
|||||||
/// Backtick: `[^`]*`
|
/// Backtick: `[^`]*`
|
||||||
/// Cooked string: "[^"]*" # also processes escape sequences
|
/// Cooked string: "[^"]*" # also processes escape sequences
|
||||||
/// Raw string: '[^']*'
|
/// Raw string: '[^']*'
|
||||||
fn lex_string(&mut self, kind: StringKind) -> CompilationResult<'src, ()> {
|
fn lex_string(&mut self) -> CompilationResult<'src, ()> {
|
||||||
self.presume(kind.delimiter())?;
|
let kind = if let Some(kind) = StringKind::from_token_start(self.rest()) {
|
||||||
|
kind
|
||||||
|
} else {
|
||||||
|
self.advance()?;
|
||||||
|
return Err(self.internal_error("Lexer::lex_string: invalid string start"));
|
||||||
|
};
|
||||||
|
|
||||||
|
self.presume_str(kind.delimiter())?;
|
||||||
|
|
||||||
let mut escape = false;
|
let mut escape = false;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match self.next {
|
if self.next == None {
|
||||||
Some(c) if c == kind.delimiter() && !escape => break,
|
return Err(self.error(kind.unterminated_error_kind()));
|
||||||
Some('\\') if kind.processes_escape_sequences() && !escape => escape = true,
|
} else if kind.processes_escape_sequences() && self.next_is('\\') && !escape {
|
||||||
Some(_) => escape = false,
|
escape = true;
|
||||||
None => return Err(self.error(kind.unterminated_error_kind())),
|
} else if self.rest_starts_with(kind.delimiter()) && !escape {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
escape = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.advance()?;
|
self.advance()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.presume(kind.delimiter())?;
|
self.presume_str(kind.delimiter())?;
|
||||||
self.token(kind.token_kind());
|
self.token(kind.token_kind());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -789,10 +811,6 @@ mod tests {
|
|||||||
|
|
||||||
use pretty_assertions::assert_eq;
|
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 {
|
macro_rules! test {
|
||||||
{
|
{
|
||||||
name: $name:ident,
|
name: $name:ident,
|
||||||
@ -805,7 +823,22 @@ mod tests {
|
|||||||
|
|
||||||
let lexemes: &[&str] = &[$(lexeme!($kind $(, $lexeme)?),)* ""];
|
let lexemes: &[&str] = &[$(lexeme!($kind $(, $lexeme)?),)* ""];
|
||||||
|
|
||||||
test($text, kinds, lexemes);
|
test($text, true, kinds, lexemes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
{
|
||||||
|
name: $name:ident,
|
||||||
|
text: $text:expr,
|
||||||
|
tokens: ($($kind:ident $(: $lexeme:literal)?),* $(,)?)$(,)?
|
||||||
|
unindent: $unindent:expr,
|
||||||
|
} => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
let kinds: &[TokenKind] = &[$($kind,)* Eof];
|
||||||
|
|
||||||
|
let lexemes: &[&str] = &[$(lexeme!($kind $(, $lexeme)?),)* ""];
|
||||||
|
|
||||||
|
test($text, $unindent, kinds, lexemes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -823,8 +856,12 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test(text: &str, want_kinds: &[TokenKind], want_lexemes: &[&str]) {
|
fn test(text: &str, unindent_text: bool, want_kinds: &[TokenKind], want_lexemes: &[&str]) {
|
||||||
let text = testing::unindent(text);
|
let text = if unindent_text {
|
||||||
|
unindent(text)
|
||||||
|
} else {
|
||||||
|
text.to_owned()
|
||||||
|
};
|
||||||
|
|
||||||
let have = Lexer::lex(&text).unwrap();
|
let have = Lexer::lex(&text).unwrap();
|
||||||
|
|
||||||
@ -901,7 +938,7 @@ mod tests {
|
|||||||
Dedent | Eof => "",
|
Dedent | Eof => "",
|
||||||
|
|
||||||
// Variable lexemes
|
// Variable lexemes
|
||||||
Text | StringToken(_) | Identifier | Comment | Unspecified =>
|
Text | StringToken | Backtick | Identifier | Comment | Unspecified =>
|
||||||
panic!("Token {:?} has no default lexeme", kind),
|
panic!("Token {:?} has no default lexeme", kind),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -965,37 +1002,43 @@ mod tests {
|
|||||||
test! {
|
test! {
|
||||||
name: backtick,
|
name: backtick,
|
||||||
text: "`echo`",
|
text: "`echo`",
|
||||||
tokens: (STRING_BACKTICK:"`echo`"),
|
tokens: (Backtick:"`echo`"),
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: backtick_multi_line,
|
name: backtick_multi_line,
|
||||||
text: "`echo\necho`",
|
text: "`echo\necho`",
|
||||||
tokens: (STRING_BACKTICK:"`echo\necho`"),
|
tokens: (Backtick:"`echo\necho`"),
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: raw_string,
|
name: raw_string,
|
||||||
text: "'hello'",
|
text: "'hello'",
|
||||||
tokens: (STRING_RAW:"'hello'"),
|
tokens: (StringToken:"'hello'"),
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: raw_string_multi_line,
|
name: raw_string_multi_line,
|
||||||
text: "'hello\ngoodbye'",
|
text: "'hello\ngoodbye'",
|
||||||
tokens: (STRING_RAW:"'hello\ngoodbye'"),
|
tokens: (StringToken:"'hello\ngoodbye'"),
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: cooked_string,
|
name: cooked_string,
|
||||||
text: "\"hello\"",
|
text: "\"hello\"",
|
||||||
tokens: (STRING_COOKED:"\"hello\""),
|
tokens: (StringToken:"\"hello\""),
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: cooked_string_multi_line,
|
name: cooked_string_multi_line,
|
||||||
text: "\"hello\ngoodbye\"",
|
text: "\"hello\ngoodbye\"",
|
||||||
tokens: (STRING_COOKED:"\"hello\ngoodbye\""),
|
tokens: (StringToken:"\"hello\ngoodbye\""),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: cooked_multiline_string,
|
||||||
|
text: "\"\"\"hello\ngoodbye\"\"\"",
|
||||||
|
tokens: (StringToken:"\"\"\"hello\ngoodbye\"\"\""),
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
@ -1056,11 +1099,11 @@ mod tests {
|
|||||||
Whitespace,
|
Whitespace,
|
||||||
Equals,
|
Equals,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
STRING_RAW:"'foo'",
|
StringToken:"'foo'",
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Plus,
|
Plus,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
STRING_RAW:"'bar'",
|
StringToken:"'bar'",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1075,16 +1118,16 @@ mod tests {
|
|||||||
Equals,
|
Equals,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
ParenL,
|
ParenL,
|
||||||
STRING_RAW:"'foo'",
|
StringToken:"'foo'",
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Plus,
|
Plus,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
STRING_RAW:"'bar'",
|
StringToken:"'bar'",
|
||||||
ParenR,
|
ParenR,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Plus,
|
Plus,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
STRING_BACKTICK:"`baz`",
|
Backtick:"`baz`",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1092,12 +1135,14 @@ mod tests {
|
|||||||
name: eol_linefeed,
|
name: eol_linefeed,
|
||||||
text: "\n",
|
text: "\n",
|
||||||
tokens: (Eol),
|
tokens: (Eol),
|
||||||
|
unindent: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: eol_carriage_return_linefeed,
|
name: eol_carriage_return_linefeed,
|
||||||
text: "\r\n",
|
text: "\r\n",
|
||||||
tokens: (Eol:"\r\n"),
|
tokens: (Eol:"\r\n"),
|
||||||
|
unindent: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
@ -1142,6 +1187,7 @@ mod tests {
|
|||||||
Eol,
|
Eol,
|
||||||
Dedent,
|
Dedent,
|
||||||
),
|
),
|
||||||
|
unindent: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
@ -1324,6 +1370,7 @@ mod tests {
|
|||||||
Eol,
|
Eol,
|
||||||
Dedent,
|
Dedent,
|
||||||
),
|
),
|
||||||
|
unindent: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
@ -1411,11 +1458,11 @@ mod tests {
|
|||||||
Indent:" ",
|
Indent:" ",
|
||||||
Text:"echo ",
|
Text:"echo ",
|
||||||
InterpolationStart,
|
InterpolationStart,
|
||||||
STRING_BACKTICK:"`echo hello`",
|
Backtick:"`echo hello`",
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Plus,
|
Plus,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
STRING_BACKTICK:"`echo goodbye`",
|
Backtick:"`echo goodbye`",
|
||||||
InterpolationEnd,
|
InterpolationEnd,
|
||||||
Dedent,
|
Dedent,
|
||||||
),
|
),
|
||||||
@ -1431,7 +1478,7 @@ mod tests {
|
|||||||
Indent:" ",
|
Indent:" ",
|
||||||
Text:"echo ",
|
Text:"echo ",
|
||||||
InterpolationStart,
|
InterpolationStart,
|
||||||
STRING_RAW:"'\n'",
|
StringToken:"'\n'",
|
||||||
InterpolationEnd,
|
InterpolationEnd,
|
||||||
Dedent,
|
Dedent,
|
||||||
),
|
),
|
||||||
@ -1503,19 +1550,19 @@ mod tests {
|
|||||||
Whitespace,
|
Whitespace,
|
||||||
Equals,
|
Equals,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
STRING_COOKED:"\"'a'\"",
|
StringToken:"\"'a'\"",
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Plus,
|
Plus,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
STRING_RAW:"'\"b\"'",
|
StringToken:"'\"b\"'",
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Plus,
|
Plus,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
STRING_COOKED:"\"'c'\"",
|
StringToken:"\"'c'\"",
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Plus,
|
Plus,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
STRING_RAW:"'\"d\"'",
|
StringToken:"'\"d\"'",
|
||||||
Comment:"#echo hello",
|
Comment:"#echo hello",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -1583,7 +1630,7 @@ mod tests {
|
|||||||
Whitespace,
|
Whitespace,
|
||||||
Plus,
|
Plus,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
STRING_COOKED:"\"z\"",
|
StringToken:"\"z\"",
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Plus,
|
Plus,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
@ -1707,7 +1754,7 @@ mod tests {
|
|||||||
Eol,
|
Eol,
|
||||||
Identifier:"A",
|
Identifier:"A",
|
||||||
Equals,
|
Equals,
|
||||||
STRING_RAW:"'1'",
|
StringToken:"'1'",
|
||||||
Eol,
|
Eol,
|
||||||
Identifier:"echo",
|
Identifier:"echo",
|
||||||
Colon,
|
Colon,
|
||||||
@ -1732,11 +1779,11 @@ mod tests {
|
|||||||
Indent:" ",
|
Indent:" ",
|
||||||
Text:"echo ",
|
Text:"echo ",
|
||||||
InterpolationStart,
|
InterpolationStart,
|
||||||
STRING_BACKTICK:"`echo hello`",
|
Backtick:"`echo hello`",
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Plus,
|
Plus,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
STRING_BACKTICK:"`echo goodbye`",
|
Backtick:"`echo goodbye`",
|
||||||
InterpolationEnd,
|
InterpolationEnd,
|
||||||
Dedent
|
Dedent
|
||||||
),
|
),
|
||||||
@ -1765,11 +1812,11 @@ mod tests {
|
|||||||
Whitespace,
|
Whitespace,
|
||||||
Equals,
|
Equals,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
STRING_BACKTICK:"`echo hello`",
|
Backtick:"`echo hello`",
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Plus,
|
Plus,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
STRING_BACKTICK:"`echo goodbye`",
|
Backtick:"`echo goodbye`",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2011,7 +2058,7 @@ mod tests {
|
|||||||
line: 0,
|
line: 0,
|
||||||
column: 4,
|
column: 4,
|
||||||
width: 1,
|
width: 1,
|
||||||
kind: UnterminatedString(StringKind::Cooked),
|
kind: UnterminatedString,
|
||||||
}
|
}
|
||||||
|
|
||||||
error! {
|
error! {
|
||||||
@ -2021,7 +2068,7 @@ mod tests {
|
|||||||
line: 0,
|
line: 0,
|
||||||
column: 4,
|
column: 4,
|
||||||
width: 1,
|
width: 1,
|
||||||
kind: UnterminatedString(StringKind::Raw),
|
kind: UnterminatedString,
|
||||||
}
|
}
|
||||||
|
|
||||||
error! {
|
error! {
|
||||||
@ -2042,7 +2089,7 @@ mod tests {
|
|||||||
line: 0,
|
line: 0,
|
||||||
column: 0,
|
column: 0,
|
||||||
width: 1,
|
width: 1,
|
||||||
kind: UnterminatedString(StringKind::Backtick),
|
kind: UnterminatedBacktick,
|
||||||
}
|
}
|
||||||
|
|
||||||
error! {
|
error! {
|
||||||
@ -2102,7 +2149,7 @@ mod tests {
|
|||||||
line: 0,
|
line: 0,
|
||||||
column: 4,
|
column: 4,
|
||||||
width: 1,
|
width: 1,
|
||||||
kind: UnterminatedString(StringKind::Cooked),
|
kind: UnterminatedString,
|
||||||
}
|
}
|
||||||
|
|
||||||
error! {
|
error! {
|
||||||
@ -2200,7 +2247,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Lexer::new("!").presume('-').unwrap_err().to_string(),
|
Lexer::new("!").presume('-').unwrap_err().to_string(),
|
||||||
testing::unindent(
|
unindent(
|
||||||
"
|
"
|
||||||
Internal error, this may indicate a bug in just: Lexer presumed character `-`
|
Internal error, this may indicate a bug in just: Lexer presumed character `-`
|
||||||
\
|
\
|
||||||
|
@ -125,6 +125,7 @@ mod table;
|
|||||||
mod thunk;
|
mod thunk;
|
||||||
mod token;
|
mod token;
|
||||||
mod token_kind;
|
mod token_kind;
|
||||||
|
mod unindent;
|
||||||
mod unresolved_dependency;
|
mod unresolved_dependency;
|
||||||
mod unresolved_recipe;
|
mod unresolved_recipe;
|
||||||
mod use_color;
|
mod use_color;
|
||||||
@ -134,5 +135,9 @@ mod warning;
|
|||||||
|
|
||||||
pub use crate::run::run;
|
pub use crate::run::run;
|
||||||
|
|
||||||
|
// Used in integration tests.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub use unindent::unindent;
|
||||||
|
|
||||||
#[cfg(feature = "summary")]
|
#[cfg(feature = "summary")]
|
||||||
pub mod summary;
|
pub mod summary;
|
||||||
|
181
src/parser.rs
181
src/parser.rs
@ -100,8 +100,8 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
///
|
///
|
||||||
/// The first token in `kinds` will be added to the expected token set.
|
/// The first token in `kinds` will be added to the expected token set.
|
||||||
fn next_are(&mut self, kinds: &[TokenKind]) -> bool {
|
fn next_are(&mut self, kinds: &[TokenKind]) -> bool {
|
||||||
if let Some(kind) = kinds.first() {
|
if let Some(&kind) = kinds.first() {
|
||||||
self.expected.insert(*kind);
|
self.expected.insert(kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut rest = self.rest();
|
let mut rest = self.rest();
|
||||||
@ -150,17 +150,6 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return an error if the next token is not one of kinds `kinds`.
|
|
||||||
fn expect_any(&mut self, expected: &[TokenKind]) -> CompilationResult<'src, Token<'src>> {
|
|
||||||
for expected in expected.iter().cloned() {
|
|
||||||
if let Some(token) = self.accept(expected)? {
|
|
||||||
return Ok(token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(self.unexpected_token()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return an unexpected token error if the next token is not an EOL
|
/// Return an unexpected token error if the next token is not an EOL
|
||||||
fn expect_eol(&mut self) -> CompilationResult<'src, ()> {
|
fn expect_eol(&mut self) -> CompilationResult<'src, ()> {
|
||||||
self.accept(Comment)?;
|
self.accept(Comment)?;
|
||||||
@ -453,15 +442,26 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
|
|
||||||
/// Parse a value, e.g. `(bar)`
|
/// Parse a value, e.g. `(bar)`
|
||||||
fn parse_value(&mut self) -> CompilationResult<'src, Expression<'src>> {
|
fn parse_value(&mut self) -> CompilationResult<'src, Expression<'src>> {
|
||||||
if self.next_is(StringToken(StringKind::Cooked)) || self.next_is(StringToken(StringKind::Raw)) {
|
if self.next_is(StringToken) {
|
||||||
Ok(Expression::StringLiteral {
|
Ok(Expression::StringLiteral {
|
||||||
string_literal: self.parse_string_literal()?,
|
string_literal: self.parse_string_literal()?,
|
||||||
})
|
})
|
||||||
} else if self.next_is(StringToken(StringKind::Backtick)) {
|
} else if self.next_is(Backtick) {
|
||||||
let next = self.next()?;
|
let next = self.next()?;
|
||||||
|
let kind = StringKind::from_string_or_backtick(next)?;
|
||||||
let contents = &next.lexeme()[1..next.lexeme().len() - 1];
|
let contents =
|
||||||
|
&next.lexeme()[kind.delimiter_len()..next.lexeme().len() - kind.delimiter_len()];
|
||||||
let token = self.advance()?;
|
let token = self.advance()?;
|
||||||
|
let contents = if kind.indented() {
|
||||||
|
unindent(contents)
|
||||||
|
} else {
|
||||||
|
contents.to_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
if contents.starts_with("#!") {
|
||||||
|
return Err(next.error(CompilationErrorKind::BacktickShebang));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Expression::Backtick { contents, token })
|
Ok(Expression::Backtick { contents, token })
|
||||||
} else if self.next_is(Identifier) {
|
} else if self.next_is(Identifier) {
|
||||||
let name = self.parse_name()?;
|
let name = self.parse_name()?;
|
||||||
@ -486,28 +486,31 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
|
|
||||||
/// Parse a string literal, e.g. `"FOO"`
|
/// Parse a string literal, e.g. `"FOO"`
|
||||||
fn parse_string_literal(&mut self) -> CompilationResult<'src, StringLiteral<'src>> {
|
fn parse_string_literal(&mut self) -> CompilationResult<'src, StringLiteral<'src>> {
|
||||||
let token = self.expect_any(&[
|
let token = self.expect(StringToken)?;
|
||||||
StringToken(StringKind::Raw),
|
|
||||||
StringToken(StringKind::Cooked),
|
|
||||||
])?;
|
|
||||||
|
|
||||||
let raw = &token.lexeme()[1..token.lexeme().len() - 1];
|
let kind = StringKind::from_string_or_backtick(token)?;
|
||||||
|
|
||||||
match token.kind {
|
let delimiter_len = kind.delimiter_len();
|
||||||
StringToken(StringKind::Raw) => Ok(StringLiteral {
|
|
||||||
raw,
|
let raw = &token.lexeme()[delimiter_len..token.lexeme().len() - delimiter_len];
|
||||||
cooked: Cow::Borrowed(raw),
|
|
||||||
}),
|
let unindented = if kind.indented() {
|
||||||
StringToken(StringKind::Cooked) => {
|
unindent(raw)
|
||||||
|
} else {
|
||||||
|
raw.to_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
let cooked = if kind.processes_escape_sequences() {
|
||||||
let mut cooked = String::new();
|
let mut cooked = String::new();
|
||||||
let mut escape = false;
|
let mut escape = false;
|
||||||
for c in raw.chars() {
|
for c in unindented.chars() {
|
||||||
if escape {
|
if escape {
|
||||||
match c {
|
match c {
|
||||||
'n' => cooked.push('\n'),
|
'n' => cooked.push('\n'),
|
||||||
'r' => cooked.push('\r'),
|
'r' => cooked.push('\r'),
|
||||||
't' => cooked.push('\t'),
|
't' => cooked.push('\t'),
|
||||||
'\\' => cooked.push('\\'),
|
'\\' => cooked.push('\\'),
|
||||||
|
'\n' => {},
|
||||||
'"' => cooked.push('"'),
|
'"' => cooked.push('"'),
|
||||||
other => {
|
other => {
|
||||||
return Err(
|
return Err(
|
||||||
@ -522,15 +525,12 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
cooked.push(c);
|
cooked.push(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(StringLiteral {
|
cooked
|
||||||
raw,
|
} else {
|
||||||
cooked: Cow::Owned(cooked),
|
unindented
|
||||||
})
|
};
|
||||||
},
|
|
||||||
_ => Err(token.error(CompilationErrorKind::Internal {
|
Ok(StringLiteral { cooked, raw, kind })
|
||||||
message: "`Parser::parse_string_literal` called on non-string token".to_owned(),
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a name from an identifier token
|
/// Parse a name from an identifier token
|
||||||
@ -757,7 +757,6 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use testing::unindent;
|
|
||||||
use CompilationErrorKind::*;
|
use CompilationErrorKind::*;
|
||||||
|
|
||||||
macro_rules! test {
|
macro_rules! test {
|
||||||
@ -1186,6 +1185,15 @@ mod tests {
|
|||||||
tree: (justfile (assignment x "foo\nbar")),
|
tree: (justfile (assignment x "foo\nbar")),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: string_escape_suppress_newline,
|
||||||
|
text: r#"
|
||||||
|
x := "foo\
|
||||||
|
bar"
|
||||||
|
"#,
|
||||||
|
tree: (justfile (assignment x "foobar")),
|
||||||
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: string_escape_carriage_return,
|
name: string_escape_carriage_return,
|
||||||
text: r#"x := "foo\rbar""#,
|
text: r#"x := "foo\rbar""#,
|
||||||
@ -1204,6 +1212,72 @@ mod tests {
|
|||||||
tree: (justfile (assignment x "foo\"bar")),
|
tree: (justfile (assignment x "foo\"bar")),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: indented_string_raw_with_dedent,
|
||||||
|
text: "
|
||||||
|
x := '''
|
||||||
|
foo\\t
|
||||||
|
bar\\n
|
||||||
|
'''
|
||||||
|
",
|
||||||
|
tree: (justfile (assignment x "foo\\t\nbar\\n\n")),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: indented_string_raw_no_dedent,
|
||||||
|
text: "
|
||||||
|
x := '''
|
||||||
|
foo\\t
|
||||||
|
bar\\n
|
||||||
|
'''
|
||||||
|
",
|
||||||
|
tree: (justfile (assignment x "foo\\t\n bar\\n\n")),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: indented_string_cooked,
|
||||||
|
text: r#"
|
||||||
|
x := """
|
||||||
|
\tfoo\t
|
||||||
|
\tbar\n
|
||||||
|
"""
|
||||||
|
"#,
|
||||||
|
tree: (justfile (assignment x "\tfoo\t\n\tbar\n\n")),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: indented_string_cooked_no_dedent,
|
||||||
|
text: r#"
|
||||||
|
x := """
|
||||||
|
\tfoo\t
|
||||||
|
\tbar\n
|
||||||
|
"""
|
||||||
|
"#,
|
||||||
|
tree: (justfile (assignment x "\tfoo\t\n \tbar\n\n")),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: indented_backtick,
|
||||||
|
text: r#"
|
||||||
|
x := ```
|
||||||
|
\tfoo\t
|
||||||
|
\tbar\n
|
||||||
|
```
|
||||||
|
"#,
|
||||||
|
tree: (justfile (assignment x (backtick "\\tfoo\\t\n\\tbar\\n\n"))),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: indented_backtick_no_dedent,
|
||||||
|
text: r#"
|
||||||
|
x := ```
|
||||||
|
\tfoo\t
|
||||||
|
\tbar\n
|
||||||
|
```
|
||||||
|
"#,
|
||||||
|
tree: (justfile (assignment x (backtick "\\tfoo\\t\n \\tbar\\n\n"))),
|
||||||
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: recipe_variadic_with_default_after_default,
|
name: recipe_variadic_with_default_after_default,
|
||||||
text: r#"
|
text: r#"
|
||||||
@ -1724,11 +1798,10 @@ mod tests {
|
|||||||
width: 1,
|
width: 1,
|
||||||
kind: UnexpectedToken {
|
kind: UnexpectedToken {
|
||||||
expected: vec![
|
expected: vec![
|
||||||
|
Backtick,
|
||||||
Identifier,
|
Identifier,
|
||||||
ParenL,
|
ParenL,
|
||||||
StringToken(StringKind::Backtick),
|
StringToken,
|
||||||
StringToken(StringKind::Cooked),
|
|
||||||
StringToken(StringKind::Raw)
|
|
||||||
],
|
],
|
||||||
found: Eol
|
found: Eol
|
||||||
},
|
},
|
||||||
@ -1743,11 +1816,10 @@ mod tests {
|
|||||||
width: 0,
|
width: 0,
|
||||||
kind: UnexpectedToken {
|
kind: UnexpectedToken {
|
||||||
expected: vec![
|
expected: vec![
|
||||||
|
Backtick,
|
||||||
Identifier,
|
Identifier,
|
||||||
ParenL,
|
ParenL,
|
||||||
StringToken(StringKind::Backtick),
|
StringToken,
|
||||||
StringToken(StringKind::Cooked),
|
|
||||||
StringToken(StringKind::Raw)
|
|
||||||
],
|
],
|
||||||
found: Eof,
|
found: Eof,
|
||||||
},
|
},
|
||||||
@ -1785,12 +1857,11 @@ mod tests {
|
|||||||
width: 0,
|
width: 0,
|
||||||
kind: UnexpectedToken{
|
kind: UnexpectedToken{
|
||||||
expected: vec![
|
expected: vec![
|
||||||
|
Backtick,
|
||||||
Identifier,
|
Identifier,
|
||||||
ParenL,
|
ParenL,
|
||||||
ParenR,
|
ParenR,
|
||||||
StringToken(StringKind::Backtick),
|
StringToken,
|
||||||
StringToken(StringKind::Cooked),
|
|
||||||
StringToken(StringKind::Raw)
|
|
||||||
],
|
],
|
||||||
found: Eof,
|
found: Eof,
|
||||||
},
|
},
|
||||||
@ -1805,12 +1876,11 @@ mod tests {
|
|||||||
width: 2,
|
width: 2,
|
||||||
kind: UnexpectedToken{
|
kind: UnexpectedToken{
|
||||||
expected: vec![
|
expected: vec![
|
||||||
|
Backtick,
|
||||||
Identifier,
|
Identifier,
|
||||||
ParenL,
|
ParenL,
|
||||||
ParenR,
|
ParenR,
|
||||||
StringToken(StringKind::Backtick),
|
StringToken,
|
||||||
StringToken(StringKind::Cooked),
|
|
||||||
StringToken(StringKind::Raw)
|
|
||||||
],
|
],
|
||||||
found: InterpolationEnd,
|
found: InterpolationEnd,
|
||||||
},
|
},
|
||||||
@ -1887,7 +1957,9 @@ mod tests {
|
|||||||
column: 14,
|
column: 14,
|
||||||
width: 1,
|
width: 1,
|
||||||
kind: UnexpectedToken {
|
kind: UnexpectedToken {
|
||||||
expected: vec![StringToken(StringKind::Cooked), StringToken(StringKind::Raw)],
|
expected: vec![
|
||||||
|
StringToken,
|
||||||
|
],
|
||||||
found: BracketR,
|
found: BracketR,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -1926,7 +1998,10 @@ mod tests {
|
|||||||
column: 21,
|
column: 21,
|
||||||
width: 0,
|
width: 0,
|
||||||
kind: UnexpectedToken {
|
kind: UnexpectedToken {
|
||||||
expected: vec![BracketR, StringToken(StringKind::Cooked), StringToken(StringKind::Raw)],
|
expected: vec![
|
||||||
|
BracketR,
|
||||||
|
StringToken,
|
||||||
|
],
|
||||||
found: Eof,
|
found: Eof,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,8 @@ impl PlatformInterface for Platform {
|
|||||||
command: &str,
|
command: &str,
|
||||||
argument: Option<&str>,
|
argument: Option<&str>,
|
||||||
) -> Result<Command, OutputError> {
|
) -> Result<Command, OutputError> {
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
// If the path contains forward slashes…
|
// If the path contains forward slashes…
|
||||||
let command = if command.contains('/') {
|
let command = if command.contains('/') {
|
||||||
// …translate path to the interpreter from unix style to windows style.
|
// …translate path to the interpreter from unix style to windows style.
|
||||||
|
@ -1,33 +1,94 @@
|
|||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)]
|
#[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)]
|
||||||
pub(crate) enum StringKind {
|
pub(crate) struct StringKind {
|
||||||
|
indented: bool,
|
||||||
|
delimiter: StringDelimiter,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)]
|
||||||
|
enum StringDelimiter {
|
||||||
Backtick,
|
Backtick,
|
||||||
Cooked,
|
QuoteDouble,
|
||||||
Raw,
|
QuoteSingle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StringKind {
|
impl StringKind {
|
||||||
pub(crate) fn delimiter(self) -> char {
|
// Indented values must come before un-indented values, or else
|
||||||
match self {
|
// `Self::from_token_start` will incorrectly return indented = false
|
||||||
Self::Backtick => '`',
|
// for indented strings.
|
||||||
Self::Cooked => '"',
|
const ALL: &'static [Self] = &[
|
||||||
Self::Raw => '\'',
|
Self::new(StringDelimiter::Backtick, true),
|
||||||
|
Self::new(StringDelimiter::Backtick, false),
|
||||||
|
Self::new(StringDelimiter::QuoteDouble, true),
|
||||||
|
Self::new(StringDelimiter::QuoteDouble, false),
|
||||||
|
Self::new(StringDelimiter::QuoteSingle, true),
|
||||||
|
Self::new(StringDelimiter::QuoteSingle, false),
|
||||||
|
];
|
||||||
|
|
||||||
|
const fn new(delimiter: StringDelimiter, indented: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
delimiter,
|
||||||
|
indented,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn delimiter(self) -> &'static str {
|
||||||
|
match (self.delimiter, self.indented) {
|
||||||
|
(StringDelimiter::Backtick, false) => "`",
|
||||||
|
(StringDelimiter::Backtick, true) => "```",
|
||||||
|
(StringDelimiter::QuoteDouble, false) => "\"",
|
||||||
|
(StringDelimiter::QuoteDouble, true) => "\"\"\"",
|
||||||
|
(StringDelimiter::QuoteSingle, false) => "'",
|
||||||
|
(StringDelimiter::QuoteSingle, true) => "'''",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn delimiter_len(self) -> usize {
|
||||||
|
self.delimiter().len()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn token_kind(self) -> TokenKind {
|
pub(crate) fn token_kind(self) -> TokenKind {
|
||||||
TokenKind::StringToken(self)
|
match self.delimiter {
|
||||||
|
StringDelimiter::QuoteDouble | StringDelimiter::QuoteSingle => TokenKind::StringToken,
|
||||||
|
StringDelimiter::Backtick => TokenKind::Backtick,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn unterminated_error_kind(self) -> CompilationErrorKind<'static> {
|
pub(crate) fn unterminated_error_kind(self) -> CompilationErrorKind<'static> {
|
||||||
CompilationErrorKind::UnterminatedString(self)
|
match self.delimiter {
|
||||||
|
StringDelimiter::QuoteDouble | StringDelimiter::QuoteSingle =>
|
||||||
|
CompilationErrorKind::UnterminatedString,
|
||||||
|
StringDelimiter::Backtick => CompilationErrorKind::UnterminatedBacktick,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn processes_escape_sequences(self) -> bool {
|
pub(crate) fn processes_escape_sequences(self) -> bool {
|
||||||
match self {
|
match self.delimiter {
|
||||||
Self::Backtick | Self::Raw => false,
|
StringDelimiter::QuoteDouble => true,
|
||||||
Self::Cooked => true,
|
StringDelimiter::Backtick | StringDelimiter::QuoteSingle => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn indented(self) -> bool {
|
||||||
|
self.indented
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_string_or_backtick(token: Token) -> CompilationResult<Self> {
|
||||||
|
Self::from_token_start(token.lexeme()).ok_or_else(|| {
|
||||||
|
token.error(CompilationErrorKind::Internal {
|
||||||
|
message: "StringKind::from_token: Expected String or Backtick".to_owned(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_token_start(token_start: &str) -> Option<Self> {
|
||||||
|
for &kind in Self::ALL {
|
||||||
|
if token_start.starts_with(kind.delimiter()) {
|
||||||
|
return Some(kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,19 @@ use crate::common::*;
|
|||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
pub(crate) struct StringLiteral<'src> {
|
pub(crate) struct StringLiteral<'src> {
|
||||||
|
pub(crate) kind: StringKind,
|
||||||
pub(crate) raw: &'src str,
|
pub(crate) raw: &'src str,
|
||||||
pub(crate) cooked: Cow<'src, str>,
|
pub(crate) cooked: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for StringLiteral<'_> {
|
impl Display for StringLiteral<'_> {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
match self.cooked {
|
write!(
|
||||||
Cow::Borrowed(raw) => write!(f, "'{}'", raw),
|
f,
|
||||||
Cow::Owned(_) => write!(f, "\"{}\"", self.raw),
|
"{}{}{}",
|
||||||
}
|
self.kind.delimiter(),
|
||||||
|
self.raw,
|
||||||
|
self.kind.delimiter()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ pub(crate) fn search(config: &Config) -> Search {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) use test_utilities::{tempdir, unindent};
|
pub(crate) use test_utilities::tempdir;
|
||||||
|
|
||||||
macro_rules! analysis_error {
|
macro_rules! analysis_error {
|
||||||
(
|
(
|
||||||
@ -94,7 +94,7 @@ macro_rules! run_error {
|
|||||||
let search = $crate::testing::search(&config);
|
let search = $crate::testing::search(&config);
|
||||||
|
|
||||||
if let Subcommand::Run{ overrides, arguments } = &config.subcommand {
|
if let Subcommand::Run{ overrides, arguments } = &config.subcommand {
|
||||||
match $crate::compiler::Compiler::compile(&$crate::testing::unindent($src))
|
match $crate::compiler::Compiler::compile(&$crate::unindent::unindent($src))
|
||||||
.expect("Expected successful compilation")
|
.expect("Expected successful compilation")
|
||||||
.run(
|
.run(
|
||||||
&config,
|
&config,
|
||||||
|
@ -4,6 +4,7 @@ use crate::common::*;
|
|||||||
pub(crate) enum TokenKind {
|
pub(crate) enum TokenKind {
|
||||||
Asterisk,
|
Asterisk,
|
||||||
At,
|
At,
|
||||||
|
Backtick,
|
||||||
BangEquals,
|
BangEquals,
|
||||||
BraceL,
|
BraceL,
|
||||||
BraceR,
|
BraceR,
|
||||||
@ -26,7 +27,7 @@ pub(crate) enum TokenKind {
|
|||||||
ParenL,
|
ParenL,
|
||||||
ParenR,
|
ParenR,
|
||||||
Plus,
|
Plus,
|
||||||
StringToken(StringKind),
|
StringToken,
|
||||||
Text,
|
Text,
|
||||||
Unspecified,
|
Unspecified,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
@ -38,6 +39,7 @@ impl Display for TokenKind {
|
|||||||
write!(f, "{}", match *self {
|
write!(f, "{}", match *self {
|
||||||
Asterisk => "'*'",
|
Asterisk => "'*'",
|
||||||
At => "'@'",
|
At => "'@'",
|
||||||
|
Backtick => "backtick",
|
||||||
BangEquals => "'!='",
|
BangEquals => "'!='",
|
||||||
BraceL => "'{'",
|
BraceL => "'{'",
|
||||||
BraceR => "'}'",
|
BraceR => "'}'",
|
||||||
@ -60,12 +62,10 @@ impl Display for TokenKind {
|
|||||||
ParenL => "'('",
|
ParenL => "'('",
|
||||||
ParenR => "')'",
|
ParenR => "')'",
|
||||||
Plus => "'+'",
|
Plus => "'+'",
|
||||||
StringToken(StringKind::Backtick) => "backtick",
|
StringToken => "string",
|
||||||
StringToken(StringKind::Cooked) => "cooked string",
|
|
||||||
StringToken(StringKind::Raw) => "raw string",
|
|
||||||
Text => "command text",
|
Text => "command text",
|
||||||
Whitespace => "whitespace",
|
|
||||||
Unspecified => "unspecified",
|
Unspecified => "unspecified",
|
||||||
|
Whitespace => "whitespace",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
use std::mem;
|
use std::{borrow::Cow, mem};
|
||||||
|
|
||||||
/// Construct a `Tree` from a symbolic expression literal. This macro, and the
|
/// Construct a `Tree` from a symbolic expression literal. This macro, and the
|
||||||
/// Tree type, are only used in the Parser unit tests, providing a concise
|
/// Tree type, are only used in the Parser unit tests, providing a concise
|
||||||
|
134
src/unindent.rs
Normal file
134
src/unindent.rs
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
#[must_use]
|
||||||
|
pub fn unindent(text: &str) -> String {
|
||||||
|
// find line start and end indices
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
let mut start = 0;
|
||||||
|
for (i, c) in text.char_indices() {
|
||||||
|
if c == '\n' || i == text.len() - c.len_utf8() {
|
||||||
|
let end = i + 1;
|
||||||
|
lines.push(&text[start..end]);
|
||||||
|
start = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let common_indentation = lines
|
||||||
|
.iter()
|
||||||
|
.filter(|line| !blank(line))
|
||||||
|
.cloned()
|
||||||
|
.map(indentation)
|
||||||
|
.fold(
|
||||||
|
None,
|
||||||
|
|common_indentation, line_indentation| match common_indentation {
|
||||||
|
Some(common_indentation) => Some(common(common_indentation, line_indentation)),
|
||||||
|
None => Some(line_indentation),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
let mut replacements = Vec::with_capacity(lines.len());
|
||||||
|
|
||||||
|
for (i, line) in lines.iter().enumerate() {
|
||||||
|
let blank = blank(line);
|
||||||
|
let first = i == 0;
|
||||||
|
let last = i == lines.len() - 1;
|
||||||
|
|
||||||
|
let replacement = match (blank, first, last) {
|
||||||
|
(true, false, false) => "\n",
|
||||||
|
(true, _, _) => "",
|
||||||
|
(false, _, _) => &line[common_indentation.len()..],
|
||||||
|
};
|
||||||
|
|
||||||
|
replacements.push(replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
replacements.into_iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn indentation(line: &str) -> &str {
|
||||||
|
let i = line
|
||||||
|
.char_indices()
|
||||||
|
.take_while(|(_, c)| matches!(c, ' ' | '\t'))
|
||||||
|
.map(|(i, _)| i + 1)
|
||||||
|
.last()
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
&line[..i]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blank(line: &str) -> bool {
|
||||||
|
line.chars().all(|c| matches!(c, ' ' | '\t' | '\r' | '\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common<'s>(a: &'s str, b: &'s str) -> &'s str {
|
||||||
|
let i = a
|
||||||
|
.char_indices()
|
||||||
|
.zip(b.chars())
|
||||||
|
.take_while(|((_, ac), bc)| ac == bc)
|
||||||
|
.map(|((i, c), _)| i + c.len_utf8())
|
||||||
|
.last()
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
&a[0..i]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unindents() {
|
||||||
|
assert_eq!(unindent("foo"), "foo");
|
||||||
|
assert_eq!(unindent("foo\nbar\nbaz\n"), "foo\nbar\nbaz\n");
|
||||||
|
assert_eq!(unindent(""), "");
|
||||||
|
assert_eq!(unindent(" foo\n bar"), "foo\nbar");
|
||||||
|
assert_eq!(unindent(" foo\n bar\n\n"), "foo\nbar\n");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
unindent(
|
||||||
|
"
|
||||||
|
hello
|
||||||
|
bar
|
||||||
|
"
|
||||||
|
),
|
||||||
|
"hello\nbar\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(unindent("hello\n bar\n foo"), "hello\n bar\n foo");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
unindent(
|
||||||
|
"
|
||||||
|
|
||||||
|
hello
|
||||||
|
bar
|
||||||
|
|
||||||
|
"
|
||||||
|
),
|
||||||
|
"\nhello\nbar\n\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn indentations() {
|
||||||
|
assert_eq!(indentation(""), "");
|
||||||
|
assert_eq!(indentation("foo"), "");
|
||||||
|
assert_eq!(indentation(" foo"), " ");
|
||||||
|
assert_eq!(indentation("\t\tfoo"), "\t\t");
|
||||||
|
assert_eq!(indentation("\t \t foo"), "\t \t ");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn blanks() {
|
||||||
|
assert!(blank(" \n"));
|
||||||
|
assert!(!blank(" foo\n"));
|
||||||
|
assert!(blank("\t\t\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn commons() {
|
||||||
|
assert_eq!(common("foo", "foobar"), "foo");
|
||||||
|
assert_eq!(common("foo", "bar"), "");
|
||||||
|
assert_eq!(common("", ""), "");
|
||||||
|
assert_eq!(common("", "bar"), "");
|
||||||
|
}
|
||||||
|
}
|
@ -20,67 +20,6 @@ pub fn assert_stdout(output: &Output, stdout: &str) {
|
|||||||
assert_eq!(String::from_utf8_lossy(&output.stdout), stdout);
|
assert_eq!(String::from_utf8_lossy(&output.stdout), stdout);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unindent(text: &str) -> String {
|
|
||||||
// find line start and end indices
|
|
||||||
let mut lines = Vec::new();
|
|
||||||
let mut start = 0;
|
|
||||||
for (i, c) in text.char_indices() {
|
|
||||||
if c == '\n' {
|
|
||||||
let end = i + 1;
|
|
||||||
lines.push((start, end));
|
|
||||||
start = end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the text isn't newline-terminated, add the final line
|
|
||||||
if text.chars().last() != Some('\n') {
|
|
||||||
lines.push((start, text.len()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// find the longest common indentation
|
|
||||||
let mut common_indentation = None;
|
|
||||||
for (start, end) in lines.iter().cloned() {
|
|
||||||
let line = &text[start..end];
|
|
||||||
|
|
||||||
// skip blank lines
|
|
||||||
if blank(line) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate new common indentation
|
|
||||||
common_indentation = match common_indentation {
|
|
||||||
Some(common_indentation) => Some(common(common_indentation, indentation(line))),
|
|
||||||
None => Some(indentation(line)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// if common indentation is present, process the text
|
|
||||||
if let Some(common_indentation) = common_indentation {
|
|
||||||
if common_indentation != "" {
|
|
||||||
let mut output = String::new();
|
|
||||||
|
|
||||||
for (i, (start, end)) in lines.iter().cloned().enumerate() {
|
|
||||||
let line = &text[start..end];
|
|
||||||
|
|
||||||
if blank(line) {
|
|
||||||
// skip intial and final blank line
|
|
||||||
if i != 0 && i != lines.len() - 1 {
|
|
||||||
output.push('\n');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// otherwise push the line without the common indentation
|
|
||||||
output.push_str(&line[common_indentation.len()..]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise just return the input string
|
|
||||||
text.to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Entry {
|
pub enum Entry {
|
||||||
File {
|
File {
|
||||||
contents: &'static str,
|
contents: &'static str,
|
||||||
@ -180,42 +119,6 @@ macro_rules! tmptree {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn indentation(line: &str) -> &str {
|
|
||||||
for (i, c) in line.char_indices() {
|
|
||||||
if c != ' ' && c != '\t' {
|
|
||||||
return &line[0..i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
line
|
|
||||||
}
|
|
||||||
|
|
||||||
fn blank(line: &str) -> bool {
|
|
||||||
for (i, c) in line.char_indices() {
|
|
||||||
if c == ' ' || c == '\t' {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if c == '\n' && i == line.len() - 1 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn common<'s>(a: &'s str, b: &'s str) -> &'s str {
|
|
||||||
for ((i, ac), bc) in a.char_indices().zip(b.chars()) {
|
|
||||||
if ac != bc {
|
|
||||||
return &a[0..i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -9,6 +9,7 @@ pub(crate) use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) use executable_path::executable_path;
|
pub(crate) use executable_path::executable_path;
|
||||||
|
pub(crate) use just::unindent;
|
||||||
pub(crate) use libc::{EXIT_FAILURE, EXIT_SUCCESS};
|
pub(crate) use libc::{EXIT_FAILURE, EXIT_SUCCESS};
|
||||||
pub(crate) use test_utilities::{assert_stdout, tempdir, tmptree, unindent};
|
pub(crate) use test_utilities::{assert_stdout, tempdir, tmptree};
|
||||||
pub(crate) use which::which;
|
pub(crate) use which::which;
|
||||||
|
@ -278,7 +278,7 @@ hello:
|
|||||||
recipe:
|
recipe:
|
||||||
@exit 100",
|
@exit 100",
|
||||||
args: ("recipe"),
|
args: ("recipe"),
|
||||||
stderr: "error: Recipe `recipe` failed on line 6 with exit code 100\n",
|
stderr: "error: Recipe `recipe` failed on line 5 with exit code 100\n",
|
||||||
status: 100,
|
status: 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,7 +352,7 @@ backtick-fail:
|
|||||||
",
|
",
|
||||||
stderr: " error: Backtick failed with exit code 200
|
stderr: " error: Backtick failed with exit code 200
|
||||||
|
|
|
|
||||||
3 | echo {{`exit 200`}}
|
2 | echo {{`exit 200`}}
|
||||||
| ^^^^^^^^^^
|
| ^^^^^^^^^^
|
||||||
",
|
",
|
||||||
status: 200,
|
status: 200,
|
||||||
@ -366,7 +366,7 @@ backtick-fail:
|
|||||||
",
|
",
|
||||||
stderr: "error: Backtick failed with exit code 200
|
stderr: "error: Backtick failed with exit code 200
|
||||||
|
|
|
|
||||||
3 | echo {{ `exit 200`}}
|
2 | echo {{ `exit 200`}}
|
||||||
| ^^^^^^^^^^
|
| ^^^^^^^^^^
|
||||||
",
|
",
|
||||||
status: 200,
|
status: 200,
|
||||||
@ -381,7 +381,7 @@ backtick-fail:
|
|||||||
stderr: "
|
stderr: "
|
||||||
error: Backtick failed with exit code 200
|
error: Backtick failed with exit code 200
|
||||||
|
|
|
|
||||||
3 | echo {{ `exit 200`}}
|
2 | echo {{ `exit 200`}}
|
||||||
| ^^^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^^^
|
||||||
",
|
",
|
||||||
status: 200,
|
status: 200,
|
||||||
@ -396,7 +396,7 @@ backtick-fail:
|
|||||||
stderr: "
|
stderr: "
|
||||||
error: Backtick failed with exit code 200
|
error: Backtick failed with exit code 200
|
||||||
|
|
|
|
||||||
3 | echo 😬{{`exit 200`}}
|
2 | echo 😬{{`exit 200`}}
|
||||||
| ^^^^^^^^^^
|
| ^^^^^^^^^^
|
||||||
",
|
",
|
||||||
status: 200,
|
status: 200,
|
||||||
@ -411,7 +411,7 @@ backtick-fail:
|
|||||||
stderr: "
|
stderr: "
|
||||||
error: Backtick failed with exit code 200
|
error: Backtick failed with exit code 200
|
||||||
|
|
|
|
||||||
3 | echo 😬鎌鼬{{ `exit 200 # abc`}} 😬鎌鼬
|
2 | echo 😬鎌鼬{{ `exit 200 # abc`}} 😬鎌鼬
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
",
|
",
|
||||||
status: 200,
|
status: 200,
|
||||||
@ -419,7 +419,18 @@ backtick-fail:
|
|||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: backtick_code_long,
|
name: backtick_code_long,
|
||||||
justfile: "\n\n\n\n\n\nb := a\na := `echo hello`\nbar:\n echo '{{`exit 200`}}'",
|
justfile: "
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
b := a
|
||||||
|
a := `echo hello`
|
||||||
|
bar:
|
||||||
|
echo '{{`exit 200`}}'
|
||||||
|
",
|
||||||
stderr: "
|
stderr: "
|
||||||
error: Backtick failed with exit code 200
|
error: Backtick failed with exit code 200
|
||||||
|
|
|
|
||||||
@ -580,6 +591,7 @@ test! {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
???
|
???
|
||||||
"#,
|
"#,
|
||||||
stdout: "",
|
stdout: "",
|
||||||
@ -727,7 +739,7 @@ recipe:
|
|||||||
args: ("--color=always"),
|
args: ("--color=always"),
|
||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: "\u{1b}[1;31merror\u{1b}[0m: \u{1b}[1m\
|
stderr: "\u{1b}[1;31merror\u{1b}[0m: \u{1b}[1m\
|
||||||
Recipe `recipe` failed on line 3 with exit code 100\u{1b}[0m\n",
|
Recipe `recipe` failed on line 2 with exit code 100\u{1b}[0m\n",
|
||||||
status: 100,
|
status: 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1217,7 +1229,7 @@ infallable:
|
|||||||
"#,
|
"#,
|
||||||
stderr: r#"exit 101
|
stderr: r#"exit 101
|
||||||
exit 202
|
exit 202
|
||||||
error: Recipe `infallable` failed on line 4 with exit code 202
|
error: Recipe `infallable` failed on line 3 with exit code 202
|
||||||
"#,
|
"#,
|
||||||
status: 202,
|
status: 202,
|
||||||
}
|
}
|
||||||
@ -1260,8 +1272,8 @@ quiet:
|
|||||||
|
|
||||||
|
|
||||||
"#,
|
"#,
|
||||||
stdout: "#!/usr/bin/env cat
|
stdout: "
|
||||||
|
#!/usr/bin/env cat
|
||||||
|
|
||||||
|
|
||||||
a
|
a
|
||||||
@ -1404,7 +1416,7 @@ test! {
|
|||||||
args: ("foo"),
|
args: ("foo"),
|
||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: "error: Expected comment, end of file, end of line, \
|
stderr: "error: Expected comment, end of file, end of line, \
|
||||||
identifier, or '(', but found raw string
|
identifier, or '(', but found string
|
||||||
|
|
|
|
||||||
1 | foo: 'bar'
|
1 | foo: 'bar'
|
||||||
| ^^^^^
|
| ^^^^^
|
||||||
@ -1417,7 +1429,7 @@ test! {
|
|||||||
justfile: "foo 'bar'",
|
justfile: "foo 'bar'",
|
||||||
args: ("foo"),
|
args: ("foo"),
|
||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: "error: Expected '*', ':', '$', identifier, or '+', but found raw string
|
stderr: "error: Expected '*', ':', '$', identifier, or '+', but found string
|
||||||
|
|
|
|
||||||
1 | foo 'bar'
|
1 | foo 'bar'
|
||||||
| ^^^^^
|
| ^^^^^
|
||||||
@ -1575,7 +1587,7 @@ foo *a +b:
|
|||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: "error: Expected \':\' or \'=\', but found \'+\'
|
stderr: "error: Expected \':\' or \'=\', but found \'+\'
|
||||||
|
|
|
|
||||||
2 | foo *a +b:
|
1 | foo *a +b:
|
||||||
| ^
|
| ^
|
||||||
",
|
",
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
@ -1590,7 +1602,7 @@ foo +a *b:
|
|||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: "error: Expected \':\' or \'=\', but found \'*\'
|
stderr: "error: Expected \':\' or \'=\', but found \'*\'
|
||||||
|
|
|
|
||||||
2 | foo +a *b:
|
1 | foo +a *b:
|
||||||
| ^
|
| ^
|
||||||
",
|
",
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
@ -1623,7 +1635,7 @@ a: x y
|
|||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: "error: Recipe `a` has unknown dependency `y`
|
stderr: "error: Recipe `a` has unknown dependency `y`
|
||||||
|
|
|
|
||||||
4 | a: x y
|
3 | a: x y
|
||||||
| ^
|
| ^
|
||||||
",
|
",
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
@ -1775,7 +1787,7 @@ X := "\'"
|
|||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: r#"error: `\'` is not a valid escape sequence
|
stderr: r#"error: `\'` is not a valid escape sequence
|
||||||
|
|
|
|
||||||
2 | X := "\'"
|
1 | X := "\'"
|
||||||
| ^^^^
|
| ^^^^
|
||||||
"#,
|
"#,
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
@ -1789,7 +1801,7 @@ foo x=bar:
|
|||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: r#"error: Variable `bar` not defined
|
stderr: r#"error: Variable `bar` not defined
|
||||||
|
|
|
|
||||||
2 | foo x=bar:
|
1 | foo x=bar:
|
||||||
| ^^^
|
| ^^^
|
||||||
"#,
|
"#,
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
@ -1803,7 +1815,7 @@ foo x=bar():
|
|||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: r#"error: Call to unknown function `bar`
|
stderr: r#"error: Call to unknown function `bar`
|
||||||
|
|
|
|
||||||
2 | foo x=bar():
|
1 | foo x=bar():
|
||||||
| ^^^
|
| ^^^
|
||||||
"#,
|
"#,
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
@ -1869,7 +1881,7 @@ foo:
|
|||||||
stderr: r#"
|
stderr: r#"
|
||||||
error: Unterminated interpolation
|
error: Unterminated interpolation
|
||||||
|
|
|
|
||||||
3 | echo {{
|
2 | echo {{
|
||||||
| ^^
|
| ^^
|
||||||
"#,
|
"#,
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
@ -1879,11 +1891,12 @@ test! {
|
|||||||
name: unterminated_interpolation_eof,
|
name: unterminated_interpolation_eof,
|
||||||
justfile: "
|
justfile: "
|
||||||
foo:
|
foo:
|
||||||
echo {{",
|
echo {{
|
||||||
|
",
|
||||||
stderr: r#"
|
stderr: r#"
|
||||||
error: Unterminated interpolation
|
error: Unterminated interpolation
|
||||||
|
|
|
|
||||||
3 | echo {{
|
2 | echo {{
|
||||||
| ^^
|
| ^^
|
||||||
"#,
|
"#,
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
@ -1897,7 +1910,7 @@ assembly_source_files = %(wildcard src/arch/$(arch)/*.s)
|
|||||||
stderr: r#"
|
stderr: r#"
|
||||||
error: Unknown start of token:
|
error: Unknown start of token:
|
||||||
|
|
|
|
||||||
2 | assembly_source_files = %(wildcard src/arch/$(arch)/*.s)
|
1 | assembly_source_files = %(wildcard src/arch/$(arch)/*.s)
|
||||||
| ^
|
| ^
|
||||||
"#,
|
"#,
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
@ -1934,15 +1947,13 @@ default stdin = `cat justfile`:
|
|||||||
echo '{{stdin}}'
|
echo '{{stdin}}'
|
||||||
",
|
",
|
||||||
stdout: "
|
stdout: "
|
||||||
|
|
||||||
default stdin = `cat justfile`:
|
default stdin = `cat justfile`:
|
||||||
echo {{stdin}}
|
echo {{stdin}}
|
||||||
|
|
||||||
set dotenv-load := true
|
set dotenv-load := true
|
||||||
",
|
",
|
||||||
stderr: "
|
stderr: "
|
||||||
echo '
|
echo 'default stdin = `cat justfile`:
|
||||||
default stdin = `cat justfile`:
|
|
||||||
echo '{{stdin}}'
|
echo '{{stdin}}'
|
||||||
|
|
||||||
set dotenv-load := true'
|
set dotenv-load := true'
|
||||||
|
199
tests/string.rs
199
tests/string.rs
@ -65,6 +65,22 @@ whatever'
|
|||||||
",
|
",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: cooked_string_suppress_newline,
|
||||||
|
justfile: r#"
|
||||||
|
a := """
|
||||||
|
foo\
|
||||||
|
bar
|
||||||
|
"""
|
||||||
|
|
||||||
|
@default:
|
||||||
|
printf %s '{{a}}'
|
||||||
|
"#,
|
||||||
|
stdout: "
|
||||||
|
foobar
|
||||||
|
",
|
||||||
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: invalid_escape_sequence,
|
name: invalid_escape_sequence,
|
||||||
justfile: r#"x := "\q"
|
justfile: r#"x := "\q"
|
||||||
@ -93,7 +109,7 @@ a:
|
|||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: "error: Variable `foo` not defined
|
stderr: "error: Variable `foo` not defined
|
||||||
|
|
|
|
||||||
7 | echo '{{foo}}'
|
6 | echo '{{foo}}'
|
||||||
| ^^^
|
| ^^^
|
||||||
",
|
",
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
@ -113,7 +129,7 @@ a:
|
|||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: "error: Variable `bar` not defined
|
stderr: "error: Variable `bar` not defined
|
||||||
|
|
|
|
||||||
4 | whatever' + bar
|
3 | whatever' + bar
|
||||||
| ^^^
|
| ^^^
|
||||||
",
|
",
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
@ -150,7 +166,7 @@ a:
|
|||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: "error: Variable `b` not defined
|
stderr: "error: Variable `b` not defined
|
||||||
|
|
|
|
||||||
6 | echo {{b}}
|
5 | echo {{b}}
|
||||||
| ^
|
| ^
|
||||||
",
|
",
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
@ -163,9 +179,10 @@ a b= ':
|
|||||||
",
|
",
|
||||||
args: ("a"),
|
args: ("a"),
|
||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: "error: Unterminated string
|
stderr: "
|
||||||
|
error: Unterminated string
|
||||||
|
|
|
|
||||||
2 | a b= ':
|
1 | a b= ':
|
||||||
| ^
|
| ^
|
||||||
",
|
",
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
@ -178,9 +195,10 @@ a b= ":
|
|||||||
"#,
|
"#,
|
||||||
args: ("a"),
|
args: ("a"),
|
||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: r#"error: Unterminated string
|
stderr: r#"
|
||||||
|
error: Unterminated string
|
||||||
|
|
|
|
||||||
2 | a b= ":
|
1 | a b= ":
|
||||||
| ^
|
| ^
|
||||||
"#,
|
"#,
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
@ -190,12 +208,175 @@ test! {
|
|||||||
name: unterminated_backtick,
|
name: unterminated_backtick,
|
||||||
justfile: "
|
justfile: "
|
||||||
foo a=\t`echo blaaaaaah:
|
foo a=\t`echo blaaaaaah:
|
||||||
echo {{a}}",
|
echo {{a}}
|
||||||
|
",
|
||||||
stderr: r#"
|
stderr: r#"
|
||||||
error: Unterminated backtick
|
error: Unterminated backtick
|
||||||
|
|
|
|
||||||
2 | foo a= `echo blaaaaaah:
|
1 | foo a= `echo blaaaaaah:
|
||||||
| ^
|
| ^
|
||||||
"#,
|
"#,
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: unterminated_indented_raw_string,
|
||||||
|
justfile: "
|
||||||
|
a b= ''':
|
||||||
|
",
|
||||||
|
args: ("a"),
|
||||||
|
stdout: "",
|
||||||
|
stderr: "
|
||||||
|
error: Unterminated string
|
||||||
|
|
|
||||||
|
1 | a b= ''':
|
||||||
|
| ^^^
|
||||||
|
",
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: unterminated_indented_string,
|
||||||
|
justfile: r#"
|
||||||
|
a b= """:
|
||||||
|
"#,
|
||||||
|
args: ("a"),
|
||||||
|
stdout: "",
|
||||||
|
stderr: r#"
|
||||||
|
error: Unterminated string
|
||||||
|
|
|
||||||
|
1 | a b= """:
|
||||||
|
| ^^^
|
||||||
|
"#,
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: unterminated_indented_backtick,
|
||||||
|
justfile: "
|
||||||
|
foo a=\t```echo blaaaaaah:
|
||||||
|
echo {{a}}
|
||||||
|
",
|
||||||
|
stderr: r#"
|
||||||
|
error: Unterminated backtick
|
||||||
|
|
|
||||||
|
1 | foo a= ```echo blaaaaaah:
|
||||||
|
| ^^^
|
||||||
|
"#,
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: indented_raw_string_contents_indentation_removed,
|
||||||
|
justfile: "
|
||||||
|
a := '''
|
||||||
|
foo
|
||||||
|
bar
|
||||||
|
'''
|
||||||
|
|
||||||
|
@default:
|
||||||
|
printf '{{a}}'
|
||||||
|
",
|
||||||
|
stdout: "
|
||||||
|
foo
|
||||||
|
bar
|
||||||
|
",
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: indented_cooked_string_contents_indentation_removed,
|
||||||
|
justfile: r#"
|
||||||
|
a := """
|
||||||
|
foo
|
||||||
|
bar
|
||||||
|
"""
|
||||||
|
|
||||||
|
@default:
|
||||||
|
printf '{{a}}'
|
||||||
|
"#,
|
||||||
|
stdout: "
|
||||||
|
foo
|
||||||
|
bar
|
||||||
|
",
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: indented_backtick_string_contents_indentation_removed,
|
||||||
|
justfile: r#"
|
||||||
|
a := ```
|
||||||
|
printf '
|
||||||
|
foo
|
||||||
|
bar
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
@default:
|
||||||
|
printf '{{a}}'
|
||||||
|
"#,
|
||||||
|
stdout: "\n\nfoo\nbar",
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: indented_raw_string_escapes,
|
||||||
|
justfile: r#"
|
||||||
|
a := '''
|
||||||
|
foo\n
|
||||||
|
bar
|
||||||
|
'''
|
||||||
|
|
||||||
|
@default:
|
||||||
|
printf %s '{{a}}'
|
||||||
|
"#,
|
||||||
|
stdout: r#"
|
||||||
|
foo\n
|
||||||
|
bar
|
||||||
|
"#,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: indented_cooked_string_escapes,
|
||||||
|
justfile: r#"
|
||||||
|
a := """
|
||||||
|
foo\n
|
||||||
|
bar
|
||||||
|
"""
|
||||||
|
|
||||||
|
@default:
|
||||||
|
printf %s '{{a}}'
|
||||||
|
"#,
|
||||||
|
stdout: "
|
||||||
|
foo
|
||||||
|
|
||||||
|
bar
|
||||||
|
",
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: indented_backtick_string_escapes,
|
||||||
|
justfile: r#"
|
||||||
|
a := ```
|
||||||
|
printf %s '
|
||||||
|
foo\n
|
||||||
|
bar
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
@default:
|
||||||
|
printf %s '{{a}}'
|
||||||
|
"#,
|
||||||
|
stdout: "\n\nfoo\\n\nbar",
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: shebang_backtick,
|
||||||
|
justfile: "
|
||||||
|
x := `#!/usr/bin/env sh`
|
||||||
|
",
|
||||||
|
stderr: "
|
||||||
|
error: Backticks may not start with `#!`
|
||||||
|
|
|
||||||
|
1 | x := `#!/usr/bin/env sh`
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^
|
||||||
|
",
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user