diff --git a/src/builtin/cd.rs b/src/builtin/cd.rs new file mode 100644 index 0000000..0eb548a --- /dev/null +++ b/src/builtin/cd.rs @@ -0,0 +1,29 @@ +use crate::{jobs::{ChildProc, JobBldr}, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{execute::prepare_argv, NdRule, Node}, prelude::*, state::write_vars}; + +pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> { + let NdRule::Command { assignments: _, argv } = node.class else { + unreachable!() + }; + + let child_pgid = if let Some(pgid) = job.pgid() { + pgid + } else { + job.set_pgid(Pid::this()); + Pid::this() + }; + let child = ChildProc::new(Pid::this(), Some("cd"), Some(child_pgid))?; + job.push_child(child); + + let argv = prepare_argv(argv); + let new_dir = if let Some((arg,_)) = argv.into_iter().skip(1).next() { + PathBuf::from(arg) + } else { + PathBuf::from(env::var("HOME").unwrap()) + }; + + env::set_current_dir(new_dir).unwrap(); + let new_dir = env::current_dir().unwrap(); + env::set_var("PWD", new_dir); + + Ok(()) +} diff --git a/src/builtin/echo.rs b/src/builtin/echo.rs index 2511f3b..b237c4c 100644 --- a/src/builtin/echo.rs +++ b/src/builtin/echo.rs @@ -15,9 +15,7 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( let child = ChildProc::new(Pid::this(), Some("echo"), Some(child_pgid))?; job.push_child(child); - for redir in node.redirs { - io_stack.push_to_frame(redir); - } + io_stack.append_to_frame(node.redirs); let mut io_frame = io_stack.pop_frame(); io_frame.redirect()?; @@ -26,6 +24,7 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( let mut echo_output = prepare_argv(argv) .into_iter() + .map(|a| a.0) // Extract the String from the tuple of (String,Span) .skip(1) // Skip 'echo' .collect::>() .join(" "); diff --git a/src/builtin/export.rs b/src/builtin/export.rs new file mode 100644 index 0000000..8c7085c --- /dev/null +++ b/src/builtin/export.rs @@ -0,0 +1,32 @@ +use crate::{jobs::{ChildProc, JobBldr}, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{execute::prepare_argv, NdRule, Node}, prelude::*}; + +pub fn export(node: Node, job: &mut JobBldr) -> ShResult<()> { + let NdRule::Command { assignments: _, argv } = node.class else { + unreachable!() + }; + + let child_pgid = if let Some(pgid) = job.pgid() { + pgid + } else { + job.set_pgid(Pid::this()); + Pid::this() + }; + let child = ChildProc::new(Pid::this(), Some("export"), Some(child_pgid))?; + job.push_child(child); + + let argv = prepare_argv(argv); + + for (arg,span) in argv { + let Some((var,val)) = arg.split_once('=') else { + return Err( + ShErr::full( + ShErrKind::ExecFail, + "Expected an assignment in export args", + span.into() + ) + ) + }; + env::set_var(var, val); + } + Ok(()) +} diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index 612bde4..e272e1d 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -1,5 +1,15 @@ pub mod echo; +pub mod cd; +pub mod export; +pub mod pwd; +pub mod source; +pub mod shift; -pub const BUILTINS: [&str;1] = [ - "echo" +pub const BUILTINS: [&str;6] = [ + "echo", + "cd", + "export", + "pwd", + "source", + "shift" ]; diff --git a/src/builtin/pwd.rs b/src/builtin/pwd.rs new file mode 100644 index 0000000..81b194e --- /dev/null +++ b/src/builtin/pwd.rs @@ -0,0 +1,30 @@ +use crate::{jobs::{ChildProc, JobBldr}, libsh::error::ShResult, parse::{NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}}; + +pub fn pwd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { + let NdRule::Command { assignments, argv } = node.class else { + unreachable!() + }; + + let child_pgid = if let Some(pgid) = job.pgid() { + pgid + } else { + job.set_pgid(Pid::this()); + Pid::this() + }; + let child = ChildProc::new(Pid::this(), Some("pwd"), Some(child_pgid))?; + job.push_child(child); + + io_stack.append_to_frame(node.redirs); + let mut io_frame = io_stack.pop_frame(); + + io_frame.redirect()?; + + let stdout = borrow_fd(STDOUT_FILENO); + + let mut curr_dir = env::current_dir().unwrap().to_str().unwrap().to_string(); + curr_dir.push('\n'); + write(stdout, curr_dir.as_bytes())?; + io_frame.restore().unwrap(); + + Ok(()) +} diff --git a/src/builtin/shift.rs b/src/builtin/shift.rs new file mode 100644 index 0000000..6ea8481 --- /dev/null +++ b/src/builtin/shift.rs @@ -0,0 +1,35 @@ +use crate::{jobs::{ChildProc, JobBldr}, libsh::error::{ErrSpan, ShErr, ShErrKind, ShResult}, parse::{execute::prepare_argv, NdRule, Node}, prelude::*, state::write_vars}; + +pub fn shift(node: Node, job: &mut JobBldr) -> ShResult<()> { + let NdRule::Command { assignments: _, argv } = node.class else { + unreachable!() + }; + + let child_pgid = if let Some(pgid) = job.pgid() { + pgid + } else { + job.set_pgid(Pid::this()); + Pid::this() + }; + let child = ChildProc::new(Pid::this(), Some("shift"), Some(child_pgid))?; + job.push_child(child); + + let mut argv = prepare_argv(argv).into_iter().skip(1); + + if let Some((arg,span)) = argv.next() { + let Ok(count) = arg.parse::() else { + return Err( + ShErr::full( + ShErrKind::ExecFail, + "Expected a number in shift args", + span.into() + ) + ) + }; + for _ in 0..count { + write_vars(|v| v.fpop_arg()); + } + } + + Ok(()) +} diff --git a/src/builtin/source.rs b/src/builtin/source.rs new file mode 100644 index 0000000..989a585 --- /dev/null +++ b/src/builtin/source.rs @@ -0,0 +1,43 @@ +use crate::{jobs::{ChildProc, JobBldr}, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{execute::prepare_argv, NdRule, Node}, prelude::*, state::source_file}; + +pub fn source(node: Node, job: &mut JobBldr) -> ShResult<()> { + let NdRule::Command { assignments: _, argv } = node.class else { + unreachable!() + }; + + let child_pgid = if let Some(pgid) = job.pgid() { + pgid + } else { + job.set_pgid(Pid::this()); + Pid::this() + }; + let child = ChildProc::new(Pid::this(), Some("source"), Some(child_pgid))?; + job.push_child(child); + + let argv = prepare_argv(argv).into_iter().skip(1); + + for (arg,span) in argv { + let path = PathBuf::from(arg); + if !path.exists() { + return Err( + ShErr::full( + ShErrKind::ExecFail, + "source: File not found", + span.into() + ) + ); + } + if !path.is_file() { + return Err( + ShErr::full( + ShErrKind::ExecFail, + "source: Given path is not a file", + span.into() + ) + ); + } + source_file(path)?; + } + + Ok(()) +} diff --git a/src/expand.rs b/src/expand.rs index 0405640..56bb0e8 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -1,4 +1,4 @@ -use crate::{parse::lex::{is_hard_sep, LexFlags, LexStream, Tk, Span, TkErr, TkFlags, TkRule}, state::read_vars}; +use crate::{parse::lex::{is_hard_sep, LexFlags, LexStream, Tk, Span, TkFlags, TkRule}, state::read_vars}; /// Variable substitution marker pub const VAR_SUB: char = '\u{fdd0}'; @@ -13,7 +13,7 @@ impl<'t> Tk<'t> { pub fn expand(self, span: Span<'t>, flags: TkFlags) -> Self { let exp = Expander::new(self).expand(); let class = TkRule::Expanded { exp }; - Self { class, span, err_span: None, flags, err: TkErr::Null } + Self { class, span, flags, } } pub fn get_words(&self) -> Vec { match &self.class { @@ -34,9 +34,10 @@ impl<'t> Expander { } pub fn expand(&'t mut self) -> Vec { self.raw = self.expand_raw(); + // Unwrap here is safe because LexFlags::RAW has no error states let tokens: Vec<_> = LexStream::new(&self.raw, LexFlags::RAW) - .filter(|tk| !matches!(tk.class, TkRule::EOI | TkRule::SOI)) - .map(|tk| tk.to_string()) + .filter(|tk| !matches!(tk.as_ref().unwrap().class, TkRule::EOI | TkRule::SOI)) + .map(|tk| tk.unwrap().to_string()) .collect(); tokens } @@ -82,6 +83,8 @@ impl<'t> Expander { } } +/// Processes strings into intermediate representations that are more readable by the program +/// /// Clean up a single layer of escape characters, and then replace control characters like '$' with a non-character unicode representation that is unmistakable by the rest of the code pub fn unescape_str(raw: &str) -> String { let mut chars = raw.chars(); diff --git a/src/fern.rs b/src/fern.rs index bd34268..587dab2 100644 --- a/src/fern.rs +++ b/src/fern.rs @@ -11,6 +11,7 @@ pub mod signal; #[cfg(test)] pub mod tests; +use libsh::error::ShResult; use parse::{execute::Dispatcher, lex::{LexFlags, LexStream}, ParseStream}; use termios::{LocalFlags, Termios}; use crate::prelude::*; @@ -44,34 +45,32 @@ fn set_termios() { } } +pub fn exec_input(input: &str) -> ShResult<()> { + let mut tokens = vec![]; + for token in LexStream::new(&input, LexFlags::empty()) { + tokens.push(token?); + } + + let mut nodes = vec![]; + for result in ParseStream::new(tokens) { + nodes.push(result?); + } + + let mut dispatcher = Dispatcher::new(nodes); + dispatcher.begin_dispatch()?; + Ok(()) +} + fn main() { - 'main: loop { + save_termios(); + set_termios(); + loop { let input = prompt::read_line().unwrap(); - if input == "quit" { break }; let start = Instant::now(); - let mut tokens = vec![]; - for token in LexStream::new(&input, LexFlags::empty()) { - if token.is_err() { - let error = format!("{:?}: {}",token.err,token.err_span.unwrap().as_str()); - panic!("{error}"); - } - tokens.push(token); + if let Err(e) = exec_input(&input) { + eprintln!("{e}"); } - - let mut nodes = vec![]; - for result in ParseStream::new(tokens) { - match result { - Ok(node) => nodes.push(node), - Err(e) => { - eprintln!("{:?}",e); - continue 'main // Isn't rust cool - } - } - } - - let mut dispatcher = Dispatcher::new(nodes); - dispatcher.begin_dispatch().unwrap(); - flog!(INFO, "elapsed: {:?}", start.elapsed()); + flog!(INFO, "cmd duration: {:?}", start.elapsed()); } } diff --git a/src/jobs.rs b/src/jobs.rs index 8ded499..b49a218 100644 --- a/src/jobs.rs +++ b/src/jobs.rs @@ -120,6 +120,204 @@ impl<'a> ChildProc { } } +pub struct JobTab { + fg: Option, + order: Vec, + new_updates: Vec, + jobs: Vec> +} + +impl JobTab { + pub fn new() -> Self { + Self { fg: None, order: vec![], new_updates: vec![], jobs: vec![] } + } + pub fn take_fg(&mut self) -> Option { + self.fg.take() + } + fn next_open_pos(&self) -> usize { + if let Some(position) = self.jobs.iter().position(|slot| slot.is_none()) { + position + } else { + self.jobs.len() + } + } + pub fn jobs(&self) -> &Vec> { + &self.jobs + } + pub fn jobs_mut(&mut self) -> &mut Vec> { + &mut self.jobs + } + pub fn curr_job(&self) -> Option { + self.order.last().copied() + } + pub fn prev_job(&self) -> Option { + self.order.last().copied() + } + fn prune_jobs(&mut self) { + while let Some(job) = self.jobs.last() { + if job.is_none() { + self.jobs.pop(); + } else { + break + } + } + } + pub fn insert_job(&mut self, mut job: Job, silent: bool) -> ShResult { + self.prune_jobs(); + let tab_pos = if let Some(id) = job.tabid() { id } else { self.next_open_pos() }; + job.set_tabid(tab_pos); + self.order.push(tab_pos); + if !silent { + write(borrow_fd(1),format!("{}", job.display(&self.order, JobCmdFlags::INIT)).as_bytes())?; + } + if tab_pos == self.jobs.len() { + self.jobs.push(Some(job)) + } else { + self.jobs[tab_pos] = Some(job); + } + Ok(tab_pos) + } + pub fn order(&self) -> &[usize] { + &self.order + } + pub fn query(&self, identifier: JobID) -> Option<&Job> { + match identifier { + // Match by process group ID + JobID::Pgid(pgid) => { + self.jobs.iter().find_map(|job| { + job.as_ref().filter(|j| j.pgid() == pgid) + }) + } + // Match by process ID + JobID::Pid(pid) => { + self.jobs.iter().find_map(|job| { + job.as_ref().filter(|j| j.children().iter().any(|child| child.pid() == pid)) + }) + } + // Match by table ID (index in the job table) + JobID::TableID(id) => { + self.jobs.get(id).and_then(|job| job.as_ref()) + } + // Match by command name (partial match) + JobID::Command(cmd) => { + self.jobs.iter().find_map(|job| { + job.as_ref().filter(|j| { + j.children().iter().any(|child| { + child.cmd().as_ref().is_some_and(|c| c.contains(&cmd)) + }) + }) + }) + } + } + } + pub fn query_mut(&mut self, identifier: JobID) -> Option<&mut Job> { + match identifier { + // Match by process group ID + JobID::Pgid(pgid) => { + self.jobs.iter_mut().find_map(|job| { + job.as_mut().filter(|j| j.pgid() == pgid) + }) + } + // Match by process ID + JobID::Pid(pid) => { + self.jobs.iter_mut().find_map(|job| { + job.as_mut().filter(|j| j.children().iter().any(|child| child.pid() == pid)) + }) + } + // Match by table ID (index in the job table) + JobID::TableID(id) => { + self.jobs.get_mut(id).and_then(|job| job.as_mut()) + } + // Match by command name (partial match) + JobID::Command(cmd) => { + self.jobs.iter_mut().find_map(|job| { + job.as_mut().filter(|j| { + j.children().iter().any(|child| { + child.cmd().as_ref().is_some_and(|c| c.contains(&cmd)) + }) + }) + }) + } + } + } + pub fn get_fg(&self) -> Option<&Job> { + self.fg.as_ref() + } + pub fn get_fg_mut(&mut self) -> Option<&mut Job> { + self.fg.as_mut() + } + pub fn new_fg<'a>(&mut self, job: Job) -> ShResult> { + let pgid = job.pgid(); + self.fg = Some(job); + attach_tty(pgid)?; + let statuses = self.fg.as_mut().unwrap().wait_pgrp()?; + attach_tty(getpgrp())?; + Ok(statuses) + } + pub fn fg_to_bg(&mut self, stat: WtStat) -> ShResult<()> { + if self.fg.is_none() { + return Ok(()) + } + take_term()?; + let fg = std::mem::take(&mut self.fg); + if let Some(mut job) = fg { + job.set_stats(stat); + self.insert_job(job, false)?; + } + Ok(()) + } + pub fn bg_to_fg(&mut self, id: JobID) -> ShResult<()> { + let job = self.remove_job(id); + if let Some(job) = job { + wait_fg(job)?; + } + Ok(()) + } + pub fn remove_job(&mut self, id: JobID) -> Option { + let tabid = self.query(id).map(|job| job.tabid().unwrap()); + if let Some(tabid) = tabid { + self.jobs.get_mut(tabid).and_then(Option::take) + } else { + None + } + } + pub fn print_jobs(&mut self, flags: JobCmdFlags) -> ShResult<()> { + let jobs = if flags.contains(JobCmdFlags::NEW_ONLY) { + &self.jobs + .iter() + .filter(|job| job.as_ref().is_some_and(|job| self.new_updates.contains(&job.tabid().unwrap()))) + .map(|job| job.as_ref()) + .collect::>>() + } else { + &self.jobs + .iter() + .map(|job| job.as_ref()) + .collect::>>() + }; + let mut jobs_to_remove = vec![]; + for job in jobs.iter().flatten() { + // Skip foreground job + let id = job.tabid().unwrap(); + // Filter jobs based on flags + if flags.contains(JobCmdFlags::RUNNING) && !matches!(job.get_stats().get(id).unwrap(), WtStat::StillAlive | WtStat::Continued(_)) { + continue; + } + if flags.contains(JobCmdFlags::STOPPED) && !matches!(job.get_stats().get(id).unwrap(), WtStat::Stopped(_,_)) { + continue; + } + // Print the job in the selected format + write(borrow_fd(1), format!("{}\n",job.display(&self.order,flags)).as_bytes())?; + if job.get_stats().iter().all(|stat| matches!(stat,WtStat::Exited(_, _))) { + jobs_to_remove.push(JobID::TableID(id)); + } + } + for id in jobs_to_remove { + self.remove_job(id); + } + Ok(()) + } +} + pub struct JobBldr { table_id: Option, pgid: Option, @@ -237,8 +435,15 @@ impl Job { } pub fn wait_pgrp<'a>(&mut self) -> ShResult> { let mut stats = vec![]; + flog!(TRACE, "waiting on children"); + flog!(TRACE, self.children); for child in self.children.iter_mut() { - let result = child.wait(Some(WtFlag::WUNTRACED)); + if child.pid == Pid::this() { + // TODO: figure out some way to get the exit code of builtins + stats.push(WtStat::Exited(child.pid, 0)); + continue + } + let result = child.wait(Some(WtFlag::WSTOPPED)); match result { Ok(stat) => { stats.push(stat); diff --git a/src/libsh/error.rs b/src/libsh/error.rs index 6ba4eeb..5bc98c2 100644 --- a/src/libsh/error.rs +++ b/src/libsh/error.rs @@ -29,9 +29,8 @@ impl<'s> ShErr { let msg = msg.into(); Self::Simple { kind, msg } } - pub fn full(kind: ShErrKind, msg: impl Into, span: Span<'s>) -> Self { + pub fn full(kind: ShErrKind, msg: impl Into, span: ErrSpan) -> Self { let msg = msg.into(); - let span = span.into(); Self::Full { kind, msg, span } } pub fn unpack(self) -> (ShErrKind,String,Option) { diff --git a/src/libsh/mod.rs b/src/libsh/mod.rs index fd1f69b..a6e2e8c 100644 --- a/src/libsh/mod.rs +++ b/src/libsh/mod.rs @@ -2,3 +2,4 @@ pub mod error; pub mod term; pub mod flog; pub mod sys; +pub mod utils; diff --git a/src/libsh/utils.rs b/src/libsh/utils.rs new file mode 100644 index 0000000..76cd17b --- /dev/null +++ b/src/libsh/utils.rs @@ -0,0 +1,11 @@ +use std::collections::VecDeque; + +pub trait VecDequeExt { + fn to_vec(self) -> Vec; +} + +impl VecDequeExt for VecDeque { + fn to_vec(self) -> Vec { + self.into_iter().collect::>() + } +} diff --git a/src/parse/execute.rs b/src/parse/execute.rs index 22b1f31..658de71 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -1,9 +1,9 @@ use std::collections::VecDeque; -use crate::{builtin::echo::echo, jobs::{dispatch_job, ChildProc, Job, JobBldr}, libsh::error::ShResult, prelude::*, procio::{IoFrame, IoPipe, IoStack}, state::{self, write_vars}}; +use crate::{builtin::{cd::cd, echo::echo, export::export, pwd::pwd, shift::shift, source::source}, jobs::{dispatch_job, ChildProc, Job, JobBldr}, libsh::error::ShResult, prelude::*, procio::{IoFrame, IoPipe, IoStack}, state::{self, write_vars}}; -use super::{lex::{Tk, TkFlags}, AssignKind, ConjunctNode, ConjunctOp, NdFlags, NdRule, Node, Redir, RedirType}; +use super::{lex::{Span, Tk, TkFlags}, AssignKind, ConjunctNode, ConjunctOp, NdFlags, NdRule, Node, Redir, RedirType}; pub enum AssignBehavior { Export, @@ -26,11 +26,11 @@ impl ExecArgs { let envp = Self::get_envp(); Self { cmd, argv, envp } } - pub fn get_cmd(argv: &[String]) -> CString { - CString::new(argv[0].as_str()).unwrap() + pub fn get_cmd(argv: &[(String,Span)]) -> CString { + CString::new(argv[0].0.as_str()).unwrap() } - pub fn get_argv(argv: Vec) -> Vec { - argv.into_iter().map(|s| CString::new(s).unwrap()).collect() + pub fn get_argv(argv: Vec<(String,Span)>) -> Vec { + argv.into_iter().map(|s| CString::new(s.0).unwrap()).collect() } pub fn get_envp() -> Vec { std::env::vars().map(|v| CString::new(format!("{}={}",v.0,v.1)).unwrap()).collect() @@ -128,8 +128,14 @@ impl<'t> Dispatcher<'t> { let cmd_raw = cmd.get_command().unwrap(); flog!(TRACE, "doing builtin"); let curr_job_mut = self.curr_job.as_mut().unwrap(); + let io_stack_mut = &mut self.io_stack; let result = match cmd_raw.span.as_str() { - "echo" => echo(cmd, &mut self.io_stack, curr_job_mut), + "echo" => echo(cmd, io_stack_mut, curr_job_mut), + "cd" => cd(cmd, curr_job_mut), + "export" => export(cmd, curr_job_mut), + "pwd" => pwd(cmd, io_stack_mut, curr_job_mut), + "source" => source(cmd, curr_job_mut), + "shift" => shift(cmd, curr_job_mut), _ => unimplemented!("Have not yet added support for builtin '{}'", cmd_raw.span.as_str()) }; @@ -152,13 +158,13 @@ impl<'t> Dispatcher<'t> { }; env_vars_to_unset = self.set_assignments(assignments, assign_behavior); } - for redir in cmd.redirs { - self.io_stack.push_to_frame(redir); - } + if argv.is_empty() { return Ok(()) } + self.io_stack.append_to_frame(cmd.redirs); + let exec_args = ExecArgs::new(argv); let io_frame = self.io_stack.pop_frame(); run_fork( @@ -216,14 +222,16 @@ impl<'t> Dispatcher<'t> { } } -pub fn prepare_argv(argv: Vec) -> Vec { +pub fn prepare_argv(argv: Vec) -> Vec<(String,Span)> { let mut args = vec![]; for arg in argv { let flags = arg.flags; let span = arg.span.clone(); - let expanded = arg.expand(span, flags); - args.extend(expanded.get_words()); + let expanded = arg.expand(span.clone(), flags); + for exp in expanded.get_words() { + args.push((exp,span.clone())) + } } args } diff --git a/src/parse/lex.rs b/src/parse/lex.rs index 66df6cd..a118765 100644 --- a/src/parse/lex.rs +++ b/src/parse/lex.rs @@ -2,7 +2,7 @@ use std::{fmt::Display, ops::{Bound, Deref, Range, RangeBounds}}; use bitflags::bitflags; -use crate::{builtin::BUILTINS, prelude::*}; +use crate::{builtin::BUILTINS, libsh::error::{ShErr, ShErrKind, ShResult}, prelude::*}; pub const KEYWORDS: [&'static str;14] = [ "if", @@ -87,33 +87,9 @@ impl Default for TkRule { } } -#[derive(Clone,Copy,PartialEq,Debug)] -pub enum TkErr { - Null, - UntermQuote, - UntermSubsh, - UntermEscape, - UntermBrace, - BadRedir, - BadPipe, - HangingDelim, -} - -impl Default for TkErr { - fn default() -> Self { - TkErr::Null - } -} - -pub enum TkState { - Raw, -} - #[derive(Clone,Debug,PartialEq,Default)] pub struct Tk<'s> { pub class: TkRule, - pub err_span: Option>, - pub err: TkErr, pub span: Span<'s>, pub flags: TkFlags } @@ -121,7 +97,7 @@ pub struct Tk<'s> { // There's one impl here and then another in expand.rs which has the expansion logic impl<'s> Tk<'s> { pub fn new(class: TkRule, span: Span<'s>) -> Self { - Self { class, err_span: None, err: TkErr::Null, span, flags: TkFlags::empty() } + Self { class, span, flags: TkFlags::empty() } } pub fn to_string(&self) -> String { match &self.class { @@ -129,13 +105,6 @@ impl<'s> Tk<'s> { _ => self.span.as_str().to_string() } } - pub fn set_err(&mut self, range: Range, slice: &'s str, err: TkErr) { - self.err_span = Some(Span::new(range, slice)); - self.err = err - } - pub fn is_err(&self) -> bool { - self.err_span.is_some() - } pub fn source(&self) -> &'s str { self.span.source } @@ -231,7 +200,7 @@ impl<'t> LexStream<'t> { pub fn next_is_not_cmd(&mut self) { self.flags &= !LexFlags::NEXT_IS_CMD; } - pub fn read_redir(&mut self) -> Option> { + pub fn read_redir(&mut self) -> Option>> { assert!(self.cursor <= self.source.len()); let slice = self.slice(self.cursor..)?; let mut pos = self.cursor; @@ -259,10 +228,13 @@ impl<'t> LexStream<'t> { if !found_fd { - let err = TkErr::BadRedir; - tk = self.get_token(self.cursor..pos, TkRule::Redir); - tk.set_err(self.cursor..pos, self.source, err); - break + return Some(Err( + ShErr::full( + ShErrKind::ParseErr, + "Invalid redirection", + Span::new(self.cursor..pos, self.source).into() + ) + )); } else { tk = self.get_token(self.cursor..pos, TkRule::Redir); break @@ -304,9 +276,9 @@ impl<'t> LexStream<'t> { } self.cursor = pos; - Some(tk) + Some(Ok(tk)) } - pub fn read_string(&mut self) -> Tk<'t> { + pub fn read_string(&mut self) -> ShResult> { assert!(self.cursor <= self.source.len()); let slice = self.slice_from_cursor().unwrap(); let mut pos = self.cursor; @@ -351,10 +323,12 @@ impl<'t> LexStream<'t> { } let mut new_tk = self.get_token(self.cursor..pos, TkRule::Str); if self.in_quote && !self.flags.contains(LexFlags::LEX_UNFINISHED) { - new_tk.set_err( - quote_pos.unwrap()..pos, - self.source, - TkErr::UntermQuote + return Err( + ShErr::full( + ShErrKind::ParseErr, + "Unterminated quote", + new_tk.span.into(), + ) ); } if self.flags.contains(LexFlags::NEXT_IS_CMD) { @@ -373,7 +347,7 @@ impl<'t> LexStream<'t> { } } self.cursor = pos; - new_tk + Ok(new_tk) } pub fn get_token(&self, range: Range, class: TkRule) -> Tk<'t> { let span = Span::new(range, self.source); @@ -382,7 +356,7 @@ impl<'t> LexStream<'t> { } impl<'t> Iterator for LexStream<'t> { - type Item = Tk<'t>; + type Item = ShResult>; fn next(&mut self) -> Option { assert!(self.cursor <= self.source.len()); // We are at the end of the input @@ -394,14 +368,14 @@ impl<'t> Iterator for LexStream<'t> { // Return the EOI token let token = self.get_token(self.cursor..self.cursor, TkRule::EOI); self.flags |= LexFlags::STALE; - return Some(token) + return Some(Ok(token)) } } // Return the SOI token if self.flags.contains(LexFlags::FRESH) { self.flags &= !LexFlags::FRESH; let token = self.get_token(self.cursor..self.cursor, TkRule::SOI); - return Some(token) + return Some(Ok(token)) } // If we are just reading raw words, short circuit here @@ -486,13 +460,19 @@ impl<'t> Iterator for LexStream<'t> { _ => { if let Some(tk) = self.read_redir() { self.next_is_not_cmd(); - tk + match tk { + Ok(tk) => tk, + Err(e) => return Some(Err(e)) + } } else { - self.read_string() + match self.read_string() { + Ok(tk) => tk, + Err(e) => return Some(Err(e)) + } } } }; - Some(token) + Some(Ok(token)) } } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index d9cf4f1..6a2be86 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -25,6 +25,16 @@ impl<'t> Node<'t> { let command = argv.iter().find(|tk| tk.flags.contains(TkFlags::IS_CMD))?; Some(command) } + pub fn get_span(&'t self) -> Span<'t> { + let Some(first_tk) = self.tokens.first() else { + unreachable!() + }; + let Some(last_tk) = self.tokens.last() else { + unreachable!() + }; + + Span::new(first_tk.span.start..last_tk.span.end, first_tk.span.get_source()) + } } bitflags! { @@ -267,10 +277,12 @@ impl<'t> ParseStream<'t> { }; 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 { + let Some(tk) = self.next_tk() else { + break + }; + node_tks.push(tk); + } if conjunct_op == ConjunctOp::Null { break } @@ -385,7 +397,7 @@ impl<'t> ParseStream<'t> { ShErr::full( ShErrKind::ParseErr, "Expected a filename after this redirection", - tk.span.clone() + tk.span.clone().into() ) ) }; @@ -420,7 +432,7 @@ impl<'t> ParseStream<'t> { ShErr::full( ShErrKind::InternalErr, "Error opening file for redirection", - path_tk.span.clone() + path_tk.span.clone().into() ) ) }; @@ -565,7 +577,9 @@ impl<'t> Iterator for ParseStream<'t> { } } match self.parse_cmd_list() { - Ok(Some(node)) => return Some(Ok(node)), + Ok(Some(node)) => { + return Some(Ok(node)); + } Ok(None) => return None, Err(e) => return Some(Err(e)) } diff --git a/src/procio.rs b/src/procio.rs index de106ee..52c82f0 100644 --- a/src/procio.rs +++ b/src/procio.rs @@ -299,6 +299,9 @@ impl<'e> IoStack { pub fn push_to_frame(&mut self, redir: Redir) { self.curr_frame_mut().push(redir) } + pub fn append_to_frame(&mut self, mut other: Vec) { + self.curr_frame_mut().append(&mut other) + } /// Pop the current stack frame /// This differs from using `pop()` because it always returns a stack frame /// If `self.pop()` would empty the `IoStack`, it instead uses `std::mem::take()` to take the last frame diff --git a/src/state.rs b/src/state.rs index 22337dc..4c8c0ca 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,219 +1,25 @@ -use std::{collections::HashMap, sync::{LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard}}; +use std::{collections::{HashMap, VecDeque}, sync::{LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard}}; -use crate::{jobs::{attach_tty, take_term, wait_fg, Job, JobCmdFlags, JobID}, libsh::error::ShResult, parse::lex::get_char, prelude::*, procio::borrow_fd}; +use crate::{exec_input, jobs::JobTab, libsh::{error::ShResult, utils::VecDequeExt}, parse::lex::get_char, prelude::*}; pub static JOB_TABLE: LazyLock> = LazyLock::new(|| RwLock::new(JobTab::new())); pub static VAR_TABLE: LazyLock> = LazyLock::new(|| RwLock::new(VarTab::new())); -pub struct JobTab { - fg: Option, - order: Vec, - new_updates: Vec, - jobs: Vec> -} - -impl JobTab { - pub fn new() -> Self { - Self { fg: None, order: vec![], new_updates: vec![], jobs: vec![] } - } - pub fn take_fg(&mut self) -> Option { - self.fg.take() - } - fn next_open_pos(&self) -> usize { - if let Some(position) = self.jobs.iter().position(|slot| slot.is_none()) { - position - } else { - self.jobs.len() - } - } - pub fn jobs(&self) -> &Vec> { - &self.jobs - } - pub fn jobs_mut(&mut self) -> &mut Vec> { - &mut self.jobs - } - pub fn curr_job(&self) -> Option { - self.order.last().copied() - } - pub fn prev_job(&self) -> Option { - self.order.last().copied() - } - fn prune_jobs(&mut self) { - while let Some(job) = self.jobs.last() { - if job.is_none() { - self.jobs.pop(); - } else { - break - } - } - } - pub fn insert_job(&mut self, mut job: Job, silent: bool) -> ShResult { - self.prune_jobs(); - let tab_pos = if let Some(id) = job.tabid() { id } else { self.next_open_pos() }; - job.set_tabid(tab_pos); - self.order.push(tab_pos); - if !silent { - write(borrow_fd(1),format!("{}", job.display(&self.order, JobCmdFlags::INIT)).as_bytes())?; - } - if tab_pos == self.jobs.len() { - self.jobs.push(Some(job)) - } else { - self.jobs[tab_pos] = Some(job); - } - Ok(tab_pos) - } - pub fn order(&self) -> &[usize] { - &self.order - } - pub fn query(&self, identifier: JobID) -> Option<&Job> { - match identifier { - // Match by process group ID - JobID::Pgid(pgid) => { - self.jobs.iter().find_map(|job| { - job.as_ref().filter(|j| j.pgid() == pgid) - }) - } - // Match by process ID - JobID::Pid(pid) => { - self.jobs.iter().find_map(|job| { - job.as_ref().filter(|j| j.children().iter().any(|child| child.pid() == pid)) - }) - } - // Match by table ID (index in the job table) - JobID::TableID(id) => { - self.jobs.get(id).and_then(|job| job.as_ref()) - } - // Match by command name (partial match) - JobID::Command(cmd) => { - self.jobs.iter().find_map(|job| { - job.as_ref().filter(|j| { - j.children().iter().any(|child| { - child.cmd().as_ref().is_some_and(|c| c.contains(&cmd)) - }) - }) - }) - } - } - } - pub fn query_mut(&mut self, identifier: JobID) -> Option<&mut Job> { - match identifier { - // Match by process group ID - JobID::Pgid(pgid) => { - self.jobs.iter_mut().find_map(|job| { - job.as_mut().filter(|j| j.pgid() == pgid) - }) - } - // Match by process ID - JobID::Pid(pid) => { - self.jobs.iter_mut().find_map(|job| { - job.as_mut().filter(|j| j.children().iter().any(|child| child.pid() == pid)) - }) - } - // Match by table ID (index in the job table) - JobID::TableID(id) => { - self.jobs.get_mut(id).and_then(|job| job.as_mut()) - } - // Match by command name (partial match) - JobID::Command(cmd) => { - self.jobs.iter_mut().find_map(|job| { - job.as_mut().filter(|j| { - j.children().iter().any(|child| { - child.cmd().as_ref().is_some_and(|c| c.contains(&cmd)) - }) - }) - }) - } - } - } - pub fn get_fg(&self) -> Option<&Job> { - self.fg.as_ref() - } - pub fn get_fg_mut(&mut self) -> Option<&mut Job> { - self.fg.as_mut() - } - pub fn new_fg<'a>(&mut self, job: Job) -> ShResult> { - let pgid = job.pgid(); - self.fg = Some(job); - attach_tty(pgid)?; - let statuses = self.fg.as_mut().unwrap().wait_pgrp()?; - attach_tty(getpgrp())?; - Ok(statuses) - } - pub fn fg_to_bg(&mut self, stat: WtStat) -> ShResult<()> { - if self.fg.is_none() { - return Ok(()) - } - take_term()?; - let fg = std::mem::take(&mut self.fg); - if let Some(mut job) = fg { - job.set_stats(stat); - self.insert_job(job, false)?; - } - Ok(()) - } - pub fn bg_to_fg(&mut self, id: JobID) -> ShResult<()> { - let job = self.remove_job(id); - if let Some(job) = job { - wait_fg(job)?; - } - Ok(()) - } - pub fn remove_job(&mut self, id: JobID) -> Option { - let tabid = self.query(id).map(|job| job.tabid().unwrap()); - if let Some(tabid) = tabid { - self.jobs.get_mut(tabid).and_then(Option::take) - } else { - None - } - } - pub fn print_jobs(&mut self, flags: JobCmdFlags) -> ShResult<()> { - let jobs = if flags.contains(JobCmdFlags::NEW_ONLY) { - &self.jobs - .iter() - .filter(|job| job.as_ref().is_some_and(|job| self.new_updates.contains(&job.tabid().unwrap()))) - .map(|job| job.as_ref()) - .collect::>>() - } else { - &self.jobs - .iter() - .map(|job| job.as_ref()) - .collect::>>() - }; - let mut jobs_to_remove = vec![]; - for job in jobs.iter().flatten() { - // Skip foreground job - let id = job.tabid().unwrap(); - // Filter jobs based on flags - if flags.contains(JobCmdFlags::RUNNING) && !matches!(job.get_stats().get(id).unwrap(), WtStat::StillAlive | WtStat::Continued(_)) { - continue; - } - if flags.contains(JobCmdFlags::STOPPED) && !matches!(job.get_stats().get(id).unwrap(), WtStat::Stopped(_,_)) { - continue; - } - // Print the job in the selected format - write(borrow_fd(1), format!("{}\n",job.display(&self.order,flags)).as_bytes())?; - if job.get_stats().iter().all(|stat| matches!(stat,WtStat::Exited(_, _))) { - jobs_to_remove.push(JobID::TableID(id)); - } - } - for id in jobs_to_remove { - self.remove_job(id); - } - Ok(()) - } -} pub struct VarTab { vars: HashMap, params: HashMap, + sh_argv: VecDeque, // Using a VecDeque makes the implementation of `shift` straightforward } impl VarTab { pub fn new() -> Self { let vars = HashMap::new(); let params = Self::init_params(); - Self { vars, params } + let mut var_tab = Self { vars, params, sh_argv: VecDeque::new() }; + var_tab.init_sh_argv(); + var_tab } fn init_params() -> HashMap { let mut params = HashMap::new(); @@ -224,6 +30,37 @@ impl VarTab { params.insert('!', "".into()); // PID of the last background job (if any) params } + pub fn init_sh_argv(&mut self) { + for arg in env::args() { + self.bpush_arg(arg); + } + } + fn update_arg_params(&mut self) { + self.set_param('@', &self.sh_argv.clone().to_vec().join(" ")); + self.set_param('#', &self.sh_argv.len().to_string()); + } + /// Push an arg to the front of the arg deque + pub fn fpush_arg(&mut self, arg: String) { + self.sh_argv.push_front(arg); + self.update_arg_params(); + } + /// Push an arg to the back of the arg deque + pub fn bpush_arg(&mut self, arg: String) { + self.sh_argv.push_back(arg); + self.update_arg_params(); + } + /// Pop an arg from the front of the arg deque + pub fn fpop_arg(&mut self) -> Option { + let arg = self.sh_argv.pop_front(); + self.update_arg_params(); + arg + } + /// Pop an arg from the back of the arg deque + pub fn bpop_arg(&mut self) -> Option { + let arg = self.sh_argv.pop_back(); + self.update_arg_params(); + arg + } pub fn vars(&self) -> &HashMap { &self.vars } @@ -256,7 +93,17 @@ impl VarTab { self.params.insert(param,val.to_string()); } pub fn get_param(&self, param: char) -> String { - self.params.get(¶m).map(|s| s.to_string()).unwrap_or("0".to_string()) + if param.is_ascii_digit() { + let argv_idx = param + .to_string() + .parse::() + .unwrap(); + return self.sh_argv.get(argv_idx).map(|s| s.to_string()).unwrap_or_default() + } else if param == '?' { + self.params.get(¶m).map(|s| s.to_string()).unwrap_or("0".into()) + } else { + self.params.get(¶m).map(|s| s.to_string()).unwrap_or_default() + } } } @@ -290,3 +137,14 @@ pub fn get_status() -> i32 { pub fn set_status(code: i32) { write_vars(|v| v.set_param('?', &code.to_string())) } + +pub fn source_file(path: PathBuf) -> ShResult<()> { + let mut file = OpenOptions::new() + .read(true) + .open(path)?; + + let mut buf = String::new(); + file.read_to_string(&mut buf)?; + exec_input(&buf)?; + Ok(()) +} diff --git a/src/tests/lexer.rs b/src/tests/lexer.rs index c03fc2f..1fd6c31 100644 --- a/src/tests/lexer.rs +++ b/src/tests/lexer.rs @@ -34,3 +34,11 @@ fn lex_with_keywords() { insta::assert_debug_snapshot!(tokens) } + +#[test] +fn lex_multiline() { + let input = "echo hello world\necho foo bar\necho boo biz"; + let tokens: Vec<_> = LexStream::new(input, LexFlags::empty()).collect(); + + insta::assert_debug_snapshot!(tokens) +} diff --git a/src/tests/parser.rs b/src/tests/parser.rs index b1af7ff..64cadba 100644 --- a/src/tests/parser.rs +++ b/src/tests/parser.rs @@ -35,3 +35,15 @@ fn parse_conjunction_and_pipeline() { insta::assert_debug_snapshot!(nodes) } + +#[test] +fn parse_multiline() { + let input = " +echo hello world +echo foo bar +echo boo biz"; + let tk_stream: Vec<_> = LexStream::new(input, LexFlags::empty()).collect(); + let nodes: Vec<_> = ParseStream::new(tk_stream).collect(); + + insta::assert_debug_snapshot!(nodes) +} diff --git a/src/tests/snapshots/fern__tests__lexer__lex_multiline.snap b/src/tests/snapshots/fern__tests__lexer__lex_multiline.snap new file mode 100644 index 0000000..2d1f90c --- /dev/null +++ b/src/tests/snapshots/fern__tests__lexer__lex_multiline.snap @@ -0,0 +1,162 @@ +--- +source: src/tests/lexer.rs +expression: tokens +--- +[ + Tk { + class: SOI, + err_span: None, + err: Null, + span: Span { + range: 0..0, + source: "echo hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 0..4, + source: "echo hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + IS_CMD | BUILTIN, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 5..10, + source: "echo hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 11..16, + source: "echo hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Sep, + err_span: None, + err: Null, + span: Span { + range: 16..17, + source: "echo hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 17..21, + source: "echo hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + IS_CMD | BUILTIN, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 22..25, + source: "echo hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 26..29, + source: "echo hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Sep, + err_span: None, + err: Null, + span: Span { + range: 29..30, + source: "echo hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 30..34, + source: "echo hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + IS_CMD | BUILTIN, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 35..38, + source: "echo hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 39..42, + source: "echo hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: EOI, + err_span: None, + err: Null, + span: Span { + range: 42..42, + source: "echo hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, +] diff --git a/src/tests/snapshots/fern__tests__lexer__lex_quote_str.snap b/src/tests/snapshots/fern__tests__lexer__lex_quote_str.snap index 51585f5..c867740 100644 --- a/src/tests/snapshots/fern__tests__lexer__lex_quote_str.snap +++ b/src/tests/snapshots/fern__tests__lexer__lex_quote_str.snap @@ -24,7 +24,7 @@ expression: tokens source: "echo \"foo bar\" biz baz", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { diff --git a/src/tests/snapshots/fern__tests__lexer__lex_redir.snap b/src/tests/snapshots/fern__tests__lexer__lex_redir.snap index 32c924d..34abd9f 100644 --- a/src/tests/snapshots/fern__tests__lexer__lex_redir.snap +++ b/src/tests/snapshots/fern__tests__lexer__lex_redir.snap @@ -24,7 +24,7 @@ expression: tokens source: "echo foo > bar.txt", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { diff --git a/src/tests/snapshots/fern__tests__lexer__lex_redir_fds.snap b/src/tests/snapshots/fern__tests__lexer__lex_redir_fds.snap index 0540025..a929793 100644 --- a/src/tests/snapshots/fern__tests__lexer__lex_redir_fds.snap +++ b/src/tests/snapshots/fern__tests__lexer__lex_redir_fds.snap @@ -24,7 +24,7 @@ expression: tokens source: "echo foo 1>&2", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { diff --git a/src/tests/snapshots/fern__tests__lexer__lex_simple.snap b/src/tests/snapshots/fern__tests__lexer__lex_simple.snap index 46719bb..e624e65 100644 --- a/src/tests/snapshots/fern__tests__lexer__lex_simple.snap +++ b/src/tests/snapshots/fern__tests__lexer__lex_simple.snap @@ -24,7 +24,7 @@ expression: tokens source: "echo hello world", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { diff --git a/src/tests/snapshots/fern__tests__lexer__lex_with_keywords.snap b/src/tests/snapshots/fern__tests__lexer__lex_with_keywords.snap index 386342c..cdc351c 100644 --- a/src/tests/snapshots/fern__tests__lexer__lex_with_keywords.snap +++ b/src/tests/snapshots/fern__tests__lexer__lex_with_keywords.snap @@ -72,7 +72,7 @@ expression: tokens source: "if true; then echo foo; fi", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { diff --git a/src/tests/snapshots/fern__tests__parser__parse_conjunction.snap b/src/tests/snapshots/fern__tests__parser__parse_conjunction.snap index c9ebac7..d2fc236 100644 --- a/src/tests/snapshots/fern__tests__parser__parse_conjunction.snap +++ b/src/tests/snapshots/fern__tests__parser__parse_conjunction.snap @@ -24,7 +24,7 @@ expression: nodes source: "echo foo && echo bar", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -55,7 +55,7 @@ expression: nodes source: "echo foo && echo bar", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -89,7 +89,7 @@ expression: nodes source: "echo foo && echo bar", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -125,7 +125,7 @@ expression: nodes source: "echo foo && echo bar", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -156,7 +156,7 @@ expression: nodes source: "echo foo && echo bar", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -190,7 +190,7 @@ expression: nodes source: "echo foo && echo bar", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -225,7 +225,7 @@ expression: nodes source: "echo foo && echo bar", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -261,7 +261,7 @@ expression: nodes source: "echo foo && echo bar", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { diff --git a/src/tests/snapshots/fern__tests__parser__parse_conjunction_and_pipeline.snap b/src/tests/snapshots/fern__tests__parser__parse_conjunction_and_pipeline.snap index c3331b8..64b9485 100644 --- a/src/tests/snapshots/fern__tests__parser__parse_conjunction_and_pipeline.snap +++ b/src/tests/snapshots/fern__tests__parser__parse_conjunction_and_pipeline.snap @@ -24,7 +24,7 @@ expression: nodes source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -55,7 +55,7 @@ expression: nodes source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -150,7 +150,7 @@ expression: nodes source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -222,7 +222,7 @@ expression: nodes source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -253,7 +253,7 @@ expression: nodes source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -348,7 +348,7 @@ expression: nodes source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -420,7 +420,7 @@ expression: nodes source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -463,7 +463,7 @@ expression: nodes source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -618,7 +618,7 @@ expression: nodes source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -725,7 +725,7 @@ expression: nodes source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -797,7 +797,7 @@ expression: nodes source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -869,7 +869,7 @@ expression: nodes source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { diff --git a/src/tests/snapshots/fern__tests__parser__parse_multiline.snap b/src/tests/snapshots/fern__tests__parser__parse_multiline.snap new file mode 100644 index 0000000..22ffecb --- /dev/null +++ b/src/tests/snapshots/fern__tests__parser__parse_multiline.snap @@ -0,0 +1,639 @@ +--- +source: src/tests/parser.rs +expression: nodes +--- +[ + Ok( + Node { + class: CmdList { + elements: [ + ConjunctNode { + cmd: Node { + class: Pipeline { + cmds: [ + Node { + class: Command { + assignments: [], + argv: [ + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 1..5, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + IS_CMD | BUILTIN, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 6..11, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 12..17, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + ], + }, + flags: NdFlags( + 0x0, + ), + redirs: [], + tokens: [ + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 1..5, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + IS_CMD | BUILTIN, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 6..11, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 12..17, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Sep, + err_span: None, + err: Null, + span: Span { + range: 17..18, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + ], + }, + ], + pipe_err: false, + }, + flags: NdFlags( + 0x0, + ), + redirs: [], + tokens: [ + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 1..5, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + IS_CMD | BUILTIN, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 6..11, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 12..17, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Sep, + err_span: None, + err: Null, + span: Span { + range: 17..18, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + ], + }, + operator: Null, + }, + ], + }, + flags: NdFlags( + 0x0, + ), + redirs: [], + tokens: [ + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 1..5, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + IS_CMD | BUILTIN, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 6..11, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 12..17, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Sep, + err_span: None, + err: Null, + span: Span { + range: 17..18, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + ], + }, + ), + Ok( + Node { + class: CmdList { + elements: [ + ConjunctNode { + cmd: Node { + class: Pipeline { + cmds: [ + Node { + class: Command { + assignments: [], + argv: [ + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 18..22, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + IS_CMD | BUILTIN, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 23..26, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 27..30, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + ], + }, + flags: NdFlags( + 0x0, + ), + redirs: [], + tokens: [ + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 18..22, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + IS_CMD | BUILTIN, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 23..26, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 27..30, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Sep, + err_span: None, + err: Null, + span: Span { + range: 30..31, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + ], + }, + ], + pipe_err: false, + }, + flags: NdFlags( + 0x0, + ), + redirs: [], + tokens: [ + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 18..22, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + IS_CMD | BUILTIN, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 23..26, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 27..30, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Sep, + err_span: None, + err: Null, + span: Span { + range: 30..31, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + ], + }, + operator: Null, + }, + ], + }, + flags: NdFlags( + 0x0, + ), + redirs: [], + tokens: [ + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 18..22, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + IS_CMD | BUILTIN, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 23..26, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 27..30, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Sep, + err_span: None, + err: Null, + span: Span { + range: 30..31, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + ], + }, + ), + Ok( + Node { + class: CmdList { + elements: [ + ConjunctNode { + cmd: Node { + class: Pipeline { + cmds: [ + Node { + class: Command { + assignments: [], + argv: [ + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 31..35, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + IS_CMD | BUILTIN, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 36..39, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 40..43, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + ], + }, + flags: NdFlags( + 0x0, + ), + redirs: [], + tokens: [ + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 31..35, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + IS_CMD | BUILTIN, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 36..39, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 40..43, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + ], + }, + ], + pipe_err: false, + }, + flags: NdFlags( + 0x0, + ), + redirs: [], + tokens: [ + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 31..35, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + IS_CMD | BUILTIN, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 36..39, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 40..43, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + ], + }, + operator: Null, + }, + ], + }, + flags: NdFlags( + 0x0, + ), + redirs: [], + tokens: [ + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 31..35, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + IS_CMD | BUILTIN, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 36..39, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Str, + err_span: None, + err: Null, + span: Span { + range: 40..43, + source: "\necho hello world\necho foo bar\necho boo biz", + }, + flags: TkFlags( + 0x0, + ), + }, + ], + }, + ), +] diff --git a/src/tests/snapshots/fern__tests__parser__parse_pipeline.snap b/src/tests/snapshots/fern__tests__parser__parse_pipeline.snap index 86e66aa..0974b8f 100644 --- a/src/tests/snapshots/fern__tests__parser__parse_pipeline.snap +++ b/src/tests/snapshots/fern__tests__parser__parse_pipeline.snap @@ -24,7 +24,7 @@ expression: nodes source: "echo foo | sed s/foo/bar", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -55,7 +55,7 @@ expression: nodes source: "echo foo | sed s/foo/bar", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -150,7 +150,7 @@ expression: nodes source: "echo foo | sed s/foo/bar", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -221,7 +221,7 @@ expression: nodes source: "echo foo | sed s/foo/bar", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { diff --git a/src/tests/snapshots/fern__tests__parser__parse_simple.snap b/src/tests/snapshots/fern__tests__parser__parse_simple.snap index a6452bf..8f49b08 100644 --- a/src/tests/snapshots/fern__tests__parser__parse_simple.snap +++ b/src/tests/snapshots/fern__tests__parser__parse_simple.snap @@ -24,7 +24,7 @@ expression: nodes source: "echo hello world", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -67,7 +67,7 @@ expression: nodes source: "echo hello world", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -113,7 +113,7 @@ expression: nodes source: "echo hello world", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk { @@ -160,7 +160,7 @@ expression: nodes source: "echo hello world", }, flags: TkFlags( - IS_CMD, + IS_CMD | BUILTIN, ), }, Tk {