396 lines
13 KiB
Rust
396 lines
13 KiB
Rust
//! 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,
|
|
/// Whether externally spawned commands are killed when their spawning shell is dropped.
|
|
pub kill_external_commands_on_drop: 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,
|
|
kill_external_commands_on_drop: create_options.kill_external_commands_on_drop,
|
|
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']);
|
|
}
|
|
}
|