use super::cmds;
pub fn zsh_var_name(name: &str) -> &str {
match name {
"home" => "HOME",
"user" => "USER",
"term" => "TERM",
"shell" => "SHELL",
"cwd" => "PWD",
"owd" => "OLDPWD",
"uid" => "UID",
"euid" => "EUID",
"gid" => "GID",
"*" => "argv",
other => other,
}
}
pub fn translate_word(word: &str) -> String {
translate(word, false, false)
}
pub fn translate_word_globbed(word: &str) -> String {
translate(word, false, true)
}
pub fn translate_word_checked(word: &str) -> String {
translate(word, true, true)
}
fn translate(word: &str, checked: bool, glob: bool) -> String {
let chars: Vec<char> = word.chars().collect();
if chars == ['{', '}'] {
return "{}".to_string();
}
let mut out = String::new();
let cx = Cx {
checked,
glob,
in_brace: false,
};
if checked {
if let Some(lines) = whole_word_backquote(&chars) {
return lines;
}
}
bare(&chars, &mut out, true, cx);
out
}
fn whole_word_backquote(c: &[char]) -> Option<String> {
let inner = c.strip_prefix(&['"'])?.strip_suffix(&['"'])?;
let body = inner.strip_prefix(&['`'])?.strip_suffix(&['`'])?;
if body.contains(&'`') || body.contains(&'"') {
return None;
}
let src: String = body.iter().collect();
let zsh = cmds::translate_line(&src).unwrap_or(src);
Some(format!("${{(@)${{(@f)\"$({})\"}}:#}}", zsh.trim()))
}
#[derive(Clone, Copy, PartialEq)]
enum Ctx {
Bare,
Quoted,
}
#[derive(Clone, Copy)]
struct Cx {
checked: bool,
glob: bool,
in_brace: bool,
}
fn bare(c: &[char], out: &mut String, word_start: bool, cx: Cx) {
let mut i = 0;
while i < c.len() {
let ch = c[i];
match ch {
'\\' => match c.get(i + 1) {
Some('\n') => i += 2,
Some(&next) => {
out.push('\\');
out.push(next);
i += 2;
}
None => {
out.push_str("\\\\");
i += 1;
}
},
'\'' => match c[i + 1..].iter().position(|&x| x == '\'') {
Some(n) => {
out.extend(&c[i..=i + 1 + n]);
i += n + 2;
}
None => {
out.push('\'');
out.extend(&c[i + 1..]);
out.push('\'');
i = c.len();
}
},
'"' => i = dquoted(c, i + 1, out, cx),
'`' => i = backquote(c, i, Ctx::Bare, out),
'$' => i = dollar(c, i + 1, Ctx::Bare, cx, out),
'{' => i = brace(c, i, word_start && i == 0, cx, out),
'[' => match c[i + 1..].iter().position(|&x| x == ']') {
Some(0) => {
out.push_str("\\[");
i += 1;
}
Some(n) if c[i + 1..i + 1 + n].iter().any(|x| "$`\\'\"{".contains(*x)) => {
out.push('[');
i += 1;
}
Some(n) => {
out.extend(&c[i..=i + 1 + n]);
i += n + 2;
}
None => {
out.push('[');
i += 1;
}
},
'^' | '#' => {
out.push('\\');
out.push(ch);
i += 1;
}
'~' if word_start && i == 0 => {
let user: String = c[1..].iter().take_while(|&&x| is_user_char(x)).collect();
if cx.checked && !user.is_empty() {
out.push_str(&known_user(&user));
i += 1 + user.chars().count();
} else {
out.push('~');
i += 1;
}
}
'~' | '=' | '}' => {
let literal = ch != '=' || (word_start && i == 0);
if literal {
out.push('\\');
}
out.push(ch);
i += 1;
}
_ => {
out.push(ch);
i += 1;
}
}
}
}
fn dquoted(c: &[char], mut i: usize, out: &mut String, cx: Cx) -> usize {
out.push('"');
while i < c.len() {
match c[i] {
'"' => {
out.push('"');
return i + 1;
}
'\\' => match c.get(i + 1) {
Some('\n') => {
out.push_str("\"$'\\n'\"");
i += 2;
}
Some('!') => {
out.push('!');
i += 2;
}
_ => {
out.push_str("\\\\");
i += 1;
}
},
'$' => i = dollar(c, i + 1, Ctx::Quoted, cx, out),
'`' => i = backquote(c, i, Ctx::Quoted, out),
ch => {
out.push(ch);
i += 1;
}
}
}
out.push('"');
c.len()
}
fn backquote(c: &[char], i: usize, ctx: Ctx, out: &mut String) -> usize {
let Some(n) = c[i + 1..].iter().position(|&x| x == '`') else {
out.push_str("\\`");
return i + 1;
};
let body: String = c[i + 1..i + 1 + n].iter().collect();
let zsh = cmds::translate_line(&body).unwrap_or(body);
let zsh = zsh.trim();
match ctx {
Ctx::Bare => out.push_str(&format!("$({zsh})")),
Ctx::Quoted => out.push_str(&format!("${{(@)${{(@f)\"$({zsh})\"}}:#}}")),
}
i + n + 2
}
fn find_brace_end(c: &[char], open: usize) -> Option<usize> {
let mut depth = 0;
let mut i = open;
while i < c.len() {
match c[i] {
'\\' => i += 1,
q @ ('\'' | '"' | '`') => i += c[i + 1..].iter().position(|&x| x == q)? + 1,
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
i += 1;
}
None
}
fn split_alternatives(c: &[char]) -> Vec<&[char]> {
let mut parts = Vec::new();
let mut depth = 0;
let mut start = 0;
let mut i = 0;
while i < c.len() {
match c[i] {
'\\' => i += 1,
q @ ('\'' | '"' | '`') => {
if let Some(n) = c[i + 1..].iter().position(|&x| x == q) {
i += n + 1;
}
}
'{' => depth += 1,
'}' => depth -= 1,
',' if depth == 0 => {
parts.push(&c[start..i]);
start = i + 1;
}
_ => {}
}
i += 1;
}
parts.push(&c[start..]);
parts
}
fn brace(c: &[char], open: usize, word_start: bool, cx: Cx, out: &mut String) -> usize {
let Some(end) = find_brace_end(c, open) else {
if c == ['{'] {
out.push_str("\\{");
} else {
out.push_str(&abort("Missing '}'."));
}
return open + 1;
};
let alternatives = split_alternatives(&c[open + 1..end]);
let inner = Cx { in_brace: true, ..cx };
if alternatives.len() > 1 {
out.push('{');
for (n, alt) in alternatives.iter().enumerate() {
if n > 0 {
out.push(',');
}
bare(alt, out, word_start, inner);
}
out.push('}');
} else {
bare(alternatives[0], out, word_start, inner);
}
end + 1
}
#[derive(Clone, Copy, PartialEq)]
enum Kind {
Value,
Count,
IsSet,
Length,
}
enum Sub {
All,
One(String),
Range(String, String),
}
struct Modifier {
letter: char,
all_occurrences: bool,
subst: Option<(String, String)>,
}
struct Reference {
kind: Kind,
name: String,
sub: Option<Sub>,
modifiers: Vec<Modifier>,
global: bool,
quote: bool,
no_glob: bool,
bound_names: Vec<String>,
bad_modifier: Option<char>,
}
fn dollar(c: &[char], i: usize, ctx: Ctx, cx: Cx, out: &mut String) -> usize {
match c.get(i) {
None => {
out.push_str("\\$");
i
}
Some('$') => {
out.push_str("$$");
i + 1
}
Some('<') => {
out.push_str("$(IFS= read -r; print -r -- $REPLY)");
i + 1
}
Some('{') => {
let end = c[i..].iter().position(|&x| x == '}').map(|n| i + n);
let parsed = end.and_then(|e| parse_reference(&c[i + 1..e]).map(|(r, used)| (r, used, e)));
match parsed {
Some((r, used, e)) if used == e - i - 1 => {
render(&r, ctx, cx, out);
e + 1
}
_ => {
out.push_str("\\$");
i
}
}
}
Some(_) => match parse_reference(&c[i..]) {
Some((r, used)) => {
render(&r, ctx, cx, out);
i + used
}
None => {
out.push_str("\\$");
i
}
},
}
}
fn parse_reference(c: &[char]) -> Option<(Reference, usize)> {
let mut p = 0;
let kind = match c.first()? {
'#' => Kind::Count,
'?' => Kind::IsSet,
'%' => Kind::Length,
_ => Kind::Value,
};
if kind != Kind::Value {
p = 1;
}
let (name, np) = parse_name(c, p);
let mut r = Reference {
kind,
name,
sub: None,
modifiers: Vec::new(),
global: false,
quote: false,
no_glob: false,
bound_names: Vec::new(),
bad_modifier: None,
};
if r.name.is_empty() {
return (kind == Kind::Count || kind == Kind::IsSet).then_some((r, p));
}
if r.name == "*" && matches!(kind, Kind::Count | Kind::IsSet) {
return None; }
if kind == Kind::Count && r.name.chars().all(|ch| ch.is_ascii_digit()) {
return None; }
p = np;
let positional = r.name == "*" || r.name.chars().all(|ch| ch.is_ascii_digit());
if matches!(kind, Kind::Value | Kind::Length) && !positional && c.get(p) == Some(&'[') {
let mut names = Vec::new();
if let Some((sub, used)) = parse_subscript(&c[p..], &mut names) {
r.sub = Some(sub);
r.bound_names = names;
p += used;
}
}
if kind == Kind::Value {
p += parse_modifiers(&c[p..], &mut r);
}
Some((r, p))
}
fn parse_name(c: &[char], p: usize) -> (String, usize) {
let first = match c.get(p) {
Some(&ch) => ch,
None => return (String::new(), p),
};
let accept: fn(char) -> bool = if first == '*' {
|_| false
} else if first.is_ascii_digit() {
|ch| ch.is_ascii_digit()
} else if first.is_ascii_alphabetic() || first == '_' {
|ch| ch.is_ascii_alphanumeric() || ch == '_'
} else {
return (String::new(), p);
};
let mut end = p + 1;
while c.get(end).is_some_and(|&ch| accept(ch)) {
end += 1;
}
(c[p..end].iter().collect(), end)
}
fn parse_subscript(c: &[char], bound_names: &mut Vec<String>) -> Option<(Sub, usize)> {
let close = c.iter().position(|&x| x == ']')?;
let inner = &c[1..close];
if inner == ['*'] {
return Some((Sub::All, close + 1));
}
if inner.is_empty() {
return None;
}
let mut bounds: Vec<String> = Vec::new();
let mut cur: Vec<char> = Vec::new();
let mut dashes = 0;
for &ch in inner {
if ch == '-' {
bounds.push(bound(&cur, bound_names)?);
cur.clear();
dashes += 1;
} else {
cur.push(ch);
}
}
bounds.push(bound(&cur, bound_names)?);
let sub = match (dashes, bounds.as_slice()) {
(0, [n]) if !n.is_empty() => Sub::One(n.clone()),
(1, [lo, hi]) => match (lo.is_empty(), hi.is_empty()) {
(true, true) => Sub::All,
(true, false) => Sub::Range("1".into(), hi.clone()),
(false, true) => Sub::Range(lo.clone(), "-1".into()),
(false, false) => Sub::Range(lo.clone(), hi.clone()),
},
_ => return None,
};
Some((sub, close + 1))
}
fn bound(c: &[char], bound_names: &mut Vec<String>) -> Option<String> {
if c.iter().all(|ch| ch.is_ascii_digit()) {
return Some(c.iter().collect());
}
let after = c.strip_prefix(&['$'])?;
let braced = after.first() == Some(&'{') && after.last() == Some(&'}');
let text = if braced { &after[1..after.len() - 1] } else { after };
let (r, used) = parse_reference(text)?;
(used == text.len() && matches!(r.kind, Kind::Value | Kind::Count) && r.sub.is_none())
.then(|| {
bound_names.push(r.name.clone());
let name = zsh_var_name(&r.name);
match r.kind {
Kind::Count => format!("${{#{name}}}"),
_ => format!("${{{name}}}"),
}
})
}
fn parse_modifiers(c: &[char], r: &mut Reference) -> usize {
let mut p = 0;
while c.get(p) == Some(&':') {
let mut q = p + 1;
let (mut g, mut a) = (false, false);
while let Some(&f) = c.get(q) {
match f {
'g' => g = true,
'a' => a = true,
_ => break,
}
q += 1;
}
let Some(&letter) = c.get(q) else {
r.bad_modifier = Some(' ');
return c.len();
};
match letter {
'h' | 't' | 'r' | 'e' | 'u' | 'l' => r.modifiers.push(Modifier {
letter,
all_occurrences: false,
subst: None,
}),
'q' | 'x' => {
r.quote |= letter == 'q';
r.no_glob = true;
}
's' => {
let Some((subst, used)) = parse_substitution(&c[q + 1..]) else { break };
r.modifiers.push(Modifier {
letter,
all_occurrences: a,
subst,
});
q += used;
}
other => {
r.bad_modifier = Some(other);
return q + 1;
}
}
r.global |= g;
p = q + 1;
}
p
}
fn parse_substitution(c: &[char]) -> Option<(Option<(String, String)>, usize)> {
let delim = *c.first()?;
if delim.is_alphanumeric() || delim == ':' {
return None;
}
let l_end = 1 + c[1..].iter().position(|&x| x == delim)?;
let rest = &c[l_end + 1..];
let r_len = match rest.iter().position(|&x| x == delim) {
Some(n) if !rest[..n].contains(&'"') => n,
_ => rest
.iter()
.position(|&x| x.is_whitespace() || x == '"')
.unwrap_or(rest.len()),
};
let closed = rest.get(r_len) == Some(&delim);
let used = l_end + 1 + r_len + usize::from(closed);
let lhs: String = c[1..l_end].iter().collect();
let rhs: String = rest[..r_len].iter().collect();
let unsafe_char = |ch: char| ch.is_whitespace() || "/\\}$`\"'".contains(ch);
if lhs.is_empty() || lhs.chars().any(unsafe_char) || rhs.chars().any(|ch| unsafe_char(ch) && !ch.is_whitespace()) {
return Some((None, used));
}
let mut pattern = String::new();
for ch in lhs.chars() {
if !ch.is_alphanumeric() && ch != '_' {
pattern.push('\\');
}
pattern.push(ch);
}
Some((Some((pattern, rhs)), used))
}
fn render(r: &Reference, ctx: Ctx, cx: Cx, out: &mut String) {
if let Some(c) = r.bad_modifier {
out.push_str(&abort(&format!("Bad : modifier in $ '{c}'.")));
return;
}
if cx.checked {
out.push_str(&guards(r, ctx, cx));
}
let name = zsh_var_name(if r.name == "status" { "?" } else { &r.name });
match r.kind {
Kind::Count => out.push_str(&count(r, name, ctx, cx)),
Kind::IsSet => {
if r.name.is_empty() {
out.push_str("$?");
} else if r.name == "status" {
out.push('1');
} else if r.name.chars().all(|ch| ch.is_ascii_digit()) {
out.push('1');
} else if r.name == "prompt" {
out.push_str("${${${options[interactive]:#off}:+1}:-${${${options[shinstdin]:#off}:+1}:-0}}");
} else {
out.push_str(&format!("${{+{name}}}"));
}
}
Kind::Length => {
let base = base_text(name, &r.sub);
out.push_str(&format!("${{#${{(j::){base}}}}}"));
}
Kind::Value => render_value(r, name, ctx, cx, out),
}
}
fn count(r: &Reference, name: &str, ctx: Ctx, cx: Cx) -> String {
if r.name.is_empty() {
return "${#argv}".to_string();
}
if name != r.name || r.name == "status" {
return "1".to_string();
}
if !r.name.chars().any(|ch| ch.is_ascii_uppercase()) {
return format!("${{#{name}}}");
}
let kind = format!("${{(t){name}}}");
let array = format!("${{${{(M){kind}:#array*}}:+${{#{name}}}}}");
let value = match ctx {
Ctx::Bare => split_words(name, cx.glob),
Ctx::Quoted => format!("${{{name}}}"),
};
let scalar = format!("${{${{{kind}:#array*}}:+{value}}}");
format!("{array}{scalar}")
}
fn split_words(expr: &str, glob: bool) -> String {
let words = format!("${{={expr}}}");
if glob {
glob_wrap(&words)
} else {
words
}
}
fn base_text(name: &str, sub: &Option<Sub>) -> String {
match sub {
None | Some(Sub::All) => name.to_string(),
Some(Sub::One(n)) => format!("{name}[{n}]"),
Some(Sub::Range(lo, hi)) => format!("{name}[{lo},{hi}]"),
}
}
fn render_value(r: &Reference, name: &str, ctx: Ctx, cx: Cx, out: &mut String) {
if let Some(Sub::One(n)) = &r.sub {
if n.chars().all(|c| c == '0') {
return;
}
}
let split = ctx == Ctx::Bare && !r.quote;
let scalar_special = name == "?";
let plain_special = matches!(name, "PWD" | "HOME" | "USER" | "TERM" | "UID" | "EUID" | "GID" | "?");
let glob = cx.glob && !r.no_glob && !plain_special;
let mut base = base_text(name, &r.sub);
if r.sub.is_none() && r.modifiers.is_empty() && !name.is_empty() && name != "0" && name.chars().all(|c| c.is_ascii_digit()) {
base.push('-');
}
let wrap = |expr: &str| {
if split {
split_words(expr, glob)
} else {
expr.to_string()
}
};
if r.modifiers.is_empty() {
if scalar_special {
out.push_str("$?");
} else if split {
out.push_str(&split_words(&base, glob));
} else {
out.push_str(&format!("${{{base}}}"));
}
return;
}
let one_string = matches!(r.sub, Some(Sub::One(_))) || is_scalar_name(name);
let every_word = r.global || r.modifiers.len() > 1 || one_string;
if every_word {
let mut expr = base.clone();
for m in &r.modifiers {
expr = apply(m, &expr, !one_string);
}
if ctx == Ctx::Quoted && !one_string {
expr = format!("${{(j: :){expr}}}");
}
out.push_str(&wrap(&expr));
return;
}
let (first_el, rest_inner) = match &r.sub {
Some(Sub::Range(..)) => {
let elements = format!("${{(@){base}}}");
(format!("${{{elements}[1]}}"), format!("{elements}[2,-1]"))
}
_ => (format!("${{{name}[1]}}"), format!("{name}[2,-1]")),
};
let first = apply(&r.modifiers[0], &first_el, false);
let rest = format!("${{{rest_inner}}}");
match ctx {
Ctx::Bare => out.push_str(&wrap(&format!("${{:-{first}${{{rest_inner}:+ {rest}}}}}"))),
Ctx::Quoted => out.push_str(&format!("{first}${{{rest_inner}:+ {rest}}}")),
}
}
fn abort(msg: &str) -> String {
let msg = msg.replace('\\', "\\\\").replace('\'', "\\'").replace('}', "\\}");
format!("${{__csh?{msg}}}")
}
fn when(cond: &str, then: &str) -> String {
format!("${{${{${{:-$(({cond}))}}#0}}:+{then}}}")
}
fn always_defined(name: &str) -> bool {
const NAMES: [&str; 20] = [
"argv", "status", "tcsh", "version", "tty", "shlvl", "owd", "cwd", "user", "shell", "uid",
"euid", "gid", "group", "HOST", "HOSTTYPE", "OSTYPE", "MACHTYPE", "VENDOR", "*",
];
NAMES.contains(&name) || name.chars().all(|ch| ch.is_ascii_digit())
}
fn undefined_guard(name: &str) -> String {
if name.is_empty() || always_defined(name) {
return String::new();
}
let msg = "Undefined variable.";
let zname = zsh_var_name(name);
if zname == name {
format!("${{${{{name}?{msg}}}:+}}")
} else {
format!("${{${{{zname}-${{{name}?{msg}}}}}:+}}")
}
}
fn range_guard(name: &str, sub: &Sub) -> String {
match range_cond(name, sub) {
Some(cond) => when(&cond, &abort(&format!("{name}: Subscript out of range."))),
None => String::new(),
}
}
fn range_cond(name: &str, sub: &Sub) -> Option<String> {
let size = format!("${{#{}}}", zsh_var_name(name));
let digits = |s: &str| !s.is_empty() && s.chars().all(|ch| ch.is_ascii_digit());
let is_zero = |s: &str| digits(s) && s.chars().all(|ch| ch == '0');
let nonzero = |s: &str| digits(s) && !is_zero(s);
let cond = match sub {
Sub::All => return None,
Sub::One(n) if is_zero(n) => return None,
Sub::One(n) => format!("{n} > {size}"),
Sub::Range(lo, hi) if hi == "-1" => {
if nonzero(lo) {
return None;
}
if is_zero(lo) {
format!("{size} != 0")
} else {
format!("{lo} == 0 && {size} != 0")
}
}
Sub::Range(lo, hi) => {
let over = format!("{hi} > {size}");
if nonzero(lo) {
over
} else if is_zero(lo) {
format!("{hi} != 0")
} else {
format!("{over} || ({lo} == 0 && {hi} != 0)")
}
}
};
Some(cond)
}
pub fn subscript_probes(word: &str) -> Vec<String> {
let c: Vec<char> = word.chars().collect();
let mut probes = Vec::new();
let mut quote: Option<char> = None;
let mut i = 0;
while i < c.len() {
let ch = c[i];
i += 1;
match (quote, ch) {
(Some(q), _) if ch == q => quote = None,
(None, '\'' | '"') => quote = Some(ch),
(Some('\''), _) => {}
(_, '\\') => i += 1,
(_, '$') => {
let braced = c.get(i) == Some(&'{');
let (start, end) = if braced {
match c[i..].iter().position(|&x| x == '}') {
Some(n) => (i + 1, i + n),
None => continue,
}
} else {
(i, c.len())
};
let Some((r, used)) = parse_reference(&c[start..end]) else {
continue;
};
if braced && used != end - start {
continue;
}
i = if braced { end + 1 } else { start + used };
if !matches!(r.kind, Kind::Value | Kind::Length) || always_defined(&r.name) {
continue;
}
if let Some(cond) = r.sub.as_ref().and_then(|s| range_cond(&r.name, s)) {
probes.push(format!(
"{{ (( ! ( {cond} ) )) || {{ print -u2 -r -- '{}: Subscript out of range.'; [[ -o interactive ]] || exit 1; false; }}; }}",
r.name
));
}
}
_ => {}
}
}
probes
}
fn guards(r: &Reference, ctx: Ctx, cx: Cx) -> String {
let mut g = String::new();
if matches!(r.kind, Kind::Value | Kind::Count | Kind::Length) {
g.push_str(&undefined_guard(&r.name));
}
for bound in &r.bound_names {
g.push_str(&undefined_guard(bound));
}
if matches!(r.kind, Kind::Value | Kind::Length) {
if let Some(sub) = &r.sub {
g.push_str(&range_guard(&r.name, sub));
}
}
if cx.in_brace && ctx == Ctx::Bare && r.kind == Kind::Value && r.name != "status" {
let base = base_text(zsh_var_name(&r.name), &r.sub);
let elements = if r.quote {
format!("${{(@){base}}}")
} else {
format!("${{={base}}}")
};
let words = format!("${{#{elements}}}");
g.push_str(&when(&format!("{words} > 1"), &abort("Missing '}'.")));
}
g
}
fn is_user_char(ch: char) -> bool {
ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.')
}
fn known_user(user: &str) -> String {
let probe = format!("$({{ : ~{user}; }} 2>/dev/null && print known)");
let unknown = abort(&format!("Unknown user: {user}."));
format!("${{${{{probe}:-{unknown}}}:+}}\"$(print -r -- ~{user})\"")
}
const GLOB_WORD: &str = "(\\~*|*[*?\\[\\{]*)";
fn glob_wrap(words: &str) -> String {
let count = format!("${{#${{(M){words}:#{GLOB_WORD}}}}}");
let quoted = format!("${{(@){words}//(#m)[()|<>^#]/\\\\$MATCH}}");
let escaped = format!("${{(@){quoted}//(#b)([^~])~/${{match[1]}}\\\\~}}");
let globbed = format!("${{${{{count}:#0}}:+${{~{escaped}}}}}");
let plain = format!("${{${{(M){count}:#0}}:+${{{words}:+\"${{(@){words}}}\"}}}}");
format!("{globbed}{plain}")
}
fn is_scalar_name(name: &str) -> bool {
name == "?"
|| name.starts_with(|ch: char| ch.is_ascii_digit())
|| name.chars().any(|ch| ch.is_ascii_uppercase())
}
fn apply(m: &Modifier, e: &str, array: bool) -> String {
let at = if array { "(@)" } else { "" };
match m.letter {
'h' => format!("${{{at}{e}%/*}}"),
't' => format!("${{{at}{e}##*/}}"),
'r' => format!("${{{at}{e}:r}}"),
'e' => format!("${{{at}{e}:e}}"),
's' => match &m.subst {
Some((l, r)) if m.all_occurrences => format!("${{{at}{e}//{l}/{r}}}"),
Some((l, r)) => format!("${{{at}{e}/{l}/{r}}}"),
None => e.to_string(),
},
'u' => first_letter_case(e, array, 'a', 'z', 'U'),
'l' => first_letter_case(e, array, 'A', 'Z', 'L'),
_ => e.to_string(),
}
}
fn first_letter_case(e: &str, array: bool, lo: char, hi: char, flag: char) -> String {
if array {
format!("${{(@){e}/(#b)([^{lo}-{hi}]#)([{lo}-{hi}])/${{match[1]}}${{({flag})match[2]}}}}")
} else {
let prefix = format!("${{{e}%%[{lo}-{hi}]*}}");
let tail = format!("${{{e}#{prefix}}}");
format!("${{:-{prefix}${{({flag}){tail}[1]}}${{{tail}[2,-1]}}}}")
}
}
#[cfg(test)]
mod tests {
use super::*;
fn t(w: &str) -> String {
translate_word(w)
}
#[test]
fn plain_text_and_globs_pass_through() {
assert_eq!(t("foo/bar-1.c"), "foo/bar-1.c");
assert_eq!(t("*.c"), "*.c");
assert_eq!(t("[a-b]?.c"), "[a-b]?.c");
}
#[test]
fn unquoted_variable_splits_per_element() {
assert_eq!(t("$x"), "${=x}");
assert_eq!(t("${x}"), "${=x}");
assert_eq!(t("pre$x.suf"), "pre${=x}.suf");
}
#[test]
fn quoted_variable_joins_elements() {
assert_eq!(t("\"[$x]\""), "\"[${x}]\"");
assert_eq!(t("\"$x[*]\""), "\"${x}\"");
}
#[test]
fn variable_name_stops_at_non_identifier() {
assert_eq!(t("$x-y"), "${=x}-y");
assert_eq!(t("$x_y"), "${=x_y}");
}
#[test]
fn braced_variable_ends_modifier_scope() {
assert_eq!(t("${x}:h"), "${=x}:h");
}
#[test]
fn prompt_is_set_only_in_an_interactive_shell() {
assert_eq!(t("$?prompt"), "${${${options[interactive]:#off}:+1}:-${${${options[shinstdin]:#off}:+1}:-0}}");
assert_eq!(t("${?prompt}"), "${${${options[interactive]:#off}:+1}:-${${${options[shinstdin]:#off}:+1}:-0}}");
}
#[test]
fn count_and_isset_forms() {
assert_eq!(t("$#x"), "${#x}");
assert_eq!(t("\"$#x\""), "\"${#x}\"");
assert_eq!(t("${#x}"), "${#x}");
assert_eq!(t("$#"), "${#argv}");
assert_eq!(t("$?x"), "${+x}");
assert_eq!(t("${?x}"), "${+x}");
assert_eq!(t("$?"), "$?");
assert_eq!(t("$?status"), "1");
assert_eq!(t("$?2"), "1");
assert_eq!(t("$?5"), "1");
}
#[test]
fn length_form_counts_characters_across_elements() {
assert_eq!(t("$%x"), "${#${(j::)x}}");
assert_eq!(t("$%x[2]"), "${#${(j::)x[2]}}");
}
#[test]
fn subscripts_are_ranges_not_arithmetic() {
assert_eq!(t("$x[2]"), "${=x[2]}");
assert_eq!(t("$x[2-3]"), "${=x[2,3]}");
assert_eq!(t("$x[2-]"), "${=x[2,-1]}");
assert_eq!(t("$x[-2]"), "${=x[1,2]}");
assert_eq!(t("$x[-]"), "${=x}");
assert_eq!(t("$x[*]"), "${=x}");
}
#[test]
fn subscript_bounds_may_be_references() {
assert_eq!(t("$x[$#x]"), "${=x[${#x}]}");
assert_eq!(t("$x[$y-4]"), "${=x[${y},4]}");
assert_eq!(t("$x[$#x-1]"), "${=x[${#x},1]}");
}
#[test]
fn non_subscript_bracket_stays_a_glob() {
assert_eq!(t("$x[1]]"), "${=x[1]}]");
assert_eq!(t("$x[a]"), "${=x}[a]");
}
#[test]
fn positional_and_special_parameters() {
assert_eq!(t("$1"), "${=1-}");
assert_eq!(t("$1abc"), "${=1-}abc");
assert_eq!(t("$10"), "${=10-}");
assert_eq!(t("$argv[2]"), "${=argv[2]}");
assert_eq!(t("$1:r"), "${=${1:r}}");
assert_eq!(t("$*"), "${=argv}");
assert_eq!(t("$$"), "$$");
assert_eq!(t("$status"), "$?");
assert_eq!(t("$cwd"), "${=PWD}");
assert_eq!(t("$home"), "${=HOME}");
assert_eq!(t("${?home}"), "${+HOME}");
}
#[test]
fn invalid_dollar_forms_stay_literal() {
assert_eq!(t("$#*"), "\\$\\#*");
assert_eq!(t("$#1"), "\\$\\#1");
assert_eq!(t("$"), "\\$");
assert_eq!(t("a$"), "a\\$");
}
#[test]
fn read_line_form() {
assert_eq!(t("$<"), "$(IFS= read -r; print -r -- $REPLY)");
assert_eq!(t("\"$<\""), "\"$(IFS= read -r; print -r -- $REPLY)\"");
}
#[test]
fn modifiers_on_one_element() {
assert_eq!(t("$x[1]:h"), "${=${x[1]%/*}}");
assert_eq!(t("${x[1]:t}"), "${=${x[1]##*/}}");
assert_eq!(t("$x[2]:r"), "${=${x[2]:r}}");
assert_eq!(t("$x[2]:e"), "${=${x[2]:e}}");
assert_eq!(t("$cwd:t"), "${=${PWD##*/}}");
}
#[test]
fn single_modifier_touches_first_word_only() {
assert_eq!(t("$p:t"), "${=${:-${${p[1]}##*/}${p[2,-1]:+ ${p[2,-1]}}}}");
assert_eq!(t("\"$p:h\""), "\"${${p[1]}%/*}${p[2,-1]:+ ${p[2,-1]}}\"");
assert_eq!(
t("$p[1-2]:r"),
"${=${:-${${${(@)p[1,2]}[1]}:r}${${(@)p[1,2]}[2,-1]:+ ${${(@)p[1,2]}[2,-1]}}}}"
);
}
#[test]
fn global_modifier_hits_every_word() {
assert_eq!(t("$p:gh"), "${=${(@)p%/*}}");
assert_eq!(t("$p:gt"), "${=${(@)p##*/}}");
assert_eq!(t("$p:gr"), "${=${(@)p:r}}");
assert_eq!(t("${p:ge}"), "${=${(@)p:e}}");
assert_eq!(t("\"$p:gt\""), "\"${(j: :)${(@)p##*/}}\"");
}
#[test]
fn chained_modifiers_hit_every_word() {
assert_eq!(t("$p:t:r"), "${=${(@)${(@)p##*/}:r}}");
}
#[test]
fn quote_and_split_modifiers() {
assert_eq!(t("$x:q"), "${x}");
assert_eq!(t("$x:x"), "${=x}");
assert_eq!(t("\"$x:q\""), "\"${x}\"");
}
#[test]
fn colon_after_variable_without_modifier_is_an_error() {
assert!(t("\"$h:$p\"").contains("${__csh?Bad : modifier in $ \\'$\\'.}"));
assert!(t("$h:80").contains("Bad : modifier in $ \\'8\\'."));
assert_eq!(t("${h}:80"), "${=h}:80");
}
#[test]
fn substitute_modifier() {
assert_eq!(t("$p[1]:s/a/X/"), "${=${p[1]/a/X}}");
assert_eq!(t("$p[1]:as/a/X/"), "${=${p[1]//a/X}}");
assert_eq!(t("$p:gs/a/X/"), "${=${(@)p/a/X}}");
assert_eq!(t("$p[1]:s/./_/"), "${=${p[1]/\\./_}}");
assert_eq!(t("$p[1]:s,a,Z,"), "${=${p[1]/a/Z}}");
}
#[test]
fn first_letter_case_modifiers() {
assert_eq!(
t("$w[1]:u"),
"${=${:-${w[1]%%[a-z]*}${(U)${w[1]#${w[1]%%[a-z]*}}[1]}${${w[1]#${w[1]%%[a-z]*}}[2,-1]}}}"
);
assert_eq!(
t("$w[2]:l"),
"${=${:-${w[2]%%[A-Z]*}${(L)${w[2]#${w[2]%%[A-Z]*}}[1]}${${w[2]#${w[2]%%[A-Z]*}}[2,-1]}}}"
);
assert_eq!(
t("$w:gu"),
"${=${(@)w/(#b)([^a-z]#)([a-z])/${match[1]}${(U)match[2]}}}"
);
}
#[test]
fn double_quote_backslash_is_literal() {
assert_eq!(t("\"a\\\\b\""), "\"a\\\\\\\\b\"");
assert_eq!(t("\"a\\$HOME\""), "\"a\\\\${HOME}\"");
assert_eq!(t("\"a\\\""), "\"a\\\\\"");
assert_eq!(t("\"\\!\""), "\"!\"");
}
#[test]
fn single_quotes_block_everything() {
assert_eq!(t("'$x `y` \\n {a,b}'"), "'$x `y` \\n {a,b}'");
assert_eq!(t("'a\\'b"), "'a\\'b");
}
#[test]
fn mixed_quoting_in_one_word() {
assert_eq!(t("a\"$x\"'$y'b"), "a\"${x}\"'$y'b");
}
#[test]
fn unquoted_backslash_quotes_next_char() {
assert_eq!(t("\\$x"), "\\$x");
assert_eq!(t("\\*"), "\\*");
assert_eq!(t("\\#x"), "\\#x");
assert_eq!(t("\\{a,b\\}"), "\\{a,b\\}");
}
#[test]
fn backquote_forms() {
assert_eq!(t("`echo hi`"), "$(print -r -- hi)");
assert_eq!(t("a`echo b`c"), "a$(print -r -- b)c");
assert_eq!(t("\"`echo hi`\""), "\"${(@)${(@f)\"$(print -r -- hi)\"}:#}\"");
assert_eq!(t("'`echo hi`'"), "'`echo hi`'");
assert_eq!(t("\\`echo"), "\\`echo");
}
#[test]
fn tilde_expands_only_at_word_start() {
assert_eq!(t("~"), "~");
assert_eq!(t("~root/x"), "~root/x");
assert_eq!(t("a~b"), "a\\~b");
assert_eq!(t("\"~\""), "\"~\"");
assert_eq!(t("'~'"), "'~'");
assert_eq!(t("\\~"), "\\~");
}
#[test]
fn leading_equals_is_not_command_expansion() {
assert_eq!(t("=ls"), "\\=ls");
assert_eq!(t("a=b"), "a=b");
}
#[test]
fn brace_alternatives_stay() {
assert_eq!(t("{a,b}c"), "{a,b}c");
assert_eq!(t("{a,{b,c}}"), "{a,{b,c}}");
assert_eq!(t("{,a}"), "{,a}");
assert_eq!(t("{*.c,zz}"), "{*.c,zz}");
assert_eq!(t("{~,x}/a"), "{~,x}/a");
assert_eq!(t("{a,b}$x"), "{a,b}${=x}");
}
#[test]
fn brace_without_comma_loses_its_braces() {
assert_eq!(t("{a}"), "a");
assert_eq!(t("x{b}y"), "xby");
assert_eq!(t("{1..3}"), "1..3");
assert_eq!(t("{}x"), "x");
assert_eq!(t("x{}"), "x");
}
#[test]
fn empty_braces_survive_only_as_a_whole_word() {
assert_eq!(t("{}"), "{}");
}
#[test]
fn quoted_braces_are_literal() {
assert_eq!(t("\"{a,b}\""), "\"{a,b}\"");
assert_eq!(t("'{a}'c"), "'{a}'c");
assert_eq!(t("\"{a}\"{b}"), "\"{a}\"b");
}
#[test]
fn unclosed_brace_is_literal() {
assert_eq!(t("a{b"), "a${__csh?Missing \\'\\}\\'.}b");
assert_eq!(t("{"), "\\{");
assert_eq!(t("}"), "\\}");
}
#[test]
fn braced_variable_inside_alternatives() {
assert_eq!(t("{$HOME,b}"), "{${=HOME},b}");
}
#[test]
fn trailing_unterminated_quote_is_closed() {
assert_eq!(t("\"abc"), "\"abc\"");
assert_eq!(t("'abc"), "'abc'");
}
fn c(w: &str) -> String {
translate_word_checked(w)
}
#[test]
fn checked_leaves_variable_free_words_alone() {
for w in ["*.c", "a\\ b", "'$x'", "\"abc\"", "{a,b}c", "x{b}y", "~", "~/x", "=ls"] {
assert_eq!(c(w), t(w), "{w}");
}
}
#[test]
fn checked_undefined_variable_guard() {
assert_eq!(c("\"$x\""), "\"${${x?Undefined variable.}:+}${x}\"");
assert_eq!(c("$#x"), "${${x?Undefined variable.}:+}${#x}");
for w in ["${x}", "$%x", "$x:h", "$x[1]", "\"a${x}b\""] {
assert!(c(w).contains("${${x?Undefined variable.}:+}"), "{w}");
}
assert_eq!(c("$?x"), "${+x}");
assert!(!c("$1").contains("Undefined"));
assert!(!c("$argv").contains("Undefined"));
assert!(!c("$status").contains("Undefined"));
}
#[test]
fn checked_mapped_special_reports_the_csh_name() {
assert!(c("$home").starts_with("${${HOME-${home?Undefined variable.}}:+}"));
assert!(c("$term").starts_with("${${TERM-${term?Undefined variable.}}:+}"));
assert!(!c("$user").contains("Undefined"));
assert!(!c("$cwd").contains("Undefined"));
}
#[test]
fn checked_subscript_range_guard() {
let msg = "${__csh?x: Subscript out of range.}";
assert!(c("$x[4]").contains(&format!("${{${{${{:-$((4 > ${{#x}}))}}#0}}:+{msg}}}")));
assert!(c("$x[3-4]").contains("$((4 > ${#x}))"));
assert!(c("$x[-4]").contains("$((4 > ${#x}))"));
assert!(c("$x[0-2]").contains("$((2 != 0))"));
assert!(!c("$x[0]").contains("Subscript"));
assert!(!c("$x[4-]").contains("Subscript"));
assert!(!c("$x[-]").contains("Subscript"));
assert!(!c("$x[*]").contains("Subscript"));
assert!(c("$x[0-]").contains("$((${#x} != 0))"));
}
#[test]
fn checked_subscript_bounds_from_variables() {
let out = c("$x[$i]");
assert!(out.contains("${${i?Undefined variable.}:+}"));
assert!(out.contains("$((${i} > ${#x}))"));
let out = c("$x[$i-$j]");
assert!(out.contains("${${j?Undefined variable.}:+}"));
assert!(out.contains("$((${j} > ${#x} || (${i} == 0 && ${j} != 0)))"));
}
#[test]
fn positional_names_take_no_subscript() {
assert_eq!(t("$1[2]"), "${=1-}[2]");
assert_eq!(t("$*[3]"), "${=argv}[3]");
assert_eq!(t("$argv[2]"), "${=argv[2]}");
}
#[test]
fn count_of_environment_variable_is_its_value() {
let out = t("$#E");
assert!(out.contains("${${(M)${(t)E}:#array*}:+${#E}}"));
assert!(out.contains("${${${(t)E}:#array*}:+${=E}}"));
assert_eq!(t("\"$#E\""), "\"${${(M)${(t)E}:#array*}:+${#E}}${${${(t)E}:#array*}:+${E}}\"");
assert_eq!(t("$#x"), "${#x}");
assert_eq!(t("$#home"), "1");
assert_eq!(t("$#cwd"), "1");
}
#[test]
fn checked_unknown_user() {
let out = c("~nouser/x");
assert!(out.contains("${__csh?Unknown user: nouser.}"));
assert!(out.ends_with("\"$(print -r -- ~nouser)\"/x"));
assert!(out.starts_with("${${$({ : ~nouser; } 2>/dev/null && print known):-"));
assert_eq!(c("~/x"), "~/x");
assert_eq!(c("\"~nouser\""), "\"~nouser\"");
}
#[test]
fn checked_brace_with_multi_word_variable() {
for w in ["{$v,z}", "x{$v}y", "{$v:q,z}"] {
assert!(c(w).contains("${__csh?Missing \\'\\}\\'.}"), "{w}");
}
assert!(!c("{\"$v\",z}").contains("Missing"));
assert!(!c("\"{$v,z}\"").contains("Missing"));
}
#[test]
fn checked_globs_unquoted_values_only() {
let out = c("$x");
assert!(out.contains("${~${(@)"));
assert!(out.contains("(\\~*|*[*?\\[\\{]*)"));
for w in ["\"$x\"", "$x:q", "$x:x"] {
assert!(!c(w).contains("${~"), "{w}");
}
assert_eq!(t("$x"), "${=x}");
}
#[test]
fn checked_whole_word_backquote_in_quotes() {
assert_eq!(c("\"`ls`\""), "${(@)${(@f)\"$(ls)\"}:#}");
assert_eq!(c("\"x`ls`y\""), "\"x${(@)${(@f)\"$(ls)\"}:#}y\"");
assert_eq!(t("\"`ls`\""), "\"${(@)${(@f)\"$(ls)\"}:#}\"");
}
}