06aa0c8f创建于 24 天前历史提交
//! Defines runtime options for the shell.

use itertools::Itertools;

use crate::{CreateOptions, extensions, namedoptions};

/// Runtime changeable options for a shell instance.
#[derive(Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[expect(clippy::module_name_repetitions)]
pub struct RuntimeOptions {
	//
	// Single-character options.
	/// -a
	pub export_variables_on_modification: bool,
	/// -b
	pub notify_job_termination_immediately: bool,
	/// -e
	pub exit_on_nonzero_command_exit: bool,
	/// -f
	pub disable_filename_globbing: bool,
	/// -h
	pub remember_command_locations: bool,
	/// -k
	pub place_all_assignment_args_in_command_env: bool,
	/// -m
	pub enable_job_control: bool,
	/// -n
	pub do_not_execute_commands: bool,
	/// -p
	pub real_effective_uid_mismatch: bool,
	/// -t
	pub exit_after_one_command: bool,
	/// -u
	pub treat_unset_variables_as_error: bool,
	/// -v
	pub print_shell_input_lines: bool,
	/// -x
	pub print_commands_and_arguments: bool,
	/// -B
	pub perform_brace_expansion: bool,
	/// -C
	pub disallow_overwriting_regular_files_via_output_redirection: bool,
	/// -E
	pub shell_functions_inherit_err_trap: bool,
	/// -H
	pub enable_bang_style_history_substitution: bool,
	/// -P
	pub do_not_resolve_symlinks_when_changing_dir: bool,
	/// -T
	pub shell_functions_inherit_debug_and_return_traps: bool,

	//
	// Options set through -o.
	/// 'emacs'
	pub emacs_mode: bool,
	/// 'history'
	pub enable_command_history: bool,
	/// 'ignoreeof'
	pub ignore_eof: bool,
	/// 'pipefail'
	pub return_last_failure_from_pipeline: bool,
	/// 'posix'
	pub posix_mode: bool,
	/// 'vi'
	pub vi_mode: bool,

	//
	// Options set through shopt.
	/// `array_expand_once`
	pub array_expand_once: bool,
	/// `assoc_expand_once`
	pub assoc_expand_once: bool,
	/// 'autocd'
	pub auto_cd: bool,
	/// `bash_source_full_path`
	pub bash_source_full_path: bool,
	/// `cdable_vars`
	pub cdable_vars: bool,
	/// 'cdspell'
	pub cd_autocorrect_spelling: bool,
	/// 'checkhash'
	pub check_hashtable_before_command_exec: bool,
	/// 'checkjobs'
	pub check_jobs_before_exit: bool,
	/// 'checkwinsize'
	pub check_window_size_after_external_commands: bool,
	/// 'cmdhist'
	pub save_multiline_cmds_in_history: bool,
	/// 'compat31'
	pub compat31: bool,
	/// 'compat32'
	pub compat32: bool,
	/// 'compat40'
	pub compat40: bool,
	/// 'compat41'
	pub compat41: bool,
	/// 'compat42'
	pub compat42: bool,
	/// 'compat43'
	pub compat43: bool,
	/// 'compat44'
	pub compat44: bool,
	/// `complete_fullquote`
	pub quote_all_metachars_in_completion: bool,
	/// 'direxpand'
	pub expand_dir_names_on_completion: bool,
	/// 'dirspell'
	pub autocorrect_dir_spelling_on_completion: bool,
	/// 'dotglob'
	pub glob_matches_dotfiles: bool,
	/// 'execfail'
	pub exit_on_exec_fail: bool,
	/// `expand_aliases`
	pub expand_aliases: bool,
	/// 'extdebug'
	pub enable_debugger: bool,
	/// 'extglob'
	pub extended_globbing: bool,
	/// 'extquote'
	pub extquote: bool,
	/// 'failglob'
	pub fail_expansion_on_globs_without_match: bool,
	/// `force_fignore`
	pub force_fignore: bool,
	/// 'globasciiranges'
	pub glob_ranges_use_c_locale: bool,
	/// 'globskipdots'
	pub glob_skip_dots: bool,
	/// 'globstar'
	pub enable_star_star_glob: bool,
	/// `gnu_errfmt`
	pub errors_in_gnu_format: bool,
	/// 'histappend'
	pub append_to_history_file: bool,
	/// 'histreedit'
	pub allow_reedit_failed_history_subst: bool,
	/// 'histverify'
	pub allow_modifying_history_substitution: bool,
	/// 'hostcomplete'
	pub enable_hostname_completion: bool,
	/// 'huponexit'
	pub send_sighup_to_all_jobs_on_exit: bool,
	/// `inherit_errexit`
	pub command_subst_inherits_errexit: bool,
	/// `interactive_comments`
	pub interactive_comments: bool,
	/// 'lastpipe'
	pub run_last_pipeline_cmd_in_current_shell: bool,
	/// 'lithist'
	pub embed_newlines_in_multiline_cmds_in_history: bool,
	/// `localvar_inherit`
	pub local_vars_inherit_value_and_attrs: bool,
	/// `localvar_unset`
	pub localvar_unset: bool,
	/// `login_shell`
	pub login_shell: bool,
	/// 'mailwarn'
	pub mail_warn: bool,
	/// `no_empty_cmd_completion`
	pub no_empty_cmd_completion: bool,
	/// 'nocaseglob'
	pub case_insensitive_pathname_expansion: bool,
	/// 'nocasematch'
	pub case_insensitive_conditionals: bool,
	/// `noexpand_translation`
	pub no_expand_translation: bool,
	/// 'nullglob'
	pub expand_non_matching_patterns_to_null: bool,
	/// `patsub_replacement`
	pub patsub_replacement: bool,
	/// 'progcomp'
	pub programmable_completion: bool,
	/// `progcomp_alias`
	pub programmable_completion_alias: bool,
	/// 'promptvars'
	pub expand_prompt_strings: bool,
	/// `restricted_shell`
	pub restricted_shell: bool,
	/// `shift_verbose`
	pub shift_verbose: bool,
	/// `sourcepath`
	pub source_builtin_searches_path: bool,
	/// `varredir_close`
	pub var_redir_close: bool,
	/// `xpg_echo`
	pub echo_builtin_expands_escape_sequences: bool,

