use super::Lexer;
pub fn parse_heredoc_delimiter(raw: &str) -> (String, bool) {
let mut result = String::new();
let mut quoted = false;
let mut chars = raw.chars();
while let Some(c) = chars.next() {
match c {
'\'' => {
quoted = true;
for c in chars.by_ref() {
if c == '\'' {
break;
}
result.push(c);
}
}
'"' => {
quoted = true;
for c in chars.by_ref() {
if c == '"' {
break;
}
result.push(c);
}
}
'\\' => {
quoted = true;
if let Some(next) = chars.next() {
result.push(next);
}
}
_ => result.push(c),
}
}
(result, quoted)
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct PendingHereDoc {
pub delimiter: String,
pub strip_tabs: bool,
pub quoted: bool,
}
struct ReadLine {
raw: String,
line: String,
eof_after_backslash: bool,
}
impl Lexer {
pub fn queue_heredoc(&mut self, delimiter: String, strip_tabs: bool, quoted: bool) {
self.pending_heredocs.push(PendingHereDoc {
delimiter,
strip_tabs,
quoted,
});
}
pub(super) fn read_pending_heredocs(&mut self) {
let pending: Vec<_> = self.pending_heredocs.drain(..).collect();
for hd in pending {
let content = self.read_heredoc_body(&hd.delimiter, hd.strip_tabs);
self.heredoc_contents.push(content);
}
}
pub(super) fn read_heredoc_body(&mut self, delimiter: &str, strip_tabs: bool) -> String {
self.read_heredoc_body_teed(delimiter, strip_tabs, false, |_| {})
}
pub(super) fn read_heredoc_body_teed<F: FnMut(char)>(
&mut self,
delimiter: &str,
strip_tabs: bool,
cmdsub_mode: bool,
mut tee: F,
) -> String {
let mut content = String::new();
loop {
if self.at_end() {
break;
}
let ReadLine {
mut raw,
line,
eof_after_backslash,
} = self.read_heredoc_line();
let check_line = if strip_tabs {
line.trim_start_matches('\t')
} else {
line.as_str()
};
let mut matched = check_line == delimiter || check_line.trim_end() == delimiter;
if !matched && cmdsub_mode {
matched = self.try_rewind_cmdsub_close(&mut raw, &line, delimiter, strip_tabs);
}
for c in raw.chars() {
tee(c);
}
if matched {
break;
}
if strip_tabs {
content.push_str(line.trim_start_matches('\t'));
} else {
content.push_str(&line);
}
if !eof_after_backslash {
content.push('\n');
}
}
content
}
fn read_heredoc_line(&mut self) -> ReadLine {
let mut raw = String::new();
let mut line = String::new();
let mut prev_backslash = false;
let mut eof_after_backslash = false;
while let Some(c) = self.peek_char() {
self.advance_char();
raw.push(c);
if c == '\n' {
break;
}
if c == '\\' && !prev_backslash && self.peek_char() == Some('\n') {
self.advance_char();
raw.push('\n');
prev_backslash = false;
continue;
}
if c == '\\' && !prev_backslash && self.peek_char().is_none() {
line.push('\\');
line.push('\\');
prev_backslash = false;
eof_after_backslash = true;
} else {
prev_backslash = c == '\\' && !prev_backslash;
line.push(c);
}
}
ReadLine {
raw,
line,
eof_after_backslash,
}
}
fn try_rewind_cmdsub_close(
&mut self,
raw: &mut String,
line: &str,
delimiter: &str,
strip_tabs: bool,
) -> bool {
if raw.ends_with('\n') || !raw.ends_with(')') {
return false;
}
let n_paren = raw.chars().rev().take_while(|&c| c == ')').count();
let trimmed_len = line.len().saturating_sub(n_paren);
let without = &line[..trimmed_len];
let without_check = if strip_tabs {
without.trim_start_matches('\t')
} else {
without
};
if without_check != delimiter && without_check.trim_end() != delimiter {
return false;
}
for _ in 0..n_paren {
self.pos -= 1;
raw.pop();
}
true
}
pub fn take_heredoc_content(&mut self) -> Option<String> {
if self.heredoc_contents.is_empty() {
None
} else {
Some(self.heredoc_contents.remove(0))
}
}
}