Skip to main content

brush_core/
options.rs

1//! Defines runtime options for the shell.
2
3use itertools::Itertools;
4
5use crate::{CreateOptions, extensions, namedoptions};
6
7/// Runtime changeable options for a shell instance.
8#[derive(Clone, Default)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10#[expect(clippy::module_name_repetitions)]
11pub struct RuntimeOptions {
12    //
13    // Single-character options.
14    /// -a
15    pub export_variables_on_modification: bool,
16    /// -b
17    pub notify_job_termination_immediately: bool,
18    /// -e
19    pub exit_on_nonzero_command_exit: bool,
20    /// -f
21    pub disable_filename_globbing: bool,
22    /// -h
23    pub remember_command_locations: bool,
24    /// -k
25    pub place_all_assignment_args_in_command_env: bool,
26    /// -m
27    pub enable_job_control: bool,
28    /// -n
29    pub do_not_execute_commands: bool,
30    /// -p
31    pub real_effective_uid_mismatch: bool,
32    /// -t
33    pub exit_after_one_command: bool,
34    /// -u
35    pub treat_unset_variables_as_error: bool,
36    /// -v
37    pub print_shell_input_lines: bool,
38    /// -x
39    pub print_commands_and_arguments: bool,
40    /// -B
41    pub perform_brace_expansion: bool,
42    /// -C
43    pub disallow_overwriting_regular_files_via_output_redirection: bool,
44    /// -E
45    pub shell_functions_inherit_err_trap: bool,
46    /// -H
47    pub enable_bang_style_history_substitution: bool,
48    /// -P
49    pub do_not_resolve_symlinks_when_changing_dir: bool,
50    /// -T
51    pub shell_functions_inherit_debug_and_return_traps: bool,
52
53    //
54    // Options set through -o.
55    /// 'emacs'
56    pub emacs_mode: bool,
57    /// 'history'
58    pub enable_command_history: bool,
59    /// 'ignoreeof'
60    pub ignore_eof: bool,
61    /// 'pipefail'
62    pub return_last_failure_from_pipeline: bool,
63    /// 'posix'
64    pub posix_mode: bool,
65    /// 'vi'
66    pub vi_mode: bool,
67
68    //
69    // Options set through shopt.
70    /// `array_expand_once`
71    pub array_expand_once: bool,
72    /// `assoc_expand_once`
73    pub assoc_expand_once: bool,
74    /// 'autocd'
75    pub auto_cd: bool,
76    /// `bash_source_full_path`
77    pub bash_source_full_path: bool,
78    /// `cdable_vars`
79    pub cdable_vars: bool,
80    /// 'cdspell'
81    pub cd_autocorrect_spelling: bool,
82    /// 'checkhash'
83    pub check_hashtable_before_command_exec: bool,
84    /// 'checkjobs'
85    pub check_jobs_before_exit: bool,
86    /// 'checkwinsize'
87    pub check_window_size_after_external_commands: bool,
88    /// 'cmdhist'
89    pub save_multiline_cmds_in_history: bool,
90    /// 'compat31'
91    pub compat31: bool,
92    /// 'compat32'
93    pub compat32: bool,
94    /// 'compat40'
95    pub compat40: bool,
96    /// 'compat41'
97    pub compat41: bool,
98    /// 'compat42'
99    pub compat42: bool,
100    /// 'compat43'
101    pub compat43: bool,
102    /// 'compat44'
103    pub compat44: bool,
104    /// `complete_fullquote`
105    pub quote_all_metachars_in_completion: bool,
106    /// 'direxpand'
107    pub expand_dir_names_on_completion: bool,
108    /// 'dirspell'
109    pub autocorrect_dir_spelling_on_completion: bool,
110    /// 'dotglob'
111    pub glob_matches_dotfiles: bool,
112    /// 'execfail'
113    pub exit_on_exec_fail: bool,
114    /// `expand_aliases`
115    pub expand_aliases: bool,
116    /// 'extdebug'
117    pub enable_debugger: bool,
118    /// 'extglob'
119    pub extended_globbing: bool,
120    /// 'extquote'
121    pub extquote: bool,
122    /// 'failglob'
123    pub fail_expansion_on_globs_without_match: bool,
124    /// `force_fignore`
125    pub force_fignore: bool,
126    /// 'globasciiranges'
127    pub glob_ranges_use_c_locale: bool,
128    /// 'globskipdots'
129    pub glob_skip_dots: bool,
130    /// 'globstar'
131    pub enable_star_star_glob: bool,
132    /// `gnu_errfmt`
133    pub errors_in_gnu_format: bool,
134    /// 'histappend'
135    pub append_to_history_file: bool,
136    /// 'histreedit'
137    pub allow_reedit_failed_history_subst: bool,
138    /// 'histverify'
139    pub allow_modifying_history_substitution: bool,
140    /// 'hostcomplete'
141    pub enable_hostname_completion: bool,
142    /// 'huponexit'
143    pub send_sighup_to_all_jobs_on_exit: bool,
144    /// `inherit_errexit`
145    pub command_subst_inherits_errexit: bool,
146    /// `interactive_comments`
147    pub interactive_comments: bool,
148    /// 'lastpipe'
149    pub run_last_pipeline_cmd_in_current_shell: bool,
150    /// 'lithist'
151    pub embed_newlines_in_multiline_cmds_in_history: bool,
152    /// `localvar_inherit`
153    pub local_vars_inherit_value_and_attrs: bool,
154    /// `localvar_unset`
155    pub localvar_unset: bool,
156    /// `login_shell`
157    pub login_shell: bool,
158    /// 'mailwarn'
159    pub mail_warn: bool,
160    /// `no_empty_cmd_completion`
161    pub no_empty_cmd_completion: bool,
162    /// 'nocaseglob'
163    pub case_insensitive_pathname_expansion: bool,
164    /// 'nocasematch'
165    pub case_insensitive_conditionals: bool,
166    /// `noexpand_translation`
167    pub no_expand_translation: bool,
168    /// 'nullglob'
169    pub expand_non_matching_patterns_to_null: bool,
170    /// `patsub_replacement`
171    pub patsub_replacement: bool,
172    /// 'progcomp'
173    pub programmable_completion: bool,
174    /// `progcomp_alias`
175    pub programmable_completion_alias: bool,
176    /// 'promptvars'
177    pub expand_prompt_strings: bool,
178    /// `restricted_shell`
179    pub restricted_shell: bool,
180    /// `shift_verbose`
181    pub shift_verbose: bool,
182    /// `sourcepath`
183    pub source_builtin_searches_path: bool,
184    /// `varredir_close`
185    pub var_redir_close: bool,
186    /// `xpg_echo`
187    pub echo_builtin_expands_escape_sequences: bool,
188
189    //
190    // Options set by the shell.
191    /// Whether or not the shell is interactive.
192    pub interactive: bool,
193    /// Whether commands are being read from stdin.
194    pub read_commands_from_stdin: bool,
195    /// Whether the shell is in command string mode (-c).
196    pub command_string_mode: bool,
197    /// Whether or not the shell is in maximal `sh` compatibility mode.    
198    pub sh_mode: bool,
199    /// Whether to treat external commands as session leaders.
200    pub external_cmd_leads_session: bool,
201    /// Maximum function call depth.
202    pub max_function_call_depth: Option<usize>,
203}
204
205impl RuntimeOptions {
206    /// Creates a default set of runtime options based on the given creation options.
207    ///
208    /// # Arguments
209    ///
210    /// * `create_options` - The options used to create the shell.
211    pub fn defaults_from<SE: extensions::ShellExtensions>(
212        create_options: &CreateOptions<SE>,
213    ) -> Self {
214        // There's a set of options enabled by default for all shells.
215        let mut options = Self {
216            interactive: create_options.interactive,
217            disallow_overwriting_regular_files_via_output_redirection: create_options
218                .disallow_overwriting_regular_files_via_output_redirection,
219            do_not_execute_commands: create_options.do_not_execute_commands,
220            enable_command_history: create_options.interactive,
221            enable_job_control: create_options.interactive,
222            exit_after_one_command: create_options.exit_after_one_command,
223            read_commands_from_stdin: create_options.read_commands_from_stdin,
224            command_string_mode: create_options.command_string_mode,
225            sh_mode: create_options.sh_mode,
226            posix_mode: create_options.posix,
227            print_commands_and_arguments: create_options.print_commands_and_arguments,
228            print_shell_input_lines: create_options.verbose,
229            treat_unset_variables_as_error: create_options.treat_unset_variables_as_error,
230            exit_on_nonzero_command_exit: create_options.exit_on_nonzero_command_exit,
231            external_cmd_leads_session: create_options.external_cmd_leads_session,
232            login_shell: create_options.login,
233            disable_filename_globbing: create_options.disable_pathname_expansion,
234            remember_command_locations: true,
235            check_window_size_after_external_commands: true,
236            save_multiline_cmds_in_history: true,
237            extquote: true,
238            force_fignore: true,
239            case_insensitive_pathname_expansion:
240                crate::sys::fs::default_case_insensitive_path_expansion(),
241            enable_hostname_completion: true,
242            interactive_comments: true,
243            expand_prompt_strings: true,
244            source_builtin_searches_path: true,
245            perform_brace_expansion: true,
246            quote_all_metachars_in_completion: true,
247            programmable_completion: true,
248            glob_ranges_use_c_locale: true,
249            glob_skip_dots: true,
250            patsub_replacement: true,
251            max_function_call_depth: create_options.max_function_call_depth,
252            ..Self::default()
253        };
254
255        // Additional options are enabled by default for interactive shells.
256        if create_options.interactive {
257            options.enable_bang_style_history_substitution = true;
258            options.emacs_mode = !create_options.no_editing;
259            options.expand_aliases = true;
260        }
261
262        // Update any options.
263        for enabled_option in &create_options.enabled_options {
264            if let Some(option) = namedoptions::options(namedoptions::ShellOptionKind::SetO)
265                .get(enabled_option.as_str())
266            {
267                option.set(&mut options, true);
268            }
269        }
270        for disabled_option in &create_options.disabled_options {
271            if let Some(option) = namedoptions::options(namedoptions::ShellOptionKind::SetO)
272                .get(disabled_option.as_str())
273            {
274                option.set(&mut options, false);
275            }
276        }
277
278        // Update any shopt options.
279        for enabled_option in &create_options.enabled_shopt_options {
280            if let Some(shopt_option) = namedoptions::options(namedoptions::ShellOptionKind::Shopt)
281                .get(enabled_option.as_str())
282            {
283                shopt_option.set(&mut options, true);
284            }
285        }
286        for disabled_option in &create_options.disabled_shopt_options {
287            if let Some(shopt_option) = namedoptions::options(namedoptions::ShellOptionKind::Shopt)
288                .get(disabled_option.as_str())
289            {
290                shopt_option.set(&mut options, false);
291            }
292        }
293
294        options
295    }
296
297    /// Returns a string representing the current `set`-style option flags set in the shell.
298    pub fn option_flags(&self) -> String {
299        let mut cs = vec![];
300
301        for o in namedoptions::options(namedoptions::ShellOptionKind::Set).iter() {
302            if o.definition.get(self)
303                && let Some(c) = o.name.chars().next()
304            {
305                cs.push(c);
306            }
307        }
308
309        // Sort the flags in a way that matches what bash does.
310        cs.sort_by_key(|flag| option_flag_sort_key(*flag));
311
312        cs.into_iter().collect()
313    }
314
315    /// Returns a colon-separated list of sorted 'set -o' options enabled.
316    pub fn seto_optstr(&self) -> String {
317        let mut cs = vec![];
318
319        for option in namedoptions::options(namedoptions::ShellOptionKind::SetO).iter() {
320            if option.definition.get(self) {
321                cs.push(option.name);
322            }
323        }
324
325        cs.sort_unstable();
326        cs.into_iter().join(":")
327    }
328
329    /// Returns a colon-separated list of sorted 'shopt' options enabled.
330    pub fn shopt_optstr(&self) -> String {
331        let mut cs = vec![];
332
333        for option in namedoptions::options(namedoptions::ShellOptionKind::Shopt).iter() {
334            if option.definition.get(self) {
335                cs.push(option.name);
336            }
337        }
338
339        cs.sort_unstable();
340        cs.into_iter().join(":")
341    }
342}
343
344/// Sort option flag character in a way that mirrors bash behavior.
345///
346/// # Arguments
347///
348/// * `ch` - The option flag character.
349const fn option_flag_sort_key(ch: char) -> (u8, char) {
350    // NOTE: bash appears to sort in 3 groups. We mimic them:
351    //    1) Lowercase letters excluding 'c' and 's' (sorted)
352    //    2) Uppercase letters (sorted)
353    //    3) All other characters (sorted)
354    let group = if ch.is_ascii_lowercase() && !matches!(ch, 'c' | 's') {
355        0
356    } else if ch.is_ascii_uppercase() {
357        1
358    } else {
359        2
360    };
361
362    (group, ch)
363}
364
365#[cfg(test)]
366mod tests {
367    use super::option_flag_sort_key;
368
369    #[test]
370    fn lowercase_excluding_c_and_s_sort_first() {
371        let mut flags = vec!['b', 'A', 'Z', 's', 'c', 'a'];
372        flags.sort_by_key(|flag| option_flag_sort_key(*flag));
373
374        assert_eq!(flags, vec!['a', 'b', 'A', 'Z', 'c', 's']);
375    }
376
377    #[test]
378    fn uppercase_sorted_before_miscellaneous() {
379        let mut flags = vec!['P', 'B', '1', 'T'];
380        flags.sort_by_key(|flag| option_flag_sort_key(*flag));
381
382        assert_eq!(flags, vec!['B', 'P', 'T', '1']);
383    }
384
385    #[test]
386    fn miscellaneous_characters_respect_ascii_order() {
387        let mut flags = vec!['s', 'c', '%', ':'];
388        flags.sort_by_key(|flag| option_flag_sort_key(*flag));
389
390        assert_eq!(flags, vec!['%', ':', 'c', 's']);
391    }
392}