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