	//
	// Options set by the shell.
	/// Whether or not the shell is interactive.
	pub interactive:                bool,
	/// Whether commands are being read from stdin.
	pub read_commands_from_stdin:   bool,
	/// Whether the shell is in command string mode (-c).
	pub command_string_mode:        bool,
	/// Whether or not the shell is in maximal `sh` compatibility mode.    
	pub sh_mode:                    bool,
	/// Whether to treat external commands as session leaders.
	pub external_cmd_leads_session: bool,
	/// Maximum function call depth.
	pub max_function_call_depth:    Option<usize>,
}

impl RuntimeOptions {
	/// Creates a default set of runtime options based on the given creation
	/// options.
	///
	/// # Arguments
	///
	/// * `create_options` - The options used to create the shell.
	pub fn defaults_from<SE: extensions::ShellExtensions>(
		create_options: &CreateOptions<SE>,
	) -> Self {
		// There's a set of options enabled by default for all shells.
		let mut options = Self {
			interactive: create_options.interactive,
			disallow_overwriting_regular_files_via_output_redirection: create_options
				.disallow_overwriting_regular_files_via_output_redirection,
			do_not_execute_commands: create_options.do_not_execute_commands,
			enable_command_history: create_options.interactive,
			enable_job_control: create_options.interactive,
			exit_after_one_command: create_options.exit_after_one_command,
			read_commands_from_stdin: create_options.read_commands_from_stdin,
			command_string_mode: create_options.command_string_mode,
			sh_mode: create_options.sh_mode,
			posix_mode: create_options.posix,
			print_commands_and_arguments: create_options.print_commands_and_arguments,
			print_shell_input_lines: create_options.verbose,
			treat_unset_variables_as_error: create_options.treat_unset_variables_as_error,
			exit_on_nonzero_command_exit: create_options.exit_on_nonzero_command_exit,
			external_cmd_leads_session: create_options.external_cmd_leads_session,
			login_shell: create_options.login,
			disable_filename_globbing: create_options.disable_pathname_expansion,
			remember_command_locations: true,
			check_window_size_after_external_commands: true,
			save_multiline_cmds_in_history: true,
			extquote: true,
			force_fignore: true,
			case_insensitive_pathname_expansion:
				crate::sys::fs::default_case_insensitive_path_expansion(),
			enable_hostname_completion: true,
			interactive_comments: true,
			expand_prompt_strings: true,
			source_builtin_searches_path: true,
			perform_brace_expansion: true,
			quote_all_metachars_in_completion: true,
			programmable_completion: true,
			glob_ranges_use_c_locale: true,
			glob_skip_dots: true,
			patsub_replacement: true,
			max_function_call_depth: create_options.max_function_call_depth,
			..Self::default()
		};

		// Additional options are enabled by default for interactive shells.
		if create_options.interactive {
			options.enable_bang_style_history_substitution = true;
			options.emacs_mode = !create_options.no_editing;
			options.expand_aliases = true;
		}

		// Update any options.
		for enabled_option in &create_options.enabled_options {
			if let Some(option) =
				namedoptions::options(namedoptions::ShellOptionKind::SetO).get(enabled_option.as_str())
			{
				option.set(&mut options, true);
			}
		}
		for disabled_option in &create_options.disabled_options {
			if let Some(option) =
				namedoptions::options(namedoptions::ShellOptionKind::SetO).get(disabled_option.as_str())
			{
				option.set(&mut options, false);
			}
		}

		// Update any shopt options.
		for enabled_option in &create_options.enabled_shopt_options {
			if let Some(shopt_option) =
				namedoptions::options(namedoptions::ShellOptionKind::Shopt).get(enabled_option.as_str())
			{
				shopt_option.set(&mut options, true);
			}
		}
		for disabled_option in &create_options.disabled_shopt_options {
			if let Some(shopt_option) = namedoptions::options(namedoptions::ShellOptionKind::Shopt)
				.get(disabled_option.as_str())
			{
				shopt_option.set(&mut options, false);
			}
		}

		options
	}

