use super::*;
impl<'a> Lexer<'a> {
pub(in crate::parser) fn read_arithmetic_expansion_into(
&mut self,
content: &mut Option<String>,
) -> bool {
debug_assert_eq!(self.peek_char(), Some('('));
debug_assert_eq!(self.second_char(), Some('('));
Self::push_capture_char(content, '(');
self.advance();
Self::push_capture_char(content, '(');
self.advance();
let mut depth = 2;
while let Some(c) = self.peek_char() {
match c {
'\\' => {
Self::push_capture_char(content, c);
self.advance();
if let Some(next) = self.peek_char() {
Self::push_capture_char(content, next);
self.advance();
}
}
'\'' => {
Self::push_capture_char(content, c);
self.advance();
while let Some(quoted) = self.peek_char() {
Self::push_capture_char(content, quoted);
self.advance();
if quoted == '\'' {
break;
}
}
}
'"' => {
let mut escaped = false;
Self::push_capture_char(content, c);
self.advance();
while let Some(quoted) = self.peek_char() {
Self::push_capture_char(content, quoted);
self.advance();
if escaped {
escaped = false;
continue;
}
match quoted {
'\\' => escaped = true,
'"' => break,
_ => {}
}
}
}
'`' => {
let mut escaped = false;
Self::push_capture_char(content, c);
self.advance();
while let Some(quoted) = self.peek_char() {
Self::push_capture_char(content, quoted);
self.advance();
if escaped {
escaped = false;
continue;
}
match quoted {
'\\' => escaped = true,
'`' => break,
_ => {}
}
}
}
'(' => {
Self::push_capture_char(content, c);
self.advance();
depth += 1;
}
')' => {
Self::push_capture_char(content, c);
self.advance();
depth -= 1;
if depth == 0 {
return true;
}
}
_ => {
Self::push_capture_char(content, c);
self.advance();
}
}
}
false
}
pub(in crate::parser) fn read_legacy_arithmetic_into(
&mut self,
content: &mut Option<String>,
segment_start: Position,
) -> bool {
let mut bracket_depth = 1;
while let Some(c) = self.peek_char() {
match c {
'\\' => {
Self::push_capture_char(content, c);
self.advance();
if let Some(next) = self.peek_char() {
Self::push_capture_char(content, next);
self.advance();
}
}
'\'' => {
Self::push_capture_char(content, c);
self.advance();
while let Some(quoted) = self.peek_char() {
Self::push_capture_char(content, quoted);
self.advance();
if quoted == '\'' {
break;
}
}
}
'"' => {
let mut escaped = false;
Self::push_capture_char(content, c);
self.advance();
while let Some(quoted) = self.peek_char() {
Self::push_capture_char(content, quoted);
self.advance();
if escaped {
escaped = false;
continue;
}
match quoted {
'\\' => escaped = true,
'"' => break,
_ => {}
}
}
}
'`' => {
let mut escaped = false;
Self::push_capture_char(content, c);
self.advance();
while let Some(quoted) = self.peek_char() {
Self::push_capture_char(content, quoted);
self.advance();
if escaped {
escaped = false;
continue;
}
match quoted {
'\\' => escaped = true,
'`' => break,
_ => {}
}
}
}
'[' => {
Self::push_capture_char(content, c);
self.advance();
bracket_depth += 1;
}
']' => {
Self::push_capture_char(content, c);
self.advance();
bracket_depth -= 1;
if bracket_depth == 0 {
return true;
}
}
'$' => {
Self::push_capture_char(content, c);
self.advance();
if self.peek_char() == Some('(') {
if self.second_char() == Some('(') {
if !self.read_arithmetic_expansion_into(content) {
return false;
}
} else {
Self::push_capture_char(content, '(');
self.advance();
if !self.read_command_subst_into(content) {
return false;
}
}
} else if self.peek_char() == Some('{') {
Self::push_capture_char(content, '{');
self.advance();
if !self.read_param_expansion_into(content, segment_start, false) {
return false;
}
} else if self.peek_char() == Some('[') {
Self::push_capture_char(content, '[');
self.advance();
if !self.read_legacy_arithmetic_into(content, segment_start) {
return false;
}
}
}
_ => {
Self::push_capture_char(content, c);
self.advance();
}
}
}
false
}
pub(in crate::parser) fn read_command_subst_into(
&mut self,
content: &mut Option<String>,
) -> bool {
self.read_command_subst_into_depth(content, 0)
}
pub(in crate::parser) fn flush_command_subst_keyword(
current_word: &mut String,
pending_case_headers: &mut usize,
case_clause_depths: &mut SmallVec<[usize; 4]>,
depth: usize,
word_started_at_command_start: &mut bool,
) {
if current_word.is_empty() {
*word_started_at_command_start = false;
return;
}
match current_word.as_str() {
"case" if *word_started_at_command_start => *pending_case_headers += 1,
"in" if *pending_case_headers > 0 => {
*pending_case_headers -= 1;
case_clause_depths.push(depth);
}
"esac" if *word_started_at_command_start => {
case_clause_depths.pop();
}
_ => {}
}
current_word.clear();
*word_started_at_command_start = false;
}
pub(in crate::parser) fn read_command_subst_heredoc_delimiter_into(
&mut self,
content: &mut Option<String>,
) -> Option<String> {
while let Some(ch) = self.peek_char() {
if !matches!(ch, ' ' | '\t') {
break;
}
Self::push_capture_char(content, ch);
self.advance();
}
let mut cooked = String::new();
let mut in_single = false;
let mut in_double = false;
let mut escaped = false;
let mut saw_any = false;
while let Some(ch) = self.peek_char() {
if heredoc_delimiter_is_terminator(ch, in_single, in_double, escaped) {
break;
}
saw_any = true;
Self::push_capture_char(content, ch);
self.advance();
if escaped {
cooked.push(ch);
escaped = false;
continue;
}
match ch {
'\\' if !in_single => escaped = true,
'\'' if !in_double => in_single = !in_single,
'"' if !in_single => in_double = !in_double,
_ => cooked.push(ch),
}
}
saw_any.then_some(cooked)
}
pub(in crate::parser) fn read_command_subst_backtick_segment_into(
&mut self,
content: &mut Option<String>,
) {
Self::push_capture_char(content, '`');
self.advance();
while let Some(ch) = self.peek_char() {
Self::push_capture_char(content, ch);
self.advance();
if ch == '\\' {
if let Some(esc) = self.peek_char() {
Self::push_capture_char(content, esc);
self.advance();
}
continue;
}
if ch == '`' {
break;
}
}
}
pub(in crate::parser) fn read_command_subst_pending_heredoc_into(
&mut self,
content: &mut Option<String>,
delimiter: &str,
strip_tabs: bool,
) -> bool {
loop {
let mut line = String::new();
let mut saw_newline = false;
while let Some(ch) = self.peek_char() {
self.advance();
if ch == '\n' {
saw_newline = true;
break;
}
line.push(ch);
}
Self::push_capture_str(content, &line);
if saw_newline {
Self::push_capture_char(content, '\n');
}
if heredoc_line_matches_delimiter(&line, delimiter, strip_tabs) || !saw_newline {
return true;
}
}
}
pub(in crate::parser) fn read_command_subst_into_depth(
&mut self,
content: &mut Option<String>,
subst_depth: usize,
) -> bool {
if subst_depth >= self.max_subst_depth {
let mut depth = 1;
while let Some(c) = self.peek_char() {
self.advance();
match c {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
Self::push_capture_char(content, ')');
return true;
}
}
_ => {}
}
}
return false;
}
let mut depth = 1;
let mut pending_heredocs = SmallVec::<[(String, bool); 2]>::new();
let mut pending_case_headers = 0usize;
let mut case_clause_depths = SmallVec::<[usize; 4]>::new();
let mut current_word = String::with_capacity(16);
let mut at_command_start = true;
let mut expecting_redirection_target = false;
let mut current_word_started_at_command_start = false;
while let Some(c) = self.peek_char() {
match c {
'#' if !self.should_treat_hash_as_word_char() => {
let had_word = !current_word.is_empty();
Self::flush_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
if had_word && expecting_redirection_target {
expecting_redirection_target = false;
}
Self::push_capture_char(content, '#');
self.advance();
while let Some(comment_ch) = self.peek_char() {
Self::push_capture_char(content, comment_ch);
self.advance();
if comment_ch == '\n' {
for (delimiter, strip_tabs) in pending_heredocs.drain(..) {
if !self.read_command_subst_pending_heredoc_into(
content, &delimiter, strip_tabs,
) {
return false;
}
}
at_command_start = true;
expecting_redirection_target = false;
break;
}
}
}
'(' => {
Self::flush_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
depth += 1;
Self::push_capture_char(content, c);
self.advance();
at_command_start = true;
expecting_redirection_target = false;
}
')' => {
Self::flush_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
if case_clause_depths
.last()
.is_some_and(|case_depth| *case_depth == depth)
{
Self::push_capture_char(content, ')');
self.advance();
at_command_start = true;
expecting_redirection_target = false;
continue;
}
depth -= 1;
self.advance();
if depth == 0 {
Self::push_capture_char(content, ')');
return true;
}
Self::push_capture_char(content, c);
at_command_start = false;
expecting_redirection_target = false;
}
'"' => {
let had_word = !current_word.is_empty();
Self::flush_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
if had_word && expecting_redirection_target {
expecting_redirection_target = false;
}
Self::push_capture_char(content, '"');
self.advance();
while let Some(qc) = self.peek_char() {
match qc {
'"' => {
Self::push_capture_char(content, '"');
self.advance();
break;
}
'\\' => {
Self::push_capture_char(content, '\\');
self.advance();
if let Some(esc) = self.peek_char() {
Self::push_capture_char(content, esc);
self.advance();
}
}
'$' => {
Self::push_capture_char(content, '$');
self.advance();
if self.peek_char() == Some('(') {
if self.second_char() == Some('(') {
if !self.read_arithmetic_expansion_into(content) {
return false;
}
} else {
Self::push_capture_char(content, '(');
self.advance();
if !self
.read_command_subst_into_depth(content, subst_depth + 1)
{
return false;
}
}
}
}
_ => {
Self::push_capture_char(content, qc);
self.advance();
}
}
}
if expecting_redirection_target {
expecting_redirection_target = false;
} else {
at_command_start = false;
}
}
'\'' => {
let had_word = !current_word.is_empty();
Self::flush_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
if had_word && expecting_redirection_target {
expecting_redirection_target = false;
}
Self::push_capture_char(content, '\'');
self.advance();
while let Some(qc) = self.peek_char() {
Self::push_capture_char(content, qc);
self.advance();
if qc == '\'' {
break;
}
}
if expecting_redirection_target {
expecting_redirection_target = false;
} else {
at_command_start = false;
}
}
'`' => {
let had_word = !current_word.is_empty();
Self::flush_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
if had_word && expecting_redirection_target {
expecting_redirection_target = false;
}
self.read_command_subst_backtick_segment_into(content);
if expecting_redirection_target {
expecting_redirection_target = false;
} else {
at_command_start = false;
}
}
'$' if self.second_char() == Some('\'') => {
let had_word = !current_word.is_empty();
Self::flush_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
if had_word && expecting_redirection_target {
expecting_redirection_target = false;
}
Self::push_capture_char(content, '$');
self.advance();
Self::push_capture_char(content, '\'');
self.advance();
while let Some(qc) = self.peek_char() {
Self::push_capture_char(content, qc);
self.advance();
if qc == '\\' {
if let Some(esc) = self.peek_char() {
Self::push_capture_char(content, esc);
self.advance();
}
continue;
}
if qc == '\'' {
break;
}
}
if expecting_redirection_target {
expecting_redirection_target = false;
} else {
at_command_start = false;
}
}
'\\' => {
let had_word = !current_word.is_empty();
Self::flush_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
if had_word && expecting_redirection_target {
expecting_redirection_target = false;
}
Self::push_capture_char(content, '\\');
self.advance();
if let Some(esc) = self.peek_char() {
Self::push_capture_char(content, esc);
self.advance();
}
if expecting_redirection_target {
expecting_redirection_target = false;
} else {
at_command_start = false;
}
}
'<' if self.second_char() == Some('<') => {
let word_was_redirection_fd = current_word_started_at_command_start
&& !current_word.is_empty()
&& current_word.chars().all(|current| current.is_ascii_digit());
Self::flush_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
if word_was_redirection_fd {
at_command_start = true;
}
Self::push_capture_char(content, '<');
self.advance();
Self::push_capture_char(content, '<');
self.advance();
if self.peek_char() == Some('<') {
Self::push_capture_char(content, '<');
self.advance();
expecting_redirection_target = true;
continue;
}
let strip_tabs = if self.peek_char() == Some('-') {
Self::push_capture_char(content, '-');
self.advance();
true
} else {
false
};
if let Some(delimiter) = self.read_command_subst_heredoc_delimiter_into(content)
{
pending_heredocs.push((delimiter, strip_tabs));
expecting_redirection_target = false;
} else {
expecting_redirection_target = true;
}
}
'>' | '<' => {
let word_was_redirection_fd = current_word_started_at_command_start
&& !current_word.is_empty()
&& current_word.chars().all(|current| current.is_ascii_digit());
Self::flush_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
if word_was_redirection_fd {
at_command_start = true;
}
Self::push_capture_char(content, c);
self.advance();
expecting_redirection_target = true;
}
'\n' => {
Self::flush_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
Self::push_capture_char(content, '\n');
self.advance();
for (delimiter, strip_tabs) in pending_heredocs.drain(..) {
if !self.read_command_subst_pending_heredoc_into(
content, &delimiter, strip_tabs,
) {
return false;
}
}
at_command_start = true;
expecting_redirection_target = false;
}
_ => {
if c.is_ascii_alphanumeric() || c == '_' {
if current_word.is_empty()
&& !expecting_redirection_target
&& at_command_start
{
current_word_started_at_command_start = true;
at_command_start = false;
}
current_word.push(c);
} else {
let had_word = !current_word.is_empty();
Self::flush_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
if had_word && expecting_redirection_target {
expecting_redirection_target = false;
}
match c {
' ' | '\t' => {}
';' | '|' | '&' => {
at_command_start = true;
expecting_redirection_target = false;
}
_ => {
if !expecting_redirection_target {
at_command_start = false;
}
}
}
}
Self::push_capture_char(content, c);
self.advance();
}
}
}
false
}
pub(in crate::parser) fn read_param_expansion_into(
&mut self,
content: &mut Option<String>,
segment_start: Position,
preserve_escape_width: bool,
) -> bool {
let mut borrowable = true;
let mut depth = 1;
let mut literal_brace_depth = 0usize;
let mut in_single = false;
let mut in_double = false;
let mut double_quote_depth = 0usize;
while let Some(c) = self.peek_char() {
if in_single {
match c {
'\\' => {
let escape_start = self.current_position();
if self.second_char() == Some('"') {
self.advance();
borrowable = false;
self.ensure_capture_from_source(content, segment_start, escape_start);
Self::push_capture_char(content, '"');
self.advance();
} else {
Self::push_capture_char(content, '\\');
self.advance();
}
}
'\'' => {
Self::push_capture_char(content, c);
self.advance();
in_single = false;
}
_ => {
Self::push_capture_char(content, c);
self.advance();
}
}
continue;
}
match c {
'}' if !in_single && (!in_double || depth > double_quote_depth) => {
self.advance();
Self::push_capture_char(content, '}');
if depth == 1
&& literal_brace_depth > 0
&& self.has_later_top_level_param_expansion_closer(depth)
{
literal_brace_depth -= 1;
continue;
}
depth -= 1;
if depth == 0 {
break;
}
}
'{' if !in_single && !in_double => {
literal_brace_depth += 1;
Self::push_capture_char(content, '{');
self.advance();
}
'"' => {
Self::push_capture_char(content, '"');
self.advance();
in_double = !in_double;
double_quote_depth = if in_double { depth } else { 0 };
}
'\'' => {
Self::push_capture_char(content, '\'');
self.advance();
if !in_double {
in_single = true;
}
}
'\\' => {
let escape_start = self.current_position();
self.advance();
if let Some(esc) = self.peek_char() {
match esc {
'$' => {
borrowable = false;
self.ensure_capture_from_source(
content,
segment_start,
escape_start,
);
Self::push_capture_char(content, '\x00');
Self::push_capture_char(content, '$');
self.advance();
}
'"' | '\\' | '`' => {
borrowable = false;
self.ensure_capture_from_source(
content,
segment_start,
escape_start,
);
if preserve_escape_width {
Self::push_capture_char(content, '\x00');
}
Self::push_capture_char(content, esc);
self.advance();
}
'}' => {
Self::push_capture_char(content, '\\');
Self::push_capture_char(content, '}');
self.advance();
literal_brace_depth = literal_brace_depth.saturating_sub(1);
}
_ => {
Self::push_capture_char(content, '\\');
Self::push_capture_char(content, esc);
self.advance();
}
}
} else {
Self::push_capture_char(content, '\\');
}
}
'$' => {
Self::push_capture_char(content, '$');
self.advance();
if self.peek_char() == Some('(') {
if self.second_char() == Some('(') {
if !self.read_arithmetic_expansion_into(content) {
borrowable = false;
}
} else {
Self::push_capture_char(content, '(');
self.advance();
self.read_command_subst_into(content);
}
} else if self.peek_char() == Some('{') {
Self::push_capture_char(content, '{');
self.advance();
borrowable &= self.read_param_expansion_into(
content,
segment_start,
preserve_escape_width,
);
}
}
_ => {
Self::push_capture_char(content, c);
self.advance();
}
}
}
borrowable
}
pub(in crate::parser) fn has_later_top_level_param_expansion_closer(
&self,
target_depth: usize,
) -> bool {
let mut chars = self.lookahead_chars().peekable();
let mut depth = target_depth;
let mut in_single = false;
let mut in_double = false;
let mut double_quote_depth = 0usize;
while let Some(ch) = chars.next() {
if in_single {
match ch {
'\'' => in_single = false,
'\\' if chars.peek() == Some(&'"') => {
chars.next();
}
'\\' => {}
_ => {}
}
continue;
}
if in_double {
match ch {
'"' => {
in_double = false;
double_quote_depth = 0;
}
'\\' => {
chars.next();
}
'$' if chars.peek() == Some(&'{') => {
chars.next();
depth += 1;
}
'}' if depth > double_quote_depth => {
depth -= 1;
}
_ => {}
}
continue;
}
match ch {
'\n' if depth == target_depth => return false,
'\'' => in_single = true,
'"' => {
in_double = true;
double_quote_depth = depth;
}
'\\' => {
chars.next();
}
'$' if chars.peek() == Some(&'{') => {
chars.next();
depth += 1;
}
'}' => {
if depth == target_depth {
return true;
}
depth -= 1;
}
_ => {}
}
}
false
}
}
fn next_char_boundary(input: &str, index: usize) -> Option<(char, usize)> {
let ch = input.get(index..)?.chars().next()?;
Some((ch, index + ch.len_utf8()))
}
pub(in crate::parser) fn line_has_unclosed_double_paren(prefix: &str) -> bool {
let mut index = 0usize;
let mut depth = 0usize;
let mut in_single = false;
let mut in_double = false;
let mut in_backtick = false;
let mut escaped = false;
while let Some((ch, next_index)) = next_char_boundary(prefix, index) {
let was_escaped = escaped;
if ch == '\\' && !in_single {
escaped = !escaped;
index = next_index;
continue;
}
escaped = false;
match ch {
'\'' if !in_double && !in_backtick && !was_escaped => in_single = !in_single,
'"' if !in_single && !in_backtick && !was_escaped => in_double = !in_double,
'`' if !in_single && !in_double && !was_escaped => in_backtick = !in_backtick,
'(' if !in_single
&& !in_double
&& !in_backtick
&& !was_escaped
&& prefix[next_index..].starts_with('(') =>
{
depth += 1;
index = next_index + '('.len_utf8();
continue;
}
')' if !in_single
&& !in_double
&& !in_backtick
&& !was_escaped
&& prefix[next_index..].starts_with(')') =>
{
depth = depth.saturating_sub(1);
index = next_index + ')'.len_utf8();
continue;
}
_ => {}
}
index = next_index;
}
depth > 0
}
fn inside_unclosed_double_paren_on_line(input: &str, index: usize) -> bool {
let line_start = input[..index].rfind('\n').map_or(0, |found| found + 1);
let prefix = &input[line_start..index];
line_has_unclosed_double_paren(prefix)
}
pub(in crate::parser) fn hash_starts_comment(input: &str, index: usize) -> bool {
if inside_unclosed_double_paren_on_line(input, index) {
return false;
}
let next = &input[index + '#'.len_utf8()..];
input[..index]
.chars()
.next_back()
.is_none_or(|prev| match prev {
'(' => {
let whitespace_index = next.find(char::is_whitespace);
let close_index = next.find(')');
match (whitespace_index, close_index) {
(Some(whitespace), Some(close)) => whitespace < close,
(Some(_), None) | (None, None) => true,
(None, Some(_)) => false,
}
}
_ => prev.is_whitespace() || matches!(prev, ';' | '|' | '&' | '<' | '>' | ')'),
})
}
fn heredoc_delimiter_is_terminator(
ch: char,
in_single: bool,
in_double: bool,
escaped: bool,
) -> bool {
!in_single
&& !in_double
&& !escaped
&& (ch.is_whitespace() || matches!(ch, '|' | '&' | ';' | '<' | '>' | '(' | ')'))
}
fn scan_double_quoted_command_substitution_segment(
input: &str,
mut index: usize,
subst_depth: usize,
) -> Option<usize> {
while let Some((ch, next_index)) = next_char_boundary(input, index) {
match ch {
'"' => return Some(next_index),
'\\' => {
index = next_index;
if let Some((_, escaped_next)) = next_char_boundary(input, index) {
index = escaped_next;
}
}
'$' if input[next_index..].starts_with('{') => {
let consumed = scan_command_subst_parameter_expansion_len(
&input[next_index + '{'.len_utf8()..],
subst_depth,
0,
)?;
index = next_index + '{'.len_utf8() + consumed;
}
'$' if input[next_index..].starts_with('(')
&& !input[next_index + '('.len_utf8()..].starts_with('(') =>
{
let consumed = scan_command_substitution_body_len_inner(
&input[next_index + '('.len_utf8()..],
subst_depth + 1,
)?;
index = next_index + '('.len_utf8() + consumed;
}
_ => index = next_index,
}
}
None
}
fn scan_command_subst_parameter_expansion_len(
input: &str,
subst_depth: usize,
parameter_depth: usize,
) -> Option<usize> {
if parameter_depth >= MAX_PARAMETER_EXPANSION_SCAN_DEPTH {
return scan_command_subst_parameter_expansion_len_balanced(input, subst_depth);
}
let mut index = 0usize;
let mut in_single = false;
let mut in_double = false;
let mut in_ansi_c_single = false;
let mut in_backtick = false;
let mut escaped = false;
let mut ansi_c_quote_pending = false;
while let Some((ch, next_index)) = next_char_boundary(input, index) {
let was_escaped = escaped;
if ch == '\\' && !in_single {
escaped = !escaped;
index = next_index;
ansi_c_quote_pending = false;
continue;
}
escaped = false;
if !in_single && !in_ansi_c_single && !in_backtick && !was_escaped && ch == '$' {
if input[next_index..].starts_with('{')
&& let Some(consumed) = scan_command_subst_parameter_expansion_len(
&input[next_index + '{'.len_utf8()..],
subst_depth,
parameter_depth + 1,
)
{
index = next_index + '{'.len_utf8() + consumed;
ansi_c_quote_pending = false;
continue;
}
if input[next_index..].starts_with('(')
&& !input[next_index + '('.len_utf8()..].starts_with('(')
&& let Some(consumed) = scan_command_substitution_body_len_inner(
&input[next_index + '('.len_utf8()..],
subst_depth + 1,
)
{
index = next_index + '('.len_utf8() + consumed;
ansi_c_quote_pending = false;
continue;
}
}
if !in_single
&& !in_ansi_c_single
&& !in_double
&& !in_backtick
&& !was_escaped
&& matches!(ch, '<' | '>')
&& input[next_index..].starts_with('(')
&& let Some(consumed) = scan_command_substitution_body_len_inner(
&input[next_index + '('.len_utf8()..],
subst_depth + 1,
)
{
index = next_index + '('.len_utf8() + consumed;
ansi_c_quote_pending = false;
continue;
}
match ch {
'\'' if !in_double && !in_backtick && !was_escaped => {
if in_ansi_c_single {
in_ansi_c_single = false;
} else if !in_single && ansi_c_quote_pending {
in_ansi_c_single = true;
} else {
in_single = !in_single;
}
}
'"' if !in_single && !in_ansi_c_single && !in_backtick && !was_escaped => {
in_double = !in_double
}
'`' if !in_single && !in_ansi_c_single && !in_double && !was_escaped => {
in_backtick = !in_backtick
}
'}' if !in_single
&& !in_ansi_c_single
&& !in_double
&& !in_backtick
&& !was_escaped =>
{
return Some(next_index);
}
_ => {}
}
ansi_c_quote_pending = ch == '$'
&& !in_single
&& !in_ansi_c_single
&& !in_double
&& !in_backtick
&& !was_escaped;
index = next_index;
}
None
}
fn scan_command_subst_parameter_expansion_len_balanced(
input: &str,
subst_depth: usize,
) -> Option<usize> {
let mut index = 0usize;
let mut brace_depth = 1usize;
let mut in_single = false;
let mut in_double = false;
let mut in_ansi_c_single = false;
let mut in_backtick = false;
let mut escaped = false;
let mut ansi_c_quote_pending = false;
while let Some((ch, next_index)) = next_char_boundary(input, index) {
let was_escaped = escaped;
if ch == '\\' && !in_single {
escaped = !escaped;
index = next_index;
ansi_c_quote_pending = false;
continue;
}
escaped = false;
if !in_single && !in_ansi_c_single && !in_backtick && !was_escaped && ch == '$' {
if input[next_index..].starts_with('{') {
brace_depth = brace_depth.saturating_add(1);
index = next_index + '{'.len_utf8();
ansi_c_quote_pending = false;
continue;
}
if input[next_index..].starts_with('(')
&& !input[next_index + '('.len_utf8()..].starts_with('(')
&& let Some(consumed) = scan_command_substitution_body_len_inner(
&input[next_index + '('.len_utf8()..],
subst_depth + 1,
)
{
index = next_index + '('.len_utf8() + consumed;
ansi_c_quote_pending = false;
continue;
}
}
if !in_single
&& !in_ansi_c_single
&& !in_double
&& !in_backtick
&& !was_escaped
&& matches!(ch, '<' | '>')
&& input[next_index..].starts_with('(')
&& let Some(consumed) = scan_command_substitution_body_len_inner(
&input[next_index + '('.len_utf8()..],
subst_depth + 1,
)
{
index = next_index + '('.len_utf8() + consumed;
ansi_c_quote_pending = false;
continue;
}
match ch {
'\'' if !in_double && !in_backtick && !was_escaped => {
if in_ansi_c_single {
in_ansi_c_single = false;
} else if !in_single && ansi_c_quote_pending {
in_ansi_c_single = true;
} else {
in_single = !in_single;
}
}
'"' if !in_single && !in_ansi_c_single && !in_backtick && !was_escaped => {
in_double = !in_double
}
'`' if !in_single && !in_ansi_c_single && !in_double && !was_escaped => {
in_backtick = !in_backtick
}
'}' if !in_single
&& !in_ansi_c_single
&& !in_double
&& !in_backtick
&& !was_escaped =>
{
brace_depth = brace_depth.saturating_sub(1);
if brace_depth == 0 {
return Some(next_index);
}
}
_ => {}
}
ansi_c_quote_pending = ch == '$'
&& !in_single
&& !in_ansi_c_single
&& !in_double
&& !in_backtick
&& !was_escaped;
index = next_index;
}
None
}
fn scan_command_subst_heredoc_delimiter(input: &str, mut index: usize) -> Option<(usize, String)> {
while let Some((ch, next_index)) = next_char_boundary(input, index) {
if !matches!(ch, ' ' | '\t') {
break;
}
index = next_index;
}
let start = index;
let mut cooked = String::new();
let mut in_single = false;
let mut in_double = false;
let mut escaped = false;
while let Some((ch, next_index)) = next_char_boundary(input, index) {
if heredoc_delimiter_is_terminator(ch, in_single, in_double, escaped) {
break;
}
index = next_index;
if escaped {
cooked.push(ch);
escaped = false;
continue;
}
match ch {
'\\' if !in_single => escaped = true,
'\'' if !in_double => in_single = !in_single,
'"' if !in_single => in_double = !in_double,
_ => cooked.push(ch),
}
}
(index > start).then_some((index, cooked))
}
fn skip_command_subst_pending_heredoc(
input: &str,
mut index: usize,
delimiter: &str,
strip_tabs: bool,
) -> usize {
while index <= input.len() {
let rest = &input[index..];
let line_len = rest.find('\n').unwrap_or(rest.len());
let line = &rest[..line_len];
let has_newline = line_len < rest.len();
index += line_len;
if has_newline {
index += '\n'.len_utf8();
}
if heredoc_line_matches_delimiter(line, delimiter, strip_tabs) || !has_newline {
return index;
}
}
index
}
fn scan_command_subst_ansi_c_single_quoted_segment(
input: &str,
quote_index: usize,
) -> Option<usize> {
let mut index = quote_index + '\''.len_utf8();
while let Some((ch, next_index)) = next_char_boundary(input, index) {
index = next_index;
if ch == '\\' {
if let Some((_, escaped_next)) = next_char_boundary(input, index) {
index = escaped_next;
}
continue;
}
if ch == '\'' {
return Some(index);
}
}
None
}
fn scan_command_subst_backtick_segment(input: &str, start: usize) -> Option<usize> {
let mut index = start;
while let Some((ch, next_index)) = next_char_boundary(input, index) {
index = next_index;
if ch == '\\' {
if let Some((_, escaped_next)) = next_char_boundary(input, index) {
index = escaped_next;
}
continue;
}
if ch == '`' {
return Some(index);
}
}
None
}
fn flush_scanned_command_subst_keyword(
current_word: &mut String,
pending_case_headers: &mut usize,
case_clause_depths: &mut SmallVec<[usize; 4]>,
depth: usize,
word_started_at_command_start: &mut bool,
) {
if current_word.is_empty() {
*word_started_at_command_start = false;
return;
}
match current_word.as_str() {
"case" if *word_started_at_command_start => *pending_case_headers += 1,
"in" if *pending_case_headers > 0 => {
*pending_case_headers -= 1;
case_clause_depths.push(depth);
}
"esac" if *word_started_at_command_start => {
case_clause_depths.pop();
}
_ => {}
}
current_word.clear();
*word_started_at_command_start = false;
}
pub(in crate::parser) fn scan_command_substitution_body_len_inner(
input: &str,
subst_depth: usize,
) -> Option<usize> {
if subst_depth >= DEFAULT_MAX_SUBST_DEPTH {
return None;
}
let mut index = 0usize;
let mut depth = 1;
let mut pending_heredocs = SmallVec::<[(String, bool); 2]>::new();
let mut pending_case_headers = 0usize;
let mut case_clause_depths = SmallVec::<[usize; 4]>::new();
let mut current_word = String::with_capacity(16);
let mut at_command_start = true;
let mut expecting_redirection_target = false;
let mut current_word_started_at_command_start = false;
while let Some((ch, next_index)) = next_char_boundary(input, index) {
match ch {
'#' if hash_starts_comment(input, index) => {
let had_word = !current_word.is_empty();
flush_scanned_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
if had_word && expecting_redirection_target {
expecting_redirection_target = false;
}
index = next_index;
while let Some((comment_ch, comment_next)) = next_char_boundary(input, index) {
index = comment_next;
if comment_ch == '\n' {
for (delimiter, strip_tabs) in pending_heredocs.drain(..) {
index = skip_command_subst_pending_heredoc(
input, index, &delimiter, strip_tabs,
);
}
at_command_start = true;
expecting_redirection_target = false;
break;
}
}
}
'(' => {
flush_scanned_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
depth += 1;
index = next_index;
at_command_start = true;
expecting_redirection_target = false;
}
')' => {
flush_scanned_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
if case_clause_depths
.last()
.is_some_and(|case_depth| *case_depth == depth)
{
index = next_index;
at_command_start = true;
expecting_redirection_target = false;
continue;
}
depth -= 1;
index = next_index;
if depth == 0 {
return Some(index);
}
at_command_start = false;
expecting_redirection_target = false;
}
'"' => {
let had_word = !current_word.is_empty();
flush_scanned_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
if had_word && expecting_redirection_target {
expecting_redirection_target = false;
}
index = scan_double_quoted_command_substitution_segment(
input,
next_index,
subst_depth,
)?;
if expecting_redirection_target {
expecting_redirection_target = false;
} else {
at_command_start = false;
}
}
'\'' => {
let had_word = !current_word.is_empty();
flush_scanned_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
if had_word && expecting_redirection_target {
expecting_redirection_target = false;
}
index = next_index;
while let Some((quoted_ch, quoted_next)) = next_char_boundary(input, index) {
index = quoted_next;
if quoted_ch == '\'' {
break;
}
}
if expecting_redirection_target {
expecting_redirection_target = false;
} else {
at_command_start = false;
}
}
'`' => {
let had_word = !current_word.is_empty();
flush_scanned_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
if had_word && expecting_redirection_target {
expecting_redirection_target = false;
}
index = scan_command_subst_backtick_segment(input, next_index)?;
if expecting_redirection_target {
expecting_redirection_target = false;
} else {
at_command_start = false;
}
}
'$' if input[next_index..].starts_with('\'') => {
let had_word = !current_word.is_empty();
flush_scanned_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
if had_word && expecting_redirection_target {
expecting_redirection_target = false;
}
index = scan_command_subst_ansi_c_single_quoted_segment(input, next_index)?;
if expecting_redirection_target {
expecting_redirection_target = false;
} else {
at_command_start = false;
}
}
'\\' => {
let had_word = !current_word.is_empty();
flush_scanned_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
if had_word && expecting_redirection_target {
expecting_redirection_target = false;
}
index = next_index;
if let Some((_, escaped_next)) = next_char_boundary(input, index) {
index = escaped_next;
}
if expecting_redirection_target {
expecting_redirection_target = false;
} else {
at_command_start = false;
}
}
'>' => {
let word_was_redirection_fd = current_word_started_at_command_start
&& !current_word.is_empty()
&& current_word.chars().all(|current| current.is_ascii_digit());
flush_scanned_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
if word_was_redirection_fd {
at_command_start = true;
}
index = next_index;
expecting_redirection_target = true;
}
'<' if input[next_index..].starts_with('<') => {
let word_was_redirection_fd = current_word_started_at_command_start
&& !current_word.is_empty()
&& current_word.chars().all(|current| current.is_ascii_digit());
let had_word = !current_word.is_empty();
flush_scanned_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
if had_word && expecting_redirection_target {
expecting_redirection_target = false;
}
if word_was_redirection_fd {
at_command_start = true;
}
if inside_unclosed_double_paren_on_line(input, index) {
index = next_index + '<'.len_utf8();
continue;
}
if input[next_index + '<'.len_utf8()..].starts_with('<') {
index = next_index + '<'.len_utf8() + '<'.len_utf8();
expecting_redirection_target = true;
continue;
}
let strip_tabs = input[next_index..].starts_with("<-");
let delimiter_start = next_index + if strip_tabs { 2 } else { 1 };
if let Some((delimiter_index, delimiter)) =
scan_command_subst_heredoc_delimiter(input, delimiter_start)
{
pending_heredocs.push((delimiter, strip_tabs));
index = delimiter_index;
expecting_redirection_target = false;
} else {
index = next_index;
expecting_redirection_target = true;
}
}
'\n' => {
flush_scanned_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
index = next_index;
for (delimiter, strip_tabs) in pending_heredocs.drain(..) {
index =
skip_command_subst_pending_heredoc(input, index, &delimiter, strip_tabs);
}
at_command_start = true;
expecting_redirection_target = false;
}
'$' if input[next_index..].starts_with('{') => {
let had_word = !current_word.is_empty();
flush_scanned_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
if had_word && expecting_redirection_target {
expecting_redirection_target = false;
}
let consumed = scan_command_subst_parameter_expansion_len(
&input[next_index + '{'.len_utf8()..],
subst_depth,
0,
)?;
index = next_index + '{'.len_utf8() + consumed;
if expecting_redirection_target {
expecting_redirection_target = false;
} else {
at_command_start = false;
}
}
'$' if input[next_index..].starts_with('(')
&& !input[next_index + '('.len_utf8()..].starts_with('(') =>
{
let had_word = !current_word.is_empty();
flush_scanned_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
if had_word && expecting_redirection_target {
expecting_redirection_target = false;
}
let consumed = scan_command_substitution_body_len_inner(
&input[next_index + '('.len_utf8()..],
subst_depth + 1,
)?;
index = next_index + '('.len_utf8() + consumed;
if expecting_redirection_target {
expecting_redirection_target = false;
} else {
at_command_start = false;
}
}
_ => {
if ch.is_ascii_alphanumeric() || ch == '_' {
if current_word.is_empty() && !expecting_redirection_target && at_command_start
{
current_word_started_at_command_start = true;
at_command_start = false;
}
current_word.push(ch);
} else {
let had_word = !current_word.is_empty();
flush_scanned_command_subst_keyword(
&mut current_word,
&mut pending_case_headers,
&mut case_clause_depths,
depth,
&mut current_word_started_at_command_start,
);
if had_word && expecting_redirection_target {
expecting_redirection_target = false;
}
match ch {
' ' | '\t' => {}
';' | '|' | '&' => {
at_command_start = true;
expecting_redirection_target = false;
}
_ => {
if !expecting_redirection_target {
at_command_start = false;
}
}
}
}
index = next_index;
}
}
}
None
}
pub(in crate::parser) fn scan_command_substitution_body_len(input: &str) -> Option<usize> {
scan_command_substitution_body_len_inner(input, 0)
}