use std::borrow::Cow;
fn contains_shell_metas(s: &str) -> bool {
let bytes = s.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
match b {
b' ' | b'\t' | b'\n' | b'\'' | b'"' | b'\\' | b'|' | b'&' | b';' | b'(' | b')'
| b'<' | b'>' | b'!' | b'{' | b'}' | b'*' | b'[' | b'?' | b']' | b'^' | b'$' | b'`' => {
return true;
}
b'~' if i == 0 || bytes[i - 1] == b'=' || bytes[i - 1] == b':' => return true,
b'#' if i == 0 => return true,
_ => {}
}
}
false
}
fn should_ansic_quote(s: &str) -> bool {
s.chars().any(|c| c.is_control())
}
fn single_quote(s: &str) -> String {
if s == "'" {
return "\\'".to_string();
}
let mut out = String::with_capacity(s.len() + 2);
out.push('\'');
for c in s.chars() {
if c == '\'' {
out.push_str("'\\''");
} else {
out.push(c);
}
}
out.push('\'');
out
}
fn ansic_quote(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 3);
out.push_str("$'");
for c in s.chars() {
let esc = match c {
'\x1b' => Some('E'),
'\x07' => Some('a'),
'\x0b' => Some('v'),
'\x08' => Some('b'),
'\x0c' => Some('f'),
'\n' => Some('n'),
'\r' => Some('r'),
'\t' => Some('t'),
'\\' | '\'' => Some(c),
_ => None,
};
if let Some(e) = esc {
out.push('\\');
out.push(e);
} else if c.is_control() {
let mut buf = [0u8; 4];
for b in c.encode_utf8(&mut buf).bytes() {
out.push_str(&format!("\\{b:03o}"));
}
} else {
out.push(c);
}
}
out.push('\'');
out
}
pub(super) fn quote_word(s: &str) -> Cow<'_, str> {
if s.is_empty() {
Cow::Borrowed("''")
} else {
quote_value(s)
}
}
pub(super) fn quote_value(s: &str) -> Cow<'_, str> {
if contains_shell_metas(s) {
Cow::Owned(single_quote(s))
} else if should_ansic_quote(s) {
Cow::Owned(ansic_quote(s))
} else {
Cow::Borrowed(s)
}
}
pub(super) fn prefix(ps4: &str, level: usize) -> String {
let mut chars = ps4.chars();
let Some(first) = chars.next() else {
return String::new();
};
let mut out = String::with_capacity(ps4.len() + level);
for _ in 0..level.max(1) {
out.push(first);
}
out.push_str(chars.as_str());
out
}
pub(super) fn cond_term(invert: bool, args: &[String]) -> String {
let show = |s: &str| -> String {
if s.is_empty() {
"''".to_string()
} else {
s.to_string()
}
};
let mut out = String::from("[[ ");
if invert {
out.push_str("! ");
}
match args {
[word] => {
out.push_str("-n ");
out.push_str(&show(word));
}
[op, arg] => {
out.push_str(op);
out.push(' ');
out.push_str(&show(arg));
}
[lhs, op, rhs] => {
out.push_str(&show(lhs));
out.push(' ');
out.push_str(op);
out.push(' ');
out.push_str(&show(rhs));
}
other => {
let words: Vec<String> = other.iter().map(|w| show(w)).collect();
out.push_str(&words.join(" "));
}
}
out.push_str(" ]]");
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn words_quote_like_bash() {
assert_eq!(quote_word("plain"), "plain");
assert_eq!(quote_word(""), "''");
assert_eq!(quote_word("a b"), "'a b'");
assert_eq!(quote_word("it's"), "'it'\\''s'");
assert_eq!(quote_word("'"), "\\'");
assert_eq!(quote_word("*"), "'*'");
assert_eq!(quote_word("a#"), "a#");
assert_eq!(quote_word("#a"), "'#a'");
assert_eq!(quote_word("~a"), "'~a'");
assert_eq!(quote_word("a~"), "a~");
assert_eq!(quote_word("a=~b"), "'a=~b'");
assert_eq!(quote_word("tab\there"), "'tab\there'");
assert_eq!(quote_word("\x01x"), "$'\\001x'");
assert_eq!(quote_word("\x1b"), "$'\\E'");
assert_eq!(quote_word("é"), "é");
}
#[test]
fn empty_value_stays_bare() {
assert_eq!(quote_value(""), "");
assert_eq!(quote_value("x y"), "'x y'");
}
#[test]
fn prefix_repeats_first_char() {
assert_eq!(prefix("+ ", 1), "+ ");
assert_eq!(prefix("+ ", 3), "+++ ");
assert_eq!(prefix("+ [3] ", 2), "++ [3] ");
assert_eq!(prefix("", 2), "");
assert_eq!(prefix("é>", 2), "éé>");
}
#[test]
fn cond_terms() {
let s = |v: &[&str]| v.iter().map(|x| x.to_string()).collect::<Vec<_>>();
assert_eq!(cond_term(false, &s(&["abc"])), "[[ -n abc ]]");
assert_eq!(cond_term(true, &s(&["-z", "abc"])), "[[ ! -z abc ]]");
assert_eq!(cond_term(false, &s(&["", "==", "a b"])), "[[ '' == a b ]]");
}
}