Skip to main content

shrub_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    /// Whether externally spawned commands are killed when their spawning shell
202    /// is dropped.
203    pub kill_external_commands_on_drop: bool,
204    /// Maximum function call depth.
205    pub max_function_call_depth: Option<usize>,
206}
207
208impl RuntimeOptions {
209    /// Creates a default set of runtime options based on the given creation
210    /// options.
211    ///
212    /// # Arguments
213    ///
214    /// * `create_options` - The options used to create the shell.
215    pub fn defaults_from<SE: extensions::ShellExtensions>(
216        create_options: &CreateOptions<SE>,
217    ) -> Self {
218        // There's a set of options enabled by default for all shells.
219        let mut options = Self {
220            interactive: create_options.interactive,
221            disallow_overwriting_regular_files_via_output_redirection: create_options
222                .disallow_overwriting_regular_files_via_output_redirection,
223            do_not_execute_commands: create_options.do_not_execute_commands,
224            enable_command_history: create_options.interactive,
225            enable_job_control: create_options.interactive,
226            exit_after_one_command: create_options.exit_after_one_command,
227            read_commands_from_stdin: create_options.read_commands_from_stdin,
228            command_string_mode: create_options.command_string_mode,
229            sh_mode: create_options.sh_mode,
230            posix_mode: create_options.posix,
231            print_commands_and_arguments: create_options.print_commands_and_arguments,
232            print_shell_input_lines: create_options.verbose,
233            treat_unset_variables_as_error: create_options.treat_unset_variables_as_error,
234            exit_on_nonzero_command_exit: create_options.exit_on_nonzero_command_exit,
235            external_cmd_leads_session: create_options.external_cmd_leads_session,
236            kill_external_commands_on_drop: create_options.kill_external_commands_on_drop,
237            login_shell: create_options.login,
238            disable_filename_globbing: create_options.disable_pathname_expansion,
239            remember_command_locations: true,
240            check_window_size_after_external_commands: true,
241            save_multiline_cmds_in_history: true,
242            extquote: true,
243            force_fignore: true,
244            case_insensitive_pathname_expansion:
245                crate::sys::fs::default_case_insensitive_path_expansion(),
246            enable_hostname_completion: true,
247            interactive_comments: true,
248            expand_prompt_strings: true,
249            source_builtin_searches_path: true,
250            perform_brace_expansion: true,
251            quote_all_metachars_in_completion: true,
252            programmable_completion: true,
253            glob_ranges_use_c_locale: true,
254            glob_skip_dots: true,
255            patsub_replacement: true,
256            max_function_call_depth: create_options.max_function_call_depth,
257            ..Self::default()
258        };
259
260        // Additional options are enabled by default for interactive shells.
261        if create_options.interactive {
262            options.enable_bang_style_history_substitution = true;
263            options.emacs_mode = !create_options.no_editing;
264            options.expand_aliases = true;
265        }
266
267        // Update any options.
268        for enabled_option in &create_options.enabled_options {
269            if let Some(option) = namedoptions::options(namedoptions::ShellOptionKind::SetO)
270                .get(enabled_option.as_str())
271            {
272                option.set(&mut options, true);
273            }
274        }
275        for disabled_option in &create_options.disabled_options {
276            if let Some(option) = namedoptions::options(namedoptions::ShellOptionKind::SetO)
277                .get(disabled_option.as_str())
278            {
279                option.set(&mut options, false);
280            }
281        }
282
283        // Update any shopt options.
284        for enabled_option in &create_options.enabled_shopt_options {
285            if let Some(shopt_option) = namedoptions::options(namedoptions::ShellOptionKind::Shopt)
286                .get(enabled_option.as_str())
287            {
288                shopt_option.set(&mut options, true);
289            }
290        }
291        for disabled_option in &create_options.disabled_shopt_options {
292            if let Some(shopt_option) = namedoptions::options(namedoptions::ShellOptionKind::Shopt)
293                .get(disabled_option.as_str())
294            {
295                shopt_option.set(&mut options, false);
296            }
297        }
298
299        options
300    }
301
302    /// Returns a string representing the current `set`-style option flags set
303    /// in the shell.
304    pub fn option_flags(&self) -> String {
305        let mut cs = vec![];
306
307        for o in namedoptions::options(namedoptions::ShellOptionKind::Set).iter() {
308            if o.definition.get(self)
309                && let Some(c) = o.name.chars().next()
310            {
311                cs.push(c);
312            }
313        }
314
315        // Sort the flags in a way that matches what bash does.
316        cs.sort_by_key(|flag| option_flag_sort_key(*flag));
317
318        cs.into_iter().collect()
319    }
320
321    /// Returns a colon-separated list of sorted 'set -o' options enabled.
322    pub fn seto_optstr(&self) -> String {
323        let mut cs = vec![];
324
325        for option in namedoptions::options(namedoptions::ShellOptionKind::SetO).iter() {
326            if option.definition.get(self) {
327                cs.push(option.name);
328            }
329        }
330
331        cs.sort_unstable();
332        cs.into_iter().join(":")
333    }
334
335    /// Returns a colon-separated list of sorted 'shopt' options enabled.
336    pub fn shopt_optstr(&self) -> String {
337        let mut cs = vec![];
338
339        for option in namedoptions::options(namedoptions::ShellOptionKind::Shopt).iter() {
340            if option.definition.get(self) {
341                cs.push(option.name);
342            }
343        }
344
345        cs.sort_unstable();
346        cs.into_iter().join(":")
347    }
348}
349
350/// Sort option flag character in a way that mirrors bash behavior.
351///
352/// # Arguments
353///
354/// * `ch` - The option flag character.
355const fn option_flag_sort_key(ch: char) -> (u8, char) {
356    // NOTE: bash appears to sort in 3 groups. We mimic them:
357    //    1) Lowercase letters excluding 'c' and 's' (sorted)
358    //    2) Uppercase letters (sorted)
359    //    3) All other characters (sorted)
360    let group = if ch.is_ascii_lowercase() && !matches!(ch, 'c' | 's') {
361        0
362    } else if ch.is_ascii_uppercase() {
363        1
364    } else {
365        2
366    };
367
368    (group, ch)
369}
370
371#[cfg(test)]
372mod tests {
373    use super::option_flag_sort_key;
374
375    #[test]
376    fn lowercase_excluding_c_and_s_sort_first() {
377        let mut flags = vec!['b', 'A', 'Z', 's', 'c', 'a'];
378        flags.sort_by_key(|flag| option_flag_sort_key(*flag));
379
380        assert_eq!(flags, vec!['a', 'b', 'A', 'Z', 'c', 's']);
381    }
382
383    #[test]
384    fn uppercase_sorted_before_miscellaneous() {
385        let mut flags = vec!['P', 'B', '1', 'T'];
386        flags.sort_by_key(|flag| option_flag_sort_key(*flag));
387
388        assert_eq!(flags, vec!['B', 'P', 'T', '1']);
389    }
390
391    #[test]
392    fn miscellaneous_characters_respect_ascii_order() {
393        let mut flags = vec!['s', 'c', '%', ':'];
394        flags.sort_by_key(|flag| option_flag_sort_key(*flag));
395
396        assert_eq!(flags, vec!['%', ':', 'c', 's']);
397    }
398}