use std::{
ffi::CString,
io::{Write, BufRead},
process::{self, Stdio},
fs::File,
os::unix::io::IntoRawFd,
env::{var, set_var}
};
use lalrpop_util::ParseError;
use nix::{
sys::wait::WaitStatus,
unistd::Pid,
};
#[cfg(feature = "raw")]
use uuid::Uuid;
use dirs::home_dir;
use crate::{
process::{ProcessGroup, Process, Wait},
program::{Runtime, Result, Error},
};
use self::ast::{Assignment, Redirect};
#[cfg(feature = "shebang-block")]
use {
std::io,
std::process::ExitStatus,
std::fs,
std::os::unix::fs::PermissionsExt,
self::ast::Interpreter,
};
pub use self::ast::Program;
pub use self::ast::Command;
pub use self::builtin::Builtin;
impl super::Program for Program {
type Command = Command;
fn parse<R: BufRead>(mut reader: R) -> Result<Self> {
let mut string = String::new();
if reader.read_to_string(&mut string).is_err() {
return Err(Error::Read);
}
let lexer = lex::Lexer::new(&string);
let parser = parse::ProgramParser::new();
match parser.parse(&string, lexer) {
Ok(parsed) => Ok(parsed),
Err(e) => {
match e {
ParseError::InvalidToken { location } => {
eprintln!("invalid token found at {}", location);
},
ParseError::UnrecognizedToken { token, expected } => {
let (s, t, e) = token;
eprintln!("unexpected token {:?} found at {}-{}, expecting one of: {}",
t, s, e, expected.join(", "));
},
ParseError::UnrecognizedEOF { location, expected } => {
if location == 0 {
return Ok(Program(vec![]))
} else {
eprintln!("unexpected EOF found at {}, expecting one of: {}",
location, expected.join(", "));
}
}
ParseError::ExtraToken { token: (i, t, _) } => {
eprintln!("extra token {:?} found at {}", t, i);
}
ParseError::User { error } => {
let lex::Error::UnrecognizedChar(s, c, e) = error;
eprintln!("unexpected character {} found at {}-{}", c, s, e);
},
}
Err(Error::Parse)
}
}
}
fn commands(&self) -> &[Self::Command] {
&self.0[..]
}
}
impl super::Command for Command {}
impl super::Run for Command {
fn run(&self, runtime: &mut Runtime) -> Result<WaitStatus> {
#[allow(unreachable_patterns)]
match *self {
Command::Simple(ref assignments, ref words, ref redirects) => {
for Assignment(name, value) in assignments {
set_var(name, expand_vars(value));
}
for r in redirects {
match r {
Redirect::RW { n, filename, .. } => {
let file = File::options()
.create(true)
.read(true)
.write(true)
.open(filename).unwrap();
let fd = file.into_raw_fd();
runtime.io.0[*n as usize] = fd;
},
Redirect::Read { n, filename, .. } => {
let file = File::options()
.read(true)
.write(false)
.open(filename).unwrap();
let fd = file.into_raw_fd();
runtime.io.0[*n as usize] = fd;
},
Redirect::Write { n, filename, append, .. } => {
let file = File::options()
.create(true)
.read(false)
.write(true)
.append(*append)
.open(filename).unwrap();
let fd = file.into_raw_fd();
runtime.io.0[*n as usize] = fd;
},
};
}
let argv: Vec<CString> = words.iter().map(|word| {
CString::new(&expand_home(&expand_vars(&word.0)) as &str)
.expect("error in word UTF-8")
}).collect();
if let Some(command) = argv.clone().first() {
match command.to_string_lossy().as_ref() {
"." => builtin::Dot.run(argv, runtime),
":" => builtin::Return(0).run(argv, runtime),
"cd" => builtin::Cd.run(argv, runtime),
"command" => builtin::Command.run(argv, runtime),
"exit" => builtin::Exit.run(argv, runtime),
"export" => builtin::Export.run(argv, runtime),
"false" => builtin::Return(1).run(argv, runtime),
"jobs" => builtin::Jobs.run(argv, runtime),
"true" => builtin::Return(0).run(argv, runtime),
"wait" => builtin::Wait.run(argv, runtime),
_ => {
let id = (runtime.jobs.borrow().len() + 1).to_string();
let name = argv[0].to_string_lossy().to_string();
let process = Process::fork(argv, runtime.io).map_err(|_| Error::Runtime)?;
if runtime.background {
let status = process.status();
eprintln!("[{}]\t{}", id, process.pid());
runtime.jobs.borrow_mut().push((id, ProcessGroup(process)));
status.map_err(|_| Error::Runtime)
} else {
let status = process.wait().map_err(|_| Error::Runtime);
if let Ok(WaitStatus::Exited(_, 127)) = status {
eprintln!("oursh: {}: command not found", name);
}
status
}
},
}
} else {
Ok(WaitStatus::Exited(Pid::this(), 0))
}
},
Command::Compound(ref commands) => {
let mut last = WaitStatus::Exited(Pid::this(), 0);
for command in commands.iter() {
last = command.run(runtime)?;
}
Ok(last)
},
Command::Not(ref command) => {
match command.run(runtime) {
Ok(WaitStatus::Exited(p, c)) => {
Ok(WaitStatus::Exited(p, (c == 0) as i32))
}
Ok(s) => Ok(s),
Err(_) => Err(Error::Runtime),
}
},
Command::And(ref left, ref right) => {
match left.run(runtime) {
Ok(WaitStatus::Exited(_, c)) if c == 0 => {
right.run(runtime).map_err(|_| Error::Runtime)
},
Ok(s) => Ok(s),
Err(_) => Err(Error::Runtime),
}
},
Command::Or(ref left, ref right) => {
match left.run(runtime) {
Ok(WaitStatus::Exited(_, c)) if c != 0 => {
right.run(runtime).map_err(|_| Error::Runtime)
},
Ok(s) => Ok(s),
Err(_) => Err(Error::Runtime),
}
},
Command::Subshell(ref program) => {
program.run(runtime)
},
Command::Pipeline(ref left, ref right) => {
if let box Command::Simple(_assigns, lwords, _redirs) = left {
let child = process::Command::new(&lwords[0].0)
.args(lwords.iter().skip(1).map(|w| &w.0))
.stdout(Stdio::piped())
.spawn()
.expect("error swawning pipeline process");
let output = child.wait_with_output()
.expect("error reading stdout");
if let box Command::Simple(_assigns, rwords, _redirs) = right {
let mut child = process::Command::new(&rwords[0].0)
.args(rwords.iter().skip(1).map(|w| &w.0))
.stdin(Stdio::piped())
.spawn()
.expect("error swawning pipeline process");
{
let stdin = child.stdin.as_mut()
.expect("error opening stdin");
stdin.write_all(&output.stdout)
.expect("error writing to stdin");
}
child.wait()
.expect("error waiting for piped command");
}
}
Ok(WaitStatus::Exited(Pid::this(), 0))
},
Command::Background(ref command) => {
runtime.background = true;
command.run(runtime)
},
#[cfg(feature = "shebang-block")]
Command::Lang(ref interpreter, ref text) => {
fn bridge(interpreter: &str, text: &str) -> io::Result<ExitStatus> {
let bridgefile = format!("/tmp/.oursh_bridge-{}", Uuid::new_v4());
{
let mut file = File::create(&bridgefile)?;
let mut interpreter = interpreter.chars()
.map(|c| c as u8)
.collect::<Vec<u8>>();
interpreter.insert(0, b'!');
interpreter.insert(0, b'#');
file.write_all(&interpreter)?;
file.write_all(b"\n")?;
let text = text.chars()
.map(|c| c as u8)
.collect::<Vec<u8>>();
file.write_all(&text)?;
let mut perms = fs::metadata(&bridgefile)?.permissions();
perms.set_mode(0o777);
fs::set_permissions(&bridgefile, perms)?;
}
process::Command::new(&bridgefile).spawn()?.wait()
}
let interpreter = match interpreter {
Interpreter::Primary => {
unimplemented!()
}
Interpreter::Alternate => {
"/bin/sh"
},
Interpreter::HashLang(ref language) => {
match language.as_str() {
"ruby" => "/usr/bin/env ruby",
"node" => "/usr/bin/env node",
"python" => "/usr/bin/env python",
"racket" => "/usr/bin/env racket",
_ => return Err(Error::Read),
}
},
Interpreter::Shebang(ref interpreter) => {
interpreter
},
};
bridge(interpreter, text).map_err(|_| Error::Read)?;
Ok(WaitStatus::Exited(Pid::this(), 0))
},
#[cfg(not(feature = "shebang-block"))]
Command::Lang(_,_) => {
unimplemented!();
},
}
}
}
fn expand_home(word: &str) -> String {
if let Some(w) = word.strip_prefix('~') {
if let Some(path) = home_dir() {
format!("{}{}", &path.to_str().expect("error: home not set"), w)
} else {
"~".into()
}
} else {
word.into()
}
}
fn expand_vars(string: &str) -> String {
let mut result = String::new();
let mut variable = String::new();
let mut variable_start = -1;
for (i, c) in string.char_indices() {
if c == '$' || c == ' ' {
if variable.is_empty() {
result.push(c);
} else {
result += &var(&variable).unwrap_or_else(|_| "".into());
}
variable.clear();
variable_start = -1;
}
if c == '$' {
variable_start = i as i32;
} else if c == ' ' {
variable_start = -1;
} else if c == '@' || c == ':' {
result += &var(&variable).unwrap_or_else(|_| "".into());
variable.clear();
variable_start = -1;
result.push(c);
} else if variable_start > -1 {
if variable.is_empty() {
result.pop(); }
variable.push(c);
} else {
result.push(c);
}
}
result += &var(&variable).unwrap_or_else(|_| "".into());
result
}
pub mod builtin;
pub mod ast;
pub mod lex;
lalrpop_mod!(
#[allow(clippy::all)]
#[allow(unknown_lints)]
pub parse, "/program/posix/mod.rs");
#[cfg(test)]
mod tests {
use crate::program::Program as ProgramTrait;
use super::*;
#[test]
fn program_parse_empty() {
let result: Result<Program> = Program::parse(b"" as &[u8]);
assert!(result.is_ok());
assert!(result.unwrap().0.is_empty());
}
}