Skip to main content

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}