pub fn translate(ps: &str) -> String {
let mut out = String::with_capacity(ps.len() + 16);
let mut chars = ps.chars().peekable();
while let Some(c) = chars.next() {
if c == '%' {
out.push_str("%%");
continue;
}
if c != '\\' {
out.push(c);
continue;
}
let Some(esc) = chars.next() else {
out.push('\\');
break;
};
match esc {
'u' => out.push_str("%n"),
'h' => out.push_str("%m"),
'H' => out.push_str("%M"),
'w' => out.push_str("%~"),
'W' => out.push_str("%1~"),
'j' => out.push_str("%j"),
'l' => out.push_str("%l"),
'!' => out.push_str("%!"),
'$' => out.push_str("%(!.#.$)"),
'#' => out.push_str("%!"),
't' => out.push_str("%D{%H:%M:%S}"),
'T' => out.push_str("%D{%I:%M:%S}"),
'@' => out.push_str("%D{%I:%M %p}"),
'A' => out.push_str("%D{%H:%M}"),
'd' => out.push_str("%D{%a %b %d}"),
'D' => {
if chars.peek() == Some(&'{') {
chars.next();
let mut fmt = String::new();
for f in chars.by_ref() {
if f == '}' {
break;
}
fmt.push(f);
}
out.push_str("%D{");
out.push_str(&fmt);
out.push('}');
} else {
out.push_str("%D{%X}");
}
}
'[' => out.push_str("%{"),
']' => out.push_str("%}"),
'n' => out.push('\n'),
'r' => out.push('\r'),
'a' => out.push('\u{07}'),
'e' => out.push('\u{1b}'),
'\\' => out.push('\\'),
's' => out.push_str(&shell_name()),
'v' => out.push_str(&format!(
"{}.{}",
crate::dash_mode::BASH_VERSION_MAJOR,
crate::dash_mode::BASH_VERSION_MINOR
)),
'V' => out.push_str(&format!(
"{}.{}.{}",
crate::dash_mode::BASH_VERSION_MAJOR,
crate::dash_mode::BASH_VERSION_MINOR,
crate::dash_mode::BASH_VERSION_PATCH
)),
'0'..='7' => {
let mut oct = String::from(esc);
while oct.len() < 3 {
match chars.peek() {
Some(d @ '0'..='7') => {
oct.push(*d);
chars.next();
}
_ => break,
}
}
match u32::from_str_radix(&oct, 8).ok().and_then(char::from_u32) {
Some(ch) if ch == '%' => out.push_str("%%"),
Some(ch) => out.push(ch),
None => out.push_str(&oct),
}
}
other => {
out.push('\\');
if other == '%' {
out.push_str("%%");
} else {
out.push(other);
}
}
}
}
out
}
fn shell_name() -> String {
crate::ported::params::getsparam("0")
.map(|z| {
let bare = z.trim_start_matches('-');
std::path::Path::new(bare)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(bare)
.to_string()
})
.unwrap_or_else(|| "bash".to_string())
}
thread_local! {
static IN_TRANSLATION: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
pub struct TranslationGuard;
impl Drop for TranslationGuard {
fn drop(&mut self) {
IN_TRANSLATION.with(|c| c.set(false));
}
}
pub fn begin_translation(s: &str) -> Option<(String, TranslationGuard)> {
if !crate::dash_mode::bash_mode() || IN_TRANSLATION.with(|c| c.get()) {
return None;
}
let translated = translate(s);
IN_TRANSLATION.with(|c| c.set(true));
Some((translated, TranslationGuard))
}
pub fn expand(ps: &str) -> String {
let expanded = crate::ported::prompt::expand_prompt(ps);
if expanded.contains(['\u{01}', '\u{02}']) {
expanded
.chars()
.filter(|c| *c != '\u{01}' && *c != '\u{02}')
.collect()
} else {
expanded
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escapes_translate_to_their_zsh_counterparts() {
assert_eq!(translate(r"\u@\h"), "%n@%m");
assert_eq!(translate(r"\u@\H"), "%n@%M");
assert_eq!(translate(r"\w"), "%~");
assert_eq!(translate(r"\W"), "%1~");
assert_eq!(translate(r"\j \l \!"), "%j %l %!");
assert_eq!(
translate(r"\t|\T|\A"),
"%D{%H:%M:%S}|%D{%I:%M:%S}|%D{%H:%M}"
);
assert_eq!(translate(r"\D{%F}"), "%D{%F}");
assert_eq!(translate(r"\d"), "%D{%a %b %d}");
assert_eq!(translate(r"\D"), "%D{%X}");
assert_eq!(translate(r"\[\e[0m\]"), "%{\u{1b}[0m%}");
}
#[test]
fn dollar_escape_is_not_zsh_percent_hash() {
let t = translate(r"\$");
assert_eq!(t, "%(!.#.$)");
assert!(!t.contains("%#"), "must not map onto zsh's %# ");
}
#[test]
fn literal_percent_is_doubled() {
assert_eq!(translate("100% done"), "100%% done");
assert_eq!(translate(r"\u 50%"), "%n 50%%");
assert_eq!(translate(r"\045"), "%%");
}
#[test]
fn octal_escapes_decode() {
assert_eq!(translate(r"\101"), "A");
assert_eq!(translate(r"\1011"), "A1");
assert_eq!(translate(r"\7"), "\u{07}");
}
#[test]
fn backslash_edge_cases_match_bash() {
assert_eq!(translate(r"\\"), r"\");
assert_eq!(translate(r"end\"), r"end\");
assert_eq!(translate(r"\q"), r"\q");
assert_eq!(translate(r"\z\u"), r"\z%n");
}
#[test]
fn control_escapes_become_characters() {
assert_eq!(translate(r"a\nb"), "a\nb");
assert_eq!(translate(r"a\rb"), "a\rb");
assert_eq!(translate(r"\a"), "\u{07}");
assert_eq!(translate(r"\e"), "\u{1b}");
}
}