use crate::ast::{ArithmExpr, ParameterOp, Position, Range, Spanned, Word};
use crate::parser::Parser;
#[derive(Clone, Copy)]
enum BackslashMode {
Raw,
DoubleQuotes,
HereDoc,
}
#[derive(Clone, Copy)]
enum EofMode {
Stop,
Error(&'static str),
}
#[derive(Clone, Copy)]
struct ScanOptions {
allow_single_quotes: bool,
allow_double_quotes: bool,
allow_dollar: bool,
allow_back_quotes: bool,
allow_continuation_line: bool,
comment_at_word_start: bool,
backslash_mode: BackslashMode,
eof_mode: EofMode,
}
fn span_for_words(words: &[Word]) -> Range {
words
.iter()
.fold(Range::default(), |span, word| span.cover(word.span()))
}
pub(crate) fn is_name_start(ch: char) -> bool {
ch == '_' || ch.is_ascii_alphabetic()
}
pub(crate) fn is_name_char(ch: char) -> bool {
ch == '_' || ch.is_ascii_alphanumeric()
}
pub(crate) fn peek_name(parser: &mut Parser, in_braces: bool) -> Option<String> {
parser.fill_buf();
if parser.buf_pos >= parser.buf.len() {
return None;
}
let remaining = &parser.buf[parser.buf_pos..];
if remaining.is_empty() {
return None;
}
let first = remaining[0] as char;
if in_braces && first.is_ascii_digit() {
return Some(first.to_string());
}
if !is_name_start(first) {
return None;
}
let mut end = 1;
while end < remaining.len() && is_name_char(remaining[end] as char) {
end += 1;
}
Some(String::from_utf8_lossy(&remaining[..end]).into_owned())
}
pub(crate) fn word(parser: &mut Parser, end_char: Option<char>) -> Option<Word> {
while parser.read_continuation_line() {
parser.skip_blanks();
}
let mut children = scan_children(
parser,
|parser, ch, buf, children| {
if let Some(ec) = end_char
&& ch == ec
&& (ch != ')' || parser.current_rparen_is_syntax())
{
return true;
}
if ch == '\n' || ch == ' ' || ch == '\t' || Parser::is_operator_start(ch) || ch == '(' {
if end_char.is_none() {
return true;
}
if ch != '(' {
return true;
}
}
if ch == ')' && parser.current_rparen_is_syntax() {
return true;
}
let _ = (buf, children);
false
},
ScanOptions {
allow_single_quotes: true,
allow_double_quotes: true,
allow_dollar: true,
allow_back_quotes: true,
allow_continuation_line: true,
comment_at_word_start: true,
backslash_mode: BackslashMode::Raw,
eof_mode: EofMode::Stop,
},
);
if children.is_empty() {
return None;
}
if children.len() == 1 {
return Some(children.remove(0));
}
Some(Word::list(
children.clone(),
false,
span_for_words(&children),
))
}
fn double_quotes(parser: &mut Parser) -> Option<Word> {
let open_pos = parser.current_pos();
parser.read_char(); let children = scan_children(
parser,
|parser, ch, _, _| {
if ch == '"' {
parser.read_char();
true
} else {
false
}
},
ScanOptions {
allow_single_quotes: false,
allow_double_quotes: false,
allow_dollar: true,
allow_back_quotes: true,
allow_continuation_line: false,
comment_at_word_start: false,
backslash_mode: BackslashMode::DoubleQuotes,
eof_mode: EofMode::Error("unterminated double quote"),
},
);
let close_pos = parser.current_pos();
Some(Word::list(
children,
true,
Range {
begin: open_pos,
end: close_pos,
},
))
}
fn single_quotes(parser: &mut Parser) -> Option<Word> {
let start = parser.current_pos();
parser.read_char();
let mut value = String::new();
loop {
let Some(ch) = parser.read_char() else {
parser.set_error("unterminated single quote".to_string());
break;
};
if ch == '\'' {
break;
}
value.push(ch);
}
let end = parser.current_pos();
let source = format!("'{value}'");
Some(Word::string(
value,
true,
false,
Some(source),
Range { begin: start, end },
))
}
fn expect_dollar(parser: &mut Parser) -> Option<Word> {
let dollar_pos = parser.current_pos();
parser.read_char();
let Some(ch) = parser.peek_char() else {
return Some(Word::string(
"$",
false,
true,
Some("$".to_string()),
Range {
begin: dollar_pos,
end: parser.current_pos(),
},
));
};
match ch {
'{' => expect_parameter_expression(parser, dollar_pos),
'(' => {
parser.read_char(); if parser.peek_char() == Some('(') {
parser.read_char(); expect_word_arithmetic(parser, dollar_pos)
} else {
expect_word_command(parser, dollar_pos)
}
}
_ if is_name_start(ch) => {
let name = read_name(parser);
Some(Word::parameter(
name,
ParameterOp::None,
false,
None,
dollar_pos,
None,
Range {
begin: dollar_pos,
end: parser.current_pos(),
},
))
}
_ if ch.is_ascii_digit() || matches!(ch, '@' | '*' | '#' | '?' | '-' | '$' | '!') => {
parser.read_char();
Some(Word::parameter(
ch.to_string(),
ParameterOp::None,
false,
None,
dollar_pos,
None,
Range {
begin: dollar_pos,
end: parser.current_pos(),
},
))
}
_ => {
Some(Word::string(
"$",
false,
true,
Some("$".to_string()),
Range {
begin: dollar_pos,
end: parser.current_pos(),
},
))
}
}
}
fn expect_parameter_expression(parser: &mut Parser, dollar_pos: Position) -> Option<Word> {
parser.read_char();
let leading_hash = parser.peek_char() == Some('#');
if leading_hash {
parser.read_char();
}
let name = if let Some(ch) = parser.peek_char() {
if is_name_start(ch) || ch.is_ascii_digit() {
read_name_or_number(parser)
} else if matches!(ch, '@' | '*' | '#' | '?' | '-' | '$' | '!') {
parser.read_char();
ch.to_string()
} else {
parser.set_error(format!("bad substitution: unexpected '{ch}'"));
return None;
}
} else {
parser.set_error("unexpected end of input in parameter expansion".to_string());
return None;
};
if leading_hash && parser.peek_char() == Some('}') {
let brace_end = parser.current_pos();
parser.read_char();
return Some(Word::parameter(
name,
ParameterOp::LeadingHash,
false,
None,
dollar_pos,
Some(brace_end),
Range {
begin: dollar_pos,
end: parser.current_pos(),
},
));
}
let (op, colon) = match parser.peek_char() {
Some('}') => {
let brace_end = parser.current_pos();
parser.read_char();
return Some(Word::parameter(
name,
ParameterOp::None,
false,
None,
dollar_pos,
Some(brace_end),
Range {
begin: dollar_pos,
end: parser.current_pos(),
},
));
}
Some(':') => {
parser.read_char();
let op = match parser.peek_char() {
Some('-') => ParameterOp::Minus,
Some('=') => ParameterOp::Equal,
Some('?') => ParameterOp::QMark,
Some('+') => ParameterOp::Plus,
_ => {
parser.set_error("bad substitution: expected operator after ':'".to_string());
return None;
}
};
parser.read_char();
(op, true)
}
Some('-') => {
parser.read_char();
(ParameterOp::Minus, false)
}
Some('=') => {
parser.read_char();
(ParameterOp::Equal, false)
}
Some('?') => {
parser.read_char();
(ParameterOp::QMark, false)
}
Some('+') => {
parser.read_char();
(ParameterOp::Plus, false)
}
Some('%') => {
parser.read_char();
if parser.peek_char() == Some('%') {
parser.read_char();
(ParameterOp::DoublePercent, false)
} else {
(ParameterOp::Percent, false)
}
}
Some('#') => {
parser.read_char();
if parser.peek_char() == Some('#') {
parser.read_char();
(ParameterOp::DoubleHash, false)
} else {
(ParameterOp::Hash, false)
}
}
Some(ch) => {
parser.set_error(format!("bad substitution: unexpected '{ch}'"));
return None;
}
None => {
parser.set_error("unexpected end of input in parameter expansion".to_string());
return None;
}
};
let arg = parameter_expansion_word(parser);
let brace_end = parser.current_pos();
if parser.peek_char() != Some('}') {
parser.set_error("expected '}' in parameter expansion".to_string());
return None;
}
parser.read_char();
Some(Word::parameter(
name,
op,
colon,
arg.map(Box::new),
dollar_pos,
Some(brace_end),
Range {
begin: dollar_pos,
end: parser.current_pos(),
},
))
}
fn parameter_expansion_word(parser: &mut Parser) -> Option<Word> {
let mut children = scan_children(
parser,
|_, ch, _, _| ch == '}',
ScanOptions {
allow_single_quotes: true,
allow_double_quotes: true,
allow_dollar: true,
allow_back_quotes: true,
allow_continuation_line: false,
comment_at_word_start: false,
backslash_mode: BackslashMode::Raw,
eof_mode: EofMode::Stop,
},
);
if children.is_empty() {
return None;
}
if children.len() == 1 {
return Some(children.remove(0));
}
Some(Word::list(
children.clone(),
false,
span_for_words(&children),
))
}
fn expect_word_command(parser: &mut Parser, dollar_pos: Position) -> Option<Word> {
let range_begin = dollar_pos;
let body_start = parser.buf_pos;
let old_disable_aliases = parser.disable_aliases;
parser.disable_aliases = true;
let program = crate::parser::program::parse_subshell_body(parser);
parser.disable_aliases = old_disable_aliases;
let body_end = parser.buf_pos;
let source = String::from_utf8_lossy(&parser.buf[body_start..body_end]).into_owned();
crate::parser::program::consume_command_substitution_closer(parser);
let range_end = parser.current_pos();
Some(Word::command(
program,
Some(source),
false,
Range {
begin: range_begin,
end: range_end,
},
))
}
fn expect_word_arithmetic(parser: &mut Parser, dollar_pos: Position) -> Option<Word> {
let range_begin = dollar_pos;
let mut expr_str = String::new();
let mut depth: u32 = 0;
loop {
let Some(ch) = parser.peek_char() else {
parser.set_error("unterminated arithmetic expansion".to_string());
break;
};
if ch == '(' {
depth += 1;
parser.read_char();
expr_str.push(ch);
} else if ch == ')' {
if depth == 0 {
parser.read_char();
if parser.peek_char() == Some(')') {
parser.read_char();
} else {
parser.set_error("expected '))' in arithmetic expansion".to_string());
}
break;
}
depth -= 1;
parser.read_char();
expr_str.push(ch);
} else {
parser.read_char();
expr_str.push(ch);
}
}
let body = match crate::parser::arithm::parse_arithm_expr_strict(&expr_str) {
Ok(body) => body,
Err(_) => ArithmExpr::Raw(crate::ast::ArithmRaw::new(expr_str, Range::default())),
};
Some(Word::arithmetic(
body,
Range {
begin: range_begin,
end: parser.current_pos(),
},
))
}
fn back_quotes(parser: &mut Parser) -> Option<Word> {
let start = parser.current_pos();
parser.read_char();
let mut content = String::new();
loop {
let Some(ch) = parser.read_char() else {
parser.set_error("unterminated backquote".to_string());
break;
};
if ch == '`' {
break;
}
if ch == '\\' {
if let Some(next) = parser.peek_char()
&& matches!(next, '$' | '`' | '\\')
{
parser.read_char();
content.push(next);
continue;
}
content.push('\\');
continue;
}
content.push(ch);
}
let end = parser.current_pos();
let mut sub_parser = Parser::from_string(&content);
sub_parser.set_alias_expansion_enabled(parser.alias_expansion_enabled);
sub_parser.set_function_definitions_enabled(parser.function_definitions_enabled);
let program = match sub_parser.parse_program() {
Ok(prog) => prog,
Err(err) => {
let quote = if err.message.contains("double quote") {
"\""
} else if err.message.contains("single quote") {
"'"
} else if err.message.contains("backquote") {
"`"
} else {
""
};
let source = parser.source_name().unwrap_or("input").to_string();
let line = start.line;
let range = Some(Range {
begin: start,
end: parser.pos,
});
if !quote.is_empty() {
parser.push_warning(format!(
"{source}: command substitution: line {line}: unexpected EOF while looking for matching `{quote}'"
), range);
} else {
parser.push_warning(
format!("{source}: command substitution: line {line}: syntax error"),
range,
);
}
parser.push_warning(
format!(
"{source}: command substitution: line {}: syntax error: unexpected end of file",
line + 1
),
range,
);
crate::ast::Program::new(Vec::new())
}
};
Some(Word::command(
program,
None,
true,
Range { begin: start, end },
))
}
fn read_name(parser: &mut Parser) -> String {
let mut name = String::new();
while let Some(ch) = parser.peek_char() {
if is_name_char(ch) {
parser.read_char();
name.push(ch);
} else {
break;
}
}
name
}
fn read_name_or_number(parser: &mut Parser) -> String {
let mut s = String::new();
if let Some(ch) = parser.peek_char()
&& ch.is_ascii_digit()
{
parser.read_char();
s.push(ch);
while let Some(d) = parser.peek_char() {
if d.is_ascii_digit() {
parser.read_char();
s.push(d);
} else {
break;
}
}
return s;
}
read_name(parser)
}
fn flush_buf(
buf: &mut String,
source: &mut String,
children: &mut Vec<Word>,
start: Position,
end: Position,
split_fields: &mut bool,
) {
if !buf.is_empty() {
children.push(Word::string(
std::mem::take(buf),
false,
*split_fields,
Some(std::mem::take(source)),
Range { begin: start, end },
));
*split_fields = true;
}
}
pub(crate) fn here_document_line(line: &str) -> Word {
let mut parser = Parser::from_string(line);
let mut children = scan_children(
&mut parser,
|_, _, _, _| false,
ScanOptions {
allow_single_quotes: false,
allow_double_quotes: false,
allow_dollar: true,
allow_back_quotes: true,
allow_continuation_line: false,
comment_at_word_start: false,
backslash_mode: BackslashMode::HereDoc,
eof_mode: EofMode::Stop,
},
);
match children.len() {
0 => Word::string("", false, false, Some(String::new()), Range::default()),
1 => children.remove(0),
_ => Word::list(children.clone(), false, span_for_words(&children)),
}
}
fn scan_children(
parser: &mut Parser,
mut should_stop: impl FnMut(&mut Parser, char, &str, &[Word]) -> bool,
options: ScanOptions,
) -> Vec<Word> {
let mut children: Vec<Word> = Vec::new();
let mut buf = String::new();
let mut source_buf = String::new();
let mut buf_split_fields = true;
let mut start_pos = parser.current_pos();
loop {
if options.allow_continuation_line && parser.read_continuation_line() {
continue;
}
let Some(ch) = parser.peek_char() else {
if let EofMode::Error(message) = options.eof_mode {
parser.set_error(message.to_string());
}
break;
};
if should_stop(parser, ch, &buf, &children) {
break;
}
if options.comment_at_word_start && ch == '#' && buf.is_empty() && children.is_empty() {
break;
}
match ch {
'\'' if options.allow_single_quotes => {
flush_buf(
&mut buf,
&mut source_buf,
&mut children,
start_pos,
parser.current_pos(),
&mut buf_split_fields,
);
if let Some(w) = single_quotes(parser) {
children.push(w);
}
start_pos = parser.current_pos();
}
'"' if options.allow_double_quotes => {
flush_buf(
&mut buf,
&mut source_buf,
&mut children,
start_pos,
parser.current_pos(),
&mut buf_split_fields,
);
if let Some(w) = double_quotes(parser) {
children.push(w);
}
start_pos = parser.current_pos();
}
'$' if options.allow_dollar => {
flush_buf(
&mut buf,
&mut source_buf,
&mut children,
start_pos,
parser.current_pos(),
&mut buf_split_fields,
);
if let Some(w) = expect_dollar(parser) {
children.push(w);
}
start_pos = parser.current_pos();
}
'`' if options.allow_back_quotes => {
flush_buf(
&mut buf,
&mut source_buf,
&mut children,
start_pos,
parser.current_pos(),
&mut buf_split_fields,
);
if let Some(w) = back_quotes(parser) {
children.push(w);
}
start_pos = parser.current_pos();
}
'\\' => match options.backslash_mode {
BackslashMode::Raw => {
parser.read_char();
source_buf.push('\\');
if let Some(escaped) = parser.read_char() {
source_buf.push(escaped);
if escaped == '\n' {
continue;
}
if escaped.is_whitespace() {
buf_split_fields = false;
}
buf.push(escaped);
}
}
BackslashMode::DoubleQuotes => {
parser.read_char();
source_buf.push('\\');
if let Some(next) = parser.peek_char() {
if matches!(next, '$' | '`' | '"' | '\\' | '\n') {
parser.read_char();
source_buf.push(next);
if next != '\n' {
buf.push(next);
}
} else {
buf.push('\\');
}
}
}
BackslashMode::HereDoc => {
parser.read_char();
source_buf.push('\\');
match parser.peek_char() {
Some('$') | Some('`') | Some('\\') => {
if let Some(next) = parser.read_char() {
source_buf.push(next);
buf.push(next);
}
}
Some('\n') => {
parser.read_char();
source_buf.push('\n');
}
_ => {
buf.push('\\');
}
}
}
},
_ => {
parser.read_char();
buf.push(ch);
source_buf.push(ch);
}
}
}
flush_buf(
&mut buf,
&mut source_buf,
&mut children,
start_pos,
parser.current_pos(),
&mut buf_split_fields,
);
children
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::ParameterOp;
macro_rules! word_string {
($($tt:tt)*) => {
Word::String(crate::ast::StringWord { $($tt)* })
};
}
macro_rules! word_parameter {
($($tt:tt)*) => {
Word::Parameter(crate::ast::ParameterExpansion { $($tt)* })
};
}
macro_rules! word_command {
($($tt:tt)*) => {
Word::Command(crate::ast::CommandSubstitution { $($tt)* })
};
}
macro_rules! word_arithmetic {
($($tt:tt)*) => {
Word::Arithmetic(crate::ast::ArithmeticExpansion { $($tt)* })
};
}
macro_rules! word_list {
($($tt:tt)*) => {
Word::List(crate::ast::WordList { $($tt)* })
};
}
#[test]
fn simple_word() {
let mut p = Parser::from_string("hello");
let w = word(&mut p, None).expect("should parse word");
assert_eq!(w.as_str().as_deref(), Some("hello"));
}
#[test]
fn single_quoted_word() {
let mut p = Parser::from_string("'hello world'");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_string! {
value,
single_quoted,
..
} => {
assert_eq!(value, "hello world");
assert!(single_quoted);
}
other => panic!("expected String, got {other:?}"),
}
}
#[test]
fn double_quoted_word() {
let mut p = Parser::from_string("\"hello world\"");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_list! { double_quoted, .. } => {
assert!(double_quoted);
}
other => panic!("expected List, got {other:?}"),
}
}
#[test]
fn dollar_name() {
let mut p = Parser::from_string("$foo");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! {
name,
op,
brace_end,
..
} => {
assert_eq!(name, "foo");
assert_eq!(*op, ParameterOp::None);
assert!(brace_end.is_none());
}
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn dollar_special() {
let mut p = Parser::from_string("$?");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! { name, .. } => {
assert_eq!(name, "?");
}
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn parameter_default() {
let mut p = Parser::from_string("${foo:-bar}");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! {
name,
op,
colon,
arg,
..
} => {
assert_eq!(name, "foo");
assert_eq!(*op, ParameterOp::Minus);
assert!(colon);
assert!(arg.is_some());
}
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn parameter_length() {
let mut p = Parser::from_string("${#foo}");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! {
name,
op,
brace_end,
..
} => {
assert_eq!(name, "foo");
assert_eq!(*op, ParameterOp::LeadingHash);
assert!(brace_end.is_some());
}
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn command_substitution() {
let mut p = Parser::from_string("$(echo hello)");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_command! { back_quoted, .. } => {
assert!(!back_quoted);
}
other => panic!("expected Command, got {other:?}"),
}
}
#[test]
fn back_quote_substitution() {
let mut p = Parser::from_string("`echo hello`");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_command! { back_quoted, .. } => {
assert!(back_quoted);
}
other => panic!("expected Command, got {other:?}"),
}
}
#[test]
fn backslash_escape() {
let mut p = Parser::from_string("hello\\ world");
let w = word(&mut p, None).expect("should parse word");
println!("backslash_escape result: {w:?}");
match &w {
word_list! { children, .. } => {
assert_eq!(children.len(), 2);
}
word_string! { value, .. } => {
assert_eq!(value, "hello world");
}
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn word_stops_at_operator() {
let mut p = Parser::from_string("hello|world");
let w = word(&mut p, None).expect("should parse word");
assert_eq!(w.as_str().as_deref(), Some("hello"));
}
#[test]
fn name_start_underscore() {
assert!(is_name_start('_'));
}
#[test]
fn name_start_alpha() {
assert!(is_name_start('a'));
assert!(is_name_start('Z'));
}
#[test]
fn name_start_rejects_digit() {
assert!(!is_name_start('0'));
assert!(!is_name_start('9'));
}
#[test]
fn name_start_rejects_special() {
assert!(!is_name_start('-'));
assert!(!is_name_start('$'));
assert!(!is_name_start(' '));
}
#[test]
fn name_char_allows_digit() {
assert!(is_name_char('0'));
assert!(is_name_char('9'));
}
#[test]
fn name_char_allows_underscore_and_alpha() {
assert!(is_name_char('_'));
assert!(is_name_char('a'));
assert!(is_name_char('Z'));
}
#[test]
fn name_char_rejects_special() {
assert!(!is_name_char('-'));
assert!(!is_name_char('.'));
assert!(!is_name_char(' '));
}
#[test]
fn peek_name_simple() {
let mut p = Parser::from_string("foo_bar baz");
let name = peek_name(&mut p, false);
assert_eq!(name.as_deref(), Some("foo_bar"));
assert_eq!(p.peek_char(), Some('f'));
}
#[test]
fn peek_name_empty_input() {
let mut p = Parser::from_string("");
assert_eq!(peek_name(&mut p, false), None);
}
#[test]
fn peek_name_starts_with_digit_no_braces() {
let mut p = Parser::from_string("1abc");
assert_eq!(peek_name(&mut p, false), None);
}
#[test]
fn peek_name_digit_in_braces() {
let mut p = Parser::from_string("1abc");
let name = peek_name(&mut p, true);
assert_eq!(name.as_deref(), Some("1"));
}
#[test]
fn peek_name_starts_with_special() {
let mut p = Parser::from_string("-foo");
assert_eq!(peek_name(&mut p, false), None);
}
#[test]
fn word_returns_none_on_empty() {
let mut p = Parser::from_string("");
assert!(word(&mut p, None).is_none());
}
#[test]
fn word_returns_none_on_blanks_only() {
let mut p = Parser::from_string(" ");
assert!(word(&mut p, None).is_none());
}
#[test]
fn word_stops_at_space() {
let mut p = Parser::from_string("abc def");
let w = word(&mut p, None).expect("should parse word");
assert_eq!(w.as_str().as_deref(), Some("abc"));
}
#[test]
fn word_stops_at_tab() {
let mut p = Parser::from_string("abc\tdef");
let w = word(&mut p, None).expect("should parse word");
assert_eq!(w.as_str().as_deref(), Some("abc"));
}
#[test]
fn word_stops_at_newline() {
let mut p = Parser::from_string("abc\ndef");
let w = word(&mut p, None).expect("should parse word");
assert_eq!(w.as_str().as_deref(), Some("abc"));
}
#[test]
fn word_stops_at_semicolon() {
let mut p = Parser::from_string("abc;def");
let w = word(&mut p, None).expect("should parse word");
assert_eq!(w.as_str().as_deref(), Some("abc"));
}
#[test]
fn word_stops_at_pipe() {
let mut p = Parser::from_string("abc|def");
let w = word(&mut p, None).expect("should parse word");
assert_eq!(w.as_str().as_deref(), Some("abc"));
}
#[test]
fn word_stops_at_ampersand() {
let mut p = Parser::from_string("abc&def");
let w = word(&mut p, None).expect("should parse word");
assert_eq!(w.as_str().as_deref(), Some("abc"));
}
#[test]
fn word_stops_at_end_char() {
let mut p = Parser::from_string("abc)def");
let w = word(&mut p, Some(')')).expect("should parse word");
assert_eq!(w.as_str().as_deref(), Some("abc"));
assert_eq!(p.peek_char(), Some(')'));
}
#[test]
fn word_comment_at_start() {
let mut p = Parser::from_string("#comment");
assert!(word(&mut p, None).is_none());
}
#[test]
fn word_hash_not_comment_midword() {
let mut p = Parser::from_string("a#b");
let w = word(&mut p, None).expect("should parse word");
assert_eq!(w.as_str().as_deref(), Some("a#b"));
}
#[test]
fn word_multiple_parts_become_list() {
let mut p = Parser::from_string("hello$world");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_list! {
children,
double_quoted,
range: _,
} => {
assert!(!double_quoted);
assert_eq!(children.len(), 2);
println!("word_multiple_parts_become_list: {children:?}");
}
other => panic!("expected List, got {other:?}"),
}
}
#[test]
fn single_quotes_preserve_literal() {
let mut p = Parser::from_string("'hello $world `cmd` \\n'");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_string! {
value,
single_quoted,
..
} => {
assert_eq!(value, "hello $world `cmd` \\n");
assert!(single_quoted);
}
other => panic!("expected String, got {other:?}"),
}
}
#[test]
fn single_quotes_empty() {
let mut p = Parser::from_string("''");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_string! {
value,
single_quoted,
..
} => {
assert_eq!(value, "");
assert!(single_quoted);
}
other => panic!("expected String, got {other:?}"),
}
}
#[test]
fn double_quotes_empty() {
let mut p = Parser::from_string("\"\"");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_list! {
children,
double_quoted,
range: _,
} => {
assert!(double_quoted);
assert!(children.is_empty());
}
other => panic!("expected List, got {other:?}"),
}
}
#[test]
fn double_quotes_with_expansion() {
let mut p = Parser::from_string("\"hello $name\"");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_list! {
children,
double_quoted,
range: _,
} => {
assert!(double_quoted);
assert_eq!(children.len(), 2);
println!("double_quotes_with_expansion: {children:?}");
match &children[0] {
word_string! { value, .. } => assert_eq!(value, "hello "),
other => panic!("expected String, got {other:?}"),
}
match &children[1] {
word_parameter! { name, .. } => assert_eq!(name, "name"),
other => panic!("expected Parameter, got {other:?}"),
}
}
other => panic!("expected List, got {other:?}"),
}
}
#[test]
fn double_quotes_backslash_dollar() {
let mut p = Parser::from_string("\"\\$\"");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_list! {
children,
double_quoted,
range: _,
} => {
assert!(double_quoted);
assert_eq!(children.len(), 1);
match &children[0] {
word_string! { value, .. } => assert_eq!(value, "$"),
other => panic!("expected String, got {other:?}"),
}
}
other => panic!("expected List, got {other:?}"),
}
}
#[test]
fn double_quotes_backslash_backslash() {
let mut p = Parser::from_string("\"\\\\\"");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_list! {
children,
double_quoted,
range: _,
} => {
assert!(double_quoted);
assert_eq!(children.len(), 1);
match &children[0] {
word_string! { value, .. } => assert_eq!(value, "\\"),
other => panic!("expected String, got {other:?}"),
}
}
other => panic!("expected List, got {other:?}"),
}
}
#[test]
fn double_quotes_backslash_normal_char() {
let mut p = Parser::from_string("\"\\a\"");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_list! {
children,
double_quoted,
range: _,
} => {
assert!(double_quoted);
assert_eq!(children.len(), 1);
match &children[0] {
word_string! { value, .. } => assert_eq!(value, "\\a"),
other => panic!("expected String, got {other:?}"),
}
}
other => panic!("expected List, got {other:?}"),
}
}
#[test]
fn dollar_bare_at_end() {
let mut p = Parser::from_string("$");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_string! { value, .. } => assert_eq!(value, "$"),
other => panic!("expected literal $, got {other:?}"),
}
}
#[test]
fn dollar_bare_before_space() {
let mut p = Parser::from_string("$ foo");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_string! { value, .. } => assert_eq!(value, "$"),
other => panic!("expected literal $, got {other:?}"),
}
}
#[test]
fn dollar_name_with_underscore() {
let mut p = Parser::from_string("$_my_var");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! { name, op, .. } => {
assert_eq!(name, "_my_var");
assert_eq!(*op, ParameterOp::None);
}
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn dollar_digit() {
let mut p = Parser::from_string("$1");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! { name, .. } => assert_eq!(name, "1"),
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn dollar_at() {
let mut p = Parser::from_string("$@");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! { name, .. } => assert_eq!(name, "@"),
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn dollar_star() {
let mut p = Parser::from_string("$*");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! { name, .. } => assert_eq!(name, "*"),
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn dollar_hash() {
let mut p = Parser::from_string("$#");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! { name, .. } => assert_eq!(name, "#"),
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn dollar_dollar() {
let mut p = Parser::from_string("$$");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! { name, .. } => assert_eq!(name, "$"),
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn dollar_exclamation() {
let mut p = Parser::from_string("$!");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! { name, .. } => assert_eq!(name, "!"),
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn dollar_dash() {
let mut p = Parser::from_string("$-");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! { name, .. } => assert_eq!(name, "-"),
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn parameter_bare_braces() {
let mut p = Parser::from_string("${foo}");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! {
name,
op,
brace_end,
..
} => {
assert_eq!(name, "foo");
assert_eq!(*op, ParameterOp::None);
assert!(brace_end.is_some());
}
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn parameter_minus_no_colon() {
let mut p = Parser::from_string("${foo-default}");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! {
name,
op,
colon,
arg,
..
} => {
assert_eq!(name, "foo");
assert_eq!(*op, ParameterOp::Minus);
assert!(!colon);
assert!(arg.is_some());
}
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn parameter_equal_colon() {
let mut p = Parser::from_string("${var:=value}");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! {
name,
op,
colon,
arg,
..
} => {
assert_eq!(name, "var");
assert_eq!(*op, ParameterOp::Equal);
assert!(colon);
assert!(arg.is_some());
}
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn parameter_qmark() {
let mut p = Parser::from_string("${var?error msg}");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! {
name, op, colon, ..
} => {
assert_eq!(name, "var");
assert_eq!(*op, ParameterOp::QMark);
assert!(!colon);
}
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn parameter_qmark_colon() {
let mut p = Parser::from_string("${var:?error msg}");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! {
name, op, colon, ..
} => {
assert_eq!(name, "var");
assert_eq!(*op, ParameterOp::QMark);
assert!(colon);
}
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn parameter_plus() {
let mut p = Parser::from_string("${var+alt}");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! {
name, op, colon, ..
} => {
assert_eq!(name, "var");
assert_eq!(*op, ParameterOp::Plus);
assert!(!colon);
}
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn parameter_plus_colon() {
let mut p = Parser::from_string("${var:+alt}");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! {
name, op, colon, ..
} => {
assert_eq!(name, "var");
assert_eq!(*op, ParameterOp::Plus);
assert!(colon);
}
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn parameter_percent() {
let mut p = Parser::from_string("${path%/*}");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! { name, op, .. } => {
assert_eq!(name, "path");
assert_eq!(*op, ParameterOp::Percent);
}
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn parameter_double_percent() {
let mut p = Parser::from_string("${path%%/*}");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! { name, op, .. } => {
assert_eq!(name, "path");
assert_eq!(*op, ParameterOp::DoublePercent);
}
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn parameter_hash_strip() {
let mut p = Parser::from_string("${path#*/}");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! { name, op, .. } => {
assert_eq!(name, "path");
assert_eq!(*op, ParameterOp::Hash);
}
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn parameter_double_hash_strip() {
let mut p = Parser::from_string("${path##*/}");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! { name, op, .. } => {
assert_eq!(name, "path");
assert_eq!(*op, ParameterOp::DoubleHash);
}
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn parameter_leading_hash_length() {
let mut p = Parser::from_string("${#var}");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! { name, op, .. } => {
assert_eq!(name, "var");
assert_eq!(*op, ParameterOp::LeadingHash);
}
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn parameter_special_in_braces() {
let mut p = Parser::from_string("${?}");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! { name, op, .. } => {
assert_eq!(name, "?");
assert_eq!(*op, ParameterOp::None);
}
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn parameter_numeric_in_braces() {
let mut p = Parser::from_string("${10}");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! { name, op, .. } => {
assert_eq!(name, "10");
assert_eq!(*op, ParameterOp::None);
}
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn parameter_empty_arg() {
let mut p = Parser::from_string("${var:-}");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_parameter! { name, op, arg, .. } => {
assert_eq!(name, "var");
assert_eq!(*op, ParameterOp::Minus);
assert!(arg.is_none());
}
other => panic!("expected Parameter, got {other:?}"),
}
}
#[test]
fn command_substitution_dollar_paren() {
let mut p = Parser::from_string("$(ls -la)");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_command! {
back_quoted,
program,
..
} => {
assert!(!back_quoted);
println!("command_substitution_dollar_paren: {program:?}");
}
other => panic!("expected Command, got {other:?}"),
}
}
#[test]
fn command_substitution_nested() {
let mut p = Parser::from_string("$(echo $(pwd))");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_command! { back_quoted, .. } => {
assert!(!back_quoted);
}
other => panic!("expected Command, got {other:?}"),
}
}
#[test]
fn arithmetic_simple() {
let mut p = Parser::from_string("$((1+2))");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_arithmetic! { body, .. } => {
println!("arithmetic_simple: {body:?}");
}
other => panic!("expected Arithmetic, got {other:?}"),
}
}
#[test]
fn arithmetic_nested_parens() {
let mut p = Parser::from_string("$(( (1+2)*3 ))");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_arithmetic! { body, .. } => {
println!("arithmetic_nested_parens: {body:?}");
}
other => panic!("expected Arithmetic, got {other:?}"),
}
}
#[test]
fn arithmetic_invalid_expression_is_deferred() {
let mut parser = Parser::from_string("echo $((1 + 2x))");
let program = parser
.parse_program()
.expect("parser should keep invalid arithmetic expansion for runtime");
let crate::ast::AndOrList::Pipeline(pipeline) = &program.body[0].and_or_list else {
panic!("expected pipeline");
};
let crate::ast::Command::Simple(simple) = &pipeline.commands[0] else {
panic!("expected simple command");
};
match &simple.arguments[0] {
word_arithmetic! {
body: ArithmExpr::Raw(expr),
..
} => assert_eq!(expr.expr(), "1 + 2x"),
other => panic!("expected raw arithmetic expression, got {other:?}"),
}
}
#[test]
fn back_quotes_simple() {
let mut p = Parser::from_string("`ls`");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_command! { back_quoted, .. } => {
assert!(back_quoted);
}
other => panic!("expected Command, got {other:?}"),
}
}
#[test]
fn back_quotes_escape_dollar() {
let mut p = Parser::from_string("`echo \\$HOME`");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_command! { back_quoted, .. } => {
assert!(back_quoted);
}
other => panic!("expected Command, got {other:?}"),
}
}
#[test]
fn back_quotes_escape_backtick() {
let mut p = Parser::from_string("`echo \\``");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_command! { back_quoted, .. } => {
assert!(back_quoted);
}
other => panic!("expected Command, got {other:?}"),
}
}
#[test]
fn back_quotes_escape_backslash() {
let mut p = Parser::from_string("`echo \\\\`");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_command! { back_quoted, .. } => {
assert!(back_quoted);
}
other => panic!("expected Command, got {other:?}"),
}
}
#[test]
fn back_quotes_backslash_other_preserved() {
let mut p = Parser::from_string("`echo \\a`");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_command! { back_quoted, .. } => {
assert!(back_quoted);
}
other => panic!("expected Command, got {other:?}"),
}
}
#[test]
fn here_doc_line_plain() {
let w = here_document_line("hello world");
match &w {
word_string! { value, .. } => assert_eq!(value, "hello world"),
other => panic!("expected String, got {other:?}"),
}
}
#[test]
fn here_doc_line_empty() {
let w = here_document_line("");
match &w {
word_string! { value, .. } => assert_eq!(value, ""),
other => panic!("expected String, got {other:?}"),
}
}
#[test]
fn here_doc_line_with_variable() {
let w = here_document_line("hello $name world");
match &w {
word_list! { children, .. } => {
assert_eq!(children.len(), 3);
println!("here_doc_line_with_variable: {children:?}");
match &children[0] {
word_string! { value, .. } => assert_eq!(value, "hello "),
other => panic!("expected String, got {other:?}"),
}
match &children[1] {
word_parameter! { name, .. } => assert_eq!(name, "name"),
other => panic!("expected Parameter, got {other:?}"),
}
match &children[2] {
word_string! { value, .. } => assert_eq!(value, " world"),
other => panic!("expected String, got {other:?}"),
}
}
other => panic!("expected List, got {other:?}"),
}
}
#[test]
fn here_doc_line_with_backtick() {
let w = here_document_line("result: `cmd`");
match &w {
word_list! { children, .. } => {
assert_eq!(children.len(), 2);
match &children[1] {
word_command! { back_quoted, .. } => assert!(back_quoted),
other => panic!("expected Command, got {other:?}"),
}
}
other => panic!("expected List, got {other:?}"),
}
}
#[test]
fn here_doc_line_backslash_dollar() {
let w = here_document_line("\\$foo");
match &w {
word_string! { value, .. } => assert_eq!(value, "$foo"),
other => panic!("expected String, got {other:?}"),
}
}
#[test]
fn here_doc_line_backslash_backtick() {
let w = here_document_line("\\`cmd\\`");
match &w {
word_string! { value, .. } => assert_eq!(value, "`cmd`"),
other => panic!("expected String, got {other:?}"),
}
}
#[test]
fn here_doc_line_backslash_backslash() {
let w = here_document_line("path\\\\file");
match &w {
word_string! { value, .. } => assert_eq!(value, "path\\file"),
other => panic!("expected String, got {other:?}"),
}
}
#[test]
fn here_doc_line_backslash_other() {
let w = here_document_line("\\a");
match &w {
word_string! { value, .. } => assert_eq!(value, "\\a"),
other => panic!("expected String, got {other:?}"),
}
}
#[test]
fn here_doc_line_no_single_quotes() {
let w = here_document_line("it's");
match &w {
word_string! { value, .. } => assert_eq!(value, "it's"),
other => panic!("expected String, got {other:?}"),
}
}
#[test]
fn here_doc_line_no_double_quotes() {
let w = here_document_line("say \"hello\"");
match &w {
word_string! { value, .. } => assert_eq!(value, "say \"hello\""),
other => panic!("expected String, got {other:?}"),
}
}
#[test]
fn backslash_newline_continuation() {
let mut p = Parser::from_string("hel\\\nlo");
let w = word(&mut p, None).expect("should parse word");
assert_eq!(w.as_str().as_deref(), Some("hello"));
}
#[test]
fn backslash_special_char() {
let mut p = Parser::from_string("hello\\;world");
let w = word(&mut p, None).expect("should parse word");
println!("backslash_special_char: {w:?}");
match &w {
word_list! { children, .. } => {
let combined: String = children
.iter()
.filter_map(|c| c.as_str().map(|s| s.into_owned()))
.collect();
assert_eq!(combined, "hello;world");
}
word_string! { value, .. } => assert_eq!(value, "hello;world"),
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn word_continuation_before_word() {
let mut p = Parser::from_string("\\\n hello");
let w = word(&mut p, None).expect("should parse word");
assert_eq!(w.as_str().as_deref(), Some("hello"));
}
#[test]
fn mixed_quoting() {
let mut p = Parser::from_string("hello'world'\"!\"");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_list! { children, .. } => {
assert_eq!(children.len(), 3);
println!("mixed_quoting: {children:?}");
}
other => panic!("expected List, got {other:?}"),
}
}
#[test]
fn dollar_in_double_quotes() {
let mut p = Parser::from_string("\"${var:-default}\"");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_list! {
double_quoted,
children,
range: _,
} => {
assert!(double_quoted);
assert_eq!(children.len(), 1);
match &children[0] {
word_parameter! {
name,
op,
colon,
arg,
..
} => {
assert_eq!(name, "var");
assert_eq!(*op, ParameterOp::Minus);
assert!(colon);
assert!(arg.is_some());
}
other => panic!("expected Parameter, got {other:?}"),
}
}
other => panic!("expected List, got {other:?}"),
}
}
#[test]
fn command_sub_in_double_quotes() {
let mut p = Parser::from_string("\"$(echo hi)\"");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_list! {
double_quoted,
children,
range: _,
} => {
assert!(double_quoted);
assert_eq!(children.len(), 1);
match &children[0] {
word_command! { back_quoted, .. } => assert!(!back_quoted),
other => panic!("expected Command, got {other:?}"),
}
}
other => panic!("expected List, got {other:?}"),
}
}
#[test]
fn backtick_in_double_quotes() {
let mut p = Parser::from_string("\"`echo hi`\"");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_list! {
double_quoted,
children,
range: _,
} => {
assert!(double_quoted);
assert_eq!(children.len(), 1);
match &children[0] {
word_command! { back_quoted, .. } => assert!(back_quoted),
other => panic!("expected Command, got {other:?}"),
}
}
other => panic!("expected List, got {other:?}"),
}
}
#[test]
fn arithmetic_in_double_quotes() {
let mut p = Parser::from_string("\"$((2+3))\"");
let w = word(&mut p, None).expect("should parse word");
match &w {
word_list! {
double_quoted,
children,
range: _,
} => {
assert!(double_quoted);
assert_eq!(children.len(), 1);
match &children[0] {
word_arithmetic! { .. } => {}
other => panic!("expected Arithmetic, got {other:?}"),
}
}
other => panic!("expected List, got {other:?}"),
}
}
}