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 on_spawn: Option<Arc<dyn Fn(SpawnedProcess) + Send + Sync>>,
61}
62
63impl std::fmt::Debug for SpawnOptions {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 f.debug_struct("SpawnOptions")
66 .field("cgroup_procs", &self.cgroup_procs)
67 .field("open_file_limit", &self.open_file_limit)
68 .field("pids_limits", &self.pids_limits)
69 .field("on_spawn", &self.on_spawn.as_ref().map(|_| "Fn"))
70 .finish()
71 }
72}
73
74#[derive(Clone, Debug)]
77pub struct PidsLimit {
78 pub current: Arc<std::fs::File>,
81 pub max: u64,
84}
85
86#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub struct SpawnedProcess {
89 pub pid: i32,
91 pub process_group: Option<i32>,
93}
94
95#[derive(Clone, Default)]
97#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
98#[expect(clippy::module_name_repetitions)]
99pub struct RuntimeOptions {
100 pub export_variables_on_modification: bool,
104 pub notify_job_termination_immediately: bool,
106 pub exit_on_nonzero_command_exit: bool,
108 pub disable_filename_globbing: bool,
110 pub remember_command_locations: bool,
112 pub place_all_assignment_args_in_command_env: bool,
114 pub enable_job_control: bool,
116 pub do_not_execute_commands: bool,
118 pub real_effective_uid_mismatch: bool,
120 pub exit_after_one_command: bool,
122 pub treat_unset_variables_as_error: bool,
124 pub print_shell_input_lines: bool,
126 pub print_commands_and_arguments: bool,
128 pub perform_brace_expansion: bool,
130 pub disallow_overwriting_regular_files_via_output_redirection: bool,
132 pub shell_functions_inherit_err_trap: bool,
134 pub enable_bang_style_history_substitution: bool,
136 pub do_not_resolve_symlinks_when_changing_dir: bool,
138 pub shell_functions_inherit_debug_and_return_traps: bool,
140
141 pub emacs_mode: bool,
145 pub enable_command_history: bool,
147 pub ignore_eof: bool,
149 pub return_last_failure_from_pipeline: bool,
151 pub posix_mode: bool,
153 pub vi_mode: bool,
155
156 pub array_expand_once: bool,
160 pub assoc_expand_once: bool,
162 pub auto_cd: bool,
164 pub bash_source_full_path: bool,
166 pub cdable_vars: bool,
168 pub cd_autocorrect_spelling: bool,
170 pub check_hashtable_before_command_exec: bool,
172 pub check_jobs_before_exit: bool,
174 pub check_window_size_after_external_commands: bool,
176 pub save_multiline_cmds_in_history: bool,
178 pub compat31: bool,
180 pub compat32: bool,
182 pub compat40: bool,
184 pub compat41: bool,
186 pub compat42: bool,
188 pub compat43: bool,
190 pub compat44: bool,
192 pub quote_all_metachars_in_completion: bool,
194 pub expand_dir_names_on_completion: bool,
196 pub autocorrect_dir_spelling_on_completion: bool,
198 pub glob_matches_dotfiles: bool,
200 pub exit_on_exec_fail: bool,
202 pub expand_aliases: bool,
204 pub enable_debugger: bool,
206 pub extended_globbing: bool,
208 pub extquote: bool,
210 pub fail_expansion_on_globs_without_match: bool,
212 pub force_fignore: bool,
214 pub glob_ranges_use_c_locale: bool,
216 pub glob_skip_dots: bool,
218 pub enable_star_star_glob: bool,
220 pub errors_in_gnu_format: bool,
222 pub append_to_history_file: bool,
224 pub allow_reedit_failed_history_subst: bool,
226 pub allow_modifying_history_substitution: bool,
228 pub enable_hostname_completion: bool,
230 pub send_sighup_to_all_jobs_on_exit: bool,
232 pub command_subst_inherits_errexit: bool,
234 pub interactive_comments: bool,
236 pub run_last_pipeline_cmd_in_current_shell: bool,
238 pub embed_newlines_in_multiline_cmds_in_history: bool,
240 pub local_vars_inherit_value_and_attrs: bool,
242 pub localvar_unset: bool,
244 pub login_shell: bool,
246 pub mail_warn: bool,
248 pub no_empty_cmd_completion: bool,
250 pub case_insensitive_pathname_expansion: bool,
252 pub case_insensitive_conditionals: bool,
254 pub no_expand_translation: bool,
256 pub expand_non_matching_patterns_to_null: bool,
258 pub patsub_replacement: bool,
260 pub programmable_completion: bool,
262 pub programmable_completion_alias: bool,
264 pub expand_prompt_strings: bool,
266 pub restricted_shell: bool,
268 pub shift_verbose: bool,
270 pub source_builtin_searches_path: bool,
272 pub var_redir_close: bool,
274 pub echo_builtin_expands_escape_sequences: bool,
276
277 pub interactive: bool,
281 pub read_commands_from_stdin: bool,
283 pub command_string_mode: bool,
285 pub sh_mode: bool,
287 pub external_cmd_leads_session: bool,
289 pub kill_external_commands_on_drop: bool,
292 #[cfg_attr(feature = "serde", serde(skip))]
295 pub spawn: SpawnOptions,
296 pub max_function_call_depth: Option<usize>,
298 pub max_kept_descriptors: Option<usize>,
303}
304
305impl RuntimeOptions {
306 pub fn defaults_from<SE: extensions::ShellExtensions>(
313 create_options: &CreateOptions<SE>,
314 ) -> Self {
315 let mut options = Self {
317 interactive: create_options.interactive,
318 disallow_overwriting_regular_files_via_output_redirection: create_options
319 .disallow_overwriting_regular_files_via_output_redirection,
320 do_not_execute_commands: create_options.do_not_execute_commands,
321 enable_command_history: create_options.interactive,
322 enable_job_control: create_options.interactive,
323 exit_after_one_command: create_options.exit_after_one_command,
324 read_commands_from_stdin: create_options.read_commands_from_stdin,
325 command_string_mode: create_options.command_string_mode,
326 sh_mode: create_options.sh_mode,
327 posix_mode: create_options.posix,
328 print_commands_and_arguments: create_options.print_commands_and_arguments,
329 print_shell_input_lines: create_options.verbose,
330 treat_unset_variables_as_error: create_options.treat_unset_variables_as_error,
331 exit_on_nonzero_command_exit: create_options.exit_on_nonzero_command_exit,
332 external_cmd_leads_session: create_options.external_cmd_leads_session,
333 kill_external_commands_on_drop: create_options.kill_external_commands_on_drop,
334 login_shell: create_options.login,
335 disable_filename_globbing: create_options.disable_pathname_expansion,
336 remember_command_locations: true,
337 check_window_size_after_external_commands: true,
338 save_multiline_cmds_in_history: true,
339 extquote: true,
340 force_fignore: true,
341 case_insensitive_pathname_expansion:
342 crate::sys::fs::default_case_insensitive_path_expansion(),
343 enable_hostname_completion: true,
344 interactive_comments: true,
345 expand_prompt_strings: true,
346 source_builtin_searches_path: true,
347 perform_brace_expansion: true,
348 quote_all_metachars_in_completion: true,
349 programmable_completion: true,
350 glob_ranges_use_c_locale: true,
351 glob_skip_dots: true,
352 patsub_replacement: true,
353 max_function_call_depth: create_options.max_function_call_depth,
354 max_kept_descriptors: Some(
355 create_options
356 .max_kept_descriptors
357 .unwrap_or(DEFAULT_MAX_KEPT_DESCRIPTORS),
358 ),
359 ..Self::default()
360 };
361
362 if create_options.interactive {
364 options.enable_bang_style_history_substitution = true;
365 options.emacs_mode = !create_options.no_editing;
366 options.expand_aliases = true;
367 }
368
369 for enabled_option in &create_options.enabled_options {
371 if let Some(option) = namedoptions::options(namedoptions::ShellOptionKind::SetO)
372 .get(enabled_option.as_str())
373 {
374 option.set(&mut options, true);
375 }
376 }
377 for disabled_option in &create_options.disabled_options {
378 if let Some(option) = namedoptions::options(namedoptions::ShellOptionKind::SetO)
379 .get(disabled_option.as_str())
380 {
381 option.set(&mut options, false);
382 }
383 }
384
385 for enabled_option in &create_options.enabled_shopt_options {
387 if let Some(shopt_option) = namedoptions::options(namedoptions::ShellOptionKind::Shopt)
388 .get(enabled_option.as_str())
389 {
390 shopt_option.set(&mut options, true);
391 }
392 }
393 for disabled_option in &create_options.disabled_shopt_options {
394 if let Some(shopt_option) = namedoptions::options(namedoptions::ShellOptionKind::Shopt)
395 .get(disabled_option.as_str())
396 {
397 shopt_option.set(&mut options, false);
398 }
399 }
400
401 options
402 }
403
404 pub fn option_flags(&self) -> String {
407 let mut cs = vec![];
408
409 for o in namedoptions::options(namedoptions::ShellOptionKind::Set).iter() {
410 if o.definition.get(self)
411 && let Some(c) = o.name.chars().next()
412 {
413 cs.push(c);
414 }
415 }
416
417 cs.sort_by_key(|flag| option_flag_sort_key(*flag));
419
420 cs.into_iter().collect()
421 }
422
423 pub fn seto_optstr(&self) -> String {
425 let mut cs = vec![];
426
427 for option in namedoptions::options(namedoptions::ShellOptionKind::SetO).iter() {
428 if option.definition.get(self) {
429 cs.push(option.name);
430 }
431 }
432
433 cs.sort_unstable();
434 cs.into_iter().join(":")
435 }
436
437 pub fn shopt_optstr(&self) -> String {
439 let mut cs = vec![];
440
441 for option in namedoptions::options(namedoptions::ShellOptionKind::Shopt).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
452const fn option_flag_sort_key(ch: char) -> (u8, char) {
458 let group = if ch.is_ascii_lowercase() && !matches!(ch, 'c' | 's') {
463 0
464 } else if ch.is_ascii_uppercase() {
465 1
466 } else {
467 2
468 };
469
470 (group, ch)
471}
472
473#[cfg(test)]
474mod tests {
475 use super::option_flag_sort_key;
476
477 #[test]
478 fn lowercase_excluding_c_and_s_sort_first() {
479 let mut flags = vec!['b', 'A', 'Z', 's', 'c', 'a'];
480 flags.sort_by_key(|flag| option_flag_sort_key(*flag));
481
482 assert_eq!(flags, vec!['a', 'b', 'A', 'Z', 'c', 's']);
483 }
484
485 #[test]
486 fn uppercase_sorted_before_miscellaneous() {
487 let mut flags = vec!['P', 'B', '1', 'T'];
488 flags.sort_by_key(|flag| option_flag_sort_key(*flag));
489
490 assert_eq!(flags, vec!['B', 'P', 'T', '1']);
491 }
492
493 #[test]
494 fn miscellaneous_characters_respect_ascii_order() {
495 let mut flags = vec!['s', 'c', '%', ':'];
496 flags.sort_by_key(|flag| option_flag_sort_key(*flag));
497
498 assert_eq!(flags, vec!['%', ':', 'c', 's']);
499 }
500}