Skip to main content

clash_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            remember_command_locations: true,
233            check_window_size_after_external_commands: true,
234            save_multiline_cmds_in_history: true,
235            extquote: true,
236            force_fignore: true,
237            enable_hostname_completion: true,
238            interactive_comments: true,
239            expand_prompt_strings: true,
240            source_builtin_searches_path: true,
241            perform_brace_expansion: true,
242            quote_all_metachars_in_completion: true,
243            programmable_completion: true,
244            glob_ranges_use_c_locale: true,
245            glob_skip_dots: true,
246            patsub_replacement: true,
247            max_function_call_depth: create_options.max_function_call_depth,
248            ..Self::default()
249        };
250
251        // Additional options are enabled by default for interactive shells.
252        if create_options.interactive {
253            options.enable_bang_style_history_substitution = true;
254            options.emacs_mode = !create_options.no_editing;
255            options.expand_aliases = true;
256        }
257
258        // Update any options.
259        for enabled_option in &create_options.enabled_options {
260            if let Some(option) = namedoptions::options(namedoptions::ShellOptionKind::SetO)
261                .get(enabled_option.as_str())
262            {
263                option.set(&mut options, true);
264            }
265        }
266        for disabled_option in &create_options.disabled_options {
267            if let Some(option) = namedoptions::options(namedoptions::ShellOptionKind::SetO)
268                .get(disabled_option.as_str())
269            {
270                option.set(&mut options, false);
271            }
272        }
273
274        // Update any shopt options.
275        for enabled_option in &create_options.enabled_shopt_options {
276            if let Some(shopt_option) = namedoptions::options(namedoptions::ShellOptionKind::Shopt)
277                .get(enabled_option.as_str())
278            {
279                shopt_option.set(&mut options, true);
280            }
281        }
282        for disabled_option in &create_options.disabled_shopt_options {
283            if let Some(shopt_option) = namedoptions::options(namedoptions::ShellOptionKind::Shopt)
284                .get(disabled_option.as_str())
285            {
286                shopt_option.set(&mut options, false);
287            }
288        }
289
290        options
291    }
292
293    /// Returns a string representing the current `set`-style option flags set in the shell.
294    pub fn option_flags(&self) -> String {
295        let mut cs = vec![];
296
297        for o in namedoptions::options(namedoptions::ShellOptionKind::Set).iter() {
298            if o.definition.get(self)
299                && let Some(c) = o.name.chars().next()
300            {
301                cs.push(c);
302            }
303        }
304
305        // Sort the flags in a way that matches what bash does.
306        cs.sort_by_key(|flag| option_flag_sort_key(*flag));
307
308        cs.into_iter().collect()
309    }
310
311    /// Returns a colon-separated list of sorted 'set -o' options enabled.
312    pub fn seto_optstr(&self) -> String {
313        let mut cs = vec![];
314
315        for option in namedoptions::options(namedoptions::ShellOptionKind::SetO).iter() {
316            if option.definition.get(self) {
317                cs.push(option.name);
318            }
319        }
320
321        cs.sort_unstable();
322        cs.into_iter().join(":")
323    }
324
325    /// Returns a colon-separated list of sorted 'shopt' options enabled.
326    pub fn shopt_optstr(&self) -> String {
327        let mut cs = vec![];
328
329        for option in namedoptions::options(namedoptions::ShellOptionKind::Shopt).iter() {
330            if option.definition.get(self) {
331                cs.push(option.name);
332            }
333        }
334
335        cs.sort_unstable();
336        cs.into_iter().join(":")
337    }
338}
339
340/// Sort option flag character in a way that mirrors bash behavior.
341///
342/// # Arguments
343///
344/// * `ch` - The option flag character.
345const fn option_flag_sort_key(ch: char) -> (u8, char) {
346    // NOTE: bash appears to sort in 3 groups. We mimic them:
347    //    1) Lowercase letters excluding 'c' and 's' (sorted)
348    //    2) Uppercase letters (sorted)
349    //    3) All other characters (sorted)
350    let group = if ch.is_ascii_lowercase() && !matches!(ch, 'c' | 's') {
351        0
352    } else if ch.is_ascii_uppercase() {
353        1
354    } else {
355        2
356    };
357
358    (group, ch)
359}
360
361#[cfg(test)]
362mod tests {
363    use super::option_flag_sort_key;
364
365    #[test]
366    fn lowercase_excluding_c_and_s_sort_first() {
367        let mut flags = vec!['b', 'A', 'Z', 's', 'c', 'a'];
368        flags.sort_by_key(|flag| option_flag_sort_key(*flag));
369
370        assert_eq!(flags, vec!['a', 'b', 'A', 'Z', 'c', 's']);
371    }
372
373    #[test]
374    fn uppercase_sorted_before_miscellaneous() {
375        let mut flags = vec!['P', 'B', '1', 'T'];
376        flags.sort_by_key(|flag| option_flag_sort_key(*flag));
377
378        assert_eq!(flags, vec!['B', 'P', 'T', '1']);
379    }
380
381    #[test]
382    fn miscellaneous_characters_respect_ascii_order() {
383        let mut flags = vec!['s', 'c', '%', ':'];
384        flags.sort_by_key(|flag| option_flag_sort_key(*flag));
385
386        assert_eq!(flags, vec!['%', ':', 'c', 's']);
387    }
388}