Skip to main content

clash_brush_core/
shell.rs

1//! Module defining the core shell structure and behavior.
2
3use std::borrow::Cow;
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use tokio::sync::Mutex;
9
10/// Hook called before spawning an external command. Receives the executable
11/// path and string arguments. Returns an optional replacement
12/// `(executable, args)` pair. If `None` is returned, the original command
13/// is executed unchanged.
14pub type ExternalCommandHook =
15    Arc<dyn Fn(&str, &[String]) -> Option<(String, Vec<String>)> + Send + Sync>;
16
17use crate::{
18    ExecutionControlFlow, ExecutionResult, builtins, env::ShellEnvironment, error, extensions,
19    functions, interfaces, jobs, keywords, openfiles, options::RuntimeOptions, pathcache,
20    wellknownvars,
21};
22
23/// Type for storing a key bindings helper.
24pub type KeyBindingsHelper = Arc<Mutex<dyn interfaces::KeyBindings>>;
25
26/// Type alias for shell file descriptors.
27pub type ShellFd = i32;
28
29// NOTE: The submodule files below (e.g., `shell/traps.rs`, `shell/callstack.rs`) contain
30// `impl Shell<SE>` blocks that provide methods coordinating with types defined in the
31// corresponding top-level modules (e.g., `traps.rs`, `callstack.rs`). This is an intentional
32// layered architecture: top-level modules define domain types and data structures, while
33// shell/ submodules implement Shell methods that operate on those types.
34
35mod builder;
36mod builtin_registry;
37mod callstack;
38mod completion;
39mod env;
40mod execution;
41mod expansion;
42mod fs;
43mod funcs;
44mod history;
45mod initscripts;
46mod io;
47mod job_control;
48mod parsing;
49mod prompts;
50mod readline;
51mod state;
52mod traps;
53
54pub use builder::{CreateOptions, ShellBuilder, ShellBuilderState};
55pub use initscripts::{ProfileLoadBehavior, RcLoadBehavior};
56pub use state::ShellState;
57
58/// Represents an instance of a shell.
59///
60/// # Type Parameters
61///
62/// * `SE` - The shell extensions implementation to use. These extensions are statically
63///   injected into the shell at compile time to provide custom behavior. When
64///   unspecified, defaults to `DefaultShellExtensions`, which provide standard
65///   behavior.
66#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
67pub struct Shell<SE: extensions::ShellExtensions = extensions::DefaultShellExtensions> {
68    /// Injected error behavior.
69    #[cfg_attr(feature = "serde", serde(skip, default = "default_error_formatter"))]
70    error_formatter: SE::ErrorFormatter,
71
72    /// Trap handler configuration for the shell.
73    traps: crate::traps::TrapHandlerConfig,
74
75    /// Manages files opened and accessible via redirection operators.
76    open_files: openfiles::OpenFiles,
77
78    /// The current working directory.
79    working_dir: PathBuf,
80
81    /// The shell environment, containing shell variables.
82    env: ShellEnvironment,
83
84    /// Shell function definitions.
85    funcs: functions::FunctionEnv,
86
87    /// Runtime shell options.
88    options: RuntimeOptions,
89
90    /// State of managed jobs.
91    /// TODO(serde): Need to warn somehow that jobs cannot be serialized.
92    #[cfg_attr(feature = "serde", serde(skip))]
93    jobs: jobs::JobManager,
94
95    /// Shell aliases.
96    aliases: HashMap<String, String>,
97
98    /// The status of the last completed command.
99    last_exit_status: u8,
100
101    /// Tracks changes to `last_exit_status`.
102    last_exit_status_change_count: usize,
103
104    /// The status of each of the commands in the last pipeline.
105    last_pipeline_statuses: Vec<u8>,
106
107    /// Clone depth from the original ancestor shell.
108    depth: usize,
109
110    /// Shell name
111    name: Option<String>,
112
113    /// Positional shell arguments (not including shell name).
114    args: Vec<String>,
115
116    /// Shell version
117    version: Option<String>,
118
119    /// Detailed display string for the shell
120    product_display_str: Option<String>,
121
122    /// Function/script call stack.
123    call_stack: crate::callstack::CallStack,
124
125    /// Directory stack used by pushd et al.
126    directory_stack: Vec<PathBuf>,
127
128    /// Completion configuration.
129    completion_config: crate::completion::Config,
130
131    /// Shell built-in commands.
132    #[cfg_attr(feature = "serde", serde(skip))]
133    builtins: HashMap<String, builtins::Registration<SE>>,
134
135    /// Shell program location cache.
136    program_location_cache: pathcache::PathCache,
137
138    /// Last "SECONDS" captured time.
139    last_stopwatch_time: std::time::SystemTime,
140
141    /// Last "SECONDS" offset requested.
142    last_stopwatch_offset: u32,
143
144    /// Parser implementation to use.
145    #[cfg_attr(feature = "serde", serde(skip))]
146    parser_impl: crate::parser::ParserImpl,
147
148    /// Key bindings for the shell, optionally implemented by an interactive shell.
149    #[cfg_attr(feature = "serde", serde(skip))]
150    key_bindings: Option<KeyBindingsHelper>,
151
152    /// History of commands executed in the shell.
153    history: Option<crate::history::History>,
154
155    /// Optional hook for intercepting external commands before execution.
156    #[cfg_attr(feature = "serde", serde(skip))]
157    external_command_hook: Option<ExternalCommandHook>,
158}
159
160impl<SE: extensions::ShellExtensions> Clone for Shell<SE> {
161    fn clone(&self) -> Self {
162        Self {
163            error_formatter: self.error_formatter.clone(),
164            traps: self.traps.clone(),
165            open_files: self.open_files.clone(),
166            working_dir: self.working_dir.clone(),
167            env: self.env.clone(),
168            funcs: self.funcs.clone(),
169            options: self.options.clone(),
170            jobs: jobs::JobManager::new(),
171            aliases: self.aliases.clone(),
172            last_exit_status: self.last_exit_status,
173            last_exit_status_change_count: self.last_exit_status_change_count,
174            last_pipeline_statuses: self.last_pipeline_statuses.clone(),
175            name: self.name.clone(),
176            args: self.args.clone(),
177            version: self.version.clone(),
178            product_display_str: self.product_display_str.clone(),
179            call_stack: self.call_stack.clone(),
180            directory_stack: self.directory_stack.clone(),
181            completion_config: self.completion_config.clone(),
182            builtins: self.builtins.clone(),
183            program_location_cache: self.program_location_cache.clone(),
184            last_stopwatch_time: self.last_stopwatch_time,
185            last_stopwatch_offset: self.last_stopwatch_offset,
186            parser_impl: self.parser_impl,
187            key_bindings: self.key_bindings.clone(),
188            history: self.history.clone(),
189            external_command_hook: self.external_command_hook.clone(),
190            depth: self.depth + 1,
191        }
192    }
193}
194
195impl<SE: extensions::ShellExtensions> AsRef<Self> for Shell<SE> {
196    fn as_ref(&self) -> &Self {
197        self
198    }
199}
200
201impl<SE: extensions::ShellExtensions> AsMut<Self> for Shell<SE> {
202    fn as_mut(&mut self) -> &mut Self {
203        self
204    }
205}
206
207impl<SE: extensions::ShellExtensions> Shell<SE> {
208    /// Returns a new shell instance created with the given options.
209    /// Does *not* load any configuration files (e.g., bashrc).
210    ///
211    /// # Arguments
212    ///
213    /// * `options` - The options to use when creating the shell.
214    pub(crate) fn new(options: CreateOptions<SE>) -> Result<Self, error::Error> {
215        // Compute runtime options before moving fields out of `options`.
216        let runtime_options = RuntimeOptions::defaults_from(&options);
217
218        // Instantiate the shell with some defaults.
219        let mut shell = Self {
220            error_formatter: options.error_formatter,
221            open_files: openfiles::OpenFiles::new(),
222            options: runtime_options,
223            name: options.shell_name,
224            args: options.shell_args.unwrap_or_default(),
225            version: options.shell_version,
226            product_display_str: options.shell_product_display_str,
227            working_dir: options.working_dir.map_or_else(std::env::current_dir, Ok)?,
228            builtins: options.builtins,
229            parser_impl: options.parser,
230            key_bindings: options.key_bindings,
231            external_command_hook: options.external_command_hook,
232            ..Self::default()
233        };
234
235        // Add in any open files provided.
236        shell.open_files.update_from(options.fds.into_iter());
237
238        // TODO(patterns): Without this a script that sets extglob will fail because we
239        // parse the entire script with the same settings.
240        shell.options.extended_globbing = true;
241
242        // If requested, seed parameters from environment.
243        if !options.do_not_inherit_env {
244            wellknownvars::inherit_env_vars(&mut shell)?;
245        }
246
247        // If requested, set well-known variables.
248        if !options.skip_well_known_vars {
249            wellknownvars::init_well_known_vars(&mut shell)?;
250        }
251
252        // Set any provided variables.
253        for (var_name, var_value) in options.vars {
254            shell.env.set_global(var_name, var_value)?;
255        }
256
257        // Set up history, if relevant. Do NOT fail if we can't load history.
258        if shell.options.enable_command_history {
259            shell.history = shell
260                .load_history()
261                .unwrap_or_default()
262                .or_else(|| Some(crate::history::History::default()));
263        }
264
265        Ok(shell)
266    }
267}
268
269impl<SE: extensions::ShellExtensions> Shell<SE> {
270    /// Increments the interactive line offset in the shell by the indicated number
271    /// of lines.
272    ///
273    /// # Arguments
274    ///
275    /// * `delta` - The number of lines to increment the current line offset by.
276    pub fn increment_interactive_line_offset(&mut self, delta: usize) {
277        self.call_stack.increment_current_line_offset(delta);
278    }
279
280    /// Updates the currently executing command in the shell.
281    pub fn set_current_cmd(&mut self, cmd: &impl brush_parser::ast::Node) {
282        self.call_stack
283            .set_current_pos(cmd.location().map(|span| span.start));
284    }
285
286    /// Applies errexit semantics to a result if enabled and appropriate.
287    /// This should be called at "statement boundaries" where errexit should be checked.
288    ///
289    /// # Arguments
290    ///
291    /// * `result` - The execution result to potentially modify.
292    pub const fn apply_errexit_if_enabled(&self, result: &mut ExecutionResult) {
293        if self.options.exit_on_nonzero_command_exit
294            && !result.is_success()
295            && result.is_normal_flow()
296        {
297            result.next_control_flow = ExecutionControlFlow::ExitShell;
298        }
299    }
300
301    /// Returns the keywords that are reserved by the shell.
302    pub(crate) fn get_keywords(&self) -> Vec<&str> {
303        if self.options.sh_mode {
304            keywords::SH_MODE_KEYWORDS.iter().copied().collect()
305        } else {
306            keywords::KEYWORDS.iter().copied().collect()
307        }
308    }
309
310    /// Checks if the given string is a keyword reserved in this shell.
311    ///
312    /// # Arguments
313    ///
314    /// * `s` - The string to check.
315    pub fn is_keyword(&self, s: &str) -> bool {
316        if self.options.sh_mode {
317            keywords::SH_MODE_KEYWORDS.contains(s)
318        } else {
319            keywords::KEYWORDS.contains(s)
320        }
321    }
322
323    pub(crate) const fn last_exit_status_change_count(&self) -> usize {
324        self.last_exit_status_change_count
325    }
326
327    /// Returns the external command hook, if set.
328    pub fn external_command_hook(&self) -> Option<&ExternalCommandHook> {
329        self.external_command_hook.as_ref()
330    }
331}
332
333#[inherent::inherent]
334impl<SE: extensions::ShellExtensions> ShellState for Shell<SE> {
335    /// Returns whether or not this shell is a subshell.
336    pub fn is_subshell(&self) -> bool {
337        self.depth > 0
338    }
339
340    /// Returns the last "SECONDS" captured time.
341    pub fn last_stopwatch_time(&self) -> std::time::SystemTime {
342        self.last_stopwatch_time
343    }
344
345    /// Returns the last "SECONDS" offset requested.
346    pub fn last_stopwatch_offset(&self) -> u32 {
347        self.last_stopwatch_offset
348    }
349
350    /// Returns the shell environment containing variables.
351    pub fn env(&self) -> &ShellEnvironment {
352        &self.env
353    }
354
355    /// Returns a mutable reference to the shell environment.
356    pub fn env_mut(&mut self) -> &mut ShellEnvironment {
357        &mut self.env
358    }
359
360    /// Returns the shell's runtime options.
361    pub fn options(&self) -> &RuntimeOptions {
362        &self.options
363    }
364
365    /// Returns a mutable reference to the shell's runtime options.
366    pub fn options_mut(&mut self) -> &mut RuntimeOptions {
367        &mut self.options
368    }
369
370    /// Returns the shell's aliases.
371    pub fn aliases(&self) -> &HashMap<String, String> {
372        &self.aliases
373    }
374
375    /// Returns a mutable reference to the shell's aliases.
376    pub fn aliases_mut(&mut self) -> &mut HashMap<String, String> {
377        &mut self.aliases
378    }
379
380    /// Returns the shell's job manager.
381    pub fn jobs(&self) -> &jobs::JobManager {
382        &self.jobs
383    }
384
385    /// Returns a mutable reference to the shell's job manager.
386    pub fn jobs_mut(&mut self) -> &mut jobs::JobManager {
387        &mut self.jobs
388    }
389
390    /// Returns the shell's trap handler configuration.
391    pub fn traps(&self) -> &crate::traps::TrapHandlerConfig {
392        &self.traps
393    }
394
395    /// Returns a mutable reference to the shell's trap handler configuration.
396    pub fn traps_mut(&mut self) -> &mut crate::traps::TrapHandlerConfig {
397        &mut self.traps
398    }
399
400    /// Returns the shell's directory stack.
401    pub fn directory_stack(&self) -> &[PathBuf] {
402        &self.directory_stack
403    }
404
405    /// Returns a mutable reference to the shell's directory stack.
406    pub fn directory_stack_mut(&mut self) -> &mut Vec<PathBuf> {
407        &mut self.directory_stack
408    }
409
410    /// Returns the statuses of commands in the last pipeline.
411    pub fn last_pipeline_statuses(&self) -> &[u8] {
412        &self.last_pipeline_statuses
413    }
414
415    /// Returns a mutable reference to the statuses of commands in the last pipeline.
416    pub fn last_pipeline_statuses_mut(&mut self) -> &mut Vec<u8> {
417        &mut self.last_pipeline_statuses
418    }
419
420    /// Returns the shell's program location cache.
421    pub fn program_location_cache(&self) -> &pathcache::PathCache {
422        &self.program_location_cache
423    }
424
425    /// Returns a mutable reference to the shell's program location cache.
426    pub fn program_location_cache_mut(&mut self) -> &mut pathcache::PathCache {
427        &mut self.program_location_cache
428    }
429
430    /// Returns the shell's completion configuration.
431    pub fn completion_config(&self) -> &crate::completion::Config {
432        &self.completion_config
433    }
434
435    /// Returns a mutable reference to the shell's completion configuration.
436    pub fn completion_config_mut(&mut self) -> &mut crate::completion::Config {
437        &mut self.completion_config
438    }
439
440    /// Returns the shell's open files.
441    pub fn open_files(&self) -> &openfiles::OpenFiles {
442        &self.open_files
443    }
444
445    /// Returns a mutable reference to the shell's open files.
446    pub fn open_files_mut(&mut self) -> &mut openfiles::OpenFiles {
447        &mut self.open_files
448    }
449
450    /// Returns the *current* name of the shell ($0).
451    /// Influenced by the current call stack.
452    pub fn current_shell_name(&self) -> Option<Cow<'_, str>> {
453        for frame in self.call_stack.iter() {
454            // Executed scripts shadow the shell name.
455            if frame.frame_type.is_run_script() {
456                return Some(frame.frame_type.name());
457            }
458        }
459
460        self.name.as_deref().map(|name| name.into())
461    }
462
463    /// Returns the current subshell depth; 0 is returned if this shell is not a subshell.
464    pub fn depth(&self) -> usize {
465        self.depth
466    }
467
468    /// Returns the call stack for the shell.
469    pub fn call_stack(&self) -> &crate::callstack::CallStack {
470        &self.call_stack
471    }
472
473    /// Returns the shell's history, if it exists.
474    pub fn history(&self) -> Option<&crate::history::History> {
475        self.history.as_ref()
476    }
477
478    /// Returns a mutable reference to the shell's history, if it exists.
479    pub fn history_mut(&mut self) -> Option<&mut crate::history::History> {
480        self.history.as_mut()
481    }
482
483    /// Returns the shell's official version string (if available).
484    pub fn version(&self) -> Option<&str> {
485        self.version.as_deref()
486    }
487
488    /// Returns the exit status of the last command executed in this shell.
489    pub fn last_exit_status(&self) -> u8 {
490        self.last_exit_status
491    }
492
493    /// Updates the last exit status.
494    pub fn set_last_exit_status(&mut self, status: u8) {
495        self.last_exit_status = status;
496        self.last_exit_status_change_count += 1;
497    }
498
499    /// Returns the key bindings helper for the shell.
500    pub fn key_bindings(&self) -> Option<&KeyBindingsHelper> {
501        self.key_bindings.as_ref()
502    }
503
504    /// Sets the key bindings helper for the shell.
505    pub fn set_key_bindings(&mut self, key_bindings: Option<KeyBindingsHelper>) {
506        self.key_bindings = key_bindings;
507    }
508
509    /// Returns the shell's current working directory.
510    pub fn working_dir(&self) -> &Path {
511        &self.working_dir
512    }
513
514    /// Returns a mutable reference to the shell's current working directory.
515    /// This is only accessible within the crate.
516    pub(crate) fn working_dir_mut(&mut self) -> &mut PathBuf {
517        &mut self.working_dir
518    }
519
520    /// Returns the product display name for this shell.
521    pub fn product_display_str(&self) -> Option<&str> {
522        self.product_display_str.as_deref()
523    }
524}
525
526#[cfg(feature = "serde")]
527fn default_error_formatter<EF: extensions::ErrorFormatter>() -> EF {
528    EF::default()
529}