Skip to main content

shrub_core/shell/
snapshot.rs

1//! Taking a snapshot of a shell's state, and applying one to another shell.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::path::Path;
5
6use crate::env::EnvironmentLookup;
7use crate::namedoptions::{self, ShellOptionKind};
8use crate::openfiles::OpenFiles;
9use crate::snapshot::{
10    NotKept, NotKeptKind, RestoreFailureReason, RestoreReport, SavedFunction, SavedValue,
11    SavedVariable, ShellSnapshot, SnapshotOptions, StateKind,
12};
13use crate::traps::TrapSignal;
14use crate::variables::{ShellValue, ShellVariable, ShellVariableUpdateTransform};
15use crate::{SourceInfo, error, extensions, wellknownvars};
16
17/// `shopt` options that record how the shell was started, which bash refuses
18/// to change; a snapshot neither copies nor restores them.
19const STARTUP_SHOPT_OPTIONS: &[&str] = &["login_shell", "restricted_shell"];
20
21/// Variables a snapshot never copies besides
22/// [`wellknownvars::PROCESS_VARIABLES`]: the working directory is restored on
23/// its own, `OLDPWD` is not carried, and `_` belongs to the last command.
24const UNCOPIED_VARIABLES: &[&str] = &["PWD", "OLDPWD", "_"];
25
26impl<SE: extensions::ShellExtensions> crate::Shell<SE> {
27    /// Copies the state a command can change, for
28    /// [`Shell::restore_snapshot`](Self::restore_snapshot) to apply to a fresh
29    /// shell, in this process or another. See [`crate::snapshot`] for what a
30    /// snapshot holds and what it cannot carry.
31    ///
32    /// # Arguments
33    ///
34    /// * `options` - The baseline values and names to leave out.
35    ///
36    /// # Errors
37    ///
38    /// Returns [`error::ErrorKind::NotBetweenCommands`] if a function or
39    /// command scope is open.
40    pub fn snapshot(&self, options: &SnapshotOptions) -> Result<ShellSnapshot, error::Error> {
41        self.check_between_commands()?;
42
43        let mut snapshot = ShellSnapshot {
44            working_dir: self.working_dir.clone(),
45            directory_stack: self.directory_stack.clone(),
46            last_exit_status: self.last_exit_status,
47            positional_parameters: self.current_shell_args().to_vec(),
48            ..ShellSnapshot::default()
49        };
50
51        for (name, var) in self.env.iter_using_policy(EnvironmentLookup::OnlyInGlobal) {
52            if is_uncopied(name, options) || !var.is_enumerable() {
53                continue;
54            }
55            if let Some(saved) = saved_variable(var)
56                && !holds_baseline(&saved, options.baseline.get(name))
57            {
58                snapshot.variables.insert(name.clone(), saved);
59            }
60        }
61
62        for name in options.baseline.keys() {
63            if !is_uncopied(name, options)
64                && self
65                    .env
66                    .get_using_policy(name, EnvironmentLookup::OnlyInGlobal)
67                    .is_none()
68            {
69                snapshot.unset_variables.insert(name.clone());
70            }
71        }
72
73        for (name, registration) in self.funcs.iter() {
74            let body = std::format!("() {}", registration.definition().body);
75            if self.parses_back(&body) {
76                snapshot.functions.insert(
77                    name.clone(),
78                    SavedFunction {
79                        body,
80                        exported: registration.is_exported(),
81                    },
82                );
83            } else {
84                snapshot.not_kept.push(NotKept {
85                    kind: NotKeptKind::Function,
86                    name: name.clone(),
87                });
88            }
89        }
90
91        snapshot.aliases = self
92            .aliases
93            .iter()
94            .map(|(name, value)| (name.clone(), value.clone()))
95            .collect();
96
97        snapshot.traps = self
98            .traps
99            .iter_handlers()
100            .map(|(signal, handler)| (signal.as_str().to_owned(), handler.command.clone()))
101            .collect();
102
103        snapshot.set_options = self.option_values(ShellOptionKind::SetO);
104        snapshot.shopt_options = self.option_values(ShellOptionKind::Shopt);
105
106        snapshot.disabled_builtins = self
107            .builtins
108            .iter()
109            .filter(|(_, registration)| registration.disabled)
110            .map(|(name, _)| name.clone())
111            .collect();
112
113        for job in &self.jobs.jobs {
114            snapshot.not_kept.push(NotKept {
115                kind: NotKeptKind::Job,
116                name: std::format!("%{} {}", job.id, job.command_name()),
117            });
118        }
119
120        let mut descriptors: Vec<_> = self
121            .open_files
122            .iter_fds()
123            .map(|(fd, _)| fd)
124            .filter(|fd| *fd > OpenFiles::STDERR_FD)
125            .collect();
126        descriptors.sort_unstable();
127        for fd in descriptors {
128            snapshot.not_kept.push(NotKept {
129                kind: NotKeptKind::OpenFile,
130                name: fd.to_string(),
131            });
132        }
133
134        Ok(snapshot)
135    }
136
137    /// Applies a snapshot that [`Shell::snapshot`](Self::snapshot) took, in
138    /// this process or another, to this shell, through the same API the
139    /// builtins use: no part of it runs as a command. Functions, aliases,
140    /// traps and disabled builtins end up exactly as the snapshot has them;
141    /// variables the snapshot does not name keep their values here.
142    ///
143    /// Each item is applied on its own. One that fails is reported in the
144    /// returned [`RestoreReport`], by name and never by value, and the rest
145    /// are still applied. The working directory is left unchanged if the
146    /// snapshot's is gone.
147    ///
148    /// # Arguments
149    ///
150    /// * `snapshot` - The snapshot to apply.
151    ///
152    /// # Errors
153    ///
154    /// Returns [`error::ErrorKind::NotBetweenCommands`] if a function or
155    /// command scope is open; nothing is applied then.
156    pub fn restore_snapshot(
157        &mut self,
158        snapshot: ShellSnapshot,
159    ) -> Result<RestoreReport, error::Error> {
160        self.check_between_commands()?;
161
162        let mut report = RestoreReport::default();
163        self.restore_working_dir(&snapshot.working_dir, &mut report)?;
164        self.directory_stack = snapshot.directory_stack;
165        self.restore_variables(&snapshot.unset_variables, snapshot.variables, &mut report)?;
166        // Options first: `posix` and `extglob` change how a function parses,
167        // and the snapshot checked each function under its own shell's options.
168        self.restore_options(ShellOptionKind::SetO, snapshot.set_options, &mut report);
169        self.restore_options(ShellOptionKind::Shopt, snapshot.shopt_options, &mut report);
170        self.restore_functions(snapshot.functions, &mut report);
171        self.aliases = snapshot.aliases.into_iter().collect();
172        self.restore_traps(snapshot.traps, &mut report);
173        self.restore_disabled_builtins(&snapshot.disabled_builtins, &mut report);
174        *self.current_shell_args_mut() = snapshot.positional_parameters;
175        self.set_last_exit_status(snapshot.last_exit_status);
176
177        Ok(report)
178    }
179
180    /// Moves to `dir`, if it is still a directory, and keeps this shell's
181    /// `OLDPWD`, which a snapshot does not carry.
182    fn restore_working_dir(
183        &mut self,
184        dir: &Path,
185        report: &mut RestoreReport,
186    ) -> Result<(), error::Error> {
187        let oldpwd = self
188            .env
189            .get_using_policy("OLDPWD", EnvironmentLookup::OnlyInGlobal)
190            .cloned();
191        if let Err(error) = self.set_working_dir(dir) {
192            let reason = match error.kind() {
193                error::ErrorKind::ReadonlyVariable => RestoreFailureReason::Readonly,
194                _ => RestoreFailureReason::NotADirectory,
195            };
196            report.fail(StateKind::WorkingDir, &dir.to_string_lossy(), reason);
197        }
198        match oldpwd {
199            Some(var) => self.env.set_global("OLDPWD", var)?,
200            None => _ = self.env.unset("OLDPWD"),
201        }
202        Ok(())
203    }
204
205    /// Unsets `unset`, then sets `variables`, leaving a readonly variable that
206    /// already holds the same value as it is.
207    fn restore_variables(
208        &mut self,
209        unset: &BTreeSet<String>,
210        variables: BTreeMap<String, SavedVariable>,
211        report: &mut RestoreReport,
212    ) -> Result<(), error::Error> {
213        for name in unset {
214            if self.env.unset(name).is_err() {
215                report.fail(
216                    StateKind::UnsetVariable,
217                    name,
218                    RestoreFailureReason::Readonly,
219                );
220            }
221        }
222
223        for (name, saved) in variables {
224            if let Some(existing) = self
225                .env
226                .get_using_policy(&name, EnvironmentLookup::OnlyInGlobal)
227                && existing.is_readonly()
228            {
229                if saved_variable(existing).as_ref() != Some(&saved) {
230                    report.fail(StateKind::Variable, &name, RestoreFailureReason::Readonly);
231                }
232                continue;
233            }
234            self.env.set_global(name, shell_variable(saved))?;
235        }
236        Ok(())
237    }
238
239    /// Replaces every function with `functions`.
240    fn restore_functions(
241        &mut self,
242        functions: BTreeMap<String, SavedFunction>,
243        report: &mut RestoreReport,
244    ) {
245        self.funcs.clear();
246        for (name, saved) in functions {
247            if self
248                .define_func_from_str(name.as_str(), &saved.body)
249                .is_err()
250            {
251                report.fail(
252                    StateKind::Function,
253                    &name,
254                    RestoreFailureReason::DoesNotParse,
255                );
256            } else if saved.exported
257                && let Some(registration) = self.funcs.get_mut(&name)
258            {
259                registration.export();
260            }
261        }
262    }
263
264    /// Replaces every trap handler with `traps`.
265    fn restore_traps(&mut self, traps: BTreeMap<String, String>, report: &mut RestoreReport) {
266        self.traps = crate::traps::TrapHandlerConfig::default();
267        for (signal, command) in traps {
268            match TrapSignal::try_from(signal.as_str()) {
269                Ok(signal) => {
270                    self.traps
271                        .register_handler(signal, command, SourceInfo::default());
272                }
273                Err(_) => report.fail(
274                    StateKind::Trap,
275                    &signal,
276                    RestoreFailureReason::UnknownSignal,
277                ),
278            }
279        }
280    }
281
282    /// Sets each option of `kind` in `values`.
283    fn restore_options(
284        &mut self,
285        kind: ShellOptionKind,
286        values: BTreeMap<String, bool>,
287        report: &mut RestoreReport,
288    ) {
289        let state_kind = match kind {
290            ShellOptionKind::Shopt => StateKind::ShoptOption,
291            ShellOptionKind::Set | ShellOptionKind::SetO => StateKind::SetOption,
292        };
293        let definitions = namedoptions::options(kind);
294        for (name, value) in values {
295            if STARTUP_SHOPT_OPTIONS.contains(&name.as_str()) {
296                continue;
297            }
298            match definitions.get(&name) {
299                Some(definition) => definition.set(&mut self.options, value),
300                None => report.fail(state_kind, &name, RestoreFailureReason::UnknownOption),
301            }
302        }
303    }
304
305    /// Turns off exactly the builtins in `names`.
306    fn restore_disabled_builtins(&mut self, names: &BTreeSet<String>, report: &mut RestoreReport) {
307        for registration in self.builtins.values_mut() {
308            registration.disabled = false;
309        }
310        for name in names {
311            match self.builtins.get_mut(name) {
312                Some(registration) => registration.disabled = true,
313                None => report.fail(
314                    StateKind::DisabledBuiltin,
315                    name,
316                    RestoreFailureReason::UnknownBuiltin,
317                ),
318            }
319        }
320    }
321
322    /// Fails unless only the global scope is open, as it is between commands.
323    fn check_between_commands(&self) -> Result<(), error::Error> {
324        if self.env.scope_depth() == 1 {
325            Ok(())
326        } else {
327            Err(error::ErrorKind::NotBetweenCommands.into())
328        }
329    }
330
331    /// Returns whether `body` parses as a function body that prints back as
332    /// `body`, so that a shell restoring it defines the same function.
333    fn parses_back(&self, body: &str) -> bool {
334        let mut parser = super::parsing::create_parser(body.as_bytes(), &self.parser_options());
335        parser
336            .parse_function_parens_and_body()
337            .is_ok_and(|parsed| std::format!("() {parsed}") == body)
338    }
339
340    /// Returns the value of every option of `kind` a command may change.
341    fn option_values(&self, kind: ShellOptionKind) -> BTreeMap<String, bool> {
342        namedoptions::options(kind)
343            .iter()
344            .filter(|option| !STARTUP_SHOPT_OPTIONS.contains(&option.name))
345            .map(|option| (option.name.to_owned(), option.definition.get(&self.options)))
346            .collect()
347    }
348}
349
350/// Returns whether a snapshot leaves the variable `name` out by its name.
351fn is_uncopied(name: &str, options: &SnapshotOptions) -> bool {
352    UNCOPIED_VARIABLES.contains(&name)
353        || wellknownvars::PROCESS_VARIABLES.contains(&name)
354        || options.exclude.contains(name)
355}
356
357/// Returns whether `saved` is exactly what the embedder's baseline value
358/// gives a new shell: that value, exported, with no other attribute.
359fn holds_baseline(saved: &SavedVariable, baseline: Option<&String>) -> bool {
360    baseline.is_some_and(|baseline| {
361        saved.value == SavedValue::String(baseline.clone())
362            && saved.exported
363            && !saved.readonly
364            && !saved.integer
365            && !saved.nameref
366            && !saved.trace
367            && saved.transform == ShellVariableUpdateTransform::None
368    })
369}
370
371/// Returns the copy of a variable, or `None` for a dynamic one, whose value
372/// the shell computes.
373fn saved_variable(var: &ShellVariable) -> Option<SavedVariable> {
374    let value = match var.value() {
375        ShellValue::Unset(unset_type) => SavedValue::Unset(*unset_type),
376        ShellValue::String(value) => SavedValue::String(value.clone()),
377        ShellValue::IndexedArray(values) => SavedValue::IndexedArray(values.clone()),
378        ShellValue::AssociativeArray(values) => SavedValue::AssociativeArray(values.clone()),
379        ShellValue::Dynamic { .. } => return None,
380    };
381    Some(SavedVariable {
382        value,
383        exported: var.is_exported(),
384        readonly: var.is_readonly(),
385        integer: var.is_treated_as_integer(),
386        nameref: var.is_treated_as_nameref(),
387        trace: var.is_trace_enabled(),
388        transform: var.get_update_transform(),
389    })
390}
391
392/// Returns the variable a copy describes.
393fn shell_variable(saved: SavedVariable) -> ShellVariable {
394    let mut var = ShellVariable::new(match saved.value {
395        SavedValue::Unset(unset_type) => ShellValue::Unset(unset_type),
396        SavedValue::String(value) => ShellValue::String(value),
397        SavedValue::IndexedArray(values) => ShellValue::IndexedArray(values),
398        SavedValue::AssociativeArray(values) => ShellValue::AssociativeArray(values),
399    });
400    if saved.exported {
401        var.export();
402    }
403    if saved.readonly {
404        var.set_readonly();
405    }
406    if saved.integer {
407        var.treat_as_integer();
408    }
409    if saved.nameref {
410        var.treat_as_nameref();
411    }
412    if saved.trace {
413        var.enable_trace();
414    }
415    var.set_update_transform(saved.transform);
416    var
417}