Skip to main content

shrub_core/
options.rs

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