1use 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#[derive(Clone, Debug, Default, Eq, PartialEq)]
76struct ExpandingPs4(bool);
77
78#[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 #[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#[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 #[inline]
145 #[must_use]
146 pub fn new() -> Self {
147 Self::default()
148 }
149
150 #[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 #[inline]
166 #[must_use]
167 pub fn words(&mut self) -> &mut (impl Write + use<>) {
168 &mut self.words
169 }
170
171 #[inline]
176 #[must_use]
177 pub fn assigns(&mut self) -> &mut (impl Write + use<>) {
178 &mut self.assigns
179 }
180
181 #[inline]
189 #[must_use]
190 pub fn redirs(&mut self) -> &mut (impl Write + use<>) {
191 &mut self.redirs
192 }
193
194 #[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 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 #[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 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 let Some(mut env) = ExpandingGuard::new(env) else {
249 return String::new();
250 };
251 let ps4 = expand_ps4(&mut env).await;
253 drop(env);
254
255 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
269pub 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
280pub 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
289pub 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 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 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 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}