use std::{
fs::File,
io::{BufRead, BufReader},
path::Path,
process::{Child, Stdio},
};
use lazy_static::lazy_static;
use nix::unistd::Pid;
use shrs_core::{
hooks::{AfterCommandCtx, BeforeCommandCtx, JobExitCtx},
prelude::{Context, Lang, Runtime, Shell},
};
use thiserror::Error;
use crate::{ast, parser, process::ExitStatus, Lexer, Parser, PosixError};
pub fn run_external_command(
sh: &Shell,
ctx: &mut Context,
rt: &mut Runtime,
cmd: &str,
args: &[String],
stdin: Stdio,
stdout: Stdio,
pgid: Option<i32>,
assigns: &Vec<ast::Assign>,
) -> anyhow::Result<ExitStatus> {
use std::process::Command;
let envs = assigns.iter().map(|word| (&word.var, &word.val));
let child = Command::new(cmd)
.args(args)
.stdin(stdin)
.stdout(stdout)
.current_dir(rt.working_dir.to_str().unwrap())
.envs(envs)
.spawn()?;
Ok(ExitStatus::Running(Pid::from_raw(child.id() as i32)))
}
fn dummy_child() -> ExitStatus {
ExitStatus::Exited(0)
}
pub fn eval_command(
sh: &Shell,
ctx: &mut Context,
rt: &mut Runtime,
cmd: &ast::Command,
stdin: Stdio,
stdout: Stdio,
_pgid: Option<i32>,
) -> anyhow::Result<ExitStatus> {
match cmd {
ast::Command::Simple {
assigns,
args,
redirects,
} => {
let mut it = args.iter();
let cmd_name = match it.next() {
Some(cmd_name) => cmd_name,
None => return Ok(dummy_child()),
};
let args = it
.map(|a| -> String {
if a.len() > 1 {
let mut chars = a.chars();
let first = chars.next().unwrap();
let last = chars.next_back().unwrap();
if first == '\'' || first == '\"' {
if first == last {
return a[1..a.len() - 1].into();
}
}
}
(*a).clone()
})
.collect::<Vec<_>>();
let mut cur_stdin = stdin;
let mut cur_stdout = stdout;
for redirect in redirects {
let filename = Path::new(&*redirect.file);
let _n = match &redirect.n {
Some(n) => *n,
None => 1,
};
match redirect.mode {
ast::RedirectMode::Read => {
let file_handle = File::options()
.read(true)
.open(filename)
.map_err(PosixError::Redirect)?;
cur_stdin = Stdio::from(file_handle);
},
ast::RedirectMode::Write => {
let file_handle = File::options()
.write(true)
.create_new(true)
.open(filename)
.map_err(PosixError::Redirect)?;
cur_stdout = Stdio::from(file_handle);
},
ast::RedirectMode::ReadAppend => {
let file_handle = File::options()
.read(true)
.append(true)
.open(filename)
.map_err(PosixError::Redirect)?;
cur_stdin = Stdio::from(file_handle);
},
ast::RedirectMode::WriteAppend => {
let file_handle = File::options()
.write(true)
.append(true)
.create_new(true)
.open(filename)
.map_err(PosixError::Redirect)?;
cur_stdout = Stdio::from(file_handle);
},
ast::RedirectMode::ReadDup => {
unimplemented!()
},
ast::RedirectMode::WriteDup => {
unimplemented!()
},
ast::RedirectMode::ReadWrite => {
let file_handle = File::options()
.read(true)
.write(true)
.create_new(true)
.open(filename)
.map_err(PosixError::Redirect)?;
cur_stdin = Stdio::from(file_handle.try_clone().unwrap());
cur_stdout = Stdio::from(file_handle);
},
};
}
let subst_args = args.iter().map(|x| envsubst(rt, x)).collect::<Vec<_>>();
for (builtin_name, builtin_cmd) in sh.builtins.iter() {
if builtin_name == &cmd_name.as_str() {
let builtin_output = builtin_cmd.run(sh, ctx, rt, &subst_args)?;
return Ok(dummy_child());
}
}
let cmd_body = None;
match cmd_body {
Some(ref cmd_body) => eval_command(
sh,
ctx,
rt,
cmd_body,
Stdio::inherit(),
Stdio::piped(),
None,
),
None => run_external_command(
sh,
ctx,
rt,
cmd_name,
&subst_args,
cur_stdin,
cur_stdout,
None,
assigns,
),
}
},
ast::Command::Or(a_cmd, b_cmd) | ast::Command::And(a_cmd, b_cmd) => {
let negate = match cmd {
ast::Command::Or { .. } => false,
ast::Command::And { .. } => true,
_ => unreachable!(),
};
let mut a_cmd_handle =
eval_command(sh, ctx, rt, a_cmd, Stdio::inherit(), Stdio::piped(), None)?;
if (a_cmd_handle == ExitStatus::Exited(0)) ^ negate {
return Ok(dummy_child());
}
let b_cmd_handle =
eval_command(sh, ctx, rt, b_cmd, Stdio::inherit(), Stdio::piped(), None)?;
Ok(b_cmd_handle)
},
ast::Command::Not(cmd) => {
let cmd_handle = eval_command(sh, ctx, rt, cmd, stdin, stdout, None)?;
Ok(cmd_handle)
},
ast::Command::AsyncList(a_cmd, b_cmd) => {
let a_cmd_handle =
eval_command(sh, ctx, rt, a_cmd, Stdio::inherit(), Stdio::piped(), None)?;
match b_cmd {
None => {
Ok(dummy_child())
},
Some(b_cmd) => {
let b_cmd_handle =
eval_command(sh, ctx, rt, b_cmd, Stdio::inherit(), Stdio::piped(), None)?;
Ok(b_cmd_handle)
},
}
},
ast::Command::SeqList(a_cmd, b_cmd) => {
let mut a_cmd_handle =
eval_command(sh, ctx, rt, a_cmd, Stdio::inherit(), Stdio::piped(), None)?;
match b_cmd {
None => Ok(a_cmd_handle),
Some(b_cmd) => {
let b_cmd_handle =
eval_command(sh, ctx, rt, b_cmd, Stdio::inherit(), Stdio::piped(), None)?;
Ok(b_cmd_handle)
},
}
},
ast::Command::Subshell(cmd) => {
let mut new_rt = rt.clone();
let cmd_handle = eval_command(
sh,
ctx,
&mut new_rt,
cmd,
Stdio::inherit(),
Stdio::piped(),
None,
)?;
Ok(cmd_handle)
},
ast::Command::If { conds, else_part } => {
assert!(!conds.is_empty());
for ast::Condition { cond, body } in conds {
let mut cond_handle =
eval_command(sh, ctx, rt, cond, Stdio::inherit(), Stdio::piped(), None)?;
if cond_handle == ExitStatus::Exited(0) {
let body_handle =
eval_command(sh, ctx, rt, body, Stdio::inherit(), Stdio::piped(), None)?;
return Ok(body_handle);
}
}
if let Some(else_part) = else_part {
let else_handle = eval_command(
sh,
ctx,
rt,
else_part,
Stdio::inherit(),
Stdio::piped(),
None,
)?;
return Ok(else_handle);
}
Ok(dummy_child())
},
ast::Command::While { cond, body } | ast::Command::Until { cond, body } => {
let negate = match cmd {
ast::Command::While { .. } => false,
ast::Command::Until { .. } => true,
_ => unreachable!(),
};
loop {
let mut cond_handle =
eval_command(sh, ctx, rt, cond, Stdio::inherit(), Stdio::piped(), None)?;
if (cond_handle == ExitStatus::Exited(0)) ^ negate {
let mut body_handle =
eval_command(sh, ctx, rt, body, Stdio::inherit(), Stdio::piped(), None)?;
} else {
break; }
}
Ok(dummy_child())
},
ast::Command::For {
name,
wordlist,
body,
} => {
let mut expanded = vec![];
for word in wordlist {
for subword in word.split(' ') {
expanded.push(subword);
}
}
for word in expanded {
rt.env.set(name, word); let mut body_handle =
eval_command(sh, ctx, rt, body, Stdio::inherit(), Stdio::piped(), None)?;
}
Ok(dummy_child())
},
ast::Command::Case { word, arms } => {
let subst_word = envsubst(rt, word);
for ast::CaseArm { pattern, body } in arms {
if pattern.iter().any(|x| x == &subst_word) {
let mut body_handle =
eval_command(sh, ctx, rt, body, Stdio::inherit(), Stdio::piped(), None)?;
}
}
Ok(dummy_child())
},
ast::Command::Fn { fname, body } => {
todo!()
},
ast::Command::None => Ok(dummy_child()),
_ => Ok(dummy_child()),
}
}
fn envsubst(rt: &mut Runtime, arg: &str) -> String {
use regex::Regex;
lazy_static! {
static ref R_0: Regex = Regex::new(r"\$(?P<env>[a-zA-Z_]+)").unwrap(); static ref R_1: Regex = Regex::new(r"\$\{(?P<env>[a-zA-Z_]+)\}").unwrap(); static ref R_2: Regex = Regex::new(r"~").unwrap(); }
let mut subst = arg.to_string();
subst = subst.as_str().replace("$?", &rt.exit_status.to_string());
subst = subst.as_str().replace("$#", &rt.args.len().to_string());
subst = subst.as_str().replace("$0", &rt.name);
for cap in R_0.captures_iter(arg) {
let var = &cap["env"];
let val = match rt.env.get(var) {
Ok(val) => val.clone(),
Err(_) => String::new(),
};
let fmt_env = format!("${var}"); subst = subst.as_str().replace(&fmt_env, &val);
}
for cap in R_1.captures_iter(arg) {
let var = &cap["env"];
let val = match rt.env.get(var) {
Ok(val) => val.clone(),
Err(_) => String::new(),
};
let fmt_env = format!("${{{var}}}"); subst = subst.as_str().replace(&fmt_env, &val);
}
let home = match rt.env.get("HOME") {
Ok(home) => home.as_str(),
Err(_) => "",
};
let subst = R_2.replace_all(&subst, home).to_string();
subst
}
pub fn command_output(
sh: &Shell,
ctx: &mut Context,
rt: &mut Runtime,
cmd_handle: &mut Child,
) -> anyhow::Result<ExitStatus> {
let output = if let Some(out) = cmd_handle.stdout.take() {
let reader = BufReader::new(out);
reader
.lines()
.map(|line| {
let line = line.unwrap();
println!("{}", line);
line
})
.collect::<Vec<_>>()
.join("\n")
} else {
String::new()
};
let exit_status = cmd_handle.wait().unwrap().code().unwrap();
rt.exit_status = exit_status;
Ok(ExitStatus::Exited(exit_status))
}