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 std::ops::{Deref, DerefMut};
40use yash_env::Env;
41use yash_env::option::OptionSet;
42use yash_env::option::State;
43use yash_env::semantics::Field;
44use yash_env::variable::PS4;
45use yash_quote::quoted;
46use yash_syntax::syntax::Text;
47
48async fn expand_ps4(env: &mut Env) -> String {
49    let value = env.variables.get_scalar(PS4).unwrap_or_default().to_owned();
50
51    let text = match value.parse::<Text>() {
52        Ok(text) => text,
53        Err(error) => {
54            _ = error.handle(env).await;
55            return value;
56        }
57    };
58
59    match expand_text(env, &text).await {
60        Ok((expansion, _exit_status)) => expansion,
61        Err(error) => {
62            _ = error.handle(env).await;
63            value
64        }
65    }
66}
67
68/// Flag to indicate whether `$PS4` is being expanded
69///
70/// This is used in [`XTrace::finish`] to prevent (possibly infinite) recursion
71/// when `$PS4` contains a command substitution that causes `XTrace::finish` to
72/// be called again.
73///
74/// This flag is stored in [`Env::any`].
75#[derive(Clone, Debug, Default, Eq, PartialEq)]
76struct ExpandingPs4(bool);
77
78/// Guard that sets the [`ExpandingPs4`] flag to true while it is alive
79/// and resets it to false when dropped
80#[derive(Debug)]
81struct ExpandingGuard<'a>(&'a mut Env);
82
83impl Deref for ExpandingGuard<'_> {
84    type Target = Env;
85    fn deref(&self) -> &Self::Target {
86        self.0
87    }
88}
89
90impl DerefMut for ExpandingGuard<'_> {
91    fn deref_mut(&mut self) -> &mut Self::Target {
92        self.0
93    }
94}
95
96impl<'a> ExpandingGuard<'a> {
97    /// Creates a new guard that sets the [`ExpandingPs4`] flag to true.
98    ///
99    /// If the flag is already true, this function returns `None`.
100    #[must_use]
101    fn new(env: &'a mut Env) -> Option<Self> {
102        let expanding_ps4 = env.any.get_or_insert_with(Box::<ExpandingPs4>::default);
103        if expanding_ps4.0 {
104            None
105        } else {
106            expanding_ps4.0 = true;
107            Some(Self(env))
108        }
109    }
110}
111
112impl Drop for ExpandingGuard<'_> {
113    fn drop(&mut self) {
114        if let Some(expanding_ps4) = self.any.get_mut::<ExpandingPs4>() {
115            expanding_ps4.0 = false;
116        }
117    }
118}
119
120/// Collection of temporary string buffers that accumulate expanded strings
121///
122/// See the [module documentation](self) for details.
123///
124/// An `XTrace` contains four string buffers that accumulate each of the
125/// following:
126///
127/// - Command words (command name and arguments)
128/// - Assignments
129/// - Redirections
130/// - Here-document contents
131///
132/// The [`finish`](Self::finish) function creates the final string to be
133/// printed.
134#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
135pub struct XTrace {
136    words: String,
137    assigns: String,
138    redirs: String,
139    here_doc_contents: String,
140}
141
142impl XTrace {
143    /// Creates a new trace buffer.
144    #[inline]
145    #[must_use]
146    pub fn new() -> Self {
147        Self::default()
148    }
149
150    /// Creates a new trace buffer if the `xtrace` option is on.
151    ///
152    /// If the option is off, this function returns `None`.
153    #[must_use]
154    pub fn from_options(options: &OptionSet) -> Option<Self> {
155        match options.get(yash_env::option::Option::XTrace) {
156            State::On => Some(Self::new()),
157            State::Off => None,
158        }
159    }
160
161    /// Returns a reference to the words buffer.
162    ///
163    /// The words buffer is for tracing command words.
164    /// When writing to the buffer, the content should end with a space.
165    #[inline]
166    #[must_use]
167    pub fn words(&mut self) -> &mut (impl Write + use<>) {
168        &mut self.words
169    }
170
171    /// Returns a reference to the assignments buffer.
172    ///
173    /// The assignments buffer is for tracing assignments.
174    /// When writing to the buffer, the content should end with a space.
175    #[inline]
176    #[must_use]
177    pub fn assigns(&mut self) -> &mut (impl Write + use<>) {
178        &mut self.assigns
179    }
180
181    /// Returns a reference to the redirections buffer.
182    ///
183    /// The redirections buffer is for tracing redirections.
184    /// When writing to the buffer, the content should end with a space.
185    ///
186    /// You should not write the contents of here-documents to this buffer.
187    /// See also [`here_doc_contents`](Self::here_doc_contents).
188    #[inline]
189    #[must_use]
190    pub fn redirs(&mut self) -> &mut (impl Write + use<>) {
191        &mut self.redirs
192    }
193
194    /// Returns a reference to the here-document contents buffer.
195    ///
196    /// You should write the contents of here-documents you wrote to the
197    /// [redirections buffer](Self::redirs()).
198    #[inline]
199    #[must_use]
200    pub fn here_doc_contents(&mut self) -> &mut (impl Write + use<>) {
201        &mut self.here_doc_contents
202    }
203
204    /// Clears the buffer contents.
205    pub fn clear(&mut self) {
206        self.words.clear();
207        self.assigns.clear();
208        self.redirs.clear();
209        self.here_doc_contents.clear();
210    }
211
212    /// Returns whether all the buffers are empty.
213    #[must_use]
214    pub fn is_empty(&self) -> bool {
215        self.words.is_empty()
216            && self.assigns.is_empty()
217            && self.redirs.is_empty()
218            && self.here_doc_contents.is_empty()
219    }
220
221    /// Constructs the final trace to be printed to stderr.
222    ///
223    /// If all the buffers are empty, the result is empty. Otherwise, the result
224    /// is the concatenation of the following:
225    ///
226    /// - The expansion of `$PS4`
227    /// - The concatenation of the `assigns`, `words`, and `redirs` buffers with
228    ///   trailing spaces trimmed and a newline appended
229    /// - The `here_doc_contents` buffer
230    ///
231    /// If `$PS4` fails to expand, this function prints an error message and
232    /// uses the variable value intact.
233    ///
234    /// If this function is called while `$PS4` is being expanded inside this
235    /// function, the expansion of `$PS4` is skipped and an empty string is
236    /// returned. This prevents infinite recursion when `$PS4` contains a
237    /// command substitution that causes `XTrace::finish` to be called again.
238    pub async fn finish(&self, env: &mut Env) -> String {
239        let len = self.assigns.len()
240            + self.words.len()
241            + self.redirs.len()
242            + self.here_doc_contents.len();
243        if len == 0 {
244            return String::new();
245        }
246
247        // Expand $PS4 while preventing infinite recursion
248        let Some(mut env) = ExpandingGuard::new(env) else {
249            return String::new();
250        };
251        // TODO Support $YASH_PS4 and $YASH_PS4S
252        let ps4 = expand_ps4(&mut env).await;
253        drop(env);
254
255        // Construct the final string
256        let ps4_len = ps4.len();
257        let mut result = ps4;
258        result.reserve_exact(len);
259        result += &self.assigns;
260        result += &self.words;
261        result += &self.redirs;
262        result.truncate(ps4_len + result[ps4_len..].trim_end_matches(' ').len());
263        result.push('\n');
264        result += &self.here_doc_contents;
265        result
266    }
267}
268
269/// Convenience function for tracing fields.
270///
271/// This function writes the field values to the words buffer of the `XTrace`.
272pub fn trace_fields(xtrace: Option<&mut XTrace>, fields: &[Field]) {
273    if let Some(xtrace) = xtrace {
274        for field in fields {
275            write!(xtrace.words(), "{} ", quoted(&field.value)).unwrap();
276        }
277    }
278}
279
280/// Convenience function for calling [`XTrace::finish`] on an optional `XTrace`.
281pub async fn finish(env: &mut Env, xtrace: Option<XTrace>) -> String {
282    if let Some(xtrace) = xtrace {
283        xtrace.finish(env).await
284    } else {
285        String::new()
286    }
287}
288
289/// Convenience function for [finish]ing and
290/// [print](yash_env::SharedSystem::print_error)ing an (optional) `XTrace`.
291pub async fn print<X: Into<Option<XTrace>>>(env: &mut Env, xtrace: X) {
292    async fn inner(env: &mut Env, xtrace: Option<XTrace>) {
293        let s = finish(env, xtrace).await;
294        env.system.print_error(&s).await;
295    }
296    inner(env, xtrace.into()).await
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302    use crate::tests::echo_builtin;
303    use futures_util::FutureExt;
304    use yash_env::variable::Scope::Global;
305    use yash_env_test_helper::in_virtual_system;
306
307    #[test]
308    fn tracing_some_fields() {
309        let mut xtrace = XTrace::new();
310        let fields = Field::dummies(["one", "two", "'three'"]);
311        trace_fields(Some(&mut xtrace), &fields);
312        assert_eq!(xtrace.words, r#"one two "'three'" "#);
313        assert_eq!(xtrace.assigns, "");
314        assert_eq!(xtrace.redirs, "");
315        assert_eq!(xtrace.here_doc_contents, "");
316    }
317
318    fn fixture() -> Env {
319        let mut env = Env::new_virtual();
320        env.variables
321            .get_or_new(PS4, Global)
322            .assign("+${X=x}+ ", None)
323            .unwrap();
324        env
325    }
326
327    #[test]
328    fn empty_finish() {
329        let mut env = fixture();
330        let result = XTrace::new().finish(&mut env).now_or_never().unwrap();
331        assert_eq!(result, "");
332
333        // $PS4 should not have been expanded
334        assert_eq!(env.variables.get("X"), None);
335    }
336
337    #[test]
338    fn finish_with_assigns() {
339        let mut env = fixture();
340
341        let mut xtrace = XTrace::new();
342        xtrace.assigns.push_str("VAR=VALUE FOO=BAR ");
343        let result = xtrace.finish(&mut env).now_or_never().unwrap();
344        assert_eq!(result, "+x+ VAR=VALUE FOO=BAR\n");
345
346        // $PS4 should have been expanded
347        assert_ne!(env.variables.get("X"), None);
348    }
349
350    #[test]
351    fn finish_with_words() {
352        let mut env = fixture();
353
354        let mut xtrace = XTrace::new();
355        xtrace.words.push_str("abc '~' foo ");
356        let result = xtrace.finish(&mut env).now_or_never().unwrap();
357        assert_eq!(result, "+x+ abc '~' foo\n");
358
359        // $PS4 should have been expanded
360        assert_ne!(env.variables.get("X"), None);
361    }
362
363    #[test]
364    fn finish_with_redirs() {
365        let mut env = fixture();
366
367        let mut xtrace = XTrace::new();
368        xtrace.redirs.push_str("0< /dev/null 1> foo/bar ");
369        let result = xtrace.finish(&mut env).now_or_never().unwrap();
370        assert_eq!(result, "+x+ 0< /dev/null 1> foo/bar\n");
371    }
372
373    #[test]
374    fn finish_with_assigns_and_words() {
375        let mut env = fixture();
376
377        let mut xtrace = XTrace::new();
378        xtrace.assigns.push_str("VAR=VALUE ");
379        xtrace.words.push_str("echo argument ");
380        let result = xtrace.finish(&mut env).now_or_never().unwrap();
381        assert_eq!(result, "+x+ VAR=VALUE echo argument\n");
382
383        let mut xtrace = XTrace::new();
384        xtrace.assigns.push(' ');
385        xtrace.words.push(' ');
386        let result = xtrace.finish(&mut env).now_or_never().unwrap();
387        assert_eq!(result, "+x+ \n");
388    }
389
390    #[test]
391    fn finish_with_words_and_redirs() {
392        let mut env = fixture();
393
394        let mut xtrace = XTrace::new();
395        xtrace.words.push_str("echo argument ");
396        xtrace.redirs.push_str("2> errors ");
397        let result = xtrace.finish(&mut env).now_or_never().unwrap();
398        assert_eq!(result, "+x+ echo argument 2> errors\n");
399
400        let mut xtrace = XTrace::new();
401        xtrace.words.push(' ');
402        xtrace.redirs.push(' ');
403        let result = xtrace.finish(&mut env).now_or_never().unwrap();
404        assert_eq!(result, "+x+ \n");
405    }
406
407    #[test]
408    fn finish_with_here_doc_contents() {
409        let mut env = fixture();
410
411        let mut xtrace = XTrace::new();
412        xtrace.here_doc_contents.push_str("EOF\n");
413        let result = xtrace.finish(&mut env).now_or_never().unwrap();
414        assert_eq!(result, "+x+ \nEOF\n");
415    }
416
417    #[test]
418    fn finish_with_redirs_and_here_doc_contents() {
419        let mut env = fixture();
420
421        let mut xtrace = XTrace::new();
422        xtrace.redirs.push_str("0<< END ");
423        xtrace.here_doc_contents.push_str(" X \nEND\n");
424        let result = xtrace.finish(&mut env).now_or_never().unwrap();
425        assert_eq!(result, "+x+ 0<< END\n X \nEND\n");
426    }
427
428    #[test]
429    fn finish_prevents_recursion() {
430        in_virtual_system(|mut env, _state| async move {
431            env.builtins.insert("echo", echo_builtin());
432            env.variables
433                .get_or_new(PS4, Global)
434                .assign("$(echo recursive) ", None)
435                .unwrap();
436
437            let mut xtrace = XTrace::new();
438            xtrace.words.push_str("foo bar ");
439            let result = xtrace.finish(&mut env).await;
440            assert_eq!(result, "recursive foo bar\n");
441        })
442    }
443}