use crate::expansion::expand_text;
use crate::{Handle as _, Runtime};
use std::fmt::Write;
use std::ops::{Deref, DerefMut};
use yash_env::Env;
use yash_env::option::OptionSet;
use yash_env::option::State;
use yash_env::semantics::Field;
use yash_env::variable::PS4;
use yash_quote::quoted;
use yash_syntax::syntax::Text;
async fn expand_ps4<S: Runtime + 'static>(env: &mut Env<S>) -> String {
let value = env.variables.get_scalar(PS4).unwrap_or_default().to_owned();
let text = match value.parse::<Text>() {
Ok(text) => text,
Err(error) => {
_ = error.handle(env).await;
return value;
}
};
match expand_text(env, &text).await {
Ok((expansion, _exit_status)) => expansion,
Err(error) => {
_ = error.handle(env).await;
value
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
struct ExpandingPs4(bool);
#[derive(Debug)]
struct ExpandingGuard<'a, S>(&'a mut Env<S>);
impl<S> Deref for ExpandingGuard<'_, S> {
type Target = Env<S>;
fn deref(&self) -> &Self::Target {
self.0
}
}
impl<S> DerefMut for ExpandingGuard<'_, S> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.0
}
}
impl<'a, S> ExpandingGuard<'a, S> {
#[must_use]
fn new(env: &'a mut Env<S>) -> Option<Self> {
let expanding_ps4 = env.any.get_or_insert_with(Box::<ExpandingPs4>::default);
if expanding_ps4.0 {
None
} else {
expanding_ps4.0 = true;
Some(Self(env))
}
}
}
impl<S> Drop for ExpandingGuard<'_, S> {
fn drop(&mut self) {
if let Some(expanding_ps4) = self.any.get_mut::<ExpandingPs4>() {
expanding_ps4.0 = false;
}
}
}
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
pub struct XTrace {
words: String,
assigns: String,
redirs: String,
here_doc_contents: String,
}
impl XTrace {
#[inline]
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn from_options(options: &OptionSet) -> Option<Self> {
match options.get(yash_env::option::Option::XTrace) {
State::On => Some(Self::new()),
State::Off => None,
}
}
#[inline]
#[must_use]
pub fn words(&mut self) -> &mut (impl Write + use<>) {
&mut self.words
}
#[inline]
#[must_use]
pub fn assigns(&mut self) -> &mut (impl Write + use<>) {
&mut self.assigns
}
#[inline]
#[must_use]
pub fn redirs(&mut self) -> &mut (impl Write + use<>) {
&mut self.redirs
}
#[inline]
#[must_use]
pub fn here_doc_contents(&mut self) -> &mut (impl Write + use<>) {
&mut self.here_doc_contents
}
pub fn clear(&mut self) {
self.words.clear();
self.assigns.clear();
self.redirs.clear();
self.here_doc_contents.clear();
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.words.is_empty()
&& self.assigns.is_empty()
&& self.redirs.is_empty()
&& self.here_doc_contents.is_empty()
}
pub async fn finish<S: Runtime + 'static>(&self, env: &mut Env<S>) -> String {
let len = self.assigns.len()
+ self.words.len()
+ self.redirs.len()
+ self.here_doc_contents.len();
if len == 0 {
return String::new();
}
let Some(mut env) = ExpandingGuard::new(env) else {
return String::new();
};
let ps4 = expand_ps4(&mut env).await;
drop(env);
let ps4_len = ps4.len();
let mut result = ps4;
result.reserve_exact(len);
result += &self.assigns;
result += &self.words;
result += &self.redirs;
result.truncate(ps4_len + result[ps4_len..].trim_end_matches(' ').len());
result.push('\n');
result += &self.here_doc_contents;
result
}
}
pub fn trace_fields(xtrace: Option<&mut XTrace>, fields: &[Field]) {
if let Some(xtrace) = xtrace {
for field in fields {
write!(xtrace.words(), "{} ", quoted(&field.value)).unwrap();
}
}
}
pub async fn finish<S: Runtime + 'static>(env: &mut Env<S>, xtrace: Option<XTrace>) -> String {
if let Some(xtrace) = xtrace {
xtrace.finish(env).await
} else {
String::new()
}
}
pub async fn print<S, X>(env: &mut Env<S>, xtrace: X)
where
S: Runtime + 'static,
X: Into<Option<XTrace>>,
{
async fn inner<S: Runtime + 'static>(env: &mut Env<S>, xtrace: Option<XTrace>) {
let s = finish(env, xtrace).await;
env.system.print_error(&s).await;
}
inner(env, xtrace.into()).await
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::echo_builtin;
use futures_util::FutureExt as _;
use std::rc::Rc;
use yash_env::system::Concurrent;
use yash_env::system::r#virtual::VirtualSystem;
use yash_env::test_helper::in_virtual_system;
use yash_env::variable::Scope::Global;
#[test]
fn tracing_some_fields() {
let mut xtrace = XTrace::new();
let fields = Field::dummies(["one", "two", "'three'"]);
trace_fields(Some(&mut xtrace), &fields);
assert_eq!(xtrace.words, r#"one two "'three'" "#);
assert_eq!(xtrace.assigns, "");
assert_eq!(xtrace.redirs, "");
assert_eq!(xtrace.here_doc_contents, "");
}
fn fixture() -> Env<Rc<Concurrent<VirtualSystem>>> {
let mut env = Env::new_virtual();
env.variables
.get_or_new(PS4, Global)
.assign("+${X=x}+ ", None)
.unwrap();
env
}
#[test]
fn empty_finish() {
let mut env = fixture();
let result = XTrace::new().finish(&mut env).now_or_never().unwrap();
assert_eq!(result, "");
assert_eq!(env.variables.get("X"), None);
}
#[test]
fn finish_with_assigns() {
let mut env = fixture();
let mut xtrace = XTrace::new();
xtrace.assigns.push_str("VAR=VALUE FOO=BAR ");
let result = xtrace.finish(&mut env).now_or_never().unwrap();
assert_eq!(result, "+x+ VAR=VALUE FOO=BAR\n");
assert_ne!(env.variables.get("X"), None);
}
#[test]
fn finish_with_words() {
let mut env = fixture();
let mut xtrace = XTrace::new();
xtrace.words.push_str("abc '~' foo ");
let result = xtrace.finish(&mut env).now_or_never().unwrap();
assert_eq!(result, "+x+ abc '~' foo\n");
assert_ne!(env.variables.get("X"), None);
}
#[test]
fn finish_with_redirs() {
let mut env = fixture();
let mut xtrace = XTrace::new();
xtrace.redirs.push_str("0< /dev/null 1> foo/bar ");
let result = xtrace.finish(&mut env).now_or_never().unwrap();
assert_eq!(result, "+x+ 0< /dev/null 1> foo/bar\n");
}
#[test]
fn finish_with_assigns_and_words() {
let mut env = fixture();
let mut xtrace = XTrace::new();
xtrace.assigns.push_str("VAR=VALUE ");
xtrace.words.push_str("echo argument ");
let result = xtrace.finish(&mut env).now_or_never().unwrap();
assert_eq!(result, "+x+ VAR=VALUE echo argument\n");
let mut xtrace = XTrace::new();
xtrace.assigns.push(' ');
xtrace.words.push(' ');
let result = xtrace.finish(&mut env).now_or_never().unwrap();
assert_eq!(result, "+x+ \n");
}
#[test]
fn finish_with_words_and_redirs() {
let mut env = fixture();
let mut xtrace = XTrace::new();
xtrace.words.push_str("echo argument ");
xtrace.redirs.push_str("2> errors ");
let result = xtrace.finish(&mut env).now_or_never().unwrap();
assert_eq!(result, "+x+ echo argument 2> errors\n");
let mut xtrace = XTrace::new();
xtrace.words.push(' ');
xtrace.redirs.push(' ');
let result = xtrace.finish(&mut env).now_or_never().unwrap();
assert_eq!(result, "+x+ \n");
}
#[test]
fn finish_with_here_doc_contents() {
let mut env = fixture();
let mut xtrace = XTrace::new();
xtrace.here_doc_contents.push_str("EOF\n");
let result = xtrace.finish(&mut env).now_or_never().unwrap();
assert_eq!(result, "+x+ \nEOF\n");
}
#[test]
fn finish_with_redirs_and_here_doc_contents() {
let mut env = fixture();
let mut xtrace = XTrace::new();
xtrace.redirs.push_str("0<< END ");
xtrace.here_doc_contents.push_str(" X \nEND\n");
let result = xtrace.finish(&mut env).now_or_never().unwrap();
assert_eq!(result, "+x+ 0<< END\n X \nEND\n");
}
#[test]
fn finish_prevents_recursion() {
in_virtual_system(|mut env, _state| async move {
env.builtins.insert("echo", echo_builtin());
env.variables
.get_or_new(PS4, Global)
.assign("$(echo recursive) ", None)
.unwrap();
let mut xtrace = XTrace::new();
xtrace.words.push_str("foo bar ");
let result = xtrace.finish(&mut env).await;
assert_eq!(result, "recursive foo bar\n");
})
}
}