bash_interop/stack/frame.rs
1//! What comes out of a walk: one frame — what it is, where its code came from,
2//! and what it was called with — and the [`Stack`] the frames make.
3
4use std::fmt;
5use std::iter::once;
6use std::path::{Path, PathBuf};
7
8use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
9
10use crate::shell::Shell;
11use bash_strings::emit_q_words;
12
13/// What a frame is, as `FUNCNAME` names it. Two of bash's words are not
14/// function names, and a script that defines a function called `main` or
15/// `source` is indistinguishable from them — bash reports the same word.
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17#[serde(rename_all = "snake_case")]
18pub enum Site {
19 Function(String),
20
21 /// `main`: the top level of the script bash was given.
22 Script,
23
24 /// `source`: the top level of a file the subject sourced.
25 Sourced,
26
27 /// The top level of a shell bash was given no script file for — `bash -c`,
28 /// or a shell fed on standard input. `FUNCNAME` has no entry for it, so it
29 /// is not a word of bash's; where it sits is read off the line the walk was
30 /// entered from.
31 Shell,
32}
33
34impl Site {
35 pub(super) fn of(funcname: &str) -> Self {
36 match funcname {
37 "main" => Self::Script,
38 "source" => Self::Sourced,
39 name => Self::Function(name.to_string()),
40 }
41 }
42}
43
44impl fmt::Display for Site {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 match self {
47 Self::Function(name) => f.write_str(name),
48 Self::Script => f.write_str("main"),
49 Self::Sourced => f.write_str("source"),
50 Self::Shell => f.write_str("shell"),
51 }
52 }
53}
54
55/// Where a frame's code came from, as `BASH_SOURCE` names it. Two of bash's
56/// words are not paths at all.
57#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
58#[serde(rename_all = "snake_case")]
59pub enum Source {
60 /// Absolute, by joining what bash reported onto the walk's own `$PWD`.
61 /// Nothing is resolved: no symlink is followed and no `..` collapsed.
62 ///
63 /// Whether the file is there is [`found`](Source::found) and not this:
64 /// bash keeps a source path as it was written, and a shell that has since
65 /// changed directory leaves a relative one pointing nowhere.
66 File(PathBuf),
67
68 /// `environment`: the function came in through the environment.
69 Environment,
70
71 /// `main`: the function was defined at an interactive prompt.
72 Prompt,
73
74 /// The code bash was given rather than read: a `-c` command line, or
75 /// standard input. Bash writes `$0` here, which is a word and not a path.
76 Shell,
77}
78
79impl Source {
80 /// Read against the shell the walk was taken in, which is the only thing
81 /// that can say what `$0` means here: where bash was handed a file, `$0`
82 /// is that file and reads as the path it is; where bash was given its code
83 /// directly, the same word stands in `BASH_SOURCE` for that code.
84 pub(super) fn of(source: &str, pwd: &Path, shell: &Shell) -> Self {
85 match source {
86 "environment" => Self::Environment,
87 "main" => Self::Prompt,
88 word if word == shell.bash.zero && !shell.bash.invocation.from_a_file() => Self::Shell,
89 // An absolute path replaces the base; a relative one joins it.
90 path => Self::File(pwd.join(path)),
91 }
92 }
93
94 /// The file, if this names one and it is there.
95 pub fn found(&self) -> Option<&Path> {
96 match self {
97 Self::File(path) if path.is_file() => Some(path),
98 _ => None,
99 }
100 }
101
102 /// The path this names but does not have, which is a source the run cannot
103 /// be read against.
104 pub fn missing(&self) -> Option<&Path> {
105 match self {
106 Self::File(path) if !path.is_file() => Some(path),
107 _ => None,
108 }
109 }
110}
111
112impl fmt::Display for Source {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 match self {
115 Self::File(path) => f.write_str(
116 path.file_name()
117 .map_or("?", |name| name.to_str().unwrap_or("?")),
118 ),
119 Self::Environment => f.write_str("environment"),
120 Self::Prompt => f.write_str("main"),
121 Self::Shell => f.write_str("-"),
122 }
123 }
124}
125
126/// One frame: what it is, where its code came from, which line it is
127/// executing, and what it was called with.
128#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
129pub struct Frame {
130 pub site: Site,
131 pub source: Source,
132 pub lineno: u32,
133
134 /// The call's arguments, when the shell was recording them. `None` is
135 /// "not recorded", never "called with none": bash keeps these only under
136 /// `extdebug`.
137 pub args: Option<Vec<String>>,
138}
139
140impl fmt::Display for Frame {
141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142 write!(
143 f,
144 "{}@{}:{}",
145 self.site, self.source, self.lineno
146 )?;
147
148 match &self.args {
149 Some(args) => write!(f, " ({})", emit_q_words(args)),
150 None => Ok(()),
151 }
152 }
153}
154
155/// A walk, innermost first. Never empty: the frame it was taken in is always
156/// one of them, and a walk that reaches no frame is refused where it is read.
157///
158/// One array in JSON, and one field wherever an instrument reports where it
159/// was. Which frame is the call site is [`top`](Stack::top), not a second field
160/// beside the rest.
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct Stack {
163 at: Frame,
164 outer: Vec<Frame>,
165}
166
167impl Stack {
168 /// `None` for no frames at all, which is not a walk.
169 pub fn of(frames: Vec<Frame>) -> Option<Self> {
170 let mut frames = frames.into_iter();
171
172 Some(Self {
173 at: frames.next()?,
174 outer: frames.collect(),
175 })
176 }
177
178 /// The frame the walk was taken in.
179 pub fn top(&self) -> &Frame {
180 &self.at
181 }
182
183 /// The frames above it, outermost last.
184 pub fn below(&self) -> &[Frame] {
185 &self.outer
186 }
187
188 pub fn frames(&self) -> impl Iterator<Item = &Frame> {
189 once(&self.at).chain(&self.outer)
190 }
191}
192
193impl Serialize for Stack {
194 fn serialize<S: Serializer>(&self, into: S) -> Result<S::Ok, S::Error> {
195 into.collect_seq(self.frames())
196 }
197}
198
199impl<'de> Deserialize<'de> for Stack {
200 fn deserialize<D: Deserializer<'de>>(from: D) -> Result<Self, D::Error> {
201 Stack::of(Vec::deserialize(from)?).ok_or_else(|| de::Error::custom("a call stack with no frames"))
202 }
203}