	/// Returns a string representing the current `set`-style option flags set in
	/// the shell.
	pub fn option_flags(&self) -> String {
		let mut cs = vec![];

		for o in namedoptions::options(namedoptions::ShellOptionKind::Set).iter() {
			if o.definition.get(self)
				&& let Some(c) = o.name.chars().next()
			{
				cs.push(c);
			}
		}

		// Sort the flags in a way that matches what bash does.
		cs.sort_by_key(|flag| option_flag_sort_key(*flag));

		cs.into_iter().collect()
	}

	/// Returns a colon-separated list of sorted 'set -o' options enabled.
	pub fn seto_optstr(&self) -> String {
		let mut cs = vec![];

		for option in namedoptions::options(namedoptions::ShellOptionKind::SetO).iter() {
			if option.definition.get(self) {
				cs.push(option.name);
			}
		}

		cs.sort_unstable();
		cs.into_iter().join(":")
	}

	/// Returns a colon-separated list of sorted 'shopt' options enabled.
	pub fn shopt_optstr(&self) -> String {
		let mut cs = vec![];

		for option in namedoptions::options(namedoptions::ShellOptionKind::Shopt).iter() {
			if option.definition.get(self) {
				cs.push(option.name);
			}
		}

		cs.sort_unstable();
		cs.into_iter().join(":")
	}
}

/// Sort option flag character in a way that mirrors bash behavior.
///
/// # Arguments
///
/// * `ch` - The option flag character.
const fn option_flag_sort_key(ch: char) -> (u8, char) {
	// NOTE: bash appears to sort in 3 groups. We mimic them:
	//    1) Lowercase letters excluding 'c' and 's' (sorted)
	//    2) Uppercase letters (sorted)
	//    3) All other characters (sorted)
	let group = if ch.is_ascii_lowercase() && !matches!(ch, 'c' | 's') {
		0
	} else if ch.is_ascii_uppercase() {
		1
	} else {
		2
	};

	(group, ch)
}

#[cfg(test)]
mod tests {
	use super::option_flag_sort_key;

	#[test]
	fn lowercase_excluding_c_and_s_sort_first() {
		let mut flags = vec!['b', 'A', 'Z', 's', 'c', 'a'];
		flags.sort_by_key(|flag| option_flag_sort_key(*flag));

		assert_eq!(flags, vec!['a', 'b', 'A', 'Z', 'c', 's']);
	}

	#[test]
	fn uppercase_sorted_before_miscellaneous() {
		let mut flags = vec!['P', 'B', '1', 'T'];
		flags.sort_by_key(|flag| option_flag_sort_key(*flag));

		assert_eq!(flags, vec!['B', 'P', 'T', '1']);
	}

	#[test]
	fn miscellaneous_characters_respect_ascii_order() {
		let mut flags = vec!['s', 'c', '%', ':'];
		flags.sort_by_key(|flag| option_flag_sort_key(*flag));

		assert_eq!(flags, vec!['%', ':', 'c', 's']);
	}
}