fn quotes_a_metachar(c: char) -> bool {
matches!(
c,
'<' | '('
| '|'
| ')'
| '>'
| '^'
| '#'
| '~'
| '['
| ']'
| '*'
| '?'
| '='
| '-'
| '!'
| '\\'
)
}
pub fn escape_data_backslashes(v: &str) -> String {
if !v.contains('\\') {
return v.to_string();
}
let cs: Vec<char> = v.chars().collect();
let mut out = String::with_capacity(v.len() + 4);
let mut i = 0;
while i < cs.len() {
let c = cs[i];
if c != '\\' {
out.push(c);
i += 1;
continue;
}
if cs.get(i + 1).copied().is_some_and(quotes_a_metachar) {
out.push(c);
out.push(cs[i + 1]);
i += 2;
} else {
out.push('\\');
out.push('\\');
i += 1;
}
}
out
}
pub fn escape_tokenized_data_backslashes(v: &str) -> String {
if !v.contains('\\') {
return v.to_string();
}
v.replace('\\', "\\\\")
}
pub fn escape_shglob_parens(v: &str, keep_ksh_groups: bool) -> String {
if !v.contains(['(', '|', ')']) {
return v.to_string();
}
let cs: Vec<char> = v.chars().collect();
let mut out = String::with_capacity(v.len() + 4);
let mut groups: Vec<bool> = Vec::new();
let mut in_class = false;
let mut i = 0;
while i < cs.len() {
let c = cs[i];
if c == '\\' && i + 1 < cs.len() {
out.push(c);
out.push(cs[i + 1]);
i += 2;
continue;
}
if in_class {
if c == ']' {
in_class = false;
}
out.push(c);
i += 1;
continue;
}
match c {
'[' => {
in_class = true;
out.push(c);
}
'(' => {
let ksh = keep_ksh_groups
&& i > 0
&& matches!(cs[i - 1], '@' | '*' | '+' | '?' | '!')
&& !(i >= 2 && cs[i - 2] == '\\');
groups.push(ksh);
if !ksh {
out.push('\\');
}
out.push(c);
}
')' => {
if !groups.pop().unwrap_or(false) {
out.push('\\');
}
out.push(c);
}
'|' => {
if !groups.last().copied().unwrap_or(false) {
out.push('\\');
}
out.push(c);
}
_ => out.push(c),
}
i += 1;
}
out
}
pub fn shglob_hides_parens() -> bool {
crate::ported::zsh_h::isset(crate::ported::zsh_h::SHGLOB)
}
pub fn dropin_keeps_ksh_groups() -> bool {
(crate::dash_mode::bash_mode() || crate::dash_mode::korn_mode())
&& crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHGLOB)
}
pub fn dropin_source_pattern_parens_literal() -> bool {
crate::dash_mode::posix_faithful() && crate::ported::zsh_h::isset(crate::ported::zsh_h::SHGLOB)
}
pub fn note_default_word_bars(word: &str, value: &str) -> bool {
use crate::ported::zsh_h::{
Bar, Bnull, Bnullkeep, Dnull, Inbrack, Inpar, Nularg, Outbrack, Outpar, Qstring, Qtick,
Snull, Tick,
};
let cs: Vec<char> = word.chars().collect();
let mut barred = String::with_capacity(word.len());
let mut paren = 0i32;
let mut bracket = false;
let mut found = false;
let mut substitutes = false;
let mut i = 0;
while i < cs.len() {
let c = cs[i];
if (c == Bnull || c == Bnullkeep) && i + 1 < cs.len() {
barred.push(cs[i + 1]); i += 2;
continue;
}
if c == Snull || c == Dnull || c == Nularg {
i += 1;
continue;
}
if matches!(c, '$' | '`' | '\u{e185}') || c == Qstring || c == Tick || c == Qtick {
substitutes = true;
}
match c {
'(' | Inpar => paren += 1,
')' | Outpar => paren = (paren - 1).max(0),
'[' | Inbrack => bracket = true,
']' | Outbrack => bracket = false,
_ => {}
}
if (c == '|' || c == Bar) && paren == 0 && !bracket {
found = true;
barred.push(Bar);
i += 1;
continue;
}
if ('\u{e184}'..='\u{e19c}').contains(&c) {
barred.push_str(&crate::ported::lex::untokenize(&c.to_string()));
} else {
barred.push(c);
}
i += 1;
}
if found && !substitutes && crate::ported::lex::untokenize(&barred) == value {
crate::ported::subst::DEFAULT_WORD_GLOB_BARS
.with(|b| b.borrow_mut().push((value.to_string(), barred)));
}
found
}
#[derive(Clone, Copy, PartialEq)]
enum Ksh93PatEngine {
Glob,
Fixed,
Ere,
Bre,
}
fn ksh93_pat_options(flags: &[char]) -> Option<(bool, Ksh93PatEngine)> {
let (mut icase, mut engine, mut off) = (false, Ksh93PatEngine::Glob, false);
for &c in flags {
match c {
'-' => off = true,
'+' => off = false,
'i' => icase = !off,
'F' if !off => engine = Ksh93PatEngine::Fixed,
'E' | 'P' if !off => engine = Ksh93PatEngine::Ere,
'G' if !off => engine = Ksh93PatEngine::Bre,
'K' | 'X' | 'N' | 'F' | 'E' | 'P' | 'G' => {}
_ => return None,
}
}
Some((icase, engine))
}
fn ksh93_pat_group_end(cs: &[char], open: usize) -> Option<usize> {
let mut depth = 0;
let mut i = open;
while i < cs.len() {
match cs[i] {
'\\' => i += 1,
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
return Some(i + 1);
}
}
_ => {}
}
i += 1;
}
None
}
fn ksh93_pat_swapcase(c: char) -> char {
if c.is_ascii_lowercase() { c.to_ascii_uppercase() } else { c.to_ascii_lowercase() }
}
fn ksh93_pat_fold_case(orig: &[char], cs: &[char], out: &mut String) {
let mut i = 0;
while i < cs.len() {
match cs[i] {
'\\' => {
out.push(orig[i]);
if let Some(&n) = orig.get(i + 1) {
out.push(n);
}
i += 2;
}
'[' => {
let start = i + 1 + usize::from(matches!(cs.get(i + 1), Some('!' | '^')));
match cs[(start + 1).min(cs.len())..].iter().position(|&c| c == ']') {
Some(rel) => {
let close = start + 1 + rel;
out.extend(&orig[i..close]);
out.extend(cs[start..close].iter().filter(|c| c.is_ascii_alphabetic()).map(|&c| ksh93_pat_swapcase(c)));
out.push(orig[close]);
i = close + 1;
}
None => {
out.push(orig[i]);
i += 1;
}
}
}
c if c.is_ascii_alphabetic() => {
out.extend(['[', c, ksh93_pat_swapcase(c), ']']);
i += 1;
}
_ => {
out.push(orig[i]);
i += 1;
}
}
}
}
fn ksh93_bre_to_ere(bre: &[char]) -> String {
let mut out = String::new();
let mut i = 0;
while i < bre.len() {
match (bre[i], bre.get(i + 1)) {
('\\', Some(&n)) if "(){}|+?".contains(n) => {
out.push(n);
i += 2;
}
('\\', Some(&n)) => {
out.extend(['\\', n]);
i += 2;
}
(c, _) if "(){}|+?".contains(c) => {
out.extend(['\\', c]);
i += 1;
}
(c, _) => {
out.push(c);
i += 1;
}
}
}
out
}
pub fn ksh93_flagged_match(s: &str, pat: &str) -> Option<bool> {
use crate::ported::zsh_h::{Inpar, Outpar, Tilde};
if !crate::dash_mode::ksh93_mode() {
return None;
}
let orig: Vec<char> = pat.chars().collect();
let cs: Vec<char> = orig
.iter()
.map(|&c| match c {
Tilde => '~',
Inpar => '(',
Outpar => ')',
c => c,
})
.collect();
let first = (0..cs.len().saturating_sub(1)).find(|&i| {
cs[i] == '~' && cs[i + 1] == '(' && (i == 0 || cs[i - 1] != '\\')
})?;
let (icase, engine) = {
let end = cs[first + 2..].iter().position(|&c| !(c.is_ascii_alphabetic() || c == '-' || c == '+'))? + first + 2;
ksh93_pat_options(&cs[first + 2..end])?
};
if engine != Ksh93PatEngine::Glob {
let end = cs[first + 2..].iter().position(|&c| !(c.is_ascii_alphabetic() || c == '-' || c == '+'))? + first + 2;
let body: Vec<char> = match cs[end] {
')' if first == 0 => cs[end + 1..].to_vec(),
':' if first == 0 && ksh93_pat_group_end(&cs, 1) == Some(cs.len()) => cs[end + 1..cs.len() - 1].to_vec(),
_ => return None,
};
let text: String = body.iter().collect();
return match engine {
Ksh93PatEngine::Fixed if icase => Some(s.to_lowercase().contains(&text.to_lowercase())),
Ksh93PatEngine::Fixed => Some(s.contains(&text)),
_ => {
let re = if engine == Ksh93PatEngine::Bre { ksh93_bre_to_ere(&body) } else { text };
regex::RegexBuilder::new(&re).case_insensitive(icase).build().ok().map(|r| r.is_match(s))
}
};
}
let mut out = String::new();
let mut fold = false;
let mut i = 0;
let mut lit_from = 0;
while i < cs.len() {
if cs[i] == '\\' {
i += 2;
continue;
}
if cs[i] == '~' && cs.get(i + 1) == Some(&'(') {
let Some(rel) = cs[i + 2..].iter().position(|&c| !(c.is_ascii_alphabetic() || c == '-' || c == '+')) else { break };
let end = i + 2 + rel;
let Some((ic, eng)) = ksh93_pat_options(&cs[i + 2..end]) else { return None };
if eng != Ksh93PatEngine::Glob || !matches!(cs[end], ')' | ':') {
return None;
}
flush_range(&mut out, &orig, &cs, lit_from, i, fold);
let off = cs[i + 2..end].contains(&'-');
let new_fold = if cs[i + 2..end].contains(&'i') { !off && ic } else { fold };
if cs[end] == ')' {
fold = new_fold;
i = end + 1;
} else {
let close = ksh93_pat_group_end(&cs, i + 1)?;
flush_range(&mut out, &orig, &cs, end + 1, close - 1, new_fold);
i = close;
}
lit_from = i;
continue;
}
i += 1;
}
flush_range(&mut out, &orig, &cs, lit_from, cs.len(), fold);
Some(crate::vm_helper::glob_match_static(s, &out))
}
fn flush_range(out: &mut String, orig: &[char], cs: &[char], from: usize, to: usize, fold: bool) {
if fold {
ksh93_pat_fold_case(&orig[from..to], &cs[from..to], out);
} else {
out.extend(&orig[from..to]);
}
}