use super::super::attr::AttrChar;
use super::super::attr::Origin;
use super::super::phrase::Phrase;
use super::Env;
use super::Error;
use crate::Handle as _;
use crate::Runtime;
use crate::expansion::ErrorCause;
use crate::read_eval_loop;
use crate::trap::run_exit_trap;
use std::cell::RefCell;
use yash_env::io::Fd;
use yash_env::job::Pid;
use yash_env::subshell::Config;
use yash_env::subshell::JobControl;
use yash_env::system::concurrency::ReadAll;
use yash_env::system::concurrency::WaitForSignals;
use yash_env::system::{Close, Errno, Wait};
use yash_env::trap::SignalSystem;
use yash_syntax::parser::lex::Lexer;
use yash_syntax::source::Location;
use yash_syntax::source::Source;
pub async fn expand<C, S>(
command: C,
location: Location,
env: &mut Env<'_, S>,
) -> Result<Phrase, Error>
where
C: AsRef<str> + 'static,
S: Runtime + 'static,
{
let original = location.clone();
let (reader, writer) = match env.inner.system.pipe() {
Ok(pipes) => pipes,
Err(errno) => {
return Err(Error {
cause: ErrorCause::CommandSubstError(errno),
location,
});
}
};
let subshell_result = Config::new()
.start(env.inner, async move |env, _job_control| {
let result = subshell_body(env, reader, writer, original, command).await;
env.apply_result(result);
run_exit_trap(env).await;
})
.await;
expand_common(reader, writer, subshell_result, location, env).await
}
async fn subshell_body<C, S>(
env: &mut yash_env::Env<S>,
reader: Fd,
writer: Fd,
original: Location,
command: C,
) -> yash_env::semantics::Result
where
C: AsRef<str>,
S: Runtime + 'static,
{
env.system.close(reader).ok();
if writer != Fd::STDOUT {
if let Err(errno) = env.system.dup2(writer, Fd::STDOUT) {
let error = Error {
cause: ErrorCause::CommandSubstError(errno),
location: original,
};
return error.handle(env).await;
}
env.system.close(writer).ok();
}
let mut lexer = Lexer::from_memory(command.as_ref(), Source::CommandSubst { original });
read_eval_loop(&RefCell::new(env), &mut lexer).await
}
async fn expand_common<S>(
reader: Fd,
writer: Fd,
subshell_result: Result<(Pid, Option<JobControl>), Errno>,
location: Location,
env: &mut Env<'_, S>,
) -> Result<Phrase, Error>
where
S: Close + ReadAll + SignalSystem + Wait + WaitForSignals,
{
let pid = match subshell_result {
Ok((pid, job_control)) => {
debug_assert_eq!(job_control, None);
pid
}
Err(errno) => {
env.inner.system.close(reader).ok();
env.inner.system.close(writer).ok();
return Err(Error {
cause: ErrorCause::CommandSubstError(errno),
location,
});
}
};
env.inner.system.close(writer).ok();
let mut result = Vec::new();
env.inner.system.read_all_to(reader, &mut result).await.ok();
env.inner.system.close(reader).ok();
match env.inner.wait_for_subshell_to_finish(pid).await {
Ok((_pid, exit_status)) => env.last_command_subst_exit_status = Some(exit_status),
Err(errno) => {
return Err(Error {
cause: ErrorCause::CommandSubstError(errno),
location,
});
}
}
let mut result = String::from_utf8(result)
.unwrap_or_else(|e| String::from_utf8_lossy(&e.into_bytes()).into());
let len = result.trim_end_matches('\n').len();
result.truncate(len);
let chars = result
.chars()
.map(|value| AttrChar {
value,
origin: Origin::SoftExpansion,
is_quoted: false,
is_quoting: false,
})
.collect();
Ok(Phrase::Field(chars))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::echo_builtin;
use crate::tests::return_builtin;
use futures_util::FutureExt as _;
use yash_env::semantics::ExitStatus;
use yash_env::test_helper::in_virtual_system;
#[test]
fn empty_substitution() {
in_virtual_system(|mut env, _state| async move {
let command = "".to_string();
let location = Location::dummy("");
let mut env = Env::new(&mut env);
let result = expand(command, location, &mut env).await;
assert_eq!(result, Ok(Phrase::one_empty_field()));
})
}
#[test]
fn one_line_substitution() {
in_virtual_system(|mut env, _state| async move {
env.builtins.insert("echo", echo_builtin());
let command = "echo ok".to_string();
let location = Location::dummy("");
let mut env = Env::new(&mut env);
let result = expand(command, location, &mut env).await;
let o = AttrChar {
value: 'o',
origin: Origin::SoftExpansion,
is_quoted: false,
is_quoting: false,
};
let k = AttrChar { value: 'k', ..o };
assert_eq!(result, Ok(Phrase::Field(vec![o, k])));
})
}
#[test]
fn many_line_substitution() {
in_virtual_system(|mut env, _state| async move {
env.builtins.insert("echo", echo_builtin());
let command = "echo 1; echo 2; echo; echo 3; echo; echo".to_string();
let location = Location::dummy("");
let mut env = Env::new(&mut env);
let result = expand(command, location, &mut env).await;
let chars = "1\n2\n\n3"
.chars()
.map(|value| AttrChar {
value,
origin: Origin::SoftExpansion,
is_quoted: false,
is_quoting: false,
})
.collect();
assert_eq!(result, Ok(Phrase::Field(chars)));
})
}
#[test]
fn exit_status_of_command_substitution() {
in_virtual_system(|mut env, _state| async move {
env.builtins.insert("return", return_builtin());
let command = "return -n 100".to_string();
let location = Location::dummy("");
let mut env = Env::new(&mut env);
let result = expand(command, location, &mut env).await;
assert_eq!(result, Ok(Phrase::one_empty_field()));
assert_eq!(env.last_command_subst_exit_status, Some(ExitStatus(100)));
})
}
#[test]
fn error_in_command_substitution() {
let command = "".to_string();
let location = Location::dummy("foo");
let mut env = yash_env::Env::new_virtual();
let mut env = Env::new(&mut env);
let result = expand(command, location.clone(), &mut env)
.now_or_never()
.unwrap();
let cause = ErrorCause::CommandSubstError(Errno::ENOSYS);
assert_eq!(result, Err(Error { cause, location }));
}
}