brush-core 0.5.0

Reusable core of a POSIX/bash shell (used by brush-shell)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
//! Module defining the core shell structure and behavior.

use std::borrow::Cow;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use tokio::sync::Mutex;

use crate::{
    ExecutionControlFlow, ExecutionResult, builtins, env::ShellEnvironment, error, extensions,
    functions, interfaces, jobs, keywords, openfiles, options::RuntimeOptions, pathcache,
    wellknownvars,
};

/// Type for storing a key bindings helper.
pub type KeyBindingsHelper = Arc<Mutex<dyn interfaces::KeyBindings>>;

/// Type alias for shell file descriptors.
pub type ShellFd = i32;

// NOTE: The submodule files below (e.g., `shell/traps.rs`, `shell/callstack.rs`) contain
// `impl Shell<SE>` blocks that provide methods coordinating with types defined in the
// corresponding top-level modules (e.g., `traps.rs`, `callstack.rs`). This is an intentional
// layered architecture: top-level modules define domain types and data structures, while
// shell/ submodules implement Shell methods that operate on those types.

mod builder;
mod builtin_registry;
mod callstack;
mod completion;
mod env;
mod execution;
mod expansion;
mod fs;
mod funcs;
mod history;
mod initscripts;
mod io;
mod job_control;
mod parsing;
mod prompts;
mod readline;
mod state;
mod traps;

pub use builder::{CreateOptions, ShellBuilder, ShellBuilderState};
pub use initscripts::{ProfileLoadBehavior, RcLoadBehavior};
pub use state::ShellState;

/// Represents an instance of a shell.
///
/// # Type Parameters
///
/// * `SE` - The shell extensions implementation to use. These extensions are statically injected
///   into the shell at compile time to provide custom behavior. When unspecified, defaults to
///   `DefaultShellExtensions`, which provide standard behavior.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Shell<SE: extensions::ShellExtensions = extensions::DefaultShellExtensions> {
    /// Injected error behavior.
    #[cfg_attr(feature = "serde", serde(skip, default = "default_error_formatter"))]
    error_formatter: SE::ErrorFormatter,

    /// Trap handler configuration for the shell.
    traps: crate::traps::TrapHandlerConfig,

    /// Manages files opened and accessible via redirection operators.
    open_files: openfiles::OpenFiles,

    /// The current working directory.
    working_dir: PathBuf,

    /// The shell environment, containing shell variables.
    env: ShellEnvironment,

    /// Shell function definitions.
    funcs: functions::FunctionEnv,

    /// Runtime shell options.
    options: RuntimeOptions,

    /// State of managed jobs.
    /// TODO(serde): Need to warn somehow that jobs cannot be serialized.
    #[cfg_attr(feature = "serde", serde(skip))]
    jobs: jobs::JobManager,

    /// Shell aliases.
    aliases: HashMap<String, String>,

    /// The status of the last completed command.
    last_exit_status: u8,

    /// Tracks changes to `last_exit_status`.
    last_exit_status_change_count: usize,

    /// The status of each of the commands in the last pipeline.
    last_pipeline_statuses: Vec<u8>,

    /// Clone depth from the original ancestor shell.
    depth: usize,

    /// Shell name
    name: Option<String>,

    /// Positional shell arguments (not including shell name).
    args: Vec<String>,

    /// Shell version
    version: Option<String>,

    /// Detailed display string for the shell
    product_display_str: Option<String>,

    /// Function/script call stack.
    call_stack: crate::callstack::CallStack,

    /// Directory stack used by pushd et al.
    directory_stack: Vec<PathBuf>,

    /// Completion configuration.
    completion_config: crate::completion::Config,

    /// Shell built-in commands.
    #[cfg_attr(feature = "serde", serde(skip))]
    builtins: HashMap<String, builtins::Registration<SE>>,

    /// Shell program location cache.
    program_location_cache: pathcache::PathCache,

    /// Last "SECONDS" captured time.
    last_stopwatch_time: std::time::SystemTime,

    /// Last "SECONDS" offset requested.
    last_stopwatch_offset: u32,

    /// Parser implementation to use.
    #[cfg_attr(feature = "serde", serde(skip))]
    parser_impl: crate::parser::ParserImpl,

    /// Key bindings for the shell, optionally implemented by an interactive shell.
    #[cfg_attr(feature = "serde", serde(skip))]
    key_bindings: Option<KeyBindingsHelper>,

    /// History of commands executed in the shell.
    history: Option<crate::history::History>,
}

impl<SE: extensions::ShellExtensions> Clone for Shell<SE> {
    fn clone(&self) -> Self {
        Self {
            error_formatter: self.error_formatter.clone(),
            traps: self.traps.clone(),
            open_files: self.open_files.clone(),
            working_dir: self.working_dir.clone(),
            env: self.env.clone(),
            funcs: self.funcs.clone(),
            options: self.options.clone(),
            jobs: jobs::JobManager::new(),
            aliases: self.aliases.clone(),
            last_exit_status: self.last_exit_status,
            last_exit_status_change_count: self.last_exit_status_change_count,
            last_pipeline_statuses: self.last_pipeline_statuses.clone(),
            name: self.name.clone(),
            args: self.args.clone(),
            version: self.version.clone(),
            product_display_str: self.product_display_str.clone(),
            call_stack: {
                // Subshells must not inherit the parent's "currently handling signal X"
                // state; otherwise a trap handler that spawns a subshell would see itself
                // as already inside that handler and skip re-entrant delivery.
                let mut cs = self.call_stack.clone();
                cs.clear_active_trap_signals();
                cs
            },
            directory_stack: self.directory_stack.clone(),
            completion_config: self.completion_config.clone(),
            builtins: self.builtins.clone(),
            program_location_cache: self.program_location_cache.clone(),
            last_stopwatch_time: self.last_stopwatch_time,
            last_stopwatch_offset: self.last_stopwatch_offset,
            parser_impl: self.parser_impl,
            key_bindings: self.key_bindings.clone(),
            history: self.history.clone(),
            depth: self.depth + 1,
        }
    }
}

impl<SE: extensions::ShellExtensions> AsRef<Self> for Shell<SE> {
    fn as_ref(&self) -> &Self {
        self
    }
}

impl<SE: extensions::ShellExtensions> AsMut<Self> for Shell<SE> {
    fn as_mut(&mut self) -> &mut Self {
        self
    }
}

impl<SE: extensions::ShellExtensions> Shell<SE> {
    /// Returns a new shell instance created with the given options.
    /// Does *not* load any configuration files (e.g., bashrc).
    ///
    /// # Arguments
    ///
    /// * `options` - The options to use when creating the shell.
    pub(crate) fn new(options: CreateOptions<SE>) -> Result<Self, error::Error> {
        // Compute runtime options before moving fields out of `options`.
        let runtime_options = RuntimeOptions::defaults_from(&options);

        // Instantiate the shell with some defaults.
        let mut shell = Self {
            error_formatter: options.error_formatter,
            open_files: openfiles::OpenFiles::new(),
            options: runtime_options,
            name: options.shell_name,
            args: options.shell_args.unwrap_or_default(),
            version: options.shell_version,
            product_display_str: options.shell_product_display_str,
            working_dir: options.working_dir.map_or_else(std::env::current_dir, Ok)?,
            builtins: options.builtins,
            parser_impl: options.parser,
            key_bindings: options.key_bindings,
            ..Self::default()
        };

        // Add in any open files provided.
        shell.open_files.update_from(options.fds.into_iter());

        // TODO(patterns): Without this a script that sets extglob will fail because we
        // parse the entire script with the same settings.
        shell.options.extended_globbing = true;

        // If requested, seed parameters from environment.
        if !options.do_not_inherit_env {
            wellknownvars::inherit_env_vars(&mut shell)?;
        }

        // If requested, set well-known variables.
        if !options.skip_well_known_vars {
            wellknownvars::init_well_known_vars(&mut shell)?;
        }

        // Set any provided variables.
        for (var_name, var_value) in options.vars {
            shell.env.set_global(var_name, var_value)?;
        }

        // Set up history, if relevant. Do NOT fail if we can't load history.
        if shell.options.enable_command_history {
            shell.history = shell
                .load_history()
                .unwrap_or_default()
                .or_else(|| Some(crate::history::History::default()));
        }

        Ok(shell)
    }
}

impl<SE: extensions::ShellExtensions> Shell<SE> {
    /// Increments the interactive line offset in the shell by the indicated number
    /// of lines.
    ///
    /// # Arguments
    ///
    /// * `delta` - The number of lines to increment the current line offset by.
    pub fn increment_interactive_line_offset(&mut self, delta: usize) {
        self.call_stack.increment_current_line_offset(delta);
    }

    /// Updates the currently executing command in the shell.
    pub fn set_current_cmd(&mut self, cmd: &impl brush_parser::ast::Node) {
        self.call_stack
            .set_current_pos(cmd.location().map(|span| span.start));
    }

    /// Updates the `$_` shell variable (last-argument of the previous simple
    /// command).
    ///
    /// Passes `Some(last_arg)` to record the last argument of the just-executed
    /// command, or `None` to clear `$_` (used for assignment-only statements,
    /// which bash treats as having no "last argument").
    ///
    /// The update is applied in-place so that attributes on `_` (notably
    /// `readonly`) are preserved: attempting to update a readonly `_` is a
    /// silent no-op, matching bash's observable stdout behavior.
    pub(crate) fn update_last_arg_variable(&mut self, last_arg: Option<String>) {
        // Bash refuses to update a readonly `_`, emitting an error to stderr
        // on each attempt. We silently skip the update here — the observable
        // stdout effect ($_ stays unchanged) matches bash; the missing stderr
        // diagnostics are harmless.
        if self
            .env
            .get_using_policy("_", crate::env::EnvironmentLookup::Anywhere)
            .is_some_and(|v| v.is_readonly())
        {
            return;
        }

        // Replace the variable entirely (fresh, non-exported). This matches
        // bash, which never exports `_` — even under `set -a` — and always
        // clears any previously-set attributes (except readonly, handled
        // above).
        let value = last_arg.unwrap_or_default();
        let _ = self
            .env
            .set_global("_", crate::variables::ShellVariable::new(value));
    }

    /// Applies errexit semantics to a result if enabled and appropriate.
    /// This should be called at "statement boundaries" where errexit should be checked.
    ///
    /// # Arguments
    ///
    /// * `result` - The execution result to potentially modify.
    pub const fn apply_errexit_if_enabled(&self, result: &mut ExecutionResult) {
        if self.options.exit_on_nonzero_command_exit
            && !result.is_success()
            && result.is_normal_flow()
        {
            result.next_control_flow = ExecutionControlFlow::ExitShell;
        }
    }

    /// Returns the keywords that are reserved by the shell.
    pub(crate) fn get_keywords(&self) -> Vec<&str> {
        if self.options.sh_mode {
            keywords::SH_MODE_KEYWORDS.iter().copied().collect()
        } else {
            keywords::KEYWORDS.iter().copied().collect()
        }
    }

    /// Checks if the given string is a keyword reserved in this shell.
    ///
    /// # Arguments
    ///
    /// * `s` - The string to check.
    pub fn is_keyword(&self, s: &str) -> bool {
        if self.options.sh_mode {
            keywords::SH_MODE_KEYWORDS.contains(s)
        } else {
            keywords::KEYWORDS.contains(s)
        }
    }

    pub(crate) const fn last_exit_status_change_count(&self) -> usize {
        self.last_exit_status_change_count
    }
}

#[inherent::inherent]
impl<SE: extensions::ShellExtensions> ShellState for Shell<SE> {
    /// Returns whether or not this shell is a subshell.
    pub fn is_subshell(&self) -> bool {
        self.depth > 0
    }

    /// Returns the last "SECONDS" captured time.
    pub fn last_stopwatch_time(&self) -> std::time::SystemTime {
        self.last_stopwatch_time
    }

    /// Returns the last "SECONDS" offset requested.
    pub fn last_stopwatch_offset(&self) -> u32 {
        self.last_stopwatch_offset
    }

    /// Returns the shell environment containing variables.
    pub fn env(&self) -> &ShellEnvironment {
        &self.env
    }

    /// Returns a mutable reference to the shell environment.
    pub fn env_mut(&mut self) -> &mut ShellEnvironment {
        &mut self.env
    }

    /// Returns the shell's runtime options.
    pub fn options(&self) -> &RuntimeOptions {
        &self.options
    }

    /// Returns a mutable reference to the shell's runtime options.
    pub fn options_mut(&mut self) -> &mut RuntimeOptions {
        &mut self.options
    }

    /// Returns the shell's aliases.
    pub fn aliases(&self) -> &HashMap<String, String> {
        &self.aliases
    }

    /// Returns a mutable reference to the shell's aliases.
    pub fn aliases_mut(&mut self) -> &mut HashMap<String, String> {
        &mut self.aliases
    }

    /// Returns the shell's job manager.
    pub fn jobs(&self) -> &jobs::JobManager {
        &self.jobs
    }

    /// Returns a mutable reference to the shell's job manager.
    pub fn jobs_mut(&mut self) -> &mut jobs::JobManager {
        &mut self.jobs
    }

    /// Returns the shell's trap handler configuration.
    pub fn traps(&self) -> &crate::traps::TrapHandlerConfig {
        &self.traps
    }

    /// Returns a mutable reference to the shell's trap handler configuration.
    pub fn traps_mut(&mut self) -> &mut crate::traps::TrapHandlerConfig {
        &mut self.traps
    }

    /// Returns the shell's directory stack.
    pub fn directory_stack(&self) -> &[PathBuf] {
        &self.directory_stack
    }

    /// Returns a mutable reference to the shell's directory stack.
    pub fn directory_stack_mut(&mut self) -> &mut Vec<PathBuf> {
        &mut self.directory_stack
    }

    /// Returns the statuses of commands in the last pipeline.
    pub fn last_pipeline_statuses(&self) -> &[u8] {
        &self.last_pipeline_statuses
    }

    /// Returns a mutable reference to the statuses of commands in the last pipeline.
    pub fn last_pipeline_statuses_mut(&mut self) -> &mut Vec<u8> {
        &mut self.last_pipeline_statuses
    }

    /// Returns the shell's program location cache.
    pub fn program_location_cache(&self) -> &pathcache::PathCache {
        &self.program_location_cache
    }

    /// Returns a mutable reference to the shell's program location cache.
    pub fn program_location_cache_mut(&mut self) -> &mut pathcache::PathCache {
        &mut self.program_location_cache
    }

    /// Returns the shell's completion configuration.
    pub fn completion_config(&self) -> &crate::completion::Config {
        &self.completion_config
    }

    /// Returns a mutable reference to the shell's completion configuration.
    pub fn completion_config_mut(&mut self) -> &mut crate::completion::Config {
        &mut self.completion_config
    }

    /// Returns the shell's open files.
    pub fn open_files(&self) -> &openfiles::OpenFiles {
        &self.open_files
    }

    /// Returns a mutable reference to the shell's open files.
    pub fn open_files_mut(&mut self) -> &mut openfiles::OpenFiles {
        &mut self.open_files
    }

    /// Returns the *current* name of the shell ($0).
    /// Influenced by the current call stack.
    pub fn current_shell_name(&self) -> Option<Cow<'_, str>> {
        for frame in self.call_stack.iter() {
            // Executed scripts shadow the shell name.
            if frame.frame_type.is_run_script() {
                return Some(frame.frame_type.name());
            }
        }

        self.name.as_deref().map(|name| name.into())
    }

    /// Returns the current subshell depth; 0 is returned if this shell is not a subshell.
    pub fn depth(&self) -> usize {
        self.depth
    }

    /// Returns the call stack for the shell.
    pub fn call_stack(&self) -> &crate::callstack::CallStack {
        &self.call_stack
    }

    /// Returns the shell's history, if it exists.
    pub fn history(&self) -> Option<&crate::history::History> {
        self.history.as_ref()
    }

    /// Returns a mutable reference to the shell's history, if it exists.
    pub fn history_mut(&mut self) -> Option<&mut crate::history::History> {
        self.history.as_mut()
    }

    /// Returns the shell's official version string (if available).
    pub fn version(&self) -> Option<&str> {
        self.version.as_deref()
    }

    /// Returns the exit status of the last command executed in this shell.
    pub fn last_exit_status(&self) -> u8 {
        self.last_exit_status
    }

    /// Updates the last exit status.
    pub fn set_last_exit_status(&mut self, status: u8) {
        self.last_exit_status = status;
        self.last_exit_status_change_count += 1;
    }

    /// Returns the key bindings helper for the shell.
    pub fn key_bindings(&self) -> Option<&KeyBindingsHelper> {
        self.key_bindings.as_ref()
    }

    /// Sets the key bindings helper for the shell.
    pub fn set_key_bindings(&mut self, key_bindings: Option<KeyBindingsHelper>) {
        self.key_bindings = key_bindings;
    }

    /// Returns the shell's current working directory.
    pub fn working_dir(&self) -> &Path {
        &self.working_dir
    }

    /// Returns a mutable reference to the shell's current working directory.
    /// This is only accessible within the crate.
    pub(crate) fn working_dir_mut(&mut self) -> &mut PathBuf {
        &mut self.working_dir
    }

    /// Returns the product display name for this shell.
    pub fn product_display_str(&self) -> Option<&str> {
        self.product_display_str.as_deref()
    }
}

#[cfg(feature = "serde")]
fn default_error_formatter<EF: extensions::ErrorFormatter>() -> EF {
    EF::default()
}