1use std::ffi::CString;
4use std::sync::Arc;
5
6use itertools::Itertools;
7
8use crate::{CreateOptions, extensions, namedoptions};
9
10pub const DEFAULT_MAX_KEPT_DESCRIPTORS: usize = 64;
16
17#[derive(Clone, Default)]
31pub struct SpawnOptions {
32 pub cgroup_procs: Option<Arc<CString>>,
38 pub open_file_limit: Option<u64>,
46 pub pids_limits: Arc<[PidsLimit]>,
58 pub before_spawn: Option<Arc<dyn Fn() -> SpawnGuard + Send + Sync>>,
67 pub on_spawn: Option<Arc<dyn Fn(SpawnedProcess) + Send + Sync>>,
70}
71
72pub type SpawnGuard = Box<dyn std::any::Any + Send>;
75
76impl std::fmt::Debug for SpawnOptions {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 f.debug_struct("SpawnOptions")
79 .field("cgroup_procs", &self.cgroup_procs)
80 .field("open_file_limit", &self.open_file_limit)
81 .field("pids_limits", &self.pids_limits)
82 .field("before_spawn", &self.before_spawn.as_ref().map(|_| "Fn"))
83 .field("on_spawn", &self.on_spawn.as_ref().map(|_| "Fn"))
84 .finish()
85 }
86}
87
88#[derive(Clone, Debug)]
91pub struct PidsLimit {
92 pub current: Arc<std::fs::File>,
95 pub max: u64,
98}
99
100#[derive(Clone, Copy, Debug, PartialEq, Eq)]
102pub struct SpawnedProcess {
103 pub pid: i32,
105 pub process_group: Option<i32>,
107}
108
109#[derive(Clone, Default)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112#[expect(clippy::module_name_repetitions)]
113pub struct RuntimeOptions {
114 pub export_variables_on_modification: bool,
118 pub notify_job_termination_immediately: bool,
120 pub exit_on_nonzero_command_exit: bool,
122 pub disable_filename_globbing: bool,
124 pub remember_command_locations: bool,
126 pub place_all_assignment_args_in_command_env: bool,
128 pub enable_job_control: bool,
130 pub do_not_execute_commands: bool,
132 pub real_effective_uid_mismatch: bool,
134 pub exit_after_one_command: bool,
136 pub treat_unset_variables_as_error: bool,
138 pub print_shell_input_lines: bool,
140 pub print_commands_and_arguments: bool,
142 pub perform_brace_expansion: bool,
144 pub disallow_overwriting_regular_files_via_output_redirection: bool,
146 pub shell_functions_inherit_err_trap: bool,
148 pub enable_bang_style_history_substitution: bool,
150 pub do_not_resolve_symlinks_when_changing_dir: bool,
152 pub shell_functions_inherit_debug_and_return_traps: bool,
154
155 pub emacs_mode: bool,
159 pub enable_command_history: bool,
161 pub ignore_eof: bool,
163 pub return_last_failure_from_pipeline: bool,
165 pub posix_mode: bool,
167 pub vi_mode: bool,
169
170 pub array_expand_once: bool,
174 pub assoc_expand_once: bool,
176 pub auto_cd: bool,
178 pub bash_source_full_path: bool,
180 pub cdable_vars: bool,
182 pub cd_autocorrect_spelling: bool,
184 pub check_hashtable_before_command_exec: bool,
186 pub check_jobs_before_exit: bool,
188 pub check_window_size_after_external_commands: bool,
190 pub save_multiline_cmds_in_history: bool,
192 pub compat31: bool,
194 pub compat32: bool,
196 pub compat40: bool,
198 pub compat41: bool,
200 pub compat42: bool,
202 pub compat43: bool,
204 pub compat44: bool,
206 pub quote_all_metachars_in_completion: bool,
208 pub expand_dir_names_on_completion: bool,
210 pub autocorrect_dir_spelling_on_completion: bool,
212 pub glob_matches_dotfiles: bool,
214 pub exit_on_exec_fail: bool,
216 pub expand_aliases: bool,
218 pub enable_debugger: bool,
220 pub extended_globbing: bool,
222 pub extquote: bool,
224 pub fail_expansion_on_globs_without_match: bool,
226 pub force_fignore: bool,
228 pub glob_ranges_use_c_locale: bool,
230 pub glob_skip_dots: bool,
232 pub enable_star_star_glob: bool,
234 pub errors_in_gnu_format: bool,
236 pub append_to_history_file: bool,
238 pub allow_reedit_failed_history_subst: bool,
240 pub allow_modifying_history_substitution: bool,
242 pub enable_hostname_completion: bool,
244 pub send_sighup_to_all_jobs_on_exit: bool,
246 pub command_subst_inherits_errexit: bool,
248 pub interactive_comments: bool,
250 pub run_last_pipeline_cmd_in_current_shell: bool,
252 pub embed_newlines_in_multiline_cmds_in_history: bool,
254 pub local_vars_inherit_value_and_attrs: bool,
256 pub localvar_unset: bool,
258 pub login_shell: bool,
260 pub mail_warn: bool,
262 pub no_empty_cmd_completion: bool,
264 pub case_insensitive_pathname_expansion: bool,
266 pub case_insensitive_conditionals: bool,
268 pub no_expand_translation: bool,
270 pub expand_non_matching_patterns_to_null: bool,
272 pub patsub_replacement: bool,
274 pub programmable_completion: bool,
276 pub programmable_completion_alias: bool,
278 pub expand_prompt_strings: bool,
280 pub restricted_shell: bool,
282 pub shift_verbose: bool,
284 pub source_builtin_searches_path: bool,
286 pub var_redir_close: bool,
288 pub echo_builtin_expands_escape_sequences: bool,
290
291 pub interactive: bool,
295 pub read_commands_from_stdin: bool,
297 pub command_string_mode: bool,
299 pub sh_mode: bool,
301 pub external_cmd_leads_session: bool,
303 pub kill_external_commands_on_drop: bool,
306 #[cfg_attr(feature = "serde", serde(skip))]
309 pub spawn: SpawnOptions,
310 pub max_function_call_depth: Option<usize>,
312 pub max_kept_descriptors: Option<usize>,
317}
318
319impl RuntimeOptions {
320 pub fn defaults_from<SE: extensions::ShellExtensions>(
327 create_options: &CreateOptions<SE>,
328 ) -> Self {
329 let mut options = Self {
331 interactive: create_options.interactive,
332 disallow_overwriting_regular_files_via_output_redirection: create_options
333 .disallow_overwriting_regular_files_via_output_redirection,
334 do_not_execute_commands: create_options.do_not_execute_commands,
335 enable_command_history: create_options.interactive,
336 enable_job_control: create_options.interactive,
337 exit_after_one_command: create_options.exit_after_one_command,
338 read_commands_from_stdin: create_options.read_commands_from_stdin,
339 command_string_mode: create_options.command_string_mode,
340 sh_mode: create_options.sh_mode,
341 posix_mode: create_options.posix,
342 print_commands_and_arguments: create_options.print_commands_and_arguments,
343 print_shell_input_lines: create_options.verbose,
344 treat_unset_variables_as_error: create_options.treat_unset_variables_as_error,
345 exit_on_nonzero_command_exit: create_options.exit_on_nonzero_command_exit,
346 external_cmd_leads_session: create_options.external_cmd_leads_session,
347 kill_external_commands_on_drop: create_options.kill_external_commands_on_drop,
348 login_shell: create_options.login,
349 disable_filename_globbing: create_options.disable_pathname_expansion,
350 remember_command_locations: true,
351 check_window_size_after_external_commands: true,
352 save_multiline_cmds_in_history: true,
353 extquote: true,
354 force_fignore: true,
355 case_insensitive_pathname_expansion:
356 crate::sys::fs::default_case_insensitive_path_expansion(),
357 enable_hostname_completion: true,
358 interactive_comments: true,
359 expand_prompt_strings: true,
360 source_builtin_searches_path: true,
361 perform_brace_expansion: true,
362 quote_all_metachars_in_completion: true,
363 programmable_completion: true,
364 glob_ranges_use_c_locale: true,
365 glob_skip_dots: true,
366 patsub_replacement: true,
367 max_function_call_depth: create_options.max_function_call_depth,
368 max_kept_descriptors: Some(
369 create_options
370 .max_kept_descriptors
371 .unwrap_or(DEFAULT_MAX_KEPT_DESCRIPTORS),
372 ),
373 ..Self::default()
374 };
375
376 if create_options.interactive {
378 options.enable_bang_style_history_substitution = true;
379 options.emacs_mode = !create_options.no_editing;
380 options.expand_aliases = true;
381 }
382
383 for enabled_option in &create_options.enabled_options {
385 if let Some(option) = namedoptions::options(namedoptions::ShellOptionKind::SetO)
386 .get(enabled_option.as_str())
387 {
388 option.set(&mut options, true);
389 }
390 }
391 for disabled_option in &create_options.disabled_options {
392 if let Some(option) = namedoptions::options(namedoptions::ShellOptionKind::SetO)
393 .get(disabled_option.as_str())
394 {
395 option.set(&mut options, false);
396 }
397 }
398
399 for enabled_option in &create_options.enabled_shopt_options {
401 if let Some(shopt_option) = namedoptions::options(namedoptions::ShellOptionKind::Shopt)
402 .get(enabled_option.as_str())
403 {
404 shopt_option.set(&mut options, true);
405 }
406 }
407 for disabled_option in &create_options.disabled_shopt_options {
408 if let Some(shopt_option) = namedoptions::options(namedoptions::ShellOptionKind::Shopt)
409 .get(disabled_option.as_str())
410 {
411 shopt_option.set(&mut options, false);
412 }
413 }
414
415 options
416 }
417
418 pub fn option_flags(&self) -> String {
421 let mut cs = vec![];
422
423 for o in namedoptions::options(namedoptions::ShellOptionKind::Set).iter() {
424 if o.definition.get(self)
425 && let Some(c) = o.name.chars().next()
426 {
427 cs.push(c);
428 }
429 }
430
431 cs.sort_by_key(|flag| option_flag_sort_key(*flag));
433
434 cs.into_iter().collect()
435 }
436
437 pub fn seto_optstr(&self) -> String {
439 let mut cs = vec![];
440
441 for option in namedoptions::options(namedoptions::ShellOptionKind::SetO).iter() {
442 if option.definition.get(self) {
443 cs.push(option.name);
444 }
445 }
446
447 cs.sort_unstable();
448 cs.into_iter().join(":")
449 }
450
451 pub fn shopt_optstr(&self) -> String {
453 let mut cs = vec![];
454
455 for option in namedoptions::options(namedoptions::ShellOptionKind::Shopt).iter() {
456 if option.definition.get(self) {
457 cs.push(option.name);
458 }
459 }
460
461 cs.sort_unstable();
462 cs.into_iter().join(":")
463 }
464}
465
466const fn option_flag_sort_key(ch: char) -> (u8, char) {
472 let group = if ch.is_ascii_lowercase() && !matches!(ch, 'c' | 's') {
477 0
478 } else if ch.is_ascii_uppercase() {
479 1
480 } else {
481 2
482 };
483
484 (group, ch)
485}
486
487#[cfg(test)]
488mod tests {
489 use super::option_flag_sort_key;
490
491 #[test]
492 fn lowercase_excluding_c_and_s_sort_first() {
493 let mut flags = vec!['b', 'A', 'Z', 's', 'c', 'a'];
494 flags.sort_by_key(|flag| option_flag_sort_key(*flag));
495
496 assert_eq!(flags, vec!['a', 'b', 'A', 'Z', 'c', 's']);
497 }
498
499 #[test]
500 fn uppercase_sorted_before_miscellaneous() {
501 let mut flags = vec!['P', 'B', '1', 'T'];
502 flags.sort_by_key(|flag| option_flag_sort_key(*flag));
503
504 assert_eq!(flags, vec!['B', 'P', 'T', '1']);
505 }
506
507 #[test]
508 fn miscellaneous_characters_respect_ascii_order() {
509 let mut flags = vec!['s', 'c', '%', ':'];
510 flags.sort_by_key(|flag| option_flag_sort_key(*flag));
511
512 assert_eq!(flags, vec!['%', ':', 'c', 's']);
513 }
514}