Repository: reubeno/brush
Pull request #105 (2024-07-01 06:52:09)

feat: enable -O and +O on command line
Enable `-O` and `+O` on the command line to specify shopt-style options.


Patch:
diff --git a/brush-core/src/builtins/set.rs b/brush-core/src/builtins/set.rs
index bc247f3..6e136ca 100644
--- a/brush-core/src/builtins/set.rs
+++ b/brush-core/src/builtins/set.rs
@@ -251,7 +251,7 @@ impl builtin::Command for SetCommand {
 
         for (option_name, value) in named_options {
             if let Some(option_def) = namedoptions::SET_O_OPTIONS.get(option_name.as_str()) {
-                (option_def.setter)(context.shell, value);
+                (option_def.setter)(&mut context.shell.options, value);
             } else {
                 result = builtin::ExitCode::InvalidUsage;
             }
diff --git a/brush-core/src/builtins/shopt.rs b/brush-core/src/builtins/shopt.rs
index b238db8..d3f5117 100644
--- a/brush-core/src/builtins/shopt.rs
+++ b/brush-core/src/builtins/shopt.rs
@@ -62,7 +62,7 @@ impl builtin::Command for ShoptCommand {
             };
 
             for (option_name, option_definition) in options {
-                let option_value = (option_definition.getter)(context.shell);
+                let option_value = (option_definition.getter)(&context.shell.options);
                 if self.set && !option_value {
                     continue;
                 }
@@ -98,11 +98,11 @@ impl builtin::Command for ShoptCommand {
 
                 if let Some(option_definition) = option_definition {
                     if self.set {
-                        (option_definition.setter)(context.shell, true);
+                        (option_definition.setter)(&mut context.shell.options, true);
                     } else if self.unset {
-                        (option_definition.setter)(context.shell, false);
+                        (option_definition.setter)(&mut context.shell.options, false);
                     } else {
-                        let option_value = (option_definition.getter)(context.shell);
+                        let option_value = (option_definition.getter)(&context.shell.options);
                         if !option_value {
                             return_value = builtin::ExitCode::Custom(1);
                         }
diff --git a/brush-core/src/namedoptions.rs b/brush-core/src/namedoptions.rs
index a4e4452..8f87adb 100644
--- a/brush-core/src/namedoptions.rs
+++ b/brush-core/src/namedoptions.rs
@@ -1,10 +1,10 @@
 use lazy_static::lazy_static;
 use std::collections::HashMap;
 
-use crate::Shell;
+use crate::options::RuntimeOptions;
 
-pub(crate) type OptionGetter = fn(shell: &Shell) -> bool;
-pub(crate) type OptionSetter = fn(shell: &mut Shell, value: bool) -> ();
+pub(crate) type OptionGetter = fn(shell: &RuntimeOptions) -> bool;
+pub(crate) type OptionSetter = fn(shell: &mut RuntimeOptions, value: bool) -> ();
 
 pub(crate) struct OptionDefinition {
     pub getter: OptionGetter,
@@ -22,116 +22,113 @@ lazy_static! {
         (
             'a',
             OptionDefinition::new(
-                |shell| shell.options.export_variables_on_modification,
-                |shell, value| shell.options.export_variables_on_modification = value
+                |options| options.export_variables_on_modification,
+                |options, value| options.export_variables_on_modification = value
             )
         ),
         (
             'b',
             OptionDefinition::new(
-                |shell| shell.options.notify_job_termination_immediately,
-                |shell, value| shell.options.notify_job_termination_immediately = value
+                |options| options.notify_job_termination_immediately,
+                |options, value| options.notify_job_termination_immediately = value
             )
         ),
         (
             'e',
             OptionDefinition::new(
-                |shell| shell.options.exit_on_nonzero_command_exit,
-                |shell, value| shell.options.exit_on_nonzero_command_exit = value
+                |options| options.exit_on_nonzero_command_exit,
+                |options, value| options.exit_on_nonzero_command_exit = value
             )
         ),
         (
             'f',
             OptionDefinition::new(
-                |shell| shell.options.disable_filename_globbing,
-                |shell, value| shell.options.disable_filename_globbing = value
+                |options| options.disable_filename_globbing,
+                |options, value| options.disable_filename_globbing = value
             )
         ),
         (
             'h',
             OptionDefinition::new(
-                |shell| shell.options.remember_command_locations,
-                |shell, value| shell.options.remember_command_locations = value
+                |options| options.remember_command_locations,
+                |options, value| options.remember_command_locations = value
             )
         ),
         (
             'i',
             OptionDefinition::new(
-                |shell| shell.options.interactive,
-                |shell, value| shell.options.interactive = value
+                |options| options.interactive,
+                |options, value| options.interactive = value
             )
         ),
         (
             'k',
             OptionDefinition::new(
-                |shell| shell.options.place_all_assignment_args_in_command_env,
-                |shell, value| shell.options.place_all_assignment_args_in_command_env = value
+                |options| options.place_all_assignment_args_in_command_env,
+                |options, value| options.place_all_assignment_args_in_command_env = value
             )
         ),
         (
             'm',
             OptionDefinition::new(
-                |shell| shell.options.enable_job_control,
-                |shell, value| shell.options.enable_job_control = value
+                |options| options.enable_job_control,
+                |options, value| options.enable_job_control = value
             )
         ),
         (
             'n',
             OptionDefinition::new(
-                |shell| shell.options.do_not_execute_commands,
-                |shell, value| shell.options.do_not_execute_commands = value
+                |options| options.do_not_execute_commands,
+                |options, value| options.do_not_execute_commands = value
             )
         ),
         (
             'p',
             OptionDefinition::new(
-                |shell| shell.options.real_effective_uid_mismatch,
-                |shell, value| shell.options.real_effective_uid_mismatch = value
+                |options| options.real_effective_uid_mismatch,
+                |options, value| options.real_effective_uid_mismatch = value
             )
         ),
         (
             't',
             OptionDefinition::new(
-                |shell| shell.options.exit_after_one_command,
-                |shell, value| shell.options.exit_after_one_command = value
+                |options| options.exit_after_one_command,
+                |options, value| options.exit_after_one_command = value
             )
         ),
         (
             'u',
             OptionDefinition::new(
-                |shell| shell.options.treat_unset_variables_as_error,
-                |shell, value| shell.options.treat_unset_variables_as_error = value
+                |options| options.treat_unset_variables_as_error,
+                |options, value| options.treat_unset_variables_as_error = value
             )
         ),
         (
             'v',
             OptionDefinition::new(
-                |shell| shell.options.print_shell_input_lines,
-                |shell, value| shell.options.print_shell_input_lines = value
+                |options| options.print_shell_input_lines,
+                |options, value| options.print_shell_input_lines = value
             )
         ),
         (
             'x',
             OptionDefinition::new(
-                |shell| shell.options.print_commands_and_arguments,
-                |shell, value| shell.options.print_commands_and_arguments = value
+                |options| options.print_commands_and_arguments,
+                |options, value| options.print_commands_and_arguments = value
             )
         ),
         (
             'B',
             OptionDefinition::new(
-                |shell| shell.options.perform_brace_expansion,
-                |shell, value| shell.options.perform_brace_expansion = value
+                |options| options.perform_brace_expansion,
+                |options, value| options.perform_brace_expansion = value
             )
         ),
         (
             'C',
             OptionDefinition::new(
-                |shell| shell
-                    .options
-                    .disallow_overwriting_regular_files_via_output_redirection,
-                |shell, value| shell
-                    .options
+                |options| options.disallow_overwriting_regular_files_via_output_redirection,
+                |options, value| options
                     .disallow_overwriting_regular_files_via_output_redirection =
                     value
             )
@@ -139,36 +136,36 @@ lazy_static! {
         (
             'E',
             OptionDefinition::new(
-                |shell| shell.options.shell_functions_inherit_err_trap,
-                |shell, value| shell.options.shell_functions_inherit_err_trap = value
+                |options| options.shell_functions_inherit_err_trap,
+                |options, value| options.shell_functions_inherit_err_trap = value
             )
         ),
         (
             'H',
             OptionDefinition::new(
-                |shell| shell.options.enable_bang_style_history_substitution,
-                |shell, value| shell.options.enable_bang_style_history_substitution = value
+                |options| options.enable_bang_style_history_substitution,
+                |options, value| options.enable_bang_style_history_substitution = value
             )
         ),
         (
             'P',
             OptionDefinition::new(
-                |shell| shell.options.do_not_resolve_symlinks_when_changing_dir,
-                |shell, value| shell.options.do_not_resolve_symlinks_when_changing_dir = value
+                |options| options.do_not_resolve_symlinks_when_changing_dir,
+                |options, value| options.do_not_resolve_symlinks_when_changing_dir = value
             )
         ),
         (
             'T',
             OptionDefinition::new(
-                |shell| shell.options.shell_functions_inherit_debug_and_return_traps,
-                |shell, value| shell.options.shell_functions_inherit_debug_and_return_traps = value
+                |options| options.shell_functions_inherit_debug_and_return_traps,
+                |options, value| options.shell_functions_inherit_debug_and_return_traps = value
             )
         ),
         (
             's',
             OptionDefinition::new(
-                |shell| shell.options.read_commands_from_stdin,
-                |shell, value| shell.options.read_commands_from_stdin = value
+                |options| options.read_commands_from_stdin,
+                |options, value| options.read_commands_from_stdin = value
             )
         ),
     ]);
@@ -176,102 +173,99 @@ lazy_static! {
         (
             "allexport",
             OptionDefinition::new(
-                |shell| shell.options.export_variables_on_modification,
-                |shell, value| shell.options.export_variables_on_modification = value
+                |options| options.export_variables_on_modification,
+                |options, value| options.export_variables_on_modification = value
             )
         ),
         (
             "braceexpand",
             OptionDefinition::new(
-                |shell| shell.options.perform_brace_expansion,
-                |shell, value| shell.options.perform_brace_expansion = value
+                |options| options.perform_brace_expansion,
+                |options, value| options.perform_brace_expansion = value
             )
         ),
         (
             "emacs",
             OptionDefinition::new(
-                |shell| shell.options.emacs_mode,
-                |shell, value| shell.options.emacs_mode = value
+                |options| options.emacs_mode,
+                |options, value| options.emacs_mode = value
             )
         ),
         (
             "errexit",
             OptionDefinition::new(
-                |shell| shell.options.exit_on_nonzero_command_exit,
-                |shell, value| shell.options.exit_on_nonzero_command_exit = value
+                |options| options.exit_on_nonzero_command_exit,
+                |options, value| options.exit_on_nonzero_command_exit = value
             )
         ),
         (
             "errtrace",
             OptionDefinition::new(
-                |shell| shell.options.shell_functions_inherit_err_trap,
-                |shell, value| shell.options.shell_functions_inherit_err_trap = value
+                |options| options.shell_functions_inherit_err_trap,
+                |options, value| options.shell_functions_inherit_err_trap = value
             )
         ),
         (
             "functrace",
             OptionDefinition::new(
-                |shell| shell.options.shell_functions_inherit_debug_and_return_traps,
-                |shell, value| shell.options.shell_functions_inherit_debug_and_return_traps = value
+                |options| options.shell_functions_inherit_debug_and_return_traps,
+                |options, value| options.shell_functions_inherit_debug_and_return_traps = value
             )
         ),
         (
             "hashall",
             OptionDefinition::new(
-                |shell| shell.options.remember_command_locations,
-                |shell, value| shell.options.remember_command_locations = value
+                |options| options.remember_command_locations,
+                |options, value| options.remember_command_locations = value
             )
         ),
         (
             "histexpand",
             OptionDefinition::new(
-                |shell| shell.options.enable_bang_style_history_substitution,
-                |shell, value| shell.options.enable_bang_style_history_substitution = value
+                |options| options.enable_bang_style_history_substitution,
+                |options, value| options.enable_bang_style_history_substitution = value
             )
         ),
         (
             "history",
             OptionDefinition::new(
-                |shell| shell.options.enable_command_history,
-                |shell, value| shell.options.enable_command_history = value
+                |options| options.enable_command_history,
+                |options, value| options.enable_command_history = value
             )
         ),
         (
             "ignoreeof",
             OptionDefinition::new(
-                |shell| shell.options.ignore_eof,
-                |shell, value| shell.options.ignore_eof = value
+                |options| options.ignore_eof,
+                |options, value| options.ignore_eof = value
             )
         ),
         (
             "interactive-comments",
             OptionDefinition::new(
-                |shell| shell.options.interactive_comments,
-                |shell, value| shell.options.interactive_comments = value
+                |options| options.interactive_comments,
+                |options, value| options.interactive_comments = value
             )
         ),
         (
             "keyword",
             OptionDefinition::new(
-                |shell| shell.options.place_all_assignment_args_in_command_env,
-                |shell, value| shell.options.place_all_assignment_args_in_command_env = value
+                |options| options.place_all_assignment_args_in_command_env,
+                |options, value| options.place_all_assignment_args_in_command_env = value
             )
         ),
         (
             "monitor",
             OptionDefinition::new(
-                |shell| shell.options.enable_job_control,
-                |shell, value| shell.options.enable_job_control = value
+                |options| options.enable_job_control,
+                |options, value| options.enable_job_control = value
             )
         ),
         (
             "noclobber",
             OptionDefinition::new(
-                |shell| shell
-                    .options
-                    .disallow_overwriting_regular_files_via_output_redirection,
-                |shell, value| shell
-                    .options
+                |options| options.disallow_overwriting_regular_files_via_output_redirection,
+                |options, value| options
                     .disallow_overwriting_regular_files_via_output_redirection =
                     value
             )
@@ -279,86 +273,86 @@ lazy_static! {
         (
             "noexec",
             OptionDefinition::new(
-                |shell| shell.options.do_not_execute_commands,
-                |shell, value| shell.options.do_not_execute_commands = value
+                |options| options.do_not_execute_commands,
+                |options, value| options.do_not_execute_commands = value
             )
         ),
         (
             "noglob",
             OptionDefinition::new(
-                |shell| shell.options.disable_filename_globbing,
-                |shell, value| shell.options.disable_filename_globbing = value
+                |options| options.disable_filename_globbing,
+                |options, value| options.disable_filename_globbing = value
             )
         ),
         ("nolog", OptionDefinition::new(|_| false, |_, _| ())),
         (
             "notify",
             OptionDefinition::new(
-                |shell| shell.options.notify_job_termination_immediately,
-                |shell, value| shell.options.notify_job_termination_immediately = value
+                |options| options.notify_job_termination_immediately,
+                |options, value| options.notify_job_termination_immediately = value
             )
         ),
         (
             "nounset",
             OptionDefinition::new(
-                |shell| shell.options.treat_unset_variables_as_error,
-                |shell, value| shell.options.treat_unset_variables_as_error = value
+                |options| options.treat_unset_variables_as_error,
+                |options, value| options.treat_unset_variables_as_error = value
             )
         ),
         (
             "onecmd",
             OptionDefinition::new(
-                |shell| shell.options.exit_after_one_command,
-                |shell, value| shell.options.exit_after_one_command = value
+                |options| options.exit_after_one_command,
+                |options, value| options.exit_after_one_command = value
             )
         ),
         (
             "physical",
             OptionDefinition::new(
-                |shell| shell.options.do_not_resolve_symlinks_when_changing_dir,
-                |shell, value| shell.options.do_not_resolve_symlinks_when_changing_dir = value
+                |options| options.do_not_resolve_symlinks_when_changing_dir,
+                |options, value| options.do_not_resolve_symlinks_when_changing_dir = value
             )
         ),
         (
             "pipefail",
             OptionDefinition::new(
-                |shell| shell.options.return_first_failure_from_pipeline,
-                |shell, value| shell.options.return_first_failure_from_pipeline = value
+                |options| options.return_first_failure_from_pipeline,
+                |options, value| options.return_first_failure_from_pipeline = value
             )
         ),
         (
             "posix",
             OptionDefinition::new(
-                |shell| shell.options.posix_mode,
-                |shell, value| shell.options.posix_mode = value
+                |options| options.posix_mode,
+                |options, value| options.posix_mode = value
             )
         ),
         (
             "privileged",
             OptionDefinition::new(
-                |shell| shell.options.real_effective_uid_mismatch,
-                |shell, value| shell.options.real_effective_uid_mismatch = value
+                |options| options.real_effective_uid_mismatch,
+                |options, value| options.real_effective_uid_mismatch = value
             )
         ),
         (
             "verbose",
             OptionDefinition::new(
-                |shell| shell.options.print_shell_input_lines,
-                |shell, value| shell.options.print_shell_input_lines = value
+                |options| options.print_shell_input_lines,
+                |options, value| options.print_shell_input_lines = value
             )
         ),
         (
             "vi",
             OptionDefinition::new(
-                |shell| shell.options.vi_mode,
-                |shell, value| shell.options.vi_mode = value
+                |options| options.vi_mode,
+                |options, value| options.vi_mode = value
             )
         ),
         (
             "xtrace",
             OptionDefinition::new(
-                |shell| shell.options.print_commands_and_arguments,
-                |shell, value| shell.options.print_commands_and_arguments = value
+                |options| options.print_commands_and_arguments,
+                |options, value| options.print_commands_and_arguments = value
             )
         ),
     ]);
@@ -366,372 +360,372 @@ lazy_static! {
         (
             "autocd",
             OptionDefinition::new(
-                |shell| shell.options.auto_cd,
-                |shell, value| shell.options.auto_cd = value
+                |options| options.auto_cd,
+                |options, value| options.auto_cd = value
             )
         ),
         (
             "assoc_expand_once",
             OptionDefinition::new(
-                |shell| shell.options.assoc_expand_once,
-                |shell, value| shell.options.assoc_expand_once = value
+                |options| options.assoc_expand_once,
+                |options, value| options.assoc_expand_once = value
             )
         ),
         (
             "cdable_vars",
             OptionDefinition::new(
-                |shell| shell.options.cdable_vars,
-                |shell, value| shell.options.cdable_vars = value
+                |options| options.cdable_vars,
+                |options, value| options.cdable_vars = value
             )
         ),
         (
             "cdspell",
             OptionDefinition::new(
-                |shell| shell.options.cd_autocorrect_spelling,
-                |shell, value| shell.options.cd_autocorrect_spelling = value
+                |options| options.cd_autocorrect_spelling,
+                |options, value| options.cd_autocorrect_spelling = value
             )
         ),
         (
             "checkhash",
             OptionDefinition::new(
-                |shell| shell.options.check_hashtable_before_command_exec,
-                |shell, value| shell.options.check_hashtable_before_command_exec = value
+                |options| options.check_hashtable_before_command_exec,
+                |options, value| options.check_hashtable_before_command_exec = value
             )
         ),
         (
             "checkjobs",
             OptionDefinition::new(
-                |shell| shell.options.check_jobs_before_exit,
-                |shell, value| shell.options.check_jobs_before_exit = value
+                |options| options.check_jobs_before_exit,
+                |options, value| options.check_jobs_before_exit = value
             )
         ),
         (
             "checkwinsize",
             OptionDefinition::new(
-                |shell| shell.options.check_window_size_after_external_commands,
-                |shell, value| shell.options.check_window_size_after_external_commands = value
+                |options| options.check_window_size_after_external_commands,
+                |options, value| options.check_window_size_after_external_commands = value
             )
         ),
         (
             "cmdhist",
             OptionDefinition::new(
-                |shell| shell.options.save_multiline_cmds_in_history,
-                |shell, value| shell.options.save_multiline_cmds_in_history = value
+                |options| options.save_multiline_cmds_in_history,
+                |options, value| options.save_multiline_cmds_in_history = value
             )
         ),
         (
             "compat31",
             OptionDefinition::new(
-                |shell| shell.options.compat31,
-                |shell, value| shell.options.compat31 = value
+                |options| options.compat31,
+                |options, value| options.compat31 = value
             )
         ),
         (
             "compat32",
             OptionDefinition::new(
-                |shell| shell.options.compat32,
-                |shell, value| shell.options.compat32 = value
+                |options| options.compat32,
+                |options, value| options.compat32 = value
             )
         ),
         (
             "compat40",
             OptionDefinition::new(
-                |shell| shell.options.compat40,
-                |shell, value| shell.options.compat40 = value
+                |options| options.compat40,
+                |options, value| options.compat40 = value
             )
         ),
         (
             "compat41",
             OptionDefinition::new(
-                |shell| shell.options.compat41,
-                |shell, value| shell.options.compat41 = value
+                |options| options.compat41,
+                |options, value| options.compat41 = value
             )
         ),
         (
             "compat42",
             OptionDefinition::new(
-                |shell| shell.options.compat42,
-                |shell, value| shell.options.compat42 = value
+                |options| options.compat42,
+                |options, value| options.compat42 = value
             )
         ),
         (
             "compat43",
             OptionDefinition::new(
-                |shell| shell.options.compat43,
-                |shell, value| shell.options.compat43 = value
+                |options| options.compat43,
+                |options, value| options.compat43 = value
             )
         ),
         (
             "compat44",
             OptionDefinition::new(
-                |shell| shell.options.compat44,
-                |shell, value| shell.options.compat44 = value
+                |options| options.compat44,
+                |options, value| options.compat44 = value
             )
         ),
         (
             "complete_fullquote",
             OptionDefinition::new(
-                |shell| shell.options.quote_all_metachars_in_completion,
-                |shell, value| shell.options.quote_all_metachars_in_completion = value
+                |options| options.quote_all_metachars_in_completion,
+                |options, value| options.quote_all_metachars_in_completion = value
             )
         ),
         (
             "direxpand",
             OptionDefinition::new(
-                |shell| shell.options.expand_dir_names_on_completion,
-                |shell, value| shell.options.expand_dir_names_on_completion = value
+                |options| options.expand_dir_names_on_completion,
+                |options, value| options.expand_dir_names_on_completion = value
             )
         ),
         (
             "dirspell",
             OptionDefinition::new(
-                |shell| shell.options.autocorrect_dir_spelling_on_completion,
-                |shell, value| shell.options.autocorrect_dir_spelling_on_completion = value
+                |options| options.autocorrect_dir_spelling_on_completion,
+                |options, value| options.autocorrect_dir_spelling_on_completion = value
             )
         ),
         (
             "dotglob",
             OptionDefinition::new(
-                |shell| shell.options.glob_matches_dotfiles,
-                |shell, value| shell.options.glob_matches_dotfiles = value
+                |options| options.glob_matches_dotfiles,
+                |options, value| options.glob_matches_dotfiles = value
             )
         ),
         (
             "execfail",
             OptionDefinition::new(
-                |shell| shell.options.exit_on_exec_fail,
-                |shell, value| shell.options.exit_on_exec_fail = value
+                |options| options.exit_on_exec_fail,
+                |options, value| options.exit_on_exec_fail = value
             )
         ),
         (
             "expand_aliases",
             OptionDefinition::new(
-                |shell| shell.options.expand_aliases,
-                |shell, value| shell.options.expand_aliases = value
+                |options| options.expand_aliases,
+                |options, value| options.expand_aliases = value
             )
         ),
         (
             "extdebug",
             OptionDefinition::new(
-                |shell| shell.options.enable_debugger,
-                |shell, value| shell.options.enable_debugger = value
+                |options| options.enable_debugger,
+                |options, value| options.enable_debugger = value
             )
         ),
         (
             "extglob",
             OptionDefinition::new(
-                |shell| shell.options.extended_globbing,
-                |shell, value| shell.options.extended_globbing = value
+                |options| options.extended_globbing,
+                |options, value| options.extended_globbing = value
             )
         ),
         (
             "extquote",
             OptionDefinition::new(
-                |shell| shell.options.extquote,
-                |shell, value| shell.options.extquote = value
+                |options| options.extquote,
+                |options, value| options.extquote = value
             )
         ),
         (
             "failglob",
             OptionDefinition::new(
-                |shell| shell.options.fail_expansion_on_globs_without_match,
-                |shell, value| shell.options.fail_expansion_on_globs_without_match = value
+                |options| options.fail_expansion_on_globs_without_match,
+                |options, value| options.fail_expansion_on_globs_without_match = value
             )
         ),
         (
             "force_fignore",
             OptionDefinition::new(
-                |shell| shell.options.force_fignore,
-                |shell, value| shell.options.force_fignore = value
+                |options| options.force_fignore,
+                |options, value| options.force_fignore = value
             )
         ),
         (
             "globasciiranges",
             OptionDefinition::new(
-                |shell| shell.options.glob_ranges_use_c_locale,
-                |shell, value| shell.options.glob_ranges_use_c_locale = value
+                |options| options.glob_ranges_use_c_locale,
+                |options, value| options.glob_ranges_use_c_locale = value
             )
         ),
         (
             "globstar",
             OptionDefinition::new(
-                |shell| shell.options.enable_star_star_glob,
-                |shell, value| shell.options.enable_star_star_glob = value
+                |options| options.enable_star_star_glob,
+                |options, value| options.enable_star_star_glob = value
             )
         ),
         (
             "gnu_errfmt",
             OptionDefinition::new(
-                |shell| shell.options.errors_in_gnu_format,
-                |shell, value| shell.options.errors_in_gnu_format = value
+                |options| options.errors_in_gnu_format,
+                |options, value| options.errors_in_gnu_format = value
             )
         ),
         (
             "histappend",
             OptionDefinition::new(
-                |shell| shell.options.append_to_history_file,
-                |shell, value| shell.options.append_to_history_file = value
+                |options| options.append_to_history_file,
+                |options, value| options.append_to_history_file = value
             )
         ),
         (
             "histreedit",
             OptionDefinition::new(
-                |shell| shell.options.allow_reedit_failed_history_subst,
-                |shell, value| shell.options.allow_reedit_failed_history_subst = value
+                |options| options.allow_reedit_failed_history_subst,
+                |options, value| options.allow_reedit_failed_history_subst = value
             )
         ),
         (
             "histverify",
             OptionDefinition::new(
-                |shell| shell.options.allow_modifying_history_substitution,
-                |shell, value| shell.options.allow_modifying_history_substitution = value
+                |options| options.allow_modifying_history_substitution,
+                |options, value| options.allow_modifying_history_substitution = value
             )
         ),
         (
             "hostcomplete",
             OptionDefinition::new(
-                |shell| shell.options.enable_hostname_completion,
-                |shell, value| shell.options.enable_hostname_completion = value
+                |options| options.enable_hostname_completion,
+                |options, value| options.enable_hostname_completion = value
             )
         ),
         (
             "huponexit",
             OptionDefinition::new(
-                |shell| shell.options.send_sighup_to_all_jobs_on_exit,
-                |shell, value| shell.options.send_sighup_to_all_jobs_on_exit = value
+                |options| options.send_sighup_to_all_jobs_on_exit,
+                |options, value| options.send_sighup_to_all_jobs_on_exit = value
             )
         ),
         (
             "inherit_errexit",
             OptionDefinition::new(
-                |shell| shell.options.command_subst_inherits_errexit,
-                |shell, value| shell.options.command_subst_inherits_errexit = value
+                |options| options.command_subst_inherits_errexit,
+                |options, value| options.command_subst_inherits_errexit = value
             )
         ),
         (
             "interactive_comments",
             OptionDefinition::new(
-                |shell| shell.options.interactive_comments,
-                |shell, value| shell.options.interactive_comments = value
+                |options| options.interactive_comments,
+                |options, value| options.interactive_comments = value
             )
         ),
         (
             "lastpipe",
             OptionDefinition::new(
-                |shell| shell.options.run_last_pipeline_cmd_in_current_shell,
-                |shell, value| shell.options.run_last_pipeline_cmd_in_current_shell = value
+                |options| options.run_last_pipeline_cmd_in_current_shell,
+                |options, value| options.run_last_pipeline_cmd_in_current_shell = value
             )
         ),
         (
             "lithist",
             OptionDefinition::new(
-                |shell| shell.options.embed_newlines_in_multiline_cmds_in_history,
-                |shell, value| shell.options.embed_newlines_in_multiline_cmds_in_history = value
+                |options| options.embed_newlines_in_multiline_cmds_in_history,
+                |options, value| options.embed_newlines_in_multiline_cmds_in_history = value
             )
         ),
         (
             "localvar_inherit",
             OptionDefinition::new(
-                |shell| shell.options.local_vars_inherit_value_and_attrs,
-                |shell, value| shell.options.local_vars_inherit_value_and_attrs = value
+                |options| options.local_vars_inherit_value_and_attrs,
+                |options, value| options.local_vars_inherit_value_and_attrs = value
             )
         ),
         (
             "localvar_unset",
             OptionDefinition::new(
-                |shell| shell.options.localvar_unset,
-                |shell, value| shell.options.localvar_unset = value
+                |options| options.localvar_unset,
+                |options, value| options.localvar_unset = value
             )
         ),
         (
             "login_shell",
             OptionDefinition::new(
-                |shell| shell.options.login_shell,
-                |shell, value| shell.options.login_shell = value
+                |options| options.login_shell,
+                |options, value| options.login_shell = value
             )
         ),
         (
             "mailwarn",
             OptionDefinition::new(
-                |shell| shell.options.mail_warn,
-                |shell, value| shell.options.mail_warn = value
+                |options| options.mail_warn,
+                |options, value| options.mail_warn = value
             )
         ),
         (
             "no_empty_cmd_completion",
             OptionDefinition::new(
-                |shell| shell.options.no_empty_cmd_completion,
-                |shell, value| shell.options.no_empty_cmd_completion = value
+                |options| options.no_empty_cmd_completion,
+                |options, value| options.no_empty_cmd_completion = value
             )
         ),
         (
             "nocaseglob",
             OptionDefinition::new(
-                |shell| shell.options.case_insensitive_pathname_expansion,
-                |shell, value| shell.options.case_insensitive_pathname_expansion = value
+                |options| options.case_insensitive_pathname_expansion,
+                |options, value| options.case_insensitive_pathname_expansion = value
             )
         ),
         (
             "nocasematch",
             OptionDefinition::new(
-                |shell| shell.options.case_insensitive_conditionals,
-                |shell, value| shell.options.case_insensitive_conditionals = value
+                |options| options.case_insensitive_conditionals,
+                |options, value| options.case_insensitive_conditionals = value
             )
         ),
         (
             "nullglob",
             OptionDefinition::new(
-                |shell| shell.options.expand_non_matching_patterns_to_null,
-                |shell, value| shell.options.expand_non_matching_patterns_to_null = value
+                |options| options.expand_non_matching_patterns_to_null,
+                |options, value| options.expand_non_matching_patterns_to_null = value
             )
         ),
         (
             "progcomp",
             OptionDefinition::new(
-                |shell| shell.options.programmable_completion,
-                |shell, value| shell.options.programmable_completion = value
+                |options| options.programmable_completion,
+                |options, value| options.programmable_completion = value
             )
         ),
         (
             "progcomp_alias",
             OptionDefinition::new(
-                |shell| shell.options.programmable_completion_alias,
-                |shell, value| shell.options.programmable_completion_alias = value
+                |options| options.programmable_completion_alias,
+                |options, value| options.programmable_completion_alias = value
             )
         ),
         (
             "promptvars",
             OptionDefinition::new(
-                |shell| shell.options.expand_prompt_strings,
-                |shell, value| shell.options.expand_prompt_strings = value
+                |options| options.expand_prompt_strings,
+                |options, value| options.expand_prompt_strings = value
             )
         ),
         (
             "restricted_shell",
             OptionDefinition::new(
-                |shell| shell.options.restricted_shell,
-                |shell, value| shell.options.restricted_shell = value
+                |options| options.restricted_shell,
+                |options, value| options.restricted_shell = value
             )
         ),
         (
             "shift_verbose",
             OptionDefinition::new(
-                |shell| shell.options.shift_verbose,
-                |shell, value| shell.options.shift_verbose = value
+                |options| options.shift_verbose,
+                |options, value| options.shift_verbose = value
             )
         ),
         (
             "sourcepath",
             OptionDefinition::new(
-                |shell| shell.options.source_builtin_searches_path,
-                |shell, value| shell.options.source_builtin_searches_path = value
+                |options| options.source_builtin_searches_path,
+                |options, value| options.source_builtin_searches_path = value
             )
         ),
         (
             "xpg_echo",
             OptionDefinition::new(
-                |shell| shell.options.echo_builtin_expands_escape_sequences,
-                |shell, value| shell.options.echo_builtin_expands_escape_sequences = value
+                |options| options.echo_builtin_expands_escape_sequences,
+                |options, value| options.echo_builtin_expands_escape_sequences = value
             )
         ),
     ]);
diff --git a/brush-core/src/options.rs b/brush-core/src/options.rs
index 8696ef2..07225e4 100644
--- a/brush-core/src/options.rs
+++ b/brush-core/src/options.rs
@@ -223,6 +223,22 @@ impl RuntimeOptions {
             options.expand_aliases = true;
         }
 
+        // Update any shopt options.
+        for enabled_option in &create_options.enabled_shopt_options {
+            if let Some(shopt_option) =
+                crate::namedoptions::SHOPT_OPTIONS.get(enabled_option.as_str())
+            {
+                (shopt_option.setter)(&mut options, true);
+            }
+        }
+        for disabled_option in &create_options.disabled_shopt_options {
+            if let Some(shopt_option) =
+                crate::namedoptions::SHOPT_OPTIONS.get(disabled_option.as_str())
+            {
+                (shopt_option.setter)(&mut options, false);
+            }
+        }
+
         options
     }
 }
diff --git a/brush-core/src/shell.rs b/brush-core/src/shell.rs
index b008233..190a42d 100644
--- a/brush-core/src/shell.rs
+++ b/brush-core/src/shell.rs
@@ -99,10 +99,14 @@ impl Clone for Shell {
 /// Options for creating a new shell.
 #[derive(Debug, Default)]
 pub struct CreateOptions {
-    /// Whether the shell is a login shell.
-    pub login: bool,
+    /// Disabled shopt options.
+    pub disabled_shopt_options: Vec<String>,
+    /// Enabled shopt options.
+    pub enabled_shopt_options: Vec<String>,
     /// Whether the shell is interactive.
     pub interactive: bool,
+    /// Whether the shell is a login shell.
+    pub login: bool,
     /// Whether to skip using a readline-like interface for input.
     pub no_editing: bool,
     /// Whether to skip sourcing the system profile.
@@ -671,7 +675,7 @@ impl Shell {
         let mut cs = vec![];
 
         for (x, y) in crate::namedoptions::SET_OPTIONS.iter() {
-            if (y.getter)(self) {
+            if (y.getter)(&self.options) {
                 cs.push(*x);
             }
         }
diff --git a/brush-shell/src/main.rs b/brush-shell/src/main.rs
index 56141b9..f4616ab 100644
--- a/brush-shell/src/main.rs
+++ b/brush-shell/src/main.rs
@@ -35,7 +35,8 @@ struct CommandLineArgs {
     #[clap(long = "noediting")]
     no_editing: bool,
 
-    /// Don't process any profile/login files (`/etc/profile`, `~/.bash_profile`, `~/.bash_login`, `~/.profile`).
+    /// Don't process any profile/login files (`/etc/profile`, `~/.bash_profile`, `~/.bash_login`,
+    /// `~/.profile`).
     #[clap(long = "noprofile")]
     no_profile: bool,
 
@@ -43,6 +44,14 @@ struct CommandLineArgs {
     #[clap(long = "norc")]
     no_rc: bool,
 
+    /// Enable shell option.
+    #[clap(short = 'O')]
+    enabled_shopt_options: Vec<String>,
+
+    /// Disable shell option.
+    #[clap(long = "+O", hide = true)]
+    disabled_shopt_options: Vec<String>,
+
     /// Disable non-POSIX extensions.
     #[clap(long = "posix")]
     posix: bool,
@@ -127,7 +136,15 @@ fn main() {
     //
     // Parse args.
     //
-    let args: Vec<_> = std::env::args().collect();
+    let mut args: Vec<_> = std::env::args().collect();
+
+    // Work around clap's limitations handling +O options.
+    for arg in &mut args {
+        if arg.starts_with("+O") {
+            arg.insert_str(0, "--");
+        }
+    }
+
     let parsed_args = CommandLineArgs::parse_from(&args);
 
     //
@@ -218,10 +235,13 @@ async fn run(
     let read_commands_from_stdin = (args.read_commands_from_stdin && args.command.is_none())
         || (args.script_path.is_none() && args.command.is_none());
 
+    let interactive = args.is_interactive();
     let options = brush_interactive::Options {
         shell: brush_core::CreateOptions {
+            disabled_shopt_options: args.disabled_shopt_options,
+            enabled_shopt_options: args.enabled_shopt_options,
             login: args.login || argv0.as_ref().map_or(false, |a0| a0.starts_with('-')),
-            interactive: args.is_interactive(),
+            interactive,
             no_editing: args.no_editing,
             no_profile: args.no_profile,
             no_rc: args.no_rc,


Test patch:
diff --git a/brush-core/src/extendedtests.rs b/brush-core/src/extendedtests.rs
index 81c92f6..44af114 100644
--- a/brush-core/src/extendedtests.rs
+++ b/brush-core/src/extendedtests.rs
@@ -165,7 +165,7 @@ pub(crate) fn apply_unary_predicate_to_str(
         ast::UnaryPredicate::ShellOptionEnabled => {
             let shopt_name = operand;
             if let Some(option) = namedoptions::SET_O_OPTIONS.get(shopt_name) {
-                Ok((option.getter)(shell))
+                Ok((option.getter)(&shell.options))
             } else {
                 Ok(false)
             }