#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum OptionValue {
On,
Off,
#[default]
Unknown,
}
impl OptionValue {
pub const fn is_definitely_on(self) -> bool {
matches!(self, Self::On)
}
pub const fn is_definitely_off(self) -> bool {
matches!(self, Self::Off)
}
#[inline]
pub const fn merge(self, other: Self) -> Self {
match (self, other) {
(Self::On, Self::On) => Self::On,
(Self::Off, Self::Off) => Self::Off,
_ => Self::Unknown,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ZshEmulationMode {
Zsh,
Sh,
Ksh,
Csh,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ZshOptionState {
pub sh_word_split: OptionValue,
pub glob_subst: OptionValue,
pub rc_expand_param: OptionValue,
pub glob: OptionValue,
pub nomatch: OptionValue,
pub null_glob: OptionValue,
pub csh_null_glob: OptionValue,
pub extended_glob: OptionValue,
pub ksh_glob: OptionValue,
pub sh_glob: OptionValue,
pub bare_glob_qual: OptionValue,
pub glob_dots: OptionValue,
pub equals: OptionValue,
pub magic_equal_subst: OptionValue,
pub sh_file_expansion: OptionValue,
pub glob_assign: OptionValue,
pub ignore_braces: OptionValue,
pub ignore_close_braces: OptionValue,
pub brace_ccl: OptionValue,
pub ksh_arrays: OptionValue,
pub ksh_zero_subscript: OptionValue,
pub short_loops: OptionValue,
pub short_repeat: OptionValue,
pub rc_quotes: OptionValue,
pub interactive_comments: OptionValue,
pub c_bases: OptionValue,
pub octal_zeroes: OptionValue,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum ZshOptionField {
ShWordSplit,
GlobSubst,
RcExpandParam,
Glob,
Nomatch,
NullGlob,
CshNullGlob,
ExtendedGlob,
KshGlob,
ShGlob,
BareGlobQual,
GlobDots,
Equals,
MagicEqualSubst,
ShFileExpansion,
GlobAssign,
IgnoreBraces,
IgnoreCloseBraces,
BraceCcl,
KshArrays,
KshZeroSubscript,
ShortLoops,
ShortRepeat,
RcQuotes,
InteractiveComments,
CBases,
OctalZeroes,
}
impl ZshOptionState {
pub const fn zsh_default() -> Self {
Self {
sh_word_split: OptionValue::Off,
glob_subst: OptionValue::Off,
rc_expand_param: OptionValue::Off,
glob: OptionValue::On,
nomatch: OptionValue::On,
null_glob: OptionValue::Off,
csh_null_glob: OptionValue::Off,
extended_glob: OptionValue::Off,
ksh_glob: OptionValue::Off,
sh_glob: OptionValue::Off,
bare_glob_qual: OptionValue::On,
glob_dots: OptionValue::Off,
equals: OptionValue::On,
magic_equal_subst: OptionValue::Off,
sh_file_expansion: OptionValue::Off,
glob_assign: OptionValue::Off,
ignore_braces: OptionValue::Off,
ignore_close_braces: OptionValue::Off,
brace_ccl: OptionValue::Off,
ksh_arrays: OptionValue::Off,
ksh_zero_subscript: OptionValue::Off,
short_loops: OptionValue::On,
short_repeat: OptionValue::On,
rc_quotes: OptionValue::Off,
interactive_comments: OptionValue::On,
c_bases: OptionValue::Off,
octal_zeroes: OptionValue::Off,
}
}
pub fn for_emulate(mode: ZshEmulationMode) -> Self {
let mut state = Self::zsh_default();
match mode {
ZshEmulationMode::Zsh => {}
ZshEmulationMode::Sh => {
state.sh_word_split = OptionValue::On;
state.glob_subst = OptionValue::On;
state.sh_glob = OptionValue::On;
state.sh_file_expansion = OptionValue::On;
state.bare_glob_qual = OptionValue::Off;
state.ksh_arrays = OptionValue::Off;
}
ZshEmulationMode::Ksh => {
state.sh_word_split = OptionValue::On;
state.glob_subst = OptionValue::On;
state.ksh_glob = OptionValue::On;
state.ksh_arrays = OptionValue::On;
state.sh_glob = OptionValue::On;
state.bare_glob_qual = OptionValue::Off;
}
ZshEmulationMode::Csh => {
state.csh_null_glob = OptionValue::On;
state.sh_word_split = OptionValue::Off;
state.glob_subst = OptionValue::Off;
}
}
state
}
pub fn apply_setopt(&mut self, name: &str) -> bool {
self.apply_named_option(name, true)
}
pub fn apply_unsetopt(&mut self, name: &str) -> bool {
self.apply_named_option(name, false)
}
fn set_field(&mut self, field: ZshOptionField, value: OptionValue) {
match field {
ZshOptionField::ShWordSplit => self.sh_word_split = value,
ZshOptionField::GlobSubst => self.glob_subst = value,
ZshOptionField::RcExpandParam => self.rc_expand_param = value,
ZshOptionField::Glob => self.glob = value,
ZshOptionField::Nomatch => self.nomatch = value,
ZshOptionField::NullGlob => self.null_glob = value,
ZshOptionField::CshNullGlob => self.csh_null_glob = value,
ZshOptionField::ExtendedGlob => self.extended_glob = value,
ZshOptionField::KshGlob => self.ksh_glob = value,
ZshOptionField::ShGlob => self.sh_glob = value,
ZshOptionField::BareGlobQual => self.bare_glob_qual = value,
ZshOptionField::GlobDots => self.glob_dots = value,
ZshOptionField::Equals => self.equals = value,
ZshOptionField::MagicEqualSubst => self.magic_equal_subst = value,
ZshOptionField::ShFileExpansion => self.sh_file_expansion = value,
ZshOptionField::GlobAssign => self.glob_assign = value,
ZshOptionField::IgnoreBraces => self.ignore_braces = value,
ZshOptionField::IgnoreCloseBraces => self.ignore_close_braces = value,
ZshOptionField::BraceCcl => self.brace_ccl = value,
ZshOptionField::KshArrays => self.ksh_arrays = value,
ZshOptionField::KshZeroSubscript => self.ksh_zero_subscript = value,
ZshOptionField::ShortLoops => self.short_loops = value,
ZshOptionField::ShortRepeat => self.short_repeat = value,
ZshOptionField::RcQuotes => self.rc_quotes = value,
ZshOptionField::InteractiveComments => self.interactive_comments = value,
ZshOptionField::CBases => self.c_bases = value,
ZshOptionField::OctalZeroes => self.octal_zeroes = value,
}
}
pub fn merge(&self, other: &Self) -> Self {
if self == other {
return *self;
}
Self {
sh_word_split: self.sh_word_split.merge(other.sh_word_split),
glob_subst: self.glob_subst.merge(other.glob_subst),
rc_expand_param: self.rc_expand_param.merge(other.rc_expand_param),
glob: self.glob.merge(other.glob),
nomatch: self.nomatch.merge(other.nomatch),
null_glob: self.null_glob.merge(other.null_glob),
csh_null_glob: self.csh_null_glob.merge(other.csh_null_glob),
extended_glob: self.extended_glob.merge(other.extended_glob),
ksh_glob: self.ksh_glob.merge(other.ksh_glob),
sh_glob: self.sh_glob.merge(other.sh_glob),
bare_glob_qual: self.bare_glob_qual.merge(other.bare_glob_qual),
glob_dots: self.glob_dots.merge(other.glob_dots),
equals: self.equals.merge(other.equals),
magic_equal_subst: self.magic_equal_subst.merge(other.magic_equal_subst),
sh_file_expansion: self.sh_file_expansion.merge(other.sh_file_expansion),
glob_assign: self.glob_assign.merge(other.glob_assign),
ignore_braces: self.ignore_braces.merge(other.ignore_braces),
ignore_close_braces: self.ignore_close_braces.merge(other.ignore_close_braces),
brace_ccl: self.brace_ccl.merge(other.brace_ccl),
ksh_arrays: self.ksh_arrays.merge(other.ksh_arrays),
ksh_zero_subscript: self.ksh_zero_subscript.merge(other.ksh_zero_subscript),
short_loops: self.short_loops.merge(other.short_loops),
short_repeat: self.short_repeat.merge(other.short_repeat),
rc_quotes: self.rc_quotes.merge(other.rc_quotes),
interactive_comments: self.interactive_comments.merge(other.interactive_comments),
c_bases: self.c_bases.merge(other.c_bases),
octal_zeroes: self.octal_zeroes.merge(other.octal_zeroes),
}
}
fn apply_named_option(&mut self, name: &str, enable: bool) -> bool {
let Some((field, value)) = parse_zsh_option_assignment(name, enable) else {
return false;
};
self.set_field(
field,
if value {
OptionValue::On
} else {
OptionValue::Off
},
);
true
}
}
fn parse_zsh_option_assignment(name: &str, enable: bool) -> Option<(ZshOptionField, bool)> {
let mut normalized = String::with_capacity(name.len());
for ch in name.chars() {
if matches!(ch, '_' | '-') {
continue;
}
normalized.push(ch.to_ascii_lowercase());
}
let (normalized, invert) = if let Some(rest) = normalized.strip_prefix("no") {
(rest, true)
} else {
(normalized.as_str(), false)
};
let field = match normalized {
"shwordsplit" => ZshOptionField::ShWordSplit,
"globsubst" => ZshOptionField::GlobSubst,
"rcexpandparam" => ZshOptionField::RcExpandParam,
"glob" | "noglob" => ZshOptionField::Glob,
"nomatch" => ZshOptionField::Nomatch,
"nullglob" => ZshOptionField::NullGlob,
"cshnullglob" => ZshOptionField::CshNullGlob,
"extendedglob" => ZshOptionField::ExtendedGlob,
"kshglob" => ZshOptionField::KshGlob,
"shglob" => ZshOptionField::ShGlob,
"bareglobqual" => ZshOptionField::BareGlobQual,
"globdots" => ZshOptionField::GlobDots,
"equals" => ZshOptionField::Equals,
"magicequalsubst" => ZshOptionField::MagicEqualSubst,
"shfileexpansion" => ZshOptionField::ShFileExpansion,
"globassign" => ZshOptionField::GlobAssign,
"ignorebraces" => ZshOptionField::IgnoreBraces,
"ignoreclosebraces" => ZshOptionField::IgnoreCloseBraces,
"braceccl" => ZshOptionField::BraceCcl,
"ksharrays" => ZshOptionField::KshArrays,
"kshzerosubscript" => ZshOptionField::KshZeroSubscript,
"shortloops" => ZshOptionField::ShortLoops,
"shortrepeat" => ZshOptionField::ShortRepeat,
"rcquotes" => ZshOptionField::RcQuotes,
"interactivecomments" => ZshOptionField::InteractiveComments,
"cbases" => ZshOptionField::CBases,
"octalzeroes" => ZshOptionField::OctalZeroes,
_ => return None,
};
Some((field, if invert { !enable } else { enable }))
}