pub(crate) fn strip_quoted_heredoc_bodies(command: &str) -> String {
if !command.contains("<<") {
return command.to_string();
}
let mut out: Vec<&str> = Vec::new();
let mut pending: Vec<String> = Vec::new();
for line in command.lines() {
if pending.is_empty() {
out.push(line);
pending = heredoc_delims(line, true);
} else if line.trim_start_matches('\t').trim() == pending[0] {
pending.remove(0);
}
}
out.join("\n")
}
pub fn strip_all_heredoc_bodies(command: &str) -> String {
if !command.contains("<<") {
return command.to_string();
}
let mut out: Vec<&str> = Vec::new();
let mut pending: Vec<String> = Vec::new();
for line in command.lines() {
if pending.is_empty() {
out.push(line);
pending = heredoc_delims(line, false);
} else if line.trim_start_matches('\t').trim() == pending[0] {
pending.remove(0);
}
}
out.join("\n")
}
pub(crate) fn heredoc_delims(line: &str, quoted_only: bool) -> Vec<String> {
let bytes = line.as_bytes();
let len = bytes.len();
let mut i = 0;
let mut in_single = false;
let mut in_double = false;
let mut delims = Vec::new();
while i < len {
let ch = bytes[i];
if in_single {
if ch == b'\'' {
in_single = false;
}
i += 1;
continue;
}
if in_double {
match ch {
b'\\' => i = (i + 2).min(len),
b'"' => {
in_double = false;
i += 1;
}
_ => i += 1,
}
continue;
}
match ch {
b'\\' => i = (i + 2).min(len),
b'\'' => {
in_single = true;
i += 1;
}
b'"' => {
in_double = true;
i += 1;
}
b'<' if i + 1 < len && bytes[i + 1] == b'<' => {
if i + 2 < len && bytes[i + 2] == b'<' {
i += 3;
continue;
}
let mut j = i + 2;
if j < len && bytes[j] == b'-' {
j += 1; }
while j < len && (bytes[j] == b' ' || bytes[j] == b'\t') {
j += 1;
}
if let Some((delim, quoted, next)) = read_heredoc_delim(bytes, j) {
if !quoted_only || quoted {
delims.push(delim);
}
i = next;
continue;
}
i = j;
}
_ => i += 1,
}
}
delims
}
pub(crate) fn read_heredoc_delim(bytes: &[u8], start: usize) -> Option<(String, bool, usize)> {
let len = bytes.len();
let mut i = start;
let mut name: Vec<u8> = Vec::new();
let mut quoted = false;
while i < len {
match bytes[i] {
b'\'' => {
quoted = true;
i += 1;
while i < len && bytes[i] != b'\'' {
name.push(bytes[i]);
i += 1;
}
i += usize::from(i < len); }
b'"' => {
quoted = true;
i += 1;
while i < len && bytes[i] != b'"' {
name.push(bytes[i]);
i += 1;
}
i += usize::from(i < len);
}
b'\\' => {
quoted = true;
i += 1;
if i < len {
name.push(bytes[i]);
i += 1;
}
}
b' ' | b'\t' | b'<' | b'>' | b'|' | b'&' | b';' => break,
c => {
name.push(c);
i += 1;
}
}
}
if name.is_empty() {
None
} else {
Some((String::from_utf8_lossy(&name).into_owned(), quoted, i))
}
}
pub(crate) fn strip_comments(command: &str) -> String {
if !command.contains('#') {
return command.to_string();
}
let bytes = command.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
let mut in_single = false;
let mut in_double = false;
let mut at_boundary = true;
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
if in_single {
out.push(c);
if c == b'\'' {
in_single = false;
}
at_boundary = false;
i += 1;
continue;
}
if in_double {
if c == b'\\' && i + 1 < bytes.len() {
out.push(c);
out.push(bytes[i + 1]);
at_boundary = false;
i += 2;
continue;
}
out.push(c);
if c == b'"' {
in_double = false;
}
at_boundary = false;
i += 1;
continue;
}
match c {
b'\'' => {
in_single = true;
out.push(c);
at_boundary = false;
i += 1;
}
b'"' => {
in_double = true;
out.push(c);
at_boundary = false;
i += 1;
}
b'\\' => {
out.push(c);
if i + 1 < bytes.len() {
out.push(bytes[i + 1]);
i += 2;
} else {
i += 1;
}
at_boundary = false;
}
b'#' if at_boundary => {
while i < bytes.len() && bytes[i] != b'\n' && bytes[i] != b'\r' {
i += 1;
}
}
b'\n' | b'\r' | b' ' | b'\t' | b';' | b'&' | b'|' | b'(' => {
out.push(c);
at_boundary = true;
i += 1;
}
_ => {
out.push(c);
at_boundary = false;
i += 1;
}
}
}
String::from_utf8(out).unwrap_or_else(|_| command.to_string())
}