466 lines
11 KiB
Rust
466 lines
11 KiB
Rust
use std::str::FromStr;
|
|
|
|
use bitflags::bitflags;
|
|
use lex::{Span, Tk, TkFlags, TkRule};
|
|
|
|
use crate::{prelude::*, libsh::error::{ShErr, ShErrKind, ShResult}, procio::{IoFd, IoFile, IoInfo}};
|
|
|
|
pub mod lex;
|
|
pub mod execute;
|
|
|
|
|
|
#[derive(Debug)]
|
|
pub struct Node<'t> {
|
|
pub class: NdRule<'t>,
|
|
pub flags: NdFlags,
|
|
pub redirs: Vec<Redir>,
|
|
pub tokens: Vec<Tk<'t>>,
|
|
}
|
|
|
|
bitflags! {
|
|
#[derive(Debug)]
|
|
pub struct NdFlags: u32 {
|
|
const BACKGROUND = 0b000001;
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct Redir {
|
|
pub io_info: Box<dyn IoInfo>,
|
|
pub class: RedirType
|
|
}
|
|
|
|
impl Redir {
|
|
pub fn new(io_info: Box<dyn IoInfo>, class: RedirType) -> Self {
|
|
Self { io_info, class }
|
|
}
|
|
}
|
|
|
|
#[derive(Default,Debug)]
|
|
pub struct RedirBldr {
|
|
pub io_info: Option<Box<dyn IoInfo>>,
|
|
pub class: Option<RedirType>,
|
|
pub tgt_fd: Option<RawFd>,
|
|
}
|
|
|
|
impl RedirBldr {
|
|
pub fn new() -> Self {
|
|
Default::default()
|
|
}
|
|
pub fn with_io_info(self, io_info: Box<dyn IoInfo>) -> Self {
|
|
let Self { io_info: _, class, tgt_fd } = self;
|
|
Self { io_info: Some(io_info), class, tgt_fd }
|
|
}
|
|
pub fn with_class(self, class: RedirType) -> Self {
|
|
let Self { io_info, class: _, tgt_fd } = self;
|
|
Self { io_info, class: Some(class), tgt_fd }
|
|
}
|
|
pub fn with_tgt(self, tgt_fd: RawFd) -> Self {
|
|
let Self { io_info, class, tgt_fd: _ } = self;
|
|
Self { io_info, class, tgt_fd: Some(tgt_fd) }
|
|
}
|
|
pub fn build(self) -> Redir {
|
|
Redir::new(self.io_info.unwrap(), self.class.unwrap())
|
|
}
|
|
}
|
|
|
|
impl FromStr for RedirBldr {
|
|
type Err = ();
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
let mut chars = s.chars().peekable();
|
|
let mut src_fd = String::new();
|
|
let mut tgt_fd = String::new();
|
|
let mut redir = RedirBldr::new();
|
|
|
|
while let Some(ch) = chars.next() {
|
|
match ch {
|
|
'>' => {
|
|
redir = redir.with_class(RedirType::Output);
|
|
if let Some('>') = chars.peek() {
|
|
chars.next();
|
|
redir = redir.with_class(RedirType::Append);
|
|
}
|
|
}
|
|
'<' => {
|
|
redir = redir.with_class(RedirType::Input);
|
|
let mut count = 0;
|
|
|
|
while count < 2 && matches!(chars.peek(), Some('<')) {
|
|
chars.next();
|
|
count += 1;
|
|
}
|
|
|
|
redir = match count {
|
|
1 => redir.with_class(RedirType::HereDoc),
|
|
2 => redir.with_class(RedirType::HereString),
|
|
_ => redir, // Default case remains RedirType::Input
|
|
};
|
|
}
|
|
'&' => {
|
|
while let Some(next_ch) = chars.next() {
|
|
if next_ch.is_ascii_digit() {
|
|
src_fd.push(next_ch)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
if src_fd.is_empty() {
|
|
return Err(())
|
|
}
|
|
}
|
|
_ if ch.is_ascii_digit() && tgt_fd.is_empty() => {
|
|
tgt_fd.push(ch);
|
|
while let Some(next_ch) = chars.peek() {
|
|
if next_ch.is_ascii_digit() {
|
|
let next_ch = chars.next().unwrap();
|
|
tgt_fd.push(next_ch);
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
_ => return Err(())
|
|
}
|
|
}
|
|
|
|
// FIXME: I am 99.999999999% sure that tgt_fd and src_fd are backwards here
|
|
let tgt_fd = tgt_fd.parse::<i32>().unwrap_or_else(|_| {
|
|
match redir.class.unwrap() {
|
|
RedirType::Input |
|
|
RedirType::HereDoc |
|
|
RedirType::HereString => 0,
|
|
_ => 1
|
|
}
|
|
});
|
|
redir = redir.with_tgt(tgt_fd);
|
|
if let Ok(src_fd) = src_fd.parse::<i32>() {
|
|
let io_info = IoFd::new(tgt_fd, src_fd);
|
|
redir = redir.with_io_info(Box::new(io_info));
|
|
}
|
|
Ok(redir)
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq,Clone,Copy,Debug)]
|
|
pub enum RedirType {
|
|
Null, // Default
|
|
Pipe, // |
|
|
PipeAnd, // |&, redirs stderr and stdout
|
|
Input, // <
|
|
Output, // >
|
|
Append, // >>
|
|
HereDoc, // <<
|
|
HereString, // <<<
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct CondNode<'t> {
|
|
pub cond: Vec<Node<'t>>,
|
|
pub body: Vec<Node<'t>>
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct CaseNode<'t> {
|
|
pub pattern: Tk<'t>,
|
|
pub body: Vec<Node<'t>>
|
|
}
|
|
|
|
#[derive(Clone,Copy,PartialEq,Debug)]
|
|
pub enum ConjunctOp {
|
|
And,
|
|
Or,
|
|
Null
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct ConjunctNode<'t> {
|
|
pub cmd: Box<Node<'t>>,
|
|
pub operator: ConjunctOp
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum LoopKind {
|
|
While,
|
|
Until
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum AssignKind {
|
|
Eq,
|
|
PlusEq,
|
|
MinusEq,
|
|
MultEq,
|
|
DivEq,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum NdRule<'t> {
|
|
IfNode { cond_blocks: Vec<CondNode<'t>>, else_block: Vec<Node<'t>> },
|
|
LoopNode { kind: LoopKind, cond_block: CondNode<'t> },
|
|
ForNode { vars: Vec<Tk<'t>>, arr: Vec<Tk<'t>>, body: Vec<Node<'t>> },
|
|
CaseNode { pattern: Tk<'t>, case_blocks: Vec<CaseNode<'t>> },
|
|
Command { assignments: Vec<Tk<'t>>, argv: Vec<Tk<'t>> },
|
|
Pipeline { cmds: Vec<Node<'t>>, pipe_err: bool },
|
|
CmdList { elements: Vec<ConjunctNode<'t>> },
|
|
Assignment { kind: AssignKind, var: Tk<'t>, val: Tk<'t> },
|
|
}
|
|
|
|
/// If the given expression returns Some(), then return it
|
|
|
|
#[derive(Debug)]
|
|
pub enum ParseResult<'t> {
|
|
NoMatch,
|
|
Error(ShErr<'t>),
|
|
Match(Node<'t>)
|
|
}
|
|
use ParseResult::*; // muh ergonomics
|
|
|
|
#[derive(Debug)]
|
|
pub struct ParseStream<'t> {
|
|
pub tokens: Vec<Tk<'t>>,
|
|
pub flags: ParseFlags
|
|
}
|
|
|
|
bitflags! {
|
|
#[derive(Debug)]
|
|
pub struct ParseFlags: u32 {
|
|
const ERROR = 0b0000001;
|
|
}
|
|
}
|
|
|
|
impl<'t> ParseStream<'t> {
|
|
pub fn new(tokens: Vec<Tk<'t>>) -> Self {
|
|
Self { tokens, flags: ParseFlags::empty() }
|
|
}
|
|
fn next_tk_class(&self) -> &TkRule {
|
|
if let Some(tk) = self.tokens.first() {
|
|
&tk.class
|
|
} else {
|
|
&TkRule::Null
|
|
}
|
|
}
|
|
fn next_tk(&mut self) -> Option<Tk<'t>> {
|
|
if !self.tokens.is_empty() {
|
|
if *self.next_tk_class() == TkRule::EOI {
|
|
return None
|
|
}
|
|
Some(self.tokens.remove(0))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
/// Slice off consumed tokens
|
|
fn commit(&mut self, num_consumed: usize) {
|
|
assert!(num_consumed <= self.tokens.len());
|
|
self.tokens = self.tokens[num_consumed..].to_vec();
|
|
}
|
|
fn parse_cmd_list(&mut self) -> ParseResult<'t> {
|
|
let mut elements = vec![];
|
|
let mut node_tks = vec![];
|
|
|
|
while let Match(block) = self.parse_block(true) {
|
|
node_tks.append(&mut block.tokens.clone());
|
|
let conjunct_op = match self.next_tk_class() {
|
|
TkRule::And => ConjunctOp::And,
|
|
TkRule::Or => ConjunctOp::Or,
|
|
_ => ConjunctOp::Null
|
|
};
|
|
let conjunction = ConjunctNode { cmd: Box::new(block), operator: conjunct_op };
|
|
elements.push(conjunction);
|
|
let Some(tk) = self.next_tk() else {
|
|
break
|
|
};
|
|
node_tks.push(tk);
|
|
if conjunct_op == ConjunctOp::Null {
|
|
break
|
|
}
|
|
}
|
|
Match(Node {
|
|
class: NdRule::CmdList { elements },
|
|
flags: NdFlags::empty(),
|
|
redirs: vec![],
|
|
tokens: node_tks
|
|
})
|
|
}
|
|
/// This tries to match on different stuff that can appear in a command position
|
|
/// Matches shell commands like if-then-fi, pipelines, etc.
|
|
/// Ordered from specialized to general, with more generally matchable stuff appearing at the bottom
|
|
/// The check_pipelines parameter is used to prevent infinite recursion in parse_pipeline
|
|
fn parse_block(&mut self, check_pipelines: bool) -> ParseResult<'t> {
|
|
if check_pipelines {
|
|
match self.parse_pipeline() {
|
|
NoMatch => { /* Continue */ }
|
|
result => return result
|
|
}
|
|
} else {
|
|
match self.parse_cmd() {
|
|
NoMatch => { /* Continue */ }
|
|
result => return result
|
|
}
|
|
}
|
|
NoMatch
|
|
}
|
|
fn parse_pipeline(&mut self) -> ParseResult<'t> {
|
|
let mut cmds = vec![];
|
|
let mut node_tks = vec![];
|
|
while let Match(cmd) = self.parse_block(false) {
|
|
let is_punctuated = node_is_punctuated(&cmd.tokens);
|
|
node_tks.append(&mut cmd.tokens.clone());
|
|
cmds.push(cmd);
|
|
if *self.next_tk_class() != TkRule::Pipe || is_punctuated {
|
|
break
|
|
} else {
|
|
if let Some(pipe) = self.next_tk() {
|
|
node_tks.push(pipe)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if cmds.is_empty() {
|
|
NoMatch
|
|
} else {
|
|
Match(Node {
|
|
// TODO: implement pipe_err support
|
|
class: NdRule::Pipeline { cmds, pipe_err: false },
|
|
flags: NdFlags::empty(),
|
|
redirs: vec![],
|
|
tokens: node_tks
|
|
})
|
|
}
|
|
}
|
|
fn parse_cmd(&mut self) -> ParseResult<'t> {
|
|
let tk_slice = self.tokens.as_slice();
|
|
let mut tk_iter = tk_slice.iter();
|
|
let mut node_tks = vec![];
|
|
let mut redirs = vec![];
|
|
let mut argv = vec![];
|
|
let mut assignments = vec![];
|
|
|
|
let Some(cmd_tk) = tk_iter.next() else {
|
|
return NoMatch
|
|
};
|
|
|
|
if !cmd_tk.flags.contains(TkFlags::IS_CMD) {
|
|
return NoMatch
|
|
} else {
|
|
node_tks.push(cmd_tk.clone());
|
|
argv.push(cmd_tk.clone());
|
|
}
|
|
|
|
while let Some(tk) = tk_iter.next() {
|
|
match tk.class {
|
|
TkRule::EOI |
|
|
TkRule::Pipe |
|
|
TkRule::And |
|
|
TkRule::Or => {
|
|
break
|
|
}
|
|
TkRule::Sep => {
|
|
node_tks.push(tk.clone());
|
|
break
|
|
}
|
|
TkRule::Str => {
|
|
argv.push(tk.clone());
|
|
node_tks.push(tk.clone());
|
|
}
|
|
TkRule::Redir => {
|
|
node_tks.push(tk.clone());
|
|
let redir_bldr = tk.span.as_str().parse::<RedirBldr>().unwrap();
|
|
if redir_bldr.io_info.is_none() {
|
|
let Some(path_tk) = tk_iter.next() else {
|
|
self.flags |= ParseFlags::ERROR;
|
|
return Error(
|
|
ShErr::full(
|
|
ShErrKind::ParseErr,
|
|
"Expected a filename after this redirection",
|
|
tk.span.clone()
|
|
)
|
|
)
|
|
};
|
|
node_tks.push(path_tk.clone());
|
|
|
|
let Ok(file) = (match redir_bldr.class.unwrap() {
|
|
RedirType::Input => {
|
|
OpenOptions::new()
|
|
.read(true)
|
|
.open(Path::new(path_tk.span.as_str()))
|
|
}
|
|
RedirType::Output => {
|
|
OpenOptions::new()
|
|
.write(true)
|
|
.create(true)
|
|
.truncate(true)
|
|
.open(Path::new(path_tk.span.as_str()))
|
|
}
|
|
RedirType::Append => {
|
|
OpenOptions::new()
|
|
.write(true)
|
|
.create(true)
|
|
.append(true)
|
|
.open(Path::new(path_tk.span.as_str()))
|
|
}
|
|
_ => unreachable!()
|
|
}) else {
|
|
self.flags |= ParseFlags::ERROR;
|
|
return Error(
|
|
ShErr::full(
|
|
ShErrKind::InternalErr,
|
|
"Error opening file for redirection",
|
|
path_tk.span.clone()
|
|
)
|
|
)
|
|
};
|
|
|
|
let io_info = IoFile::new(redir_bldr.tgt_fd.unwrap(), file);
|
|
let redir_bldr = redir_bldr.with_io_info(Box::new(io_info));
|
|
let redir = redir_bldr.build();
|
|
redirs.push(redir);
|
|
}
|
|
}
|
|
_ => unimplemented!("Unexpected token rule `{:?}` in parse_cmd()",tk.class)
|
|
}
|
|
}
|
|
self.commit(node_tks.len());
|
|
|
|
Match(Node {
|
|
class: NdRule::Command { assignments, argv },
|
|
tokens: node_tks,
|
|
flags: NdFlags::empty(),
|
|
redirs,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl<'t> Iterator for ParseStream<'t> {
|
|
type Item = ParseResult<'t>;
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
// Empty token vector or only SOI/EOI tokens, nothing to do
|
|
if self.tokens.is_empty() || self.tokens.len() == 2 {
|
|
return None
|
|
}
|
|
if self.flags.contains(ParseFlags::ERROR) {
|
|
return None
|
|
}
|
|
if let Some(tk) = self.tokens.first() {
|
|
if tk.class == TkRule::EOI {
|
|
return None
|
|
}
|
|
if tk.class == TkRule::SOI {
|
|
self.next_tk();
|
|
}
|
|
}
|
|
match self.parse_cmd_list() {
|
|
NoMatch => None,
|
|
Error(err) => Some(Error(err)),
|
|
Match(cmdlist) => Some(Match(cmdlist))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn node_is_punctuated<'t>(tokens: &Vec<Tk>) -> bool {
|
|
tokens.last().is_some_and(|tk| {
|
|
matches!(tk.class, TkRule::Sep)
|
|
})
|
|
}
|