use super::super::heredoc::{delimiter_word, is_terminator};
use super::{Heredoc, input_is_program_text};
pub(super) const HEREDOC_MARK: &str = "__TMHEREDOC";
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum OperatorCtx {
Code,
Ambiguous,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Ctx {
Code(usize),
Backtick,
Brace(bool),
Double,
Single,
Ansi,
Arith {
depth: usize,
bracket: bool,
},
}
struct Pending {
op: (usize, usize),
out: (usize, usize),
word: Option<String>,
strip_tabs: bool,
quoted: bool,
index: usize,
depth: usize,
low: usize,
}
pub(crate) fn heredoc_operators(text: &str) -> Vec<(usize, OperatorCtx)> {
let mut ops = Vec::new();
walk(text, &mut Vec::new(), &mut ops);
ops
}
pub(super) fn strip_comments_and_heredocs(text: &str, heredocs: &mut Vec<Heredoc>) -> String {
walk(text, heredocs, &mut Vec::new())
}
fn walk(text: &str, heredocs: &mut Vec<Heredoc>, ops: &mut Vec<(usize, OperatorCtx)>) -> 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);
let depth = stack.len();
for p in &mut pending {
p.low = p.low.min(depth);
}
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, len)) = dollar_opener(bytes, i, in_double) {
stack.push(ctx);
i += len - 1;
word_start = ctx == Ctx::Code(0);
}
}
_ => {}
}
}
Ctx::Arith { depth, bracket } => {
i = arith_byte(bytes, i, (depth, bracket), &mut stack, ops);
continue;
}
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'(' if at_word_start && bytes.get(i + 1) == Some(&b'(') => {
stack.push(Ctx::Arith {
depth: 0,
bracket: false,
});
i += 2;
continue;
}
b'(' | b')' => word_start = paren(b, top, &mut stack),
b'\n' if !pending.is_empty() => {
pending.retain(|p| {
let live = p.low >= p.depth;
if !live {
ops[p.index].1 = OperatorCtx::Ambiguous;
}
live
});
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, ops) {
if let Some(p) = pending.get_mut(waiting) {
(p.index, p.depth, p.low) = (ops.len() - 1, stack.len(), stack.len());
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>,
ops: &mut Vec<(usize, OperatorCtx)>,
) -> 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, len) = dollar_opener(bytes, i, false)?;
stack.push(ctx);
step(i + len, ctx == Ctx::Code(0))
}
b'<' if bytes.get(i + 1) == Some(&b'<') => {
if bytes.get(i + 2) == Some(&b'<') {
return step(i + 3, true);
}
ops.push((i, OperatorCtx::Code));
match read_operator(text, i) {
Some(p) => {
let (next, word_start) = (p.op.1, p.word.is_none());
pending.push(p);
step(next, word_start)
}
None => step(i + 2, true),
}
}
_ => None,
}
}
fn dollar_opener(bytes: &[u8], i: usize, in_double: bool) -> Option<(Ctx, usize)> {
let arith = |bracket| Ctx::Arith { depth: 0, bracket };
match (bytes.get(i + 1), bytes.get(i + 2)) {
(Some(b'('), Some(b'(')) => Some((arith(false), 3)),
(Some(b'('), _) => Some((Ctx::Code(0), 2)),
(Some(b'['), _) => Some((arith(true), 2)),
(Some(b'{'), _) => Some((Ctx::Brace(in_double), 2)),
(Some(b'\''), _) if !in_double => Some((Ctx::Ansi, 2)),
_ => None,
}
}
fn arith_byte(
bytes: &[u8],
i: usize,
(depth, bracket): (usize, bool),
stack: &mut Vec<Ctx>,
ops: &mut Vec<(usize, OperatorCtx)>,
) -> usize {
match bytes[i] {
b'\\' => return i + 2,
b'\'' => stack.push(Ctx::Single),
b'"' => stack.push(Ctx::Double),
b'`' => stack.push(Ctx::Backtick),
b'$' => {
if let Some((ctx, len)) = dollar_opener(bytes, i, false) {
stack.push(ctx);
return i + len;
}
}
b'<' if bytes.get(i + 1) == Some(&b'<') => {
ops.push((i, OperatorCtx::Ambiguous));
return i + 2;
}
b'(' | b'[' => set_top(
stack,
Ctx::Arith {
depth: depth + 1,
bracket,
},
),
b')' | b']' if depth > 0 => set_top(
stack,
Ctx::Arith {
depth: depth - 1,
bracket,
},
),
b']' if bracket => {
stack.pop();
}
b')' if !bracket => {
stack.pop();
if bytes.get(i + 1) == Some(&b')') {
return i + 2;
}
}
_ => {}
}
i + 1
}
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 line_end = text[at..].find('\n').map_or(text.len(), |n| at + n);
let (word, quoted, op_end) = match delimiter_word(&bytes[..line_end], j) {
Some((word, _, _)) if word.is_empty() => return None,
Some((word, quoted, end)) => (Some(word), quoted, end),
None => (None, true, at + 2),
};
Some(Pending {
op: (at, op_end),
out: (0, 0),
word,
strip_tabs,
quoted,
index: 0,
depth: 0,
low: 0,
})
}
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 word = p.word.as_deref()?;
let mut start = from;
while start <= text.len() {
let end = text[start..].find('\n').map_or(text.len(), |n| start + n);
if is_terminator(&text[start..end], word, p.strip_tabs, end == text.len()) {
return Some((start, (end + 1).min(text.len())));
}
if end >= text.len() {
return None;
}
start = end + 1;
}
None
}