yash_semantics/
xtrace.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2022 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Helper items for printing expansion results
18//!
19//! When the `xtrace` [shell option](yash_env::option) is on, the shell traces
20//! words expanded during command execution. For each command executed, the
21//! shell prints to the standard error a line containing the following:
22//!
23//! - An expansion of `$PS4`
24//! - Expanded command words (assignments, command name, and arguments)
25//! - Expanded redirections, possibly followed by here-document contents
26//!
27//! The trace of a command is printed at a time even though many separate steps
28//! perform expansions during the execution of the command. [`XTrace`] is a
29//! collection of string buffers that accumulates the results of expansions
30//! until they are printed to the standard error.
31//!
32//! It is no use to collect the expansions when the `xtrace` option is off, so
33//! you should create an `XTrace` only if the option is on.
34//! [`XTrace::from_options`] is a convenient method to do so.
35
36use crate::Handle;
37use crate::expansion::expand_text;
38use std::fmt::Write;
39use yash_env::Env;
40use yash_env::option::OptionSet;
41use yash_env::option::State;
42use yash_env::semantics::Field;
43use yash_env::variable::PS4;
44use yash_quote::quoted;
45use yash_syntax::syntax::Text;
46
47async fn expand_ps4(env: &mut Env) -> String {
48    let value = env.variables.get_scalar(PS4).unwrap_or_default().to_owned();
49
50    let text = match value.parse::<Text>() {
51        Ok(text) => text,
52        Err(error) => {
53            _ = error.handle(env).await;
54            return value;
55        }
56    };
57
58    match expand_text(env, &text).await {
59        Ok((expansion, _exit_status)) => expansion,
60        Err(error) => {
61            _ = error.handle(env).await;
62            value
63        }
64    }
65}
66
67/// Collection of temporary string buffers that accumulate expanded strings
68///
69/// See the [module documentation](self) for details.
70///
71/// An `XTrace` contains four string buffers that accumulate each of the
72/// following:
73///
74/// - Command words (command name and arguments)
75/// - Assignments
76/// - Redirections
77/// - Here-document contents
78///
79/// The [`finish`](Self::finish) function creates the final string to be
80/// printed.
81#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
82pub struct XTrace {
83    words: String,
84    assigns: String,
85    redirs: String,
86    here_doc_contents: String,
87}
88
89impl XTrace {
90    /// Creates a new trace buffer.
91    #[inline]
92    #[must_use]
93    pub fn new() -> Self {
94        Self::default()
95    }
96
97    /// Creates a new trace buffer if the `xtrace` option is on.
98    ///
99    /// If the option is off, this function returns `None`.
100    #[must_use]
101    pub fn from_options(options: &OptionSet) -> Option<Self> {
102        match options.get(yash_env::option::Option::XTrace) {
103            State::On => Some(Self::new()),
104            State::Off => None,
105        }
106    }
107
108    /// Returns a reference to the words buffer.
109    ///
110    /// The words buffer is for tracing command words.
111    /// When writing to the buffer, the content should end with a space.
112    #[inline]
113    #[must_use]
114    pub fn words(&mut self) -> &mut (impl Write + use<>) {
115        &mut self.words
116    }
117
118    /// Returns a reference to the assignments buffer.
119    ///
120    /// The assignments buffer is for tracing assignments.
121    /// When writing to the buffer, the content should end with a space.
122    #[inline]
123    #[must_use]
124    pub fn assigns(&mut self) -> &mut (impl Write + use<>) {
125        &mut self.assigns
126    }
127
128    /// Returns a reference to the redirections buffer.
129    ///
130    /// The redirections buffer is for tracing redirections.
131    /// When writing to the buffer, the content should end with a space.
132    ///
133    /// You should not write the contents of here-documents to this buffer.
134    /// See also [`here_doc_contents`](Self::here_doc_contents).
135    #[inline]
136    #[must_use]
137    pub fn redirs(&mut self) -> &mut (impl Write + use<>) {
138        &mut self.redirs
139    }
140
141    /// Returns a reference to the here-document contents buffer.
142    ///
143    /// You should write the contents of here-documents you wrote to the
144    /// [redirections buffer](Self::redirs()).
145    #[inline]
146    #[must_use]
147    pub fn here_doc_contents(&mut self) -> &mut (impl Write + use<>) {
148        &mut self.here_doc_contents
149    }
150
151    /// Clears the buffer contents.
152    pub fn clear(&mut self) {
153        self.words.clear();
154        self.assigns.clear();
155        self.redirs.clear();
156        self.here_doc_contents.clear();
157    }
158
159    /// Returns whether all the buffers are empty.
160    #[must_use]
161    pub fn is_empty(&self) -> bool {
162        self.words.is_empty()
163            && self.assigns.is_empty()
164            && self.redirs.is_empty()
165            && self.here_doc_contents.is_empty()
166    }
167
168    /// Constructs the final trace to be printed to stderr.
169    ///
170    /// If all the buffers are empty, the result is empty. Otherwise, the result
171    /// is the concatenation of the following:
172    ///
173    /// - The expansion of `$PS4`
174    /// - The concatenation of the `assigns`, `words`, and `redirs` buffers with
175    ///   trailing spaces trimmed and a newline appended
176    /// - The `here_doc_contents` buffer
177    ///
178    /// If `$PS4` fails to expand, this function prints an error message and
179    /// uses the variable value intact.
180    pub async fn finish(&self, env: &mut Env) -> String {
181        let len = self.assigns.len()
182            + self.words.len()
183            + self.redirs.len()
184            + self.here_doc_contents.len();
185        if len == 0 {
186            return String::new();
187        }
188
189        // TODO Support $YASH_PS4 and $YASH_PS4S
190        let mut result = expand_ps4(env).await;
191        let ps4_len = result.len();
192        result.reserve_exact(len);
193        result += &self.assigns;
194        result += &self.words;
195        result += &self.redirs;
196        result.truncate(ps4_len + result[ps4_len..].trim_end_matches(' ').len());
197        result.push('\n');
198        result += &self.here_doc_contents;
199        result
200    }
201}
202
203/// Convenience function for tracing fields.
204///
205/// This function writes the field values to the words buffer of the `XTrace`.
206pub fn trace_fields(xtrace: Option<&mut XTrace>, fields: &[Field]) {
207    if let Some(xtrace) = xtrace {
208        for field in fields {
209            write!(xtrace.words(), "{} ", quoted(&field.value)).unwrap();
210        }
211    }
212}
213
214/// Convenience function for calling [`XTrace::finish`] on an optional `XTrace`.
215pub async fn finish(env: &mut Env, xtrace: Option<XTrace>) -> String {
216    if let Some(xtrace) = xtrace {
217        xtrace.finish(env).await
218    } else {
219        String::new()
220    }
221}
222
223/// Convenience function for [finish]ing and
224/// [print](yash_env::SharedSystem::print_error)ing an (optional) `XTrace`.
225pub async fn print<X: Into<Option<XTrace>>>(env: &mut Env, xtrace: X) {
226    async fn inner(env: &mut Env, xtrace: Option<XTrace>) {
227        let s = finish(env, xtrace).await;
228        env.system.print_error(&s).await;
229    }
230    inner(env, xtrace.into()).await
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use futures_util::FutureExt;
237    use yash_env::variable::Scope::Global;
238
239    #[test]
240    fn tracing_some_fields() {
241        let mut xtrace = XTrace::new();
242        let fields = Field::dummies(["one", "two", "'three'"]);
243        trace_fields(Some(&mut xtrace), &fields);
244        assert_eq!(xtrace.words, r#"one two "'three'" "#);
245        assert_eq!(xtrace.assigns, "");
246        assert_eq!(xtrace.redirs, "");
247        assert_eq!(xtrace.here_doc_contents, "");
248    }
249
250    fn fixture() -> Env {
251        let mut env = Env::new_virtual();
252        env.variables
253            .get_or_new(PS4, Global)
254            .assign("+${X=x}+ ", None)
255            .unwrap();
256        env
257    }
258
259    #[test]
260    fn empty_finish() {
261        let mut env = fixture();
262        let result = XTrace::new().finish(&mut env).now_or_never().unwrap();
263        assert_eq!(result, "");
264
265        // $PS4 should not have been expanded
266        assert_eq!(env.variables.get("X"), None);
267    }
268
269    #[test]
270    fn finish_with_assigns() {
271        let mut env = fixture();
272
273        let mut xtrace = XTrace::new();
274        xtrace.assigns.push_str("VAR=VALUE FOO=BAR ");
275        let result = xtrace.finish(&mut env).now_or_never().unwrap();
276        assert_eq!(result, "+x+ VAR=VALUE FOO=BAR\n");
277
278        // $PS4 should have been expanded
279        assert_ne!(env.variables.get("X"), None);
280    }
281
282    #[test]
283    fn finish_with_words() {
284        let mut env = fixture();
285
286        let mut xtrace = XTrace::new();
287        xtrace.words.push_str("abc '~' foo ");
288        let result = xtrace.finish(&mut env).now_or_never().unwrap();
289        assert_eq!(result, "+x+ abc '~' foo\n");
290
291        // $PS4 should have been expanded
292        assert_ne!(env.variables.get("X"), None);
293    }
294
295    #[test]
296    fn finish_with_redirs() {
297        let mut env = fixture();
298
299        let mut xtrace = XTrace::new();
300        xtrace.redirs.push_str("0< /dev/null 1> foo/bar ");
301        let result = xtrace.finish(&mut env).now_or_never().unwrap();
302        assert_eq!(result, "+x+ 0< /dev/null 1> foo/bar\n");
303    }
304
305    #[test]
306    fn finish_with_assigns_and_words() {
307        let mut env = fixture();
308
309        let mut xtrace = XTrace::new();
310        xtrace.assigns.push_str("VAR=VALUE ");
311        xtrace.words.push_str("echo argument ");
312        let result = xtrace.finish(&mut env).now_or_never().unwrap();
313        assert_eq!(result, "+x+ VAR=VALUE echo argument\n");
314
315        let mut xtrace = XTrace::new();
316        xtrace.assigns.push(' ');
317        xtrace.words.push(' ');
318        let result = xtrace.finish(&mut env).now_or_never().unwrap();
319        assert_eq!(result, "+x+ \n");
320    }
321
322    #[test]
323    fn finish_with_words_and_redirs() {
324        let mut env = fixture();
325
326        let mut xtrace = XTrace::new();
327        xtrace.words.push_str("echo argument ");
328        xtrace.redirs.push_str("2> errors ");
329        let result = xtrace.finish(&mut env).now_or_never().unwrap();
330        assert_eq!(result, "+x+ echo argument 2> errors\n");
331
332        let mut xtrace = XTrace::new();
333        xtrace.words.push(' ');
334        xtrace.redirs.push(' ');
335        let result = xtrace.finish(&mut env).now_or_never().unwrap();
336        assert_eq!(result, "+x+ \n");
337    }
338
339    #[test]
340    fn finish_with_here_doc_contents() {
341        let mut env = fixture();
342
343        let mut xtrace = XTrace::new();
344        xtrace.here_doc_contents.push_str("EOF\n");
345        let result = xtrace.finish(&mut env).now_or_never().unwrap();
346        assert_eq!(result, "+x+ \nEOF\n");
347    }
348
349    #[test]
350    fn finish_with_redirs_and_here_doc_contents() {
351        let mut env = fixture();
352
353        let mut xtrace = XTrace::new();
354        xtrace.redirs.push_str("0<< END ");
355        xtrace.here_doc_contents.push_str(" X \nEND\n");
356        let result = xtrace.finish(&mut env).now_or_never().unwrap();
357        assert_eq!(result, "+x+ 0<< END\n X \nEND\n");
358    }
359}