use super::{Heredoc, input_is_program_text};
pub(super) const HEREDOC_MARK: &str = "__TMHEREDOC";
#[derive(Clone, Copy, PartialEq, Eq)]
enum Ctx {
Code(usize),
Backtick,
Brace(bool),
Double,
Single,
Ansi,
}
struct Pending {
op: (usize, usize),
out: (usize, usize),
word: String,
strip_tabs: bool,
quoted: bool,
}
pub(super) fn strip_comments_and_heredocs(text: &str, heredocs: &mut Vec<Heredoc>) -> String {
let bytes = text.as_bytes();
let mut out = String::with_capacity(text.len());
let mut copied = 0;
let mut stack = vec![Ctx::Code(0)];
let mut pending: Vec<Pending> = Vec::new();
let mut word_start = true;
let mut i = 0;
while i < bytes.len() {
let top = stack.last().copied().unwrap_or(Ctx::Code(0));
let b = bytes[i];
let at_word_start = std::mem::replace(&mut word_start, false);
match top {
Ctx::Single => {
if b == b'\'' {
stack.pop();
}
}
Ctx::Ansi => match b {
b'\\' => i += 1,
b'\'' => {
stack.pop();
}
_ => {}
},
Ctx::Double | Ctx::Brace(_) => {
let in_double = top != Ctx::Brace(false);
match b {
b'\\' => i += 1,
b'"' if top == Ctx::Double => {
stack.pop();
}
b'}' if top != Ctx::Double => {
stack.pop();
}
b'"' => stack.push(Ctx::Double),
b'\'' if !in_double => stack.push(Ctx::Single),
b'`' => {
stack.push(Ctx::Backtick);
word_start = true;
}
b'$' => {
if let Some(ctx) = dollar_opener(bytes, i, in_double) {
stack.push(ctx);
i += 1;
word_start = ctx == Ctx::Code(0);
}
}
_ => {}
}
}
Ctx::Code(_) | Ctx::Backtick => match b {
b'#' if at_word_start => {
let end = comment_end(text, i, top == Ctx::Backtick);
out.push_str(&text[copied..i]);
copied = end;
i = end;
continue;
}
b'`' if top == Ctx::Backtick => {
stack.pop();
}
b'(' | b')' => word_start = paren(b, top, &mut stack),
b'\n' if !pending.is_empty() => {
let done = consume_bodies(text, i + 1, &mut pending, heredocs);
for ((from, to), index) in done.stripped.into_iter().rev() {
out.replace_range(from..to, &format!(" {HEREDOC_MARK}{index}__ "));
}
out.push_str(&text[copied..=i]);
for (start, end) in done.bodies {
out.push_str(&text[start..end]);
}
copied = done.next;
i = done.next;
word_start = true;
continue;
}
_ => {
let waiting = pending.len();
if let Some(step) = code_byte(text, i, &mut stack, &mut pending) {
if let Some(p) = pending.get_mut(waiting) {
out.push_str(&text[copied..p.op.1]);
p.out = (out.len() - (p.op.1 - p.op.0), out.len());
copied = p.op.1;
}
word_start = step.word_start;
i = step.next;
continue;
}
word_start = is_word_break(b);
}
},
}
i += 1;
}
out.push_str(&text[copied.min(text.len())..]);
out
}
struct Step {
next: usize,
word_start: bool,
}
fn code_byte(
text: &str,
i: usize,
stack: &mut Vec<Ctx>,
pending: &mut Vec<Pending>,
) -> Option<Step> {
let bytes = text.as_bytes();
let step = |next, word_start| Some(Step { next, word_start });
match bytes[i] {
b'\\' => step(i + 2, false),
b'\'' => {
stack.push(Ctx::Single);
step(i + 1, false)
}
b'"' => {
stack.push(Ctx::Double);
step(i + 1, false)
}
b'`' => {
stack.push(Ctx::Backtick);
step(i + 1, true)
}
b'$' => {
let ctx = dollar_opener(bytes, i, false)?;
stack.push(ctx);
step(i + 2, ctx == Ctx::Code(0))
}
b'<' if bytes.get(i + 1) == Some(&b'<') => {
if bytes.get(i + 2) == Some(&b'<') {
return step(i + 3, true);
}
match read_operator(text, i) {
Some(p) => {
let next = p.op.1;
pending.push(p);
step(next, false)
}
None => step(i + 2, true),
}
}
_ => None,
}
}
fn dollar_opener(bytes: &[u8], i: usize, in_double: bool) -> Option<Ctx> {
match bytes.get(i + 1) {
Some(b'(') => Some(Ctx::Code(0)),
Some(b'{') => Some(Ctx::Brace(in_double)),
Some(b'\'') if !in_double => Some(Ctx::Ansi),
_ => None,
}
}
fn is_word_break(b: u8) -> bool {
matches!(b, b' ' | b'\t' | b'\n' | b';' | b'&' | b'|' | b'<' | b'>')
}
fn comment_end(text: &str, at: usize, in_backtick: bool) -> usize {
let rest = &text[at..];
let newline = rest.find('\n').unwrap_or(rest.len());
let tick = if in_backtick {
rest.find('`').unwrap_or(rest.len())
} else {
rest.len()
};
at + newline.min(tick)
}
fn paren(b: u8, top: Ctx, stack: &mut Vec<Ctx>) -> bool {
let Ctx::Code(parens) = top else {
return true;
};
if b == b'(' {
set_top(stack, Ctx::Code(parens + 1));
} else if parens == 0 && stack.len() > 1 {
stack.pop();
return false;
} else {
set_top(stack, Ctx::Code(parens.saturating_sub(1)));
}
true
}
fn set_top(stack: &mut [Ctx], ctx: Ctx) {
if let Some(top) = stack.last_mut() {
*top = ctx;
}
}
fn read_operator(text: &str, at: usize) -> Option<Pending> {
let bytes = text.as_bytes();
let mut j = at + 2;
let strip_tabs = bytes.get(j) == Some(&b'-');
if strip_tabs {
j += 1;
}
while matches!(bytes.get(j), Some(b' ' | b'\t')) {
j += 1;
}
let mut word = Vec::new();
let mut quoted = false;
while let Some(&b) = bytes.get(j) {
match b {
b' ' | b'\t' | b'\n' | b'<' | b'>' | b'|' | b';' | b'&' | b'(' | b')' => break,
b'\'' | b'"' => {
quoted = true;
let close = text[j + 1..].find(b as char)?;
word.extend_from_slice(&bytes[j + 1..j + 1 + close]);
j += close + 2;
}
b'\\' => {
quoted = true;
word.push(*bytes.get(j + 1)?);
j += 2;
}
_ => {
word.push(b);
j += 1;
}
}
}
let word = String::from_utf8(word).ok()?;
let usable = !word.is_empty() && !word.contains('\n') && text.is_char_boundary(j);
usable.then_some(Pending {
op: (at, j),
out: (0, 0),
word,
strip_tabs,
quoted,
})
}
struct Consumed {
next: usize,
stripped: Vec<((usize, usize), usize)>,
bodies: Vec<(usize, usize)>,
}
fn consume_bodies(
text: &str,
from: usize,
pending: &mut Vec<Pending>,
heredocs: &mut Vec<Heredoc>,
) -> Consumed {
let mut done = Consumed {
next: from,
stripped: Vec::new(),
bodies: Vec::new(),
};
for p in pending.drain(..) {
let Some((body_end, next)) = find_terminator(text, done.next, &p) else {
return done;
};
if p.quoted {
heredocs.push(Heredoc {
program_text: input_is_program_text(&text[done.next..body_end]),
});
done.stripped.push((p.out, heredocs.len() - 1));
} else {
done.bodies.push((done.next, next));
}
done.next = next;
}
done
}
fn find_terminator(text: &str, from: usize, p: &Pending) -> Option<(usize, usize)> {
let mut start = from;
while start <= text.len() {
let end = text[start..].find('\n').map_or(text.len(), |n| start + n);
let line = text[start..end].trim_end_matches('\r');
let line = if p.strip_tabs {
line.trim_start_matches('\t')
} else {
line
};
if line == p.word {
return Some((start, (end + 1).min(text.len())));
}
if end >= text.len() {
return None;
}
start = end + 1;
}
None
}