1use 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
17const STARTUP_SHOPT_OPTIONS: &[&str] = &["login_shell", "restricted_shell"];
20
21const UNCOPIED_VARIABLES: &[&str] = &["PWD", "OLDPWD", "_"];
25
26impl<SE: extensions::ShellExtensions> crate::Shell<SE> {
27 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 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 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 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 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 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 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 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 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 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 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 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
350fn 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
357fn 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
371fn 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
392fn 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}