diff --git a/.gitignore b/.gitignore index cff2d32..98b3eed 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ shell.nix *~ TODO.md rust-toolchain.toml -*snapshot* +*src_old # cachix tmp file store-path-pre-build diff --git a/Cargo.lock b/Cargo.lock index a383f14..73118f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,12 +29,42 @@ dependencies = [ "error-code", ] +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "endian-type" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.10" @@ -67,10 +97,20 @@ name = "fern" version = "0.1.0" dependencies = [ "bitflags", + "insta", "nix", + "pretty_assertions", "rustyline", + "serde", + "serde_yaml", ] +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + [[package]] name = "home" version = "0.5.11" @@ -80,12 +120,47 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "indexmap" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "insta" +version = "1.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50259abbaa67d11d2bcafc7ba1d094ed7a0c70e3ce893f0d0997f73558cb3084" +dependencies = [ + "console", + "linked-hash-map", + "once_cell", + "pin-project", + "similar", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -125,6 +200,42 @@ dependencies = [ "libc", ] +[[package]] +name = "once_cell" +version = "1.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro2" version = "1.0.93" @@ -200,6 +311,51 @@ dependencies = [ "syn", ] +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "smallvec" version = "1.14.0" @@ -235,6 +391,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "utf8parse" version = "0.2.2" @@ -322,3 +484,9 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" diff --git a/Cargo.toml b/Cargo.toml index d1ba8e7..8859129 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,5 +11,13 @@ debug = true [dependencies] bitflags = "2.8.0" +insta = "1.42.2" nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl"] } +pretty_assertions = "1.4.1" rustyline = { version = "15.0.0", features = [ "derive" ] } +serde = { version = "1.0.219", features = [ "derive" ] } +serde_yaml = "0.9.34" + +[[bin]] +name = "fern" +path = "src/fern.rs" diff --git a/src/builtin/echo.rs b/src/builtin/echo.rs new file mode 100644 index 0000000..6baa2e0 --- /dev/null +++ b/src/builtin/echo.rs @@ -0,0 +1,30 @@ +use crate::{libsh::error::ShResult, parse::{execute::prepare_argv, NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}}; + +pub fn echo(node: Node, io_stack: &mut IoStack) -> ShResult<()> { + let NdRule::Command { assignments: _, argv } = node.class else { + unreachable!() + }; + assert!(!argv.is_empty()); + + for redir in node.redirs { + io_stack.push_to_frame(redir); + } + let mut io_frame = io_stack.pop_frame(); + + io_frame.redirect()?; + + let stdout = borrow_fd(STDOUT_FILENO); + + let mut echo_output = prepare_argv(argv) + .into_iter() + .skip(1) // Skip 'echo' + .collect::>() + .join(" "); + + echo_output.push('\n'); + + write(stdout, echo_output.as_bytes())?; + + io_frame.restore()?; + Ok(()) +} diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs new file mode 100644 index 0000000..612bde4 --- /dev/null +++ b/src/builtin/mod.rs @@ -0,0 +1,5 @@ +pub mod echo; + +pub const BUILTINS: [&str;1] = [ + "echo" +]; diff --git a/src/fern.rs b/src/fern.rs index 448fb42..4c9834a 100644 --- a/src/fern.rs +++ b/src/fern.rs @@ -5,19 +5,19 @@ pub mod procio; pub mod parse; pub mod expand; pub mod state; +pub mod builtin; +pub mod jobs; #[cfg(test)] pub mod tests; -use std::process::exit; - -use parse::{execute::{get_pipe_stack, Dispatcher}, lex::{LexFlags, LexStream}, ParseResult, ParseStream}; -use state::write_vars; +use parse::{execute::Dispatcher, lex::{LexFlags, LexStream}, ParseStream}; +use crate::prelude::*; fn main() { - loop { + 'main: loop { let input = prompt::read_line().unwrap(); if input == "quit" { break }; - write_vars(|v| v.new_var("foo", "bar")); + let start = Instant::now(); let mut tokens = vec![]; for token in LexStream::new(&input, LexFlags::empty()) { @@ -31,13 +31,16 @@ fn main() { let mut nodes = vec![]; for result in ParseStream::new(tokens) { match result { - ParseResult::Error(e) => panic!("{}",e), - ParseResult::Match(node) => nodes.push(node), - _ => unreachable!() + 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()); } } diff --git a/src/jobs.rs b/src/jobs.rs new file mode 100644 index 0000000..af047d3 --- /dev/null +++ b/src/jobs.rs @@ -0,0 +1,390 @@ +use crate::{libsh::{error::ShResult, term::{Style, Styled}}, prelude::*, procio::borrow_fd}; + +bitflags! { + #[derive(Debug, Copy, Clone)] + pub struct JobCmdFlags: u8 { + const LONG = 0b0000_0001; // 0x01 + const PIDS = 0b0000_0010; // 0x02 + const NEW_ONLY = 0b0000_0100; // 0x04 + const RUNNING = 0b0000_1000; // 0x08 + const STOPPED = 0b0001_0000; // 0x10 + const INIT = 0b0010_0000; // 0x20 + } +} + +#[derive(Debug)] +pub struct DisplayWaitStatus(pub WtStat); + +impl fmt::Display for DisplayWaitStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.0 { + WtStat::Exited(_, code) => { + match code { + 0 => write!(f, "done"), + _ => write!(f, "failed: {}", code), + } + } + WtStat::Signaled(_, signal, _) => { + write!(f, "signaled: {:?}", signal) + } + WtStat::Stopped(_, signal) => { + write!(f, "stopped: {:?}", signal) + } + WtStat::PtraceEvent(_, signal, _) => { + write!(f, "ptrace event: {:?}", signal) + } + WtStat::PtraceSyscall(_) => { + write!(f, "ptrace syscall") + } + WtStat::Continued(_) => { + write!(f, "continued") + } + WtStat::StillAlive => { + write!(f, "running") + } + } + } +} + +#[derive(Clone,Debug)] +pub enum JobID { + Pgid(Pid), + Pid(Pid), + TableID(usize), + Command(String) +} + +#[derive(Debug,Clone)] +pub struct ChildProc { + pgid: Pid, + pid: Pid, + command: Option, + stat: WtStat +} + +impl<'a> ChildProc { + pub fn new(pid: Pid, command: Option<&str>, pgid: Option) -> ShResult { + let command = command.map(|str| str.to_string()); + let stat = if kill(pid,None).is_ok() { + WtStat::StillAlive + } else { + WtStat::Exited(pid, 0) + }; + let mut child = Self { pgid: pid, pid, command, stat }; + if let Some(pgid) = pgid { + child.set_pgid(pgid).ok(); + } + Ok(child) + } + pub fn pid(&self) -> Pid { + self.pid + } + pub fn pgid(&self) -> Pid { + self.pgid + } + pub fn cmd(&self) -> Option<&str> { + self.command.as_ref().map(|cmd| cmd.as_str()) + } + pub fn stat(&self) -> WtStat { + self.stat + } + pub fn wait(&mut self, flags: Option) -> Result { + let result = waitpid(self.pid, flags); + if let Ok(stat) = result { + self.stat = stat + } + result + } + pub fn kill>>(&self, sig: T) -> ShResult<()> { + Ok(kill(self.pid, sig)?) + } + pub fn set_pgid(&mut self, pgid: Pid) -> ShResult<()> { + setpgid(self.pid, pgid)?; + self.pgid = pgid; + Ok(()) + } + pub fn set_stat(&mut self, stat: WtStat) { + self.stat = stat + } + pub fn is_alive(&self) -> bool { + self.stat == WtStat::StillAlive + } + pub fn is_stopped(&self) -> bool { + matches!(self.stat,WtStat::Stopped(..)) + } + pub fn exited(&self) -> bool { + matches!(self.stat,WtStat::Exited(..)) + } +} + +pub struct JobBldr { + table_id: Option, + pgid: Option, + children: Vec +} + +impl Default for JobBldr { + fn default() -> Self { + Self::new() + } +} + +impl JobBldr { + pub fn new() -> Self { + Self { table_id: None, pgid: None, children: vec![] } + } + pub fn with_id(self, id: usize) -> Self { + Self { + table_id: Some(id), + pgid: self.pgid, + children: self.children + } + } + pub fn with_pgid(self, pgid: Pid) -> Self { + Self { + table_id: self.table_id, + pgid: Some(pgid), + children: self.children + } + } + pub fn with_children(self, children: Vec) -> Self { + Self { + table_id: self.table_id, + pgid: self.pgid, + children + } + } + pub fn build(self) -> Job { + Job { + table_id: self.table_id, + pgid: self.pgid.unwrap_or(Pid::from_raw(0)), + children: self.children + } + } +} + +#[derive(Debug,Clone)] +pub struct Job { + table_id: Option, + pgid: Pid, + children: Vec +} + +impl Job { + pub fn set_tabid(&mut self, id: usize) { + self.table_id = Some(id) + } + pub fn running(&self) -> bool { + !self.children.iter().all(|chld| chld.exited()) + } + pub fn tabid(&self) -> Option { + self.table_id + } + pub fn pgid(&self) -> Pid { + self.pgid + } + pub fn get_cmds(&self) -> Vec<&str> { + let mut cmds = vec![]; + for child in &self.children { + cmds.push(child.cmd().unwrap_or_default()) + } + cmds + } + pub fn set_stats(&mut self, stat: WtStat) { + for child in self.children.iter_mut() { + child.set_stat(stat); + } + } + pub fn get_stats(&self) -> Vec { + self.children + .iter() + .map(|chld| chld.stat()) + .collect::>() + } + pub fn get_pids(&self) -> Vec { + self.children + .iter() + .map(|chld| chld.pid()) + .collect::>() + } + pub fn children(&self) -> &[ChildProc] { + &self.children + } + pub fn children_mut(&mut self) -> &mut Vec { + &mut self.children + } + pub fn killpg(&mut self, sig: Signal) -> ShResult<()> { + let stat = match sig { + Signal::SIGTSTP => WtStat::Stopped(self.pgid, Signal::SIGTSTP), + Signal::SIGCONT => WtStat::Continued(self.pgid), + Signal::SIGTERM => WtStat::Signaled(self.pgid, Signal::SIGTERM, false), + _ => unimplemented!("{}",sig) + }; + self.set_stats(stat); + Ok(killpg(self.pgid, sig)?) + } + pub fn wait_pgrp<'a>(&mut self) -> ShResult> { + let mut stats = vec![]; + for child in self.children.iter_mut() { + let result = child.wait(Some(WtFlag::WUNTRACED)); + match result { + Ok(stat) => { + stats.push(stat); + } + Err(Errno::ECHILD) => break, + Err(e) => return Err(e.into()) + } + } + Ok(stats) + } + pub fn update_by_id(&mut self, id: JobID, stat: WtStat) -> ShResult<()> { + match id { + JobID::Pid(pid) => { + let query_result = self.children.iter_mut().find(|chld| chld.pid == pid); + if let Some(child) = query_result { + child.set_stat(stat); + } + } + JobID::Command(cmd) => { + let query_result = self.children + .iter_mut() + .find(|chld| chld + .cmd() + .is_some_and(|chld_cmd| chld_cmd.contains(&cmd)) + ); + if let Some(child) = query_result { + child.set_stat(stat); + } + } + JobID::TableID(tid) => { + if self.table_id.is_some_and(|tblid| tblid == tid) { + for child in self.children.iter_mut() { + child.set_stat(stat); + } + } + } + JobID::Pgid(pgid) => { + if pgid == self.pgid { + for child in self.children.iter_mut() { + child.set_stat(stat); + } + } + } + } + Ok(()) + } + pub fn display(&self, job_order: &[usize], flags: JobCmdFlags) -> String { + let long = flags.contains(JobCmdFlags::LONG); + let init = flags.contains(JobCmdFlags::INIT); + let pids = flags.contains(JobCmdFlags::PIDS); + + let current = job_order.last(); + let prev = if job_order.len() > 2 { + job_order.get(job_order.len() - 2) + } else { + None + }; + + let id = self.table_id.unwrap(); + let symbol = if current == self.table_id.as_ref() { + "+" + } else if prev == self.table_id.as_ref() { + "-" + } else { + " " + }; + let padding_count = symbol.len() + id.to_string().len() + 3; + let padding = " ".repeat(padding_count); + + let mut output = format!("[{}]{}\t", id + 1, symbol); + for (i, cmd) in self.get_cmds().iter().enumerate() { + let pid = if pids || init { + let mut pid = self.get_pids().get(i).unwrap().to_string(); + pid.push(' '); + pid + } else { + "".to_string() + }; + let job_stat = *self.get_stats().get(i).unwrap(); + let fmt_stat = DisplayWaitStatus(job_stat).to_string(); + + let mut stat_line = if init { + "".to_string() + } else { + fmt_stat.clone() + }; + stat_line = format!("{}{} ",pid,stat_line); + stat_line = format!("{} {}", stat_line, cmd); + stat_line = match job_stat { + WtStat::Stopped(..) | WtStat::Signaled(..) => stat_line.styled(Style::Magenta), + WtStat::Exited(_, code) => { + match code { + 0 => stat_line.styled(Style::Green), + _ => stat_line.styled(Style::Red), + } + } + _ => stat_line.styled(Style::Cyan) + }; + if i != self.get_cmds().len() - 1 { + stat_line = format!("{} |",stat_line); + } + + let stat_final = if long { + format!( + "{}{} {}", + if i != 0 { &padding } else { "" }, + self.get_pids().get(i).unwrap(), + stat_line + ) + } else { + format!( + "{}{}", + if i != 0 { &padding } else { "" }, + stat_line + ) + }; + output.push_str(&stat_final); + output.push('\n'); + } + output + } +} + +pub fn term_ctlr() -> Pid { + tcgetpgrp(borrow_fd(0)).unwrap_or(getpgrp()) +} + +pub fn attach_tty(pgid: Pid) -> ShResult<()> { + // If we aren't attached to a terminal, the pgid already controls it, or the process group does not exist + // Then return ok + if !isatty(0).unwrap_or(false) || pgid == term_ctlr() || killpg(pgid, None).is_err() { + return Ok(()) + } + flog!(TRACE, "Attaching tty to pgid: {}",pgid); + + if pgid == getpgrp() && term_ctlr() != getpgrp() { + kill(term_ctlr(), Signal::SIGTTOU).ok(); + } + + let mut new_mask = SigSet::empty(); + let mut mask_bkup = SigSet::empty(); + + new_mask.add(Signal::SIGTSTP); + new_mask.add(Signal::SIGTTIN); + new_mask.add(Signal::SIGTTOU); + + pthread_sigmask(SigmaskHow::SIG_BLOCK, Some(&mut new_mask), Some(&mut mask_bkup))?; + + let result = tcsetpgrp(borrow_fd(0), pgid); + + pthread_sigmask(SigmaskHow::SIG_SETMASK, Some(&mut mask_bkup), Some(&mut new_mask))?; + + match result { + Ok(_) => return Ok(()), + Err(e) => { + flog!(ERROR, "error while switching term control: {}",e); + tcsetpgrp(borrow_fd(0), getpgrp())?; + Ok(()) + } + } +} diff --git a/src/libsh/error.rs b/src/libsh/error.rs index ac4df31..21f2acb 100644 --- a/src/libsh/error.rs +++ b/src/libsh/error.rs @@ -1,25 +1,40 @@ -use std::{fmt::Display, str::FromStr}; +use std::{fmt::Display, ops::Range, str::FromStr}; use crate::{parse::lex::Span, prelude::*}; -pub type ShResult<'s,T> = Result>; +pub type ShResult = Result; #[derive(Debug)] -pub enum ShErr<'s> { - Simple { kind: ShErrKind, msg: String }, - Full { kind: ShErrKind, msg: String, span: Span<'s> } +pub struct ErrSpan { + range: Range, + source: String } -impl<'s> ShErr<'s> { +impl<'s> From> for ErrSpan { + fn from(value: Span<'s>) -> Self { + let range = value.range(); + let source = value.get_source().to_string(); + Self { range, source } + } +} + +#[derive(Debug)] +pub enum ShErr { + Simple { kind: ShErrKind, msg: String }, + Full { kind: ShErrKind, msg: String, span: ErrSpan } +} + +impl<'s> ShErr { pub fn simple(kind: ShErrKind, msg: impl Into) -> Self { let msg = msg.into(); Self::Simple { kind, msg } } pub fn full(kind: ShErrKind, msg: impl Into, span: Span<'s>) -> Self { let msg = msg.into(); + let span = span.into(); Self::Full { kind, msg, span } } - pub fn unpack(self) -> (ShErrKind,String,Option>) { + pub fn unpack(self) -> (ShErrKind,String,Option) { match self { ShErr::Simple { kind, msg } => (kind,msg,None), ShErr::Full { kind, msg, span } => (kind,msg,Some(span)) @@ -27,11 +42,12 @@ impl<'s> ShErr<'s> { } pub fn with_span(sherr: ShErr, span: Span<'s>) -> Self { let (kind,msg,_) = sherr.unpack(); + let span = span.into(); Self::Full { kind, msg, span } } } -impl<'s> Display for ShErr<'s> { +impl Display for ShErr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Simple { msg, kind: _ } => writeln!(f, "{}", msg), @@ -40,26 +56,26 @@ impl<'s> Display for ShErr<'s> { } } -impl<'s> From for ShErr<'s> { +impl From for ShErr { fn from(_: std::io::Error) -> Self { let msg = std::io::Error::last_os_error(); ShErr::simple(ShErrKind::IoErr, msg.to_string()) } } -impl<'s> From for ShErr<'s> { +impl From for ShErr { fn from(value: std::env::VarError) -> Self { ShErr::simple(ShErrKind::InternalErr, &value.to_string()) } } -impl<'s> From for ShErr<'s> { +impl From for ShErr { fn from(value: rustyline::error::ReadlineError) -> Self { ShErr::simple(ShErrKind::ParseErr, &value.to_string()) } } -impl<'s> From for ShErr<'s> { +impl From for ShErr { fn from(value: Errno) -> Self { ShErr::simple(ShErrKind::Errno, &value.to_string()) } diff --git a/src/libsh/flog.rs b/src/libsh/flog.rs new file mode 100644 index 0000000..1a3fce4 --- /dev/null +++ b/src/libsh/flog.rs @@ -0,0 +1,144 @@ +use std::fmt::Display; + +use super::term::{Style, Styled}; + +#[derive(Clone, Copy, PartialEq, PartialOrd, Ord, Eq , Debug)] +#[repr(u8)] +pub enum FernLogLevel { + NONE = 0, + ERROR = 1, + WARN = 2, + INFO = 3, + DEBUG = 4, + TRACE = 5 +} + +impl Display for FernLogLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use FernLogLevel::*; + match self { + ERROR => write!(f,"{}","ERROR".styled(Style::Red | Style::Bold)), + WARN => write!(f,"{}","WARN".styled(Style::Yellow | Style::Bold)), + INFO => write!(f,"{}","INFO".styled(Style::Green | Style::Bold)), + DEBUG => write!(f,"{}","DEBUG".styled(Style::Magenta | Style::Bold)), + TRACE => write!(f,"{}","TRACE".styled(Style::Blue | Style::Bold)), + NONE => write!(f,"") + } + } +} + +pub fn log_level() -> FernLogLevel { + use FernLogLevel::*; + let level = std::env::var("FERN_LOG_LEVEL").unwrap_or_default(); + match level.to_lowercase().as_str() { + "error" => ERROR, + "warn" => WARN, + "info" => INFO, + "debug" => DEBUG, + "trace" => TRACE, + _ => NONE + } +} + +/// A structured logging macro designed for `fern`. +/// +/// `flog!` was implemented because `rustyline` uses `env_logger`, which clutters the debug output. +/// This macro prints log messages in a structured format, including the log level, filename, and line number. +/// +/// # Usage +/// +/// The macro supports three types of arguments: +/// +/// ## 1. **Formatted Messages** +/// Similar to `println!` or `format!`, allows embedding values inside a formatted string. +/// +/// ```rust +/// flog!(ERROR, "foo is {}", foo); +/// ``` +/// **Output:** +/// ```plaintext +/// [ERROR][file.rs:10] foo is +/// ``` +/// +/// ## 2. **Literals** +/// Directly prints each literal argument as a separate line. +/// +/// ```rust +/// flog!(WARN, "foo", "bar"); +/// ``` +/// **Output:** +/// ```plaintext +/// [WARN][file.rs:10] foo +/// [WARN][file.rs:10] bar +/// ``` +/// +/// ## 3. **Expressions** +/// Logs the evaluated result of each given expression, displaying both the expression and its value. +/// +/// ```rust +/// flog!(INFO, 1.min(2)); +/// ``` +/// **Output:** +/// ```plaintext +/// [INFO][file.rs:10] 1 +/// ``` +/// +/// # Considerations +/// - This macro uses `eprintln!()` internally, so its formatting rules must be followed. +/// - **Literals and formatted messages** require arguments that implement [`std::fmt::Display`]. +/// - **Expressions** require arguments that implement [`std::fmt::Debug`]. +#[macro_export] +macro_rules! flog { + ($level:path, $fmt:literal, $($args:expr),+ $(,)?) => {{ + use $crate::libsh::flog::log_level; + use $crate::libsh::term::Styled; + use $crate::libsh::term::Style; + + if $level <= log_level() { + let file = file!().styled(Style::Cyan); + let line = line!().to_string().styled(Style::Cyan); + + eprintln!( + "[{}][{}:{}] {}", + $level, file, line, format!($fmt, $($args),+) + ); + } + }}; + + ($level:path, $($val:expr),+ $(,)?) => {{ + use $crate::libsh::flog::log_level; + use $crate::libsh::term::Styled; + use $crate::libsh::term::Style; + + if $level <= log_level() { + let file = file!().styled(Style::Cyan); + let line = line!().to_string().styled(Style::Cyan); + + $( + let val_name = stringify!($val); + eprintln!( + "[{}][{}:{}] {} = {:#?}", + $level, file, line, val_name, &$val + ); + )+ + } + }}; + + ($level:path, $($lit:literal),+ $(,)?) => {{ + use $crate::libsh::flog::log_level; + use $crate::libsh::term::Styled; + use $crate::libsh::term::Style; + + if $level <= log_level() { + let file = file!().styled(Style::Cyan); + let line = line!().to_string().styled(Style::Cyan); + + $( + eprintln!( + "[{}][{}:{}] {}", + $level, file, line, $lit + ); + )+ + } + }}; +} diff --git a/src/libsh/mod.rs b/src/libsh/mod.rs index 704a1bc..a789133 100644 --- a/src/libsh/mod.rs +++ b/src/libsh/mod.rs @@ -1,2 +1,3 @@ pub mod error; pub mod term; +pub mod flog; diff --git a/src/parse/execute.rs b/src/parse/execute.rs index 296da28..0c83eb9 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -2,9 +2,14 @@ use std::collections::VecDeque; use nix::sys::wait::WaitPidFlag; -use crate::{libsh::error::ShResult, prelude::*, procio::{IoFrame, IoPipe, IoStack}, state}; +use crate::{builtin::echo::echo, libsh::error::ShResult, prelude::*, procio::{IoFrame, IoPipe, IoStack}, state::{self, write_vars}}; -use super::{lex::Tk, ConjunctNode, ConjunctOp, NdRule, Node, Redir, RedirType}; +use super::{lex::{Tk, TkFlags}, AssignKind, ConjunctNode, ConjunctOp, NdRule, Node, Redir, RedirType}; + +pub enum AssignBehavior { + Export, + Set +} /// Arguments to the execvpe function pub struct ExecArgs { @@ -14,8 +19,9 @@ pub struct ExecArgs { } impl ExecArgs { - pub fn new(argv: Vec) -> Self { + pub fn new(argv: Vec) -> Self { assert!(!argv.is_empty()); + let argv = prepare_argv(argv); let cmd = Self::get_cmd(&argv); let argv = Self::get_argv(argv); let envp = Self::get_envp(); @@ -42,22 +48,33 @@ impl<'t> Dispatcher<'t> { let nodes = VecDeque::from(nodes); Self { nodes, io_stack: IoStack::new() } } - pub fn begin_dispatch(&mut self) -> ShResult<'t,()> { + pub fn begin_dispatch(&mut self) -> ShResult<()> { + flog!(TRACE, "beginning dispatch"); while let Some(list) = self.nodes.pop_front() { self.dispatch_node(list)?; } Ok(()) } - pub fn dispatch_node(&mut self, node: Node<'t>) -> ShResult<'t,()> { + pub fn dispatch_node(&mut self, node: Node<'t>) -> ShResult<()> { match node.class { NdRule::CmdList {..} => self.exec_conjunction(node)?, NdRule::Pipeline {..} => self.exec_pipeline(node)?, - NdRule::Command {..} => self.exec_cmd(node)?, + NdRule::Command {..} => self.dispatch_cmd(node)?, _ => unreachable!() } Ok(()) } - pub fn exec_conjunction(&mut self, conjunction: Node<'t>) -> ShResult<'t,()> { + pub fn dispatch_cmd(&mut self, node: Node<'t>) -> ShResult<()> { + let Some(cmd) = node.get_command() else { + return self.exec_cmd(node) + }; + if cmd.flags.contains(TkFlags::BUILTIN) { + self.exec_builtin(node) + } else { + self.exec_cmd(node) + } + } + pub fn exec_conjunction(&mut self, conjunction: Node<'t>) -> ShResult<()> { let NdRule::CmdList { elements } = conjunction.class else { unreachable!() }; @@ -76,7 +93,7 @@ impl<'t> Dispatcher<'t> { } Ok(()) } - pub fn exec_pipeline(&mut self, pipeline: Node<'t>) -> ShResult<'t,()> { + pub fn exec_pipeline(&mut self, pipeline: Node<'t>) -> ShResult<()> { let NdRule::Pipeline { cmds, pipe_err } = pipeline.class else { unreachable!() }; @@ -96,15 +113,45 @@ impl<'t> Dispatcher<'t> { } Ok(()) } - pub fn exec_cmd(&mut self, cmd: Node<'t>) -> ShResult<'t,()> { + pub fn exec_builtin(&mut self, mut cmd: Node<'t>) -> ShResult<()> { + let NdRule::Command { ref mut assignments, argv } = &mut cmd.class else { + unreachable!() + }; + let env_vars_to_unset = self.set_assignments(mem::take(assignments), AssignBehavior::Export); + let cmd_raw = cmd.get_command().unwrap(); + flog!(TRACE, "doing builtin"); + let result = match cmd_raw.span.as_str() { + "echo" => echo(cmd, &mut self.io_stack), + _ => unimplemented!("Have not yet added support for builtin '{}'", cmd_raw.span.as_str()) + }; + + for var in env_vars_to_unset { + env::set_var(&var, ""); + } + + Ok(result?) + } + pub fn exec_cmd(&mut self, cmd: Node<'t>) -> ShResult<()> { let NdRule::Command { assignments, argv } = cmd.class else { unreachable!() }; + let mut env_vars_to_unset = vec![]; + if !assignments.is_empty() { + let assign_behavior = if argv.is_empty() { + AssignBehavior::Set + } else { + AssignBehavior::Export + }; + 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(()) + } - let exec_args = ExecArgs::new(prepare_argv(argv)); + let exec_args = ExecArgs::new(argv); let io_frame = self.io_stack.pop_frame(); run_fork( io_frame, @@ -113,8 +160,51 @@ impl<'t> Dispatcher<'t> { def_parent_action )?; + for var in env_vars_to_unset { + std::env::set_var(&var, ""); + } + Ok(()) } + pub fn set_assignments(&self, assigns: Vec>, behavior: AssignBehavior) -> Vec { + let mut new_env_vars = vec![]; + match behavior { + AssignBehavior::Export => { + for assign in assigns { + let NdRule::Assignment { kind, var, val } = assign.class else { + unreachable!() + }; + let var = var.span.as_str(); + let val = val.span.as_str(); + match kind { + AssignKind::Eq => std::env::set_var(var, val), + AssignKind::PlusEq => todo!(), + AssignKind::MinusEq => todo!(), + AssignKind::MultEq => todo!(), + AssignKind::DivEq => todo!(), + } + new_env_vars.push(var.to_string()); + } + } + AssignBehavior::Set => { + for assign in assigns { + let NdRule::Assignment { kind, var, val } = assign.class else { + unreachable!() + }; + let var = var.span.as_str(); + let val = val.span.as_str(); + match kind { + AssignKind::Eq => write_vars(|v| v.new_var(var, val)), + AssignKind::PlusEq => todo!(), + AssignKind::MinusEq => todo!(), + AssignKind::MultEq => todo!(), + AssignKind::DivEq => todo!(), + } + } + } + } + new_env_vars + } } pub fn prepare_argv(argv: Vec) -> Vec { @@ -134,15 +224,20 @@ pub fn run_fork<'t,C,P>( exec_args: ExecArgs, child_action: C, parent_action: P, -) -> ShResult<'t,()> +) -> ShResult<()> where - C: Fn(IoFrame,ExecArgs), - P: Fn(IoFrame,Pid) -> ShResult<'t,()> + C: Fn(IoFrame,ExecArgs) -> Errno, + P: Fn(IoFrame,Pid) -> ShResult<()> { match unsafe { fork()? } { ForkResult::Child => { - child_action(io_frame,exec_args); - exit(1); + let cmd = &exec_args.cmd.to_str().unwrap().to_string(); + let errno = child_action(io_frame,exec_args); + match errno { + Errno::ENOENT => eprintln!("Command not found: {}", cmd), + _ => eprintln!("{errno}") + } + exit(errno as i32); } ForkResult::Parent { child } => { parent_action(io_frame,child) @@ -151,18 +246,21 @@ where } /// The default behavior for the child process after forking -pub fn def_child_action<'t>(mut io_frame: IoFrame, exec_args: ExecArgs) { - io_frame.redirect().unwrap(); - execvpe(&exec_args.cmd, &exec_args.argv, &exec_args.envp).unwrap(); +pub fn def_child_action<'t>(mut io_frame: IoFrame, exec_args: ExecArgs) -> Errno { + if let Err(e) = io_frame.redirect() { + eprintln!("{e}"); + } + let Err(e) = execvpe(&exec_args.cmd, &exec_args.argv, &exec_args.envp); + e } /// The default behavior for the parent process after forking -pub fn def_parent_action<'t>(io_frame: IoFrame, child: Pid) -> ShResult<'t,()> { - let status = waitpid(child, Some(WaitPidFlag::WSTOPPED))?; +pub fn def_parent_action<'t>(io_frame: IoFrame, child: Pid) -> ShResult<()> { + let status = waitpid(child, Some(WtFlag::WSTOPPED))?; match status { - WaitStatus::Exited(_, status) => state::set_status(status), + WtStat::Exited(_, status) => state::set_status(status), _ => unimplemented!() -} + } Ok(()) } diff --git a/src/parse/lex.rs b/src/parse/lex.rs index 5af2012..500b4b4 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::{libsh::error::{ShErr, ShErrKind}, prelude::*}; +use crate::{builtin::BUILTINS, libsh::error::{ShErr, ShErrKind}, prelude::*}; pub const KEYWORDS: [&'static str;14] = [ "if", @@ -48,6 +48,12 @@ impl<'s> Span<'s> { pub fn as_str(&self) -> &str { &self.source[self.start..self.end] } + pub fn get_source(&'s self) -> &'s str { + self.source + } + pub fn range(&self) -> Range { + self.range.clone() + } } /// Allows simple access to the underlying range wrapped by the span @@ -130,6 +136,9 @@ impl<'s> Tk<'s> { pub fn is_err(&self) -> bool { self.err_span.is_some() } + pub fn source(&self) -> &'s str { + self.span.source + } } impl<'s> Display for Tk<'s> { @@ -150,6 +159,8 @@ bitflags! { const OPENER = 0b0000000000000010; const IS_CMD = 0b0000000000000100; const IS_OP = 0b0000000000001000; + const ASSIGN = 0b0000000000010000; + const BUILTIN = 0b0000000000100000; } } @@ -181,6 +192,7 @@ bitflags! { impl<'t> LexStream<'t> { pub fn new(source: &'t str, flags: LexFlags) -> Self { + flog!(TRACE, "new lex stream"); let flags = flags | LexFlags::FRESH | LexFlags::NEXT_IS_CMD; Self { source, cursor: 0, in_quote: false, flags } } @@ -346,8 +358,15 @@ impl<'t> LexStream<'t> { if self.flags.contains(LexFlags::NEXT_IS_CMD) { if KEYWORDS.contains(&new_tk.span.as_str()) { new_tk.flags |= TkFlags::KEYWORD; + } else if is_assignment(&new_tk.span.as_str()) { + new_tk.flags |= TkFlags::ASSIGN; } else { new_tk.flags |= TkFlags::IS_CMD; + flog!(TRACE, new_tk.span.as_str()); + if BUILTINS.contains(&new_tk.span.as_str()) { + new_tk.flags |= TkFlags::BUILTIN; + } + flog!(TRACE, new_tk.flags); self.next_is_not_cmd(); } } @@ -480,6 +499,19 @@ pub fn get_char(src: &str, idx: usize) -> Option { src.get(idx..)?.chars().next() } +pub fn is_assignment(text: &str) -> bool { + let mut chars = text.chars(); + + while let Some(ch) = chars.next() { + match ch { + '\\' => { chars.next(); } + '=' => return true, + _ => continue + } + } + false +} + /// Is '|', '&', '>', or '<' pub fn is_op(ch: char) -> bool { matches!(ch, '|' | '&' | '>' | '<') diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 3d43ab1..ef2681d 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use bitflags::bitflags; -use lex::{Span, Tk, TkFlags, TkRule}; +use lex::{is_hard_sep, Span, Tk, TkFlags, TkRule}; use crate::{prelude::*, libsh::error::{ShErr, ShErrKind, ShResult}, procio::{IoFd, IoFile, IoInfo}}; @@ -17,6 +17,16 @@ pub struct Node<'t> { pub tokens: Vec>, } +impl<'t> Node<'t> { + pub fn get_command(&'t self) -> Option<&'t Tk<'t>> { + let NdRule::Command { assignments: _, argv } = &self.class else { + return None + }; + let command = argv.iter().find(|tk| tk.flags.contains(TkFlags::IS_CMD))?; + Some(command) + } +} + bitflags! { #[derive(Debug)] pub struct NdFlags: u32 { @@ -48,8 +58,8 @@ impl RedirBldr { Default::default() } pub fn with_io_info(self, io_info: Box) -> Self { - let Self { io_info: _, class, tgt_fd } = self; - Self { io_info: Some(io_info), class, tgt_fd } + 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; @@ -127,8 +137,8 @@ impl FromStr for RedirBldr { let tgt_fd = tgt_fd.parse::().unwrap_or_else(|_| { match redir.class.unwrap() { RedirType::Input | - RedirType::HereDoc | - RedirType::HereString => 0, + RedirType::HereDoc | + RedirType::HereString => 0, _ => 1 } }); @@ -199,22 +209,12 @@ pub enum NdRule<'t> { LoopNode { kind: LoopKind, cond_block: CondNode<'t> }, ForNode { vars: Vec>, arr: Vec>, body: Vec> }, CaseNode { pattern: Tk<'t>, case_blocks: Vec> }, - Command { assignments: Vec>, argv: Vec> }, + Command { assignments: Vec>, argv: Vec> }, Pipeline { cmds: Vec>, pipe_err: bool }, CmdList { elements: Vec> }, 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>, @@ -254,11 +254,11 @@ impl<'t> ParseStream<'t> { assert!(num_consumed <= self.tokens.len()); self.tokens = self.tokens[num_consumed..].to_vec(); } - fn parse_cmd_list(&mut self) -> ParseResult<'t> { + fn parse_cmd_list(&mut self) -> ShResult>> { let mut elements = vec![]; let mut node_tks = vec![]; - while let Match(block) = self.parse_block(true) { + while let Some(block) = self.parse_block(true)? { node_tks.append(&mut block.tokens.clone()); let conjunct_op = match self.next_tk_class() { TkRule::And => ConjunctOp::And, @@ -275,35 +275,37 @@ impl<'t> ParseStream<'t> { break } } - Match(Node { - class: NdRule::CmdList { elements }, - flags: NdFlags::empty(), - redirs: vec![], - tokens: node_tks - }) + if elements.is_empty() { + Ok(None) + } else { + Ok(Some(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> { + fn parse_block(&mut self, check_pipelines: bool) -> ShResult>> { if check_pipelines { - match self.parse_pipeline() { - NoMatch => { /* Continue */ } - result => return result + if let Some(node) = self.parse_pipeline()? { + return Ok(Some(node)) } } else { - match self.parse_cmd() { - NoMatch => { /* Continue */ } - result => return result + if let Some(node) = self.parse_cmd()? { + return Ok(Some(node)) } } - NoMatch + Ok(None) } - fn parse_pipeline(&mut self) -> ParseResult<'t> { + fn parse_pipeline(&mut self) -> ShResult>> { let mut cmds = vec![]; let mut node_tks = vec![]; - while let Match(cmd) = self.parse_block(false) { + while let Some(cmd) = self.parse_block(false)? { let is_punctuated = node_is_punctuated(&cmd.tokens); node_tks.append(&mut cmd.tokens.clone()); cmds.push(cmd); @@ -318,18 +320,18 @@ impl<'t> ParseStream<'t> { } } if cmds.is_empty() { - NoMatch + Ok(None) } else { - Match(Node { + Ok(Some(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> { + fn parse_cmd(&mut self) -> ShResult>> { let tk_slice = self.tokens.as_slice(); let mut tk_iter = tk_slice.iter(); let mut node_tks = vec![]; @@ -337,25 +339,32 @@ impl<'t> ParseStream<'t> { let mut argv = vec![]; let mut assignments = vec![]; - let Some(cmd_tk) = tk_iter.next() else { - return NoMatch - }; + while let Some(prefix_tk) = tk_iter.next() { + if prefix_tk.flags.contains(TkFlags::IS_CMD) { + node_tks.push(prefix_tk.clone()); + argv.push(prefix_tk.clone()); + break + } else if prefix_tk.flags.contains(TkFlags::ASSIGN) { + let Some(assign) = self.parse_assignment(&prefix_tk) else { + break + }; + node_tks.push(prefix_tk.clone()); + assignments.push(assign) + } + } - if !cmd_tk.flags.contains(TkFlags::IS_CMD) { - return NoMatch - } else { - node_tks.push(cmd_tk.clone()); - argv.push(cmd_tk.clone()); + if argv.is_empty() && assignments.is_empty() { + return Ok(None) } while let Some(tk) = tk_iter.next() { match tk.class { TkRule::EOI | - TkRule::Pipe | - TkRule::And | - TkRule::Or => { - break - } + TkRule::Pipe | + TkRule::And | + TkRule::Or => { + break + } TkRule::Sep => { node_tks.push(tk.clone()); break @@ -368,9 +377,11 @@ impl<'t> ParseStream<'t> { node_tks.push(tk.clone()); let redir_bldr = tk.span.as_str().parse::().unwrap(); if redir_bldr.io_info.is_none() { - let Some(path_tk) = tk_iter.next() else { + let path_tk = tk_iter.next(); + + if path_tk.is_none_or(|tk| tk.class == TkRule::EOI) { self.flags |= ParseFlags::ERROR; - return Error( + return Err( ShErr::full( ShErrKind::ParseErr, "Expected a filename after this redirection", @@ -378,6 +389,8 @@ impl<'t> ParseStream<'t> { ) ) }; + + let path_tk = path_tk.unwrap(); node_tks.push(path_tk.clone()); let Ok(file) = (match redir_bldr.class.unwrap() { @@ -403,7 +416,7 @@ impl<'t> ParseStream<'t> { _ => unreachable!() }) else { self.flags |= ParseFlags::ERROR; - return Error( + return Err( ShErr::full( ShErrKind::InternalErr, "Error opening file for redirection", @@ -423,17 +436,118 @@ impl<'t> ParseStream<'t> { } self.commit(node_tks.len()); - Match(Node { + Ok(Some(Node { class: NdRule::Command { assignments, argv }, tokens: node_tks, flags: NdFlags::empty(), redirs, - }) + })) + } + fn parse_assignment(&self, token: &Tk<'t>) -> Option> { + let mut chars = token.span.as_str().chars(); + let mut var_name = String::new(); + let mut name_range = token.span.start..token.span.start; + let mut var_val = String::new(); + let mut val_range = token.span.end..token.span.end; + let mut assign_kind = None; + let mut pos = token.span.start; + + while let Some(ch) = chars.next() { + if !assign_kind.is_none() { + match ch { + '\\' => { + pos += ch.len_utf8(); + var_val.push(ch); + if let Some(esc_ch) = chars.next() { + pos += esc_ch.len_utf8(); + var_val.push(esc_ch); + } + } + _ => { + pos += ch.len_utf8(); + var_val.push(ch); + } + } + } else { + match ch { + '=' => { + name_range.end = pos; + pos += ch.len_utf8(); + val_range.start = pos; + assign_kind = Some(AssignKind::Eq); + } + '-' => { + name_range.end = pos; + pos += ch.len_utf8(); + let Some('=') = chars.next() else { + return None + }; + pos += '='.len_utf8(); + val_range.start = pos; + assign_kind = Some(AssignKind::MinusEq); + } + '+' => { + name_range.end = pos; + pos += ch.len_utf8(); + let Some('=') = chars.next() else { + return None + }; + pos += '='.len_utf8(); + val_range.start = pos; + assign_kind = Some(AssignKind::PlusEq); + } + '/' => { + name_range.end = pos; + pos += ch.len_utf8(); + let Some('=') = chars.next() else { + return None + }; + pos += '='.len_utf8(); + val_range.start = pos; + assign_kind = Some(AssignKind::DivEq); + } + '*' => { + name_range.end = pos; + pos += ch.len_utf8(); + let Some('=') = chars.next() else { + return None + }; + pos += '='.len_utf8(); + val_range.start = pos; + assign_kind = Some(AssignKind::MultEq); + } + '\\' => { + pos += ch.len_utf8(); + var_name.push(ch); + if let Some(esc_ch) = chars.next() { + pos += esc_ch.len_utf8(); + var_name.push(esc_ch); + } + } + _ => { + pos += ch.len_utf8(); + var_name.push(ch) + } + } + } + } + if assign_kind.is_none() || var_name.is_empty() { + None + } else { + let var = Tk::new(TkRule::Str, Span::new(name_range, token.source())); + let val = Tk::new(TkRule::Str, Span::new(val_range, token.source())); + Some(Node { + class: NdRule::Assignment { kind: assign_kind.unwrap(), var, val }, + tokens: vec![token.clone()], + flags: NdFlags::empty(), + redirs: vec![] + }) + } } } impl<'t> Iterator for ParseStream<'t> { - type Item = ParseResult<'t>; + type Item = ShResult>; fn next(&mut self) -> Option { // Empty token vector or only SOI/EOI tokens, nothing to do if self.tokens.is_empty() || self.tokens.len() == 2 { @@ -451,9 +565,9 @@ impl<'t> Iterator for ParseStream<'t> { } } match self.parse_cmd_list() { - NoMatch => None, - Error(err) => Some(Error(err)), - Match(cmdlist) => Some(Match(cmdlist)) + Ok(Some(node)) => return Some(Ok(node)), + Ok(None) => return None, + Err(e) => return Some(Err(e)) } } } diff --git a/src/prelude.rs b/src/prelude.rs index 182bbb9..c427aa9 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -15,21 +15,32 @@ pub use std::fs::{ self, File, OpenOptions }; pub use std::path::{ Path, PathBuf }; pub use std::ffi::{ CStr, CString }; pub use std::process::exit; +pub use std::time::Instant; +pub use std::mem; +pub use std::env; +pub use std::fmt; // Unix-specific IO abstractions -pub use std::os::unix::io::{ AsRawFd, FromRawFd, IntoRawFd, OwnedFd, RawFd, }; +pub use std::os::unix::io::{ AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, RawFd, }; // Nix crate for POSIX APIs pub use nix::{ errno::Errno, fcntl::{ open, OFlag }, sys::{ - signal::{ self, kill, SigHandler, Signal }, + signal::{ self, kill, killpg, pthread_sigmask, SigSet, SigmaskHow, SigHandler, Signal }, stat::Mode, - wait::{ waitpid, WaitStatus }, + wait::{ waitpid, WaitPidFlag as WtFlag, WaitStatus as WtStat }, }, libc::{ STDIN_FILENO, STDERR_FILENO, STDOUT_FILENO }, - unistd::{ dup, read, write, close, dup2, execvpe, fork, pipe, Pid, ForkResult }, + unistd::{ + dup, read, isatty, write, close, setpgid, dup2, getpgrp, + execvpe, tcgetpgrp, tcsetpgrp, fork, pipe, Pid, ForkResult + }, }; +pub use bitflags::bitflags; + +pub use crate::flog; +pub use crate::libsh::flog::FernLogLevel::*; // Additional utilities, if needed, can be added here diff --git a/src/procio.rs b/src/procio.rs index b86bf2c..de106ee 100644 --- a/src/procio.rs +++ b/src/procio.rs @@ -1,6 +1,6 @@ use std::{fmt::Debug, ops::{Deref, DerefMut}}; -use crate::{libsh::error::ShResult, parse::Redir, prelude::*}; +use crate::{libsh::error::ShResult, parse::{Redir, RedirType}, prelude::*}; // Credit to fish-shell for many of the implementation ideas present in this module // https://fishshell.com/ @@ -46,6 +46,8 @@ impl Debug for Box { } } +/// A redirection to a raw fildesc +/// e.g. `2>&1` #[derive(Debug)] pub struct IoFd { tgt_fd: RawFd, @@ -81,6 +83,8 @@ impl IoInfo for IoFd { } } +/// A redirection to a file +/// e.g. `> file.txt` #[derive(Debug)] pub struct IoFile { tgt_fd: RawFd, @@ -112,6 +116,8 @@ impl IoInfo for IoFile { } } +/// A redirection to a pipe +/// e.g. `echo foo | sed s/foo/bar/` #[derive(Debug)] pub struct IoPipe { tgt_fd: RawFd, @@ -150,9 +156,32 @@ impl IoInfo for IoPipe { } } +pub struct FdWriter { + tgt: OwnedFd +} + +impl FdWriter { + pub fn new(fd: i32) -> Self { + let tgt = unsafe { OwnedFd::from_raw_fd(fd) }; + Self { tgt } + } +} + +impl Write for FdWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + Ok(write(&self.tgt, buf)?) + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +/// A struct wrapping three fildescs representing `stdin`, `stdout`, and `stderr` respectively #[derive(Debug)] pub struct IoGroup(OwnedFd,OwnedFd,OwnedFd); +/// A single stack frame used with the IoStack +/// Each stack frame represents the redirections of a single command #[derive(Default,Debug)] pub struct IoFrame { redirs: Vec, @@ -163,6 +192,42 @@ impl<'e> IoFrame { pub fn new() -> Self { Default::default() } + pub fn from_redirs(redirs: Vec) -> Self { + Self { redirs, saved_io: None } + } + + /// This method returns a 2-tuple of `IoFrames`. + /// This is to be used in the case of shell structures such as `if-then` and `while-do`. + /// # Params + /// * redirs: a vector of redirections + /// + /// # Returns + /// * An `IoFrame` containing all of the redirections which target stdin + /// * An `IoFrame` containing all of the redirections which target stdout/stderr + /// + /// # Purpose + /// In the case of something like `if cat; then echo foo; fi < input.txt > output.txt` + /// This will cleanly separate the redirections such that `cat` can receive the input from input.txt + /// and `echo foo` can redirect it's output to output.txt + pub fn cond_and_body(redirs: Vec) -> (Self, Self) { + let mut output_redirs = vec![]; + let mut input_redirs = vec![]; + for redir in redirs { + match redir.class { + RedirType::Input => input_redirs.push(redir), + RedirType::Pipe => { + match redir.io_info.tgt_fd() { + STDIN_FILENO => input_redirs.push(redir), + STDOUT_FILENO | + STDERR_FILENO => output_redirs.push(redir), + _ => unreachable!() + } + } + _ => output_redirs.push(redir) + } + } + (Self::from_redirs(input_redirs),Self::from_redirs(output_redirs)) + } pub fn save(&'e mut self) { unsafe { let saved_in = OwnedFd::from_raw_fd(dup(STDIN_FILENO).unwrap()); @@ -171,7 +236,7 @@ impl<'e> IoFrame { self.saved_io = Some(IoGroup(saved_in,saved_out,saved_err)); } } - pub fn redirect(&'e mut self) -> ShResult<'e,()> { + pub fn redirect(&mut self) -> ShResult<()> { self.save(); for redir in &mut self.redirs { let io_info = &mut redir.io_info; @@ -182,12 +247,11 @@ impl<'e> IoFrame { } Ok(()) } - pub fn restore(&'e mut self) -> ShResult<'e,()> { + pub fn restore(&mut self) -> ShResult<()> { while let Some(mut redir) = self.pop() { - redir.io_info.close().ok(); + redir.io_info.close()?; } if let Some(saved) = self.saved_io.take() { - dbg!(&saved); dup2(saved.0.as_raw_fd(), STDIN_FILENO)?; dup2(saved.1.as_raw_fd(), STDOUT_FILENO)?; dup2(saved.2.as_raw_fd(), STDERR_FILENO)?; @@ -209,6 +273,12 @@ impl DerefMut for IoFrame { } } +/// A stack that maintains the current state of I/O for commands +/// +/// This struct maintains the current state of I/O for the `Dispatcher` struct +/// Each executed command requires an `IoFrame` in order to perform redirections. +/// As nodes are walked through by the `Dispatcher`, it pushes new frames in certain contexts, and pops frames in others. +/// Each command calls pop_frame() in order to get the current IoFrame in order to perform redirection #[derive(Default)] pub struct IoStack { stack: Vec, @@ -229,13 +299,31 @@ impl<'e> IoStack { pub fn push_to_frame(&mut self, redir: Redir) { self.curr_frame_mut().push(redir) } + /// 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 + /// There will always be at least one frame in the `IoStack`. pub fn pop_frame(&mut self) -> IoFrame { if self.stack.len() > 1 { - self.stack.pop().unwrap() + self.pop().unwrap() } else { std::mem::take(self.curr_frame_mut()) } } + /// Push a new stack frame. + pub fn push_frame(&mut self, frame: IoFrame) { + self.push(frame) + } + /// Flatten the `IoStack` + /// All of the current stack frames will be flattened into a single one + /// Not sure what use this will serve, but my gut said this was worthy of writing + pub fn flatten(&mut self) { + let mut flat_frame = IoFrame::new(); + while let Some(mut frame) = self.pop() { + flat_frame.append(&mut frame) + } + self.push(flat_frame); + } } impl Deref for IoStack { @@ -250,3 +338,7 @@ impl DerefMut for IoStack { &mut self.stack } } + +pub fn borrow_fd<'f>(fd: i32) -> BorrowedFd<'f> { + unsafe { BorrowedFd::borrow_raw(fd) } +} diff --git a/src/prompt/history.rs b/src/prompt/history.rs index f299227..e93f5e7 100644 --- a/src/prompt/history.rs +++ b/src/prompt/history.rs @@ -66,7 +66,7 @@ impl<'e> FernHist { pub fn new() -> Self { Self { file_path: None, entries: HistEntries::new(), max_len: 1000, flags: HistFlags::empty() } } - pub fn from_path(file_path: PathBuf) -> ShResult<'e,Self> { + pub fn from_path(file_path: PathBuf) -> ShResult { let mut new_hist = FernHist::new(); new_hist.file_path = Some(file_path); new_hist.load_hist()?; @@ -76,14 +76,14 @@ impl<'e> FernHist { let id = self.len() + 1; HistEntry::new(body.to_string(), id) } - pub fn init_hist_file(&mut self) -> ShResult<'e,()> { + pub fn init_hist_file(&mut self) -> ShResult<()> { let Some(path) = self.file_path.clone() else { return Ok(()); }; self.save(&path)?; Ok(()) } - pub fn load_hist(&mut self) -> ShResult<'e,()> { + pub fn load_hist(&mut self) -> ShResult<()> { let Some(file_path) = self.file_path.clone() else { return Err( ShErr::simple( diff --git a/src/prompt/mod.rs b/src/prompt/mod.rs index 6e01569..5a9a036 100644 --- a/src/prompt/mod.rs +++ b/src/prompt/mod.rs @@ -9,16 +9,16 @@ use rustyline::{error::ReadlineError, history::{FileHistory, History}, Config, E use crate::{libsh::{error::ShResult, term::{Style, Styled}}, prelude::*}; -fn init_rl<'s>() -> ShResult<'s,Editor> { - let hist = FernHist::default(); +fn init_rl<'s>() -> ShResult> { let rl = FernReadline::new(); - let config = Config::default(); - let mut editor = Editor::with_history(config,hist)?; + let mut editor = Editor::new()?; editor.set_helper(Some(rl)); + editor.load_history(&Path::new("/home/pagedmov/.fernhist"))?; Ok(editor) } -pub fn read_line<'s>() -> ShResult<'s,String> { +pub fn read_line<'s>() -> ShResult { + assert!(isatty(STDIN_FILENO).unwrap()); let mut editor = init_rl()?; let prompt = "$ ".styled(Style::Green | Style::Bold); match editor.readline(&prompt) { diff --git a/src/prompt/readline.rs b/src/prompt/readline.rs index cc057c9..c804f8a 100644 --- a/src/prompt/readline.rs +++ b/src/prompt/readline.rs @@ -47,7 +47,11 @@ impl Hint for FernHint { impl Hinter for FernReadline { type Hint = FernHint; fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option { - let ent = ctx.history().search(line, pos, rustyline::history::SearchDirection::Reverse).ok()??; + let ent = ctx.history().search( + line, + ctx.history().len() - 1, + rustyline::history::SearchDirection::Reverse + ).ok()??; let entry_raw = ent.entry.get(pos..)?.to_string(); Some(FernHint::new(entry_raw)) } diff --git a/src/state.rs b/src/state.rs index e984639..e2859dc 100644 --- a/src/state.rs +++ b/src/state.rs @@ -6,8 +6,6 @@ pub static JOB_TABLE: LazyLock> = LazyLock::new(|| RwLock::new(Jo pub static VAR_TABLE: LazyLock> = LazyLock::new(|| RwLock::new(VarTab::new())); -pub static ENV_TABLE: LazyLock> = LazyLock::new(|| RwLock::new(EnvTab::new())); - pub struct JobTab { } @@ -51,7 +49,11 @@ impl VarTab { &mut self.params } pub fn get_var(&self, var: &str) -> String { - self.vars.get(var).map(|s| s.to_string()).unwrap_or_default() + if let Some(var) = self.vars.get(var).map(|s| s.to_string()) { + var + } else { + std::env::var(var).unwrap_or_default() + } } pub fn new_var(&mut self, var: &str, val: &str) { self.vars.insert(var.to_string(), val.to_string()); @@ -64,16 +66,6 @@ impl VarTab { } } -pub struct EnvTab { - -} - -impl EnvTab { - pub fn new() -> Self { - Self {} - } -} - /// Read from the job table pub fn read_jobs) -> T>(f: F) -> T { let lock = JOB_TABLE.read().unwrap(); @@ -98,18 +90,6 @@ pub fn write_vars) -> T>(f: F) -> T { f(lock) } -/// Read from the environment table -pub fn read_env) -> T>(f: F) -> T { - let lock = ENV_TABLE.read().unwrap(); - f(lock) -} - -/// Write to the environment table -pub fn write_env) -> T>(f: F) -> T { - let lock = &mut ENV_TABLE.write().unwrap(); - f(lock) -} - pub fn get_status() -> i32 { read_vars(|v| v.get_param('?')).parse::().unwrap() } diff --git a/src/tests/expand.rs b/src/tests/expand.rs index 2752408..b9711e7 100644 --- a/src/tests/expand.rs +++ b/src/tests/expand.rs @@ -1,3 +1,4 @@ +use expand::unescape_str; use parse::lex::{Tk, TkFlags, TkRule}; use state::write_vars; use super::super::*; @@ -10,10 +11,18 @@ fn simple_expansion() { let mut tokens: Vec = LexStream::new(varsub, LexFlags::empty()) .filter(|tk| !matches!(tk.class, TkRule::EOI | TkRule::SOI)) .collect(); - let var_tk = tokens.pop().unwrap(); + let var_tk = tokens.pop().unwrap(); - let var_span = var_tk.span.clone(); - let exp_tk = var_tk.expand(var_span, TkFlags::empty()); - write_vars(|v| v.vars_mut().clear()); - insta::assert_debug_snapshot!(exp_tk.get_words()) + let var_span = var_tk.span.clone(); + let exp_tk = var_tk.expand(var_span, TkFlags::empty()); + write_vars(|v| v.vars_mut().clear()); + insta::assert_debug_snapshot!(exp_tk.get_words()) +} + +#[test] +fn unescape_string() { + let string = "echo $foo \\$bar"; + let unescaped = unescape_str(string); + + insta::assert_snapshot!(unescaped) } diff --git a/src/tests/snapshots/fern__tests__expand__unescape_string.snap b/src/tests/snapshots/fern__tests__expand__unescape_string.snap new file mode 100644 index 0000000..43bdbd8 --- /dev/null +++ b/src/tests/snapshots/fern__tests__expand__unescape_string.snap @@ -0,0 +1,5 @@ +--- +source: src/tests/expand.rs +expression: unescaped +--- +echo ﷐foo $bar diff --git a/src/tests/snapshots/fern__tests__parser__parse_conjunction.snap b/src/tests/snapshots/fern__tests__parser__parse_conjunction.snap index 62044ca..c9ebac7 100644 --- a/src/tests/snapshots/fern__tests__parser__parse_conjunction.snap +++ b/src/tests/snapshots/fern__tests__parser__parse_conjunction.snap @@ -3,7 +3,7 @@ source: src/tests/parser.rs expression: nodes --- [ - Match( + Ok( Node { class: CmdList { elements: [ 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 09deeb1..c3331b8 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 @@ -3,7 +3,7 @@ source: src/tests/parser.rs expression: nodes --- [ - Match( + Ok( Node { class: CmdList { elements: [ @@ -21,7 +21,7 @@ expression: nodes err: Null, span: Span { range: 0..4, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + 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, @@ -33,7 +33,7 @@ expression: nodes err: Null, span: Span { range: 5..8, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -52,7 +52,7 @@ expression: nodes err: Null, span: Span { range: 0..4, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + 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, @@ -64,7 +64,7 @@ expression: nodes err: Null, span: Span { range: 5..8, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -82,7 +82,7 @@ expression: nodes err: Null, span: Span { range: 11..14, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + 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, @@ -93,8 +93,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 15..24, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 15..25, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -113,7 +113,7 @@ expression: nodes err: Null, span: Span { range: 11..14, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + 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, @@ -124,8 +124,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 15..24, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 15..25, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -147,7 +147,7 @@ expression: nodes err: Null, span: Span { range: 0..4, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + 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, @@ -159,7 +159,7 @@ expression: nodes err: Null, span: Span { range: 5..8, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -171,7 +171,7 @@ expression: nodes err: Null, span: Span { range: 9..10, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -183,7 +183,7 @@ expression: nodes err: Null, span: Span { range: 11..14, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + 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, @@ -194,8 +194,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 15..24, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 15..25, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -218,8 +218,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 28..32, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 29..33, + 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, @@ -230,8 +230,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 33..36, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 34..37, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -249,8 +249,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 28..32, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 29..33, + 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, @@ -261,8 +261,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 33..36, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 34..37, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -279,8 +279,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 39..42, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 40..43, + 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, @@ -291,8 +291,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 43..52, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 44..54, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -310,8 +310,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 39..42, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 40..43, + 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, @@ -322,8 +322,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 43..52, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 44..54, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -344,8 +344,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 28..32, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 29..33, + 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, @@ -356,8 +356,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 33..36, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 34..37, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -368,8 +368,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 37..38, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 38..39, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -380,8 +380,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 39..42, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 40..43, + 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, @@ -392,8 +392,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 43..52, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 44..54, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -416,8 +416,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 56..60, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 58..62, + 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, @@ -428,8 +428,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 61..64, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 63..66, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -440,8 +440,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 65..68, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 67..70, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -459,8 +459,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 56..60, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 58..62, + 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, @@ -471,8 +471,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 61..64, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 63..66, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -483,8 +483,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 65..68, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 67..70, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -501,8 +501,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 71..74, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 73..76, + 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, @@ -513,8 +513,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 75..80, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 77..82, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -525,8 +525,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 81..88, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 83..90, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -537,8 +537,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 89..93, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 91..95, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -556,8 +556,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 71..74, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 73..76, + 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, @@ -568,8 +568,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 75..80, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 77..82, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -580,8 +580,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 81..88, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 83..90, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -592,8 +592,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 89..93, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 91..95, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -614,8 +614,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 56..60, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 58..62, + 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, @@ -626,8 +626,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 61..64, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 63..66, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -638,8 +638,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 65..68, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 67..70, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -650,8 +650,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 69..70, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 71..72, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -662,8 +662,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 71..74, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 73..76, + 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, @@ -674,8 +674,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 75..80, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 77..82, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -686,8 +686,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 81..88, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 83..90, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -698,8 +698,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 89..93, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 91..95, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -722,7 +722,7 @@ expression: nodes err: Null, span: Span { range: 0..4, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + 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, @@ -734,7 +734,7 @@ expression: nodes err: Null, span: Span { range: 5..8, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -746,7 +746,7 @@ expression: nodes err: Null, span: Span { range: 9..10, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -758,7 +758,7 @@ expression: nodes err: Null, span: Span { range: 11..14, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + 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, @@ -769,8 +769,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 15..24, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 15..25, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -781,8 +781,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 25..27, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 26..28, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -793,8 +793,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 28..32, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 29..33, + 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, @@ -805,8 +805,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 33..36, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 34..37, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -817,8 +817,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 37..38, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 38..39, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -829,8 +829,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 39..42, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 40..43, + 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, @@ -841,8 +841,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 43..52, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 44..54, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -853,8 +853,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 53..55, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 55..57, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -865,8 +865,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 56..60, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 58..62, + 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, @@ -877,8 +877,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 61..64, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 63..66, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -889,8 +889,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 65..68, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 67..70, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -901,8 +901,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 69..70, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 71..72, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -913,8 +913,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 71..74, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 73..76, + 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, @@ -925,8 +925,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 75..80, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 77..82, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -937,8 +937,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 81..88, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 83..90, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, flags: TkFlags( 0x0, @@ -949,8 +949,8 @@ expression: nodes err_span: None, err: Null, span: Span { - range: 89..93, - source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/", + range: 91..95, + source: "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/", }, 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 5a1fd65..86e66aa 100644 --- a/src/tests/snapshots/fern__tests__parser__parse_pipeline.snap +++ b/src/tests/snapshots/fern__tests__parser__parse_pipeline.snap @@ -3,7 +3,7 @@ source: src/tests/parser.rs expression: nodes --- [ - Match( + Ok( Node { class: CmdList { elements: [ diff --git a/src/tests/snapshots/fern__tests__parser__parse_simple.snap b/src/tests/snapshots/fern__tests__parser__parse_simple.snap index 7bf6a39..a6452bf 100644 --- a/src/tests/snapshots/fern__tests__parser__parse_simple.snap +++ b/src/tests/snapshots/fern__tests__parser__parse_simple.snap @@ -3,7 +3,7 @@ source: src/tests/parser.rs expression: nodes --- [ - Match( + Ok( Node { class: CmdList { elements: [