use std::collections::VecDeque;
use super::{CommandBindings, Placeholder, QuoteContext, find_substitution};
pub(super) struct PosixLexicalState {
in_comment: bool,
pending_heredocs: VecDeque<HeredocDelimiter>,
heredoc_body: Option<HeredocBody>,
}
struct HeredocDelimiter {
text: String,
strips_leading_tabs: bool,
}
struct HeredocBody {
delimiter: HeredocDelimiter,
line_start: usize,
}
#[derive(Clone, Copy)]
enum DelimiterQuoteContext {
Unquoted,
Single,
Double,
}
pub(super) struct PosixCharacter<'chars, 'output> {
pub(super) chars: &'chars [char],
pub(super) pos: usize,
pub(super) ch: char,
pub(super) output: &'output mut String,
}
impl PosixLexicalState {
pub(super) const fn new() -> Self {
Self {
in_comment: false,
pending_heredocs: VecDeque::new(),
heredoc_body: None,
}
}
pub(super) fn append_inert_character(
&mut self,
character: &mut PosixCharacter<'_, '_>,
) -> bool {
if self.in_comment {
character.output.push(character.ch);
if character.ch == '\n' {
self.in_comment = false;
self.begin_next_heredoc(character.pos + 1);
}
return true;
}
let Some(body) = self.heredoc_body.as_mut() else {
return false;
};
character.output.push(character.ch);
if character.ch == '\n' {
if body.matches_terminator(character.chars, character.pos) {
self.heredoc_body = None;
self.begin_next_heredoc(character.pos + 1);
} else {
body.line_start = character.pos + 1;
}
}
true
}
pub(super) const fn begin_comment(&mut self) {
self.in_comment = true;
}
pub(super) fn starts_comment(chars: &[char], pos: usize, ch: char) -> bool {
ch == '#' && is_shell_word_boundary(chars, pos)
}
pub(super) fn append_heredoc_declaration(
&mut self,
character: &mut PosixCharacter<'_, '_>,
bindings: &CommandBindings,
) -> Option<usize> {
if character.ch != '<' || character.chars.get(character.pos + 1) != Some(&'<') {
return None;
}
let mut delimiter_start = character.pos + 2;
let strips_leading_tabs = character.chars.get(delimiter_start) == Some(&'-');
if strips_leading_tabs {
delimiter_start += 1;
}
while matches!(character.chars.get(delimiter_start), Some(' ' | '\t')) {
delimiter_start += 1;
}
let (_, end) = parse_delimiter(character.chars, delimiter_start)?;
let rendered_delimiter = render_delimiter(character.chars, delimiter_start, end, bindings);
let rendered_chars: Vec<_> = rendered_delimiter.chars().collect();
let (text, _) = parse_delimiter(&rendered_chars, 0)?;
character
.output
.extend(character.chars.get(character.pos..delimiter_start)?.iter());
character.output.push_str(&rendered_delimiter);
self.pending_heredocs.push_back(HeredocDelimiter {
text,
strips_leading_tabs,
});
Some(end)
}
pub(super) fn begin_pending_heredoc_after_newline(&mut self, next_pos: usize) {
self.begin_next_heredoc(next_pos);
}
fn begin_next_heredoc(&mut self, line_start: usize) {
let Some(delimiter) = self.pending_heredocs.pop_front() else {
return;
};
self.heredoc_body = Some(HeredocBody {
delimiter,
line_start,
});
}
}
impl HeredocBody {
fn matches_terminator(&self, chars: &[char], line_end: usize) -> bool {
let Some(line) = chars.get(self.line_start..line_end) else {
return false;
};
if self.delimiter.strips_leading_tabs {
return line
.iter()
.skip_while(|character| **character == '\t')
.copied()
.eq(self.delimiter.text.chars());
}
line.iter().copied().eq(self.delimiter.text.chars())
}
}
fn is_shell_word_boundary(chars: &[char], pos: usize) -> bool {
let Some(previous_pos) = preceding_shell_character(chars, pos) else {
return true;
};
is_unescaped_character(chars, pos)
&& is_unescaped_character(chars, previous_pos)
&& chars.get(previous_pos).is_some_and(|previous| {
previous.is_whitespace() || matches!(previous, ';' | '|' | '&' | '(' | ')' | '<' | '>')
})
}
fn preceding_shell_character(chars: &[char], pos: usize) -> Option<usize> {
let mut end = pos;
loop {
let previous = end.checked_sub(1)?;
if chars.get(previous) != Some(&'\n') || is_unescaped_character(chars, previous) {
return Some(previous);
}
end = previous.checked_sub(1)?;
}
}
fn is_unescaped_character(chars: &[char], pos: usize) -> bool {
chars
.get(..pos)
.into_iter()
.flatten()
.rev()
.take_while(|character| **character == '\\')
.count()
.rem_euclid(2)
== 0
}
fn parse_delimiter(chars: &[char], start: usize) -> Option<(String, usize)> {
let mut text = String::new();
let mut quote_context = DelimiterQuoteContext::Unquoted;
let mut pos = start;
let mut consumed = false;
while let Some(ch) = chars.get(pos).copied() {
if matches!(quote_context, DelimiterQuoteContext::Unquoted) && is_word_terminator(ch) {
break;
}
match (quote_context, ch) {
(DelimiterQuoteContext::Unquoted, '\'') => {
quote_context = DelimiterQuoteContext::Single;
pos += 1;
consumed = true;
}
(DelimiterQuoteContext::Unquoted, '"') => {
quote_context = DelimiterQuoteContext::Double;
pos += 1;
consumed = true;
}
(DelimiterQuoteContext::Unquoted, '\\') => {
let next = *chars.get(pos + 1)?;
text.push(next);
pos += 2;
consumed = true;
}
(DelimiterQuoteContext::Single, '\'') | (DelimiterQuoteContext::Double, '"') => {
quote_context = DelimiterQuoteContext::Unquoted;
pos += 1;
consumed = true;
}
(DelimiterQuoteContext::Double, '\\') => {
let next = *chars.get(pos + 1)?;
if matches!(next, '$' | '`' | '"' | '\\' | '\n') {
text.push(next);
} else {
text.push('\\');
text.push(next);
}
pos += 2;
consumed = true;
}
_ => {
text.push(ch);
pos += 1;
consumed = true;
}
}
}
consumed.then_some((text, pos))
}
fn render_delimiter(
chars: &[char],
start: usize,
end: usize,
bindings: &CommandBindings,
) -> String {
let mut rendered = String::new();
let mut quote_context = DelimiterQuoteContext::Unquoted;
let mut pos = start;
while pos < end
&& let Some(ch) = chars.get(pos).copied()
{
if let Some(next) = escaped_delimiter_character(chars, pos, end, quote_context) {
rendered.push(ch);
rendered.push(next);
pos += 2;
continue;
}
if let Some((placeholder, skip)) = matching_delimiter_marker(chars, pos, end) {
rendered.push_str(bindings.substitution(placeholder, quote_context.into()));
pos += skip;
continue;
}
rendered.push(ch);
update_delimiter_quote_context(&mut quote_context, ch);
pos += 1;
}
rendered
}
fn escaped_delimiter_character(
chars: &[char],
pos: usize,
end: usize,
quote_context: DelimiterQuoteContext,
) -> Option<char> {
let ch = chars.get(pos).copied()?;
if ch != '\\' || matches!(quote_context, DelimiterQuoteContext::Single) {
return None;
}
let next_pos = pos.checked_add(1)?;
if next_pos >= end || find_substitution(chars, next_pos).is_some() {
return None;
}
chars.get(next_pos).copied()
}
fn matching_delimiter_marker(
chars: &[char],
pos: usize,
end: usize,
) -> Option<(Placeholder, usize)> {
let (placeholder, skip) = find_substitution(chars, pos)?;
(pos + skip <= end).then_some((placeholder, skip))
}
const fn update_delimiter_quote_context(quote_context: &mut DelimiterQuoteContext, ch: char) {
match (*quote_context, ch) {
(DelimiterQuoteContext::Unquoted, '\'') => *quote_context = DelimiterQuoteContext::Single,
(DelimiterQuoteContext::Unquoted, '"') => *quote_context = DelimiterQuoteContext::Double,
(DelimiterQuoteContext::Single, '\'') | (DelimiterQuoteContext::Double, '"') => {
*quote_context = DelimiterQuoteContext::Unquoted;
}
_ => {}
}
}
impl From<DelimiterQuoteContext> for QuoteContext {
fn from(context: DelimiterQuoteContext) -> Self {
match context {
DelimiterQuoteContext::Unquoted => Self::Unquoted,
DelimiterQuoteContext::Single => Self::Single,
DelimiterQuoteContext::Double => Self::Double,
}
}
}
const fn is_word_terminator(ch: char) -> bool {
ch.is_whitespace() || matches!(ch, ';' | '|' | '&' | '(' | ')' | '<' | '>')
}