use std::fmt;
use crate::policy::ShellOptionSchema;
use crate::shell::OPT_MONITOR;
#[cfg(any(feature = "frontend", test))]
use crate::shell::ShellState;
#[cfg(test)]
use crate::shell::{
OPT_ALLEXPORT, OPT_ERREXIT, OPT_IGNOREEOF, OPT_NOCLOBBER, OPT_NOEXEC, OPT_NOGLOB, OPT_NOLOG,
OPT_NOTIFY, OPT_NOUNSET, OPT_VERBOSE, OPT_VI, OPT_XTRACE,
};
#[cfg(any(feature = "frontend", test))]
#[derive(Debug, Clone, Default)]
pub struct InitArgs {
pub command_file: Option<String>,
pub command_str: Option<String>,
pub machine_mode: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ArgError {
Usage,
MissingOptionValue(String),
UnknownShortOption(char),
UnknownLongOption(String),
}
impl fmt::Display for ArgError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Usage => write!(f, "usage error"),
Self::MissingOptionValue(opt) => write!(f, "missing value for {opt}"),
Self::UnknownShortOption(opt) => write!(f, "unknown short option: {opt}"),
Self::UnknownLongOption(opt) => write!(f, "unknown long option: {opt}"),
}
}
}
#[cfg(test)]
#[derive(Debug, Clone, Copy)]
pub struct OptionMap {
pub long_name: Option<&'static str>,
pub short_name: Option<char>,
pub value: u32,
}
#[cfg(test)]
pub const SHELL_OPTIONS: &[OptionMap] = &[
OptionMap {
long_name: Some("allexport"),
short_name: Some('a'),
value: OPT_ALLEXPORT,
},
OptionMap {
long_name: Some("notify"),
short_name: Some('b'),
value: OPT_NOTIFY,
},
OptionMap {
long_name: Some("noclobber"),
short_name: Some('C'),
value: OPT_NOCLOBBER,
},
OptionMap {
long_name: Some("errexit"),
short_name: Some('e'),
value: OPT_ERREXIT,
},
OptionMap {
long_name: Some("noglob"),
short_name: Some('f'),
value: OPT_NOGLOB,
},
OptionMap {
long_name: Some("monitor"),
short_name: Some('m'),
value: OPT_MONITOR,
},
OptionMap {
long_name: Some("noexec"),
short_name: Some('n'),
value: OPT_NOEXEC,
},
OptionMap {
long_name: Some("ignoreeof"),
short_name: None,
value: OPT_IGNOREEOF,
},
OptionMap {
long_name: Some("nolog"),
short_name: None,
value: OPT_NOLOG,
},
OptionMap {
long_name: Some("vi"),
short_name: None,
value: OPT_VI,
},
OptionMap {
long_name: Some("nounset"),
short_name: Some('u'),
value: OPT_NOUNSET,
},
OptionMap {
long_name: Some("verbose"),
short_name: Some('v'),
value: OPT_VERBOSE,
},
OptionMap {
long_name: Some("xtrace"),
short_name: Some('x'),
value: OPT_XTRACE,
},
];
#[cfg(test)]
pub fn find_short_option(ch: char) -> Option<OptionMap> {
SHELL_OPTIONS
.iter()
.copied()
.find(|opt| opt.short_name == Some(ch))
}
#[cfg(test)]
pub fn find_long_option(name: &str) -> Option<OptionMap> {
SHELL_OPTIONS
.iter()
.copied()
.find(|opt| opt.long_name.is_some_and(|n| n == name))
}
#[cfg(all(test, feature = "test-support", feature = "unix-runtime"))]
pub fn shell_option_labels() -> impl Iterator<Item = (u32, char)> {
SHELL_OPTIONS
.iter()
.filter_map(|opt| opt.short_name.map(|short| (opt.value, short)))
}
pub(crate) fn shell_option_labels_with_schema(
schema: &ShellOptionSchema,
) -> impl Iterator<Item = (u32, char)> + '_ {
schema
.options()
.iter()
.filter_map(|opt| opt.short_name.map(|short| (opt.option.bits(), short)))
}
pub(crate) fn shell_long_options_with_schema(
schema: &ShellOptionSchema,
) -> impl Iterator<Item = (&str, u32)> + '_ {
schema.options().iter().filter_map(|opt| {
opt.long_name
.as_deref()
.map(|name| (name, opt.option.bits()))
})
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ParsedOptionState {
pub options: u32,
pub next_index: usize,
pub force_positional: bool,
pub populated_monitor: bool,
pub command_str: Option<String>,
pub clear_command_source: bool,
pub machine_mode: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OptionParseMode {
Startup,
SetBuiltin,
}
fn apply_option(options: &mut u32, value: u32, enable: bool) -> bool {
if enable {
*options |= value;
} else {
*options &= !value;
}
value == OPT_MONITOR
}
fn apply_short_option_with_schema(
schema: &ShellOptionSchema,
options: &mut u32,
short: char,
enable: bool,
) -> Result<bool, ArgError> {
let Some(opt) = schema.find_short_option(short) else {
return Err(ArgError::UnknownShortOption(short));
};
Ok(apply_option(options, opt.option.bits(), enable))
}
fn apply_long_option_with_schema(
schema: &ShellOptionSchema,
options: &mut u32,
long: &str,
enable: bool,
) -> Result<bool, ArgError> {
let Some(opt) = schema.find_long_option(long) else {
return Err(ArgError::UnknownLongOption(long.to_string()));
};
Ok(apply_option(options, opt.option.bits(), enable))
}
pub(crate) fn parse_option_args_with_schema(
schema: &ShellOptionSchema,
options: u32,
argv: &[String],
start: usize,
mode: OptionParseMode,
) -> Result<ParsedOptionState, ArgError> {
let mut parsed = ParsedOptionState {
options,
next_index: start,
..ParsedOptionState::default()
};
while parsed.next_index < argv.len() {
let arg = &argv[parsed.next_index];
if arg == "--" {
parsed.force_positional = true;
parsed.next_index += 1;
break;
}
if mode == OptionParseMode::Startup && arg == "--machine" {
parsed.machine_mode = true;
parsed.next_index += 1;
continue;
}
let bytes = arg.as_bytes();
if bytes.is_empty() || (bytes[0] != b'-' && bytes[0] != b'+') {
break;
}
if arg.len() == 1 {
return Err(ArgError::Usage);
}
let enable = bytes[0] == b'-';
let mut chars = arg[1..].chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'o' => {
if chars.peek().is_some() {
return Err(ArgError::Usage);
}
parsed.next_index += 1;
let Some(long) = argv.get(parsed.next_index) else {
return Err(ArgError::MissingOptionValue("-o".to_string()));
};
parsed.populated_monitor |=
apply_long_option_with_schema(schema, &mut parsed.options, long, enable)?;
}
'c' if mode == OptionParseMode::Startup => {
if chars.peek().is_some() {
return Err(ArgError::Usage);
}
parsed.next_index += 1;
let Some(command) = argv.get(parsed.next_index) else {
return Err(ArgError::MissingOptionValue("-c".to_string()));
};
parsed.command_str = Some(command.clone());
parsed.next_index += 1;
return Ok(parsed);
}
's' if mode == OptionParseMode::Startup => {
if chars.peek().is_some() {
return Err(ArgError::Usage);
}
parsed.command_str = None;
parsed.clear_command_source = true;
parsed.next_index += 1;
return Ok(parsed);
}
short => {
parsed.populated_monitor |=
apply_short_option_with_schema(schema, &mut parsed.options, short, enable)?;
}
}
}
parsed.next_index += 1;
}
Ok(parsed)
}
#[cfg(any(feature = "frontend", test))]
pub fn process_args(state: &mut ShellState, argv: &[String]) -> Result<InitArgs, ArgError> {
let mut init = InitArgs::default();
let parsed = parse_option_args_with_schema(
&state.definition.option_schema,
state.options,
argv,
1,
OptionParseMode::Startup,
)?;
state.options = parsed.options;
let mut i = parsed.next_index;
let force_positional = parsed.force_positional;
let populated_monitor = parsed.populated_monitor;
init.command_str = parsed.command_str;
init.machine_mode = parsed.machine_mode;
if parsed.clear_command_source {
init.command_file = None;
}
if init.command_str.is_some() {
if i < argv.len() {
state.variable_store.frame = argv[i..].to_vec();
} else {
state.variable_store.frame = vec![argv[0].clone()];
}
} else if parsed.clear_command_source {
let mut frame = Vec::with_capacity(argv.len().saturating_sub(i) + 1);
frame.push(argv[0].clone());
frame.extend(argv[i..].iter().cloned());
state.variable_store.frame = frame;
} else if i < argv.len() || force_positional {
if i >= argv.len() {
state.variable_store.frame = vec![argv[0].clone()];
} else {
init.command_file = Some(argv[i].clone());
i += 1;
let mut frame = Vec::with_capacity(argv.len() - i + 1);
frame.push(init.command_file.clone().unwrap_or_else(|| argv[0].clone()));
frame.extend(argv[i..].iter().cloned());
state.variable_store.frame = frame;
}
} else {
state.variable_store.frame = vec![argv[0].clone()];
}
let auto_interactive = state.stdin_fd.is_terminal()
&& !init.machine_mode
&& init.command_str.is_none()
&& init.command_file.is_none();
if state.definition.interactive_explicit {
state.interactive = state.interactive
&& !init.machine_mode
&& init.command_str.is_none()
&& init.command_file.is_none();
} else {
state.interactive = auto_interactive;
}
if state.interactive && !populated_monitor {
state.options |= OPT_MONITOR;
}
Ok(init)
}
#[cfg(test)]
mod tests {
use super::*;
fn argv(args: &[&str]) -> Vec<String> {
args.iter().map(|s| s.to_string()).collect()
}
#[test]
fn process_c() {
let args = argv(&["mxsh", "-c", "echo hi"]);
let mut state = ShellState::new();
let init = process_args(&mut state, &args).expect("process_args must parse -c");
assert_eq!(init.command_str.as_deref(), Some("echo hi"));
assert_eq!(state.variable_store.frame, vec!["mxsh".to_string()]);
}
#[test]
fn process_c_missing_value() {
let args = argv(&["mxsh", "-c"]);
let mut state = ShellState::new();
let err = process_args(&mut state, &args).unwrap_err();
assert_eq!(err, ArgError::MissingOptionValue("-c".to_string()));
}
#[test]
fn process_c_not_last_in_group() {
let args = argv(&["mxsh", "-cx", "echo hi"]);
let mut state = ShellState::new();
let err = process_args(&mut state, &args).unwrap_err();
assert_eq!(err, ArgError::Usage);
}
#[test]
fn process_file_and_args() {
let args = argv(&["mxsh", "script.sh", "arg1", "arg2"]);
let mut state = ShellState::new();
let init = process_args(&mut state, &args).expect("process_args must parse script file");
assert_eq!(init.command_file.as_deref(), Some("script.sh"));
assert_eq!(
state.variable_store.frame,
vec!["script.sh", "arg1", "arg2"]
);
}
#[test]
fn process_file_no_extra_args() {
let args = argv(&["mxsh", "script.sh"]);
let mut state = ShellState::new();
let init = process_args(&mut state, &args).expect("process_args must parse script file");
assert_eq!(init.command_file.as_deref(), Some("script.sh"));
assert_eq!(state.variable_store.frame, vec!["script.sh"]);
}
#[test]
fn process_no_args() {
let args = argv(&["mxsh"]);
let mut state = ShellState::new();
let init = process_args(&mut state, &args).expect("process_args must handle no args");
assert!(init.command_str.is_none());
assert!(init.command_file.is_none());
assert_eq!(state.variable_store.frame, vec!["mxsh".to_string()]);
}
#[test]
fn double_dash_with_file() {
let args = argv(&["mxsh", "--", "script.sh", "a1"]);
let mut state = ShellState::new();
let init = process_args(&mut state, &args).expect("process_args must parse --");
assert_eq!(init.command_file.as_deref(), Some("script.sh"));
assert_eq!(state.variable_store.frame, vec!["script.sh", "a1"]);
}
#[test]
fn double_dash_without_file() {
let args = argv(&["mxsh", "--"]);
let mut state = ShellState::new();
let init = process_args(&mut state, &args).expect("-- without a file reads stdin");
assert!(init.command_file.is_none());
assert_eq!(state.variable_store.frame, vec!["mxsh"]);
}
#[test]
fn enable_short_option_allexport() {
let args = argv(&["mxsh", "-a"]);
let mut state = ShellState::new();
process_args(&mut state, &args).expect("process_args must parse -a");
assert_ne!(
state.options & OPT_ALLEXPORT,
0,
"-a must set OPT_ALLEXPORT"
);
}
#[test]
fn enable_short_option_errexit() {
let args = argv(&["mxsh", "-e"]);
let mut state = ShellState::new();
process_args(&mut state, &args).expect("process_args must parse -e");
assert_ne!(state.options & OPT_ERREXIT, 0, "-e must set OPT_ERREXIT");
}
#[test]
fn enable_short_option_noglob() {
let args = argv(&["mxsh", "-f"]);
let mut state = ShellState::new();
process_args(&mut state, &args).expect("process_args must parse -f");
assert_ne!(state.options & OPT_NOGLOB, 0, "-f must set OPT_NOGLOB");
}
#[test]
fn enable_short_option_verbose() {
let args = argv(&["mxsh", "-v"]);
let mut state = ShellState::new();
process_args(&mut state, &args).expect("process_args must parse -v");
assert_ne!(state.options & OPT_VERBOSE, 0, "-v must set OPT_VERBOSE");
}
#[test]
fn enable_short_option_xtrace() {
let args = argv(&["mxsh", "-x"]);
let mut state = ShellState::new();
process_args(&mut state, &args).expect("process_args must parse -x");
assert_ne!(state.options & OPT_XTRACE, 0, "-x must set OPT_XTRACE");
}
#[test]
fn enable_short_option_nounset() {
let args = argv(&["mxsh", "-u"]);
let mut state = ShellState::new();
process_args(&mut state, &args).expect("process_args must parse -u");
assert_ne!(state.options & OPT_NOUNSET, 0, "-u must set OPT_NOUNSET");
}
#[test]
fn enable_short_option_notify() {
let args = argv(&["mxsh", "-b"]);
let mut state = ShellState::new();
process_args(&mut state, &args).expect("process_args must parse -b");
assert_ne!(state.options & OPT_NOTIFY, 0, "-b must set OPT_NOTIFY");
}
#[test]
fn enable_short_option_noclobber() {
let args = argv(&["mxsh", "-C"]);
let mut state = ShellState::new();
process_args(&mut state, &args).expect("process_args must parse -C");
assert_ne!(
state.options & OPT_NOCLOBBER,
0,
"-C must set OPT_NOCLOBBER"
);
}
#[test]
fn enable_short_option_monitor() {
let args = argv(&["mxsh", "-m"]);
let mut state = ShellState::new();
process_args(&mut state, &args).expect("process_args must parse -m");
assert_ne!(state.options & OPT_MONITOR, 0, "-m must set OPT_MONITOR");
}
#[test]
fn enable_short_option_noexec() {
let args = argv(&["mxsh", "-n"]);
let mut state = ShellState::new();
process_args(&mut state, &args).expect("process_args must parse -n");
assert_ne!(state.options & OPT_NOEXEC, 0, "-n must set OPT_NOEXEC");
}
#[test]
fn combined_short_options() {
let args = argv(&["mxsh", "-aex"]);
let mut state = ShellState::new();
process_args(&mut state, &args).expect("process_args must parse -aex");
assert_ne!(state.options & OPT_ALLEXPORT, 0);
assert_ne!(state.options & OPT_ERREXIT, 0);
assert_ne!(state.options & OPT_XTRACE, 0);
}
#[test]
fn disable_short_option() {
let args = argv(&["mxsh", "-a", "+a"]);
let mut state = ShellState::new();
process_args(&mut state, &args).expect("process_args must parse -a +a");
assert_eq!(
state.options & OPT_ALLEXPORT,
0,
"+a must clear OPT_ALLEXPORT"
);
}
#[test]
fn disable_preserves_other_options() {
let args = argv(&["mxsh", "-aex", "+e"]);
let mut state = ShellState::new();
process_args(&mut state, &args).expect("process_args must parse -aex +e");
assert_ne!(state.options & OPT_ALLEXPORT, 0, "-a should remain set");
assert_eq!(state.options & OPT_ERREXIT, 0, "+e should clear errexit");
assert_ne!(state.options & OPT_XTRACE, 0, "-x should remain set");
}
#[test]
fn long_option_allexport() {
let args = argv(&["mxsh", "-o", "allexport"]);
let mut state = ShellState::new();
process_args(&mut state, &args).expect("process_args must parse -o allexport");
assert_ne!(state.options & OPT_ALLEXPORT, 0);
}
#[test]
fn long_option_ignoreeof() {
let args = argv(&["mxsh", "-o", "ignoreeof"]);
let mut state = ShellState::new();
process_args(&mut state, &args).expect("process_args must parse -o ignoreeof");
assert_ne!(state.options & OPT_IGNOREEOF, 0);
}
#[test]
fn long_option_nolog() {
let args = argv(&["mxsh", "-o", "nolog"]);
let mut state = ShellState::new();
process_args(&mut state, &args).expect("process_args must parse -o nolog");
assert_ne!(state.options & OPT_NOLOG, 0);
}
#[test]
fn long_option_vi() {
let args = argv(&["mxsh", "-o", "vi"]);
let mut state = ShellState::new();
process_args(&mut state, &args).expect("process_args must parse -o vi");
assert_ne!(state.options & OPT_VI, 0);
}
#[test]
fn disable_long_option() {
let args = argv(&["mxsh", "-o", "allexport", "+o", "allexport"]);
let mut state = ShellState::new();
process_args(&mut state, &args).expect("process_args must parse +o allexport");
assert_eq!(
state.options & OPT_ALLEXPORT,
0,
"+o allexport must clear allexport"
);
}
#[test]
fn long_option_missing_value() {
let args = argv(&["mxsh", "-o"]);
let mut state = ShellState::new();
let err = process_args(&mut state, &args).unwrap_err();
assert_eq!(err, ArgError::MissingOptionValue("-o".to_string()));
}
#[test]
fn long_option_unknown() {
let args = argv(&["mxsh", "-o", "nonexistent"]);
let mut state = ShellState::new();
let err = process_args(&mut state, &args).unwrap_err();
assert_eq!(err, ArgError::UnknownLongOption("nonexistent".to_string()));
}
#[test]
fn long_option_o_not_last_in_group() {
let args = argv(&["mxsh", "-oa", "allexport"]);
let mut state = ShellState::new();
let err = process_args(&mut state, &args).unwrap_err();
assert_eq!(err, ArgError::Usage);
}
#[test]
fn process_s_clears_command() {
let args = argv(&["mxsh", "-s"]);
let mut state = ShellState::new();
let init = process_args(&mut state, &args).expect("process_args must parse -s");
assert!(init.command_str.is_none());
assert!(init.command_file.is_none());
}
#[test]
fn process_s_not_last_in_group() {
let args = argv(&["mxsh", "-sa"]);
let mut state = ShellState::new();
let err = process_args(&mut state, &args).unwrap_err();
assert_eq!(err, ArgError::Usage);
}
#[test]
fn unknown_short_option() {
let args = argv(&["mxsh", "-z"]);
let mut state = ShellState::new();
let err = process_args(&mut state, &args).unwrap_err();
assert_eq!(err, ArgError::UnknownShortOption('z'));
}
#[test]
fn unknown_short_option_plus() {
let args = argv(&["mxsh", "+z"]);
let mut state = ShellState::new();
let err = process_args(&mut state, &args).unwrap_err();
assert_eq!(err, ArgError::UnknownShortOption('z'));
}
#[test]
fn bare_minus() {
let args = argv(&["mxsh", "-"]);
let mut state = ShellState::new();
let err = process_args(&mut state, &args).unwrap_err();
assert_eq!(err, ArgError::Usage);
}
#[test]
fn bare_plus() {
let args = argv(&["mxsh", "+"]);
let mut state = ShellState::new();
let err = process_args(&mut state, &args).unwrap_err();
assert_eq!(err, ArgError::Usage);
}
#[test]
fn options_then_file() {
let args = argv(&["mxsh", "-e", "-x", "script.sh"]);
let mut state = ShellState::new();
let init = process_args(&mut state, &args).expect("process_args must parse options + file");
assert_ne!(state.options & OPT_ERREXIT, 0);
assert_ne!(state.options & OPT_XTRACE, 0);
assert_eq!(init.command_file.as_deref(), Some("script.sh"));
assert_eq!(state.variable_store.frame, vec!["script.sh"]);
}
#[test]
fn options_then_double_dash_then_file() {
let args = argv(&["mxsh", "-e", "--", "script.sh", "a1"]);
let mut state = ShellState::new();
let init =
process_args(&mut state, &args).expect("process_args must parse options -- file");
assert_ne!(state.options & OPT_ERREXIT, 0);
assert_eq!(init.command_file.as_deref(), Some("script.sh"));
assert_eq!(state.variable_store.frame, vec!["script.sh", "a1"]);
}
#[test]
fn process_c_with_positionals() {
let args = argv(&["mxsh", "-c", "echo $0 $1", "name", "arg1"]);
let mut state = ShellState::new();
let init = process_args(&mut state, &args).expect("process_args must parse -c with args");
assert_eq!(init.command_str.as_deref(), Some("echo $0 $1"));
assert_eq!(state.variable_store.frame, vec!["name", "arg1"]);
}
#[test]
fn process_machine_payload() {
let args = argv(&["mxsh", "--machine", "-c", "{\"kind\":\"payload\"}"]);
let mut state = ShellState::new();
let init = process_args(&mut state, &args).expect("process_args must parse --machine");
assert!(init.machine_mode);
assert_eq!(init.command_str.as_deref(), Some("{\"kind\":\"payload\"}"));
assert!(!state.interactive);
}
#[test]
fn explicit_monitor_disable_is_honored() {
let args = argv(&["mxsh", "+m"]);
let mut state = ShellState::new();
process_args(&mut state, &args).expect("process_args must parse +m");
assert_eq!(
state.options & OPT_MONITOR,
0,
"+m must keep monitor cleared"
);
}
#[test]
fn explicit_monitor_via_long_option() {
let args = argv(&["mxsh", "+o", "monitor"]);
let mut state = ShellState::new();
process_args(&mut state, &args).expect("process_args must parse +o monitor");
assert_eq!(
state.options & OPT_MONITOR,
0,
"+o monitor must keep monitor cleared"
);
}
#[test]
fn arg_error_display_usage() {
assert_eq!(format!("{}", ArgError::Usage), "usage error");
}
#[test]
fn arg_error_display_missing_option_value() {
let err = ArgError::MissingOptionValue("-o".to_string());
assert_eq!(format!("{err}"), "missing value for -o");
}
#[test]
fn arg_error_display_unknown_short_option() {
let err = ArgError::UnknownShortOption('z');
assert_eq!(format!("{err}"), "unknown short option: z");
}
#[test]
fn arg_error_display_unknown_long_option() {
let err = ArgError::UnknownLongOption("bogus".to_string());
assert_eq!(format!("{err}"), "unknown long option: bogus");
}
#[test]
fn find_short_option_known() {
let opt = find_short_option('a');
assert!(opt.is_some());
assert_eq!(opt.unwrap().value, OPT_ALLEXPORT);
}
#[test]
fn find_short_option_unknown() {
assert!(find_short_option('Z').is_none());
}
#[test]
fn find_long_option_known() {
let opt = find_long_option("errexit");
assert!(opt.is_some());
assert_eq!(opt.unwrap().value, OPT_ERREXIT);
}
#[test]
fn find_long_option_unknown() {
assert!(find_long_option("nonexistent").is_none());
}
#[test]
fn find_long_option_no_accidental_short_match() {
assert!(find_long_option("h").is_none());
assert!(find_short_option('h').is_none());
}
#[test]
fn empty_string_treated_as_positional() {
let args = argv(&["mxsh", ""]);
let mut state = ShellState::new();
let init = process_args(&mut state, &args).expect("empty string as positional");
assert_eq!(init.command_file.as_deref(), Some(""));
}
#[test]
fn multiple_option_groups_mixed() {
let args = argv(&["mxsh", "-ae", "-vx"]);
let mut state = ShellState::new();
process_args(&mut state, &args).expect("process_args must parse -ae -vx");
assert_ne!(state.options & OPT_ALLEXPORT, 0);
assert_ne!(state.options & OPT_ERREXIT, 0);
assert_ne!(state.options & OPT_VERBOSE, 0);
assert_ne!(state.options & OPT_XTRACE, 0);
}
#[test]
fn enable_then_disable_then_enable() {
let args = argv(&["mxsh", "-a", "+a", "-a"]);
let mut state = ShellState::new();
process_args(&mut state, &args).expect("process_args must parse toggle sequence");
assert_ne!(
state.options & OPT_ALLEXPORT,
0,
"final -a should re-enable allexport"
);
}
}