From 80b645359777744cb846d22bc1c9aabc1613000d Mon Sep 17 00:00:00 2001 From: pagedmov Date: Sat, 29 Mar 2025 22:16:26 -0400 Subject: [PATCH] Implemented subshells and improved error handling --- src/builtin/cd.rs | 2 +- src/expand.rs | 49 +++++++++++++-- src/parse/execute.rs | 63 ++++++++++++++----- src/parse/lex.rs | 1 + src/parse/mod.rs | 11 +--- src/state.rs | 29 +++++++++ src/tests/error.rs | 52 +++++++++++++++ src/tests/expand.rs | 4 +- .../fern__tests__error__case_no_in.snap | 2 +- .../fern__tests__error__if_no_then.snap | 2 +- .../fern__tests__error__loop_no_do.snap | 2 +- .../fern__tests__error__unclosed_brc_grp.snap | 10 +++ .../fern__tests__error__unclosed_dquote.snap | 10 +++ .../fern__tests__error__unclosed_squote.snap | 10 +++ .../fern__tests__error__unclosed_subsh.snap | 10 +++ 15 files changed, 222 insertions(+), 35 deletions(-) create mode 100644 src/tests/snapshots/fern__tests__error__unclosed_brc_grp.snap create mode 100644 src/tests/snapshots/fern__tests__error__unclosed_dquote.snap create mode 100644 src/tests/snapshots/fern__tests__error__unclosed_squote.snap create mode 100644 src/tests/snapshots/fern__tests__error__unclosed_subsh.snap diff --git a/src/builtin/cd.rs b/src/builtin/cd.rs index 9239fd7..abcebeb 100644 --- a/src/builtin/cd.rs +++ b/src/builtin/cd.rs @@ -9,7 +9,7 @@ pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> { let (argv,_) = setup_builtin(argv,job,None)?; - let new_dir = if let Some((arg,_)) = argv.into_iter().skip(1).next() { + let new_dir = if let Some((arg,_)) = argv.into_iter().next() { PathBuf::from(arg) } else { PathBuf::from(env::var("HOME").unwrap()) diff --git a/src/expand.rs b/src/expand.rs index 809b9d7..9017087 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -1,6 +1,12 @@ use std::collections::HashSet; -use crate::{exec_input, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{lex::{is_field_sep, is_hard_sep, LexFlags, LexStream, Span, Tk, TkFlags, TkRule}, Redir, RedirType}, prelude::*, procio::{IoBuf, IoFrame, IoMode}, state::{read_vars, write_meta, LogTab}}; +use crate::state::{read_vars, write_meta, LogTab}; +use crate::procio::{IoBuf, IoFrame, IoMode}; +use crate::prelude::*; +use crate::parse::{Redir, RedirType}; +use crate::parse::execute::exec_input; +use crate::parse::lex::{is_field_sep, is_hard_sep, LexFlags, LexStream, Tk, TkFlags, TkRule}; +use crate::libsh::error::{ShErr, ShErrKind, ShResult}; /// Variable substitution marker pub const VAR_SUB: char = '\u{fdd0}'; @@ -10,6 +16,8 @@ pub const DUB_QUOTE: char = '\u{fdd1}'; pub const SNG_QUOTE: char = '\u{fdd2}'; /// Tilde sub marker pub const TILDE_SUB: char = '\u{fdd3}'; +/// Subshell marker +pub const SUBSH: char = '\u{fdd4}'; impl Tk { /// Create a new expanded token @@ -18,7 +26,9 @@ impl Tk { /// tokens: A vector of raw tokens lexed from the expansion result /// span: The span of the original token that is being expanded /// flags: some TkFlags - pub fn expand(self, span: Span, flags: TkFlags) -> ShResult { + pub fn expand(self) -> ShResult { + let flags = self.flags; + let span = self.span.clone(); let exp = Expander::new(self).expand()?; let class = TkRule::Expanded { exp }; Ok(Self { class, span, flags, }) @@ -57,7 +67,9 @@ impl Expander { 'outer: while let Some(ch) = chars.next() { match ch { - DUB_QUOTE | SNG_QUOTE => { + DUB_QUOTE | + SNG_QUOTE | + SUBSH => { while let Some(q_ch) = chars.next() { match q_ch { _ if q_ch == ch => continue 'outer, // Isn't rust cool @@ -119,7 +131,7 @@ impl Expander { var_name.clear(); break } - _ if is_hard_sep(ch) || ch == DUB_QUOTE => { + _ if is_hard_sep(ch) || ch == DUB_QUOTE || ch == SUBSH => { let var_val = read_vars(|v| v.get_var(&var_name)); result.push_str(&var_val); result.push(ch); @@ -211,6 +223,35 @@ pub fn unescape_str(raw: &str) -> String { result.push(next_ch) } } + '(' => { + result.push(SUBSH); + let mut paren_count = 1; + while let Some(subsh_ch) = chars.next() { + match subsh_ch { + '\\' => { + result.push(subsh_ch); + if let Some(next_ch) = chars.next() { + result.push(next_ch) + } + } + '$' => result.push(VAR_SUB), + '(' => { + paren_count += 1; + result.push(subsh_ch) + } + ')' => { + paren_count -= 1; + if paren_count == 0 { + result.push(SUBSH); + } else { + result.push(subsh_ch) + } + break + } + _ => result.push(subsh_ch) + } + } + } '"' => { result.push(DUB_QUOTE); while let Some(q_ch) = chars.next() { diff --git a/src/parse/execute.rs b/src/parse/execute.rs index 9f4bab5..a91684e 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -1,7 +1,7 @@ use std::collections::{HashSet, VecDeque}; -use crate::{builtin::{alias::alias, cd::cd, echo::echo, export::export, flowctl::flowctl, jobctl::{continue_job, jobs, JobBehavior}, pwd::pwd, shift::shift, shopt::shopt, source::source, zoltraak::zoltraak}, expand::expand_aliases, jobs::{dispatch_job, ChildProc, JobBldr, JobStack}, libsh::{error::{ShErr, ShErrKind, ShResult, ShResultExt}, utils::RedirVecUtils}, prelude::*, procio::{IoFrame, IoMode, IoStack}, state::{self, read_logic, read_vars, write_logic, write_meta, write_vars, ShFunc, VarTab, LOGIC_TABLE}}; +use crate::{builtin::{alias::alias, cd::cd, echo::echo, export::export, flowctl::flowctl, jobctl::{continue_job, jobs, JobBehavior}, pwd::pwd, shift::shift, shopt::shopt, source::source, zoltraak::zoltraak}, expand::expand_aliases, jobs::{dispatch_job, ChildProc, JobBldr, JobStack}, libsh::{error::{ShErr, ShErrKind, ShResult, ShResultExt}, utils::RedirVecUtils}, prelude::*, procio::{IoFrame, IoMode, IoStack}, state::{self, get_snapshots, read_logic, read_vars, restore_snapshot, write_logic, write_meta, write_vars, ShFunc, VarTab, LOGIC_TABLE}}; use super::{lex::{Span, Tk, TkFlags, KEYWORDS}, AssignKind, CaseNode, CondNode, ConjunctNode, ConjunctOp, LoopKind, NdFlags, NdRule, Node, ParsedSrc, Redir, RedirType}; @@ -97,6 +97,8 @@ impl Dispatcher { self.exec_builtin(node) } else if is_func(node.get_command().cloned()) { self.exec_func(node) + } else if is_subsh(node.get_command().cloned()) { + self.exec_subsh(node) } else { self.exec_cmd(node) } @@ -151,7 +153,29 @@ impl Dispatcher { write_logic(|l| l.insert_func(name, func)); // Store the AST Ok(()) } - pub fn exec_func(&mut self, func: Node) -> ShResult<()> { + fn exec_subsh(&mut self, subsh: Node) -> ShResult<()> { + let NdRule::Command { assignments, argv } = subsh.class else { + unreachable!() + }; + + self.set_assignments(assignments, AssignBehavior::Export); + self.io_stack.append_to_frame(subsh.redirs); + let mut argv = prepare_argv(argv)?; + + let subsh = argv.remove(0); + let subsh_body = subsh.0.to_string(); + flog!(DEBUG, subsh_body); + let snapshot = get_snapshots(); + + if let Err(e) = exec_input(subsh_body) { + restore_snapshot(snapshot); + return Err(e.into()) + } + + restore_snapshot(snapshot); + Ok(()) + } + fn exec_func(&mut self, func: Node) -> ShResult<()> { let blame = func.get_span().clone(); let NdRule::Command { assignments, mut argv } = func.class else { unreachable!() @@ -163,7 +187,7 @@ impl Dispatcher { let func_name = argv.remove(0).span.as_str().to_string(); if let Some(func) = read_logic(|l| l.get_func(&func_name)) { - let scope_snapshot = read_vars(|v| v.clone()); + let snapshot = get_snapshots(); // Set up the inner scope write_vars(|v| { **v = VarTab::new(); @@ -174,19 +198,21 @@ impl Dispatcher { }); if let Err(e) = self.exec_brc_grp((*func).clone()) { - write_vars(|v| **v = scope_snapshot); + restore_snapshot(snapshot); match e.kind() { ShErrKind::FuncReturn(code) => { state::set_status(*code); return Ok(()) } - _ => return Err(e.into()) + _ => return { + Err(e.into()) + } } } // Return to the outer scope - write_vars(|v| **v = scope_snapshot); + restore_snapshot(snapshot); Ok(()) } else { Err( @@ -198,7 +224,7 @@ impl Dispatcher { ) } } - pub fn exec_brc_grp(&mut self, brc_grp: Node) -> ShResult<()> { + fn exec_brc_grp(&mut self, brc_grp: Node) -> ShResult<()> { let NdRule::BraceGrp { body } = brc_grp.class else { unreachable!() }; @@ -213,7 +239,7 @@ impl Dispatcher { Ok(()) } - pub fn exec_case(&mut self, case_stmt: Node) -> ShResult<()> { + fn exec_case(&mut self, case_stmt: Node) -> ShResult<()> { let NdRule::CaseNode { pattern, case_blocks } = case_stmt.class else { unreachable!() }; @@ -221,7 +247,7 @@ impl Dispatcher { self.io_stack.append_to_frame(case_stmt.redirs); flog!(DEBUG,pattern.span.as_str()); - let exp_pattern = pattern.clone().expand(pattern.span.clone(), pattern.flags.clone())?; + let exp_pattern = pattern.clone().expand()?; let pattern_raw = exp_pattern .get_words() .first() @@ -246,7 +272,7 @@ impl Dispatcher { Ok(()) } - pub fn exec_loop(&mut self, loop_stmt: Node) -> ShResult<()> { + fn exec_loop(&mut self, loop_stmt: Node) -> ShResult<()> { let NdRule::LoopNode { kind, cond_node } = loop_stmt.class else { unreachable!(); }; @@ -297,7 +323,7 @@ impl Dispatcher { Ok(()) } - pub fn exec_if(&mut self, if_stmt: Node) -> ShResult<()> { + fn exec_if(&mut self, if_stmt: Node) -> ShResult<()> { let NdRule::IfNode { cond_nodes, else_block } = if_stmt.class else { unreachable!(); }; @@ -337,7 +363,7 @@ impl Dispatcher { Ok(()) } - pub fn exec_pipeline(&mut self, pipeline: Node) -> ShResult<()> { + fn exec_pipeline(&mut self, pipeline: Node) -> ShResult<()> { let NdRule::Pipeline { cmds, pipe_err: _ } = pipeline.class else { unreachable!() }; @@ -362,7 +388,7 @@ impl Dispatcher { dispatch_job(job, is_bg)?; Ok(()) } - pub fn exec_builtin(&mut self, mut cmd: Node) -> ShResult<()> { + fn exec_builtin(&mut self, mut cmd: Node) -> ShResult<()> { let NdRule::Command { ref mut assignments, argv: _ } = &mut cmd.class else { unreachable!() }; @@ -402,7 +428,7 @@ impl Dispatcher { } Ok(()) } - pub fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> { + fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> { let NdRule::Command { assignments, argv } = cmd.class else { unreachable!() }; @@ -438,7 +464,7 @@ impl Dispatcher { Ok(()) } - pub fn set_assignments(&self, assigns: Vec, behavior: AssignBehavior) -> Vec { + fn set_assignments(&self, assigns: Vec, behavior: AssignBehavior) -> Vec { let mut new_env_vars = vec![]; match behavior { AssignBehavior::Export => { @@ -483,9 +509,8 @@ pub fn prepare_argv(argv: Vec) -> ShResult> { let mut args = vec![]; for arg in argv { - let flags = arg.flags; let span = arg.span.clone(); - let expanded = arg.expand(span.clone(), flags)?; + let expanded = arg.expand()?; for exp in expanded.get_words() { args.push((exp,span.clone())) } @@ -602,3 +627,7 @@ pub fn is_func(tk: Option) -> bool { }; read_logic(|l| l.get_func(&tk.to_string())).is_some() } + +pub fn is_subsh(tk: Option) -> bool { + tk.is_some_and(|tk| tk.flags.contains(TkFlags::IS_SUBSH)) +} diff --git a/src/parse/lex.rs b/src/parse/lex.rs index aa0572a..388080c 100644 --- a/src/parse/lex.rs +++ b/src/parse/lex.rs @@ -401,6 +401,7 @@ impl LexStream { } } if !paren_stack.is_empty() { + self.cursor = pos; return Err( ShErr::full( ShErrKind::ParseErr, diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 8b6ee9f..7ca5fcf 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -39,18 +39,14 @@ impl ParsedSrc { } pub fn parse_src(&mut self) -> Result<(),Vec> { let mut tokens = vec![]; - let mut errors = vec![]; for lex_result in LexStream::new(self.src.clone(), LexFlags::empty()) { match lex_result { Ok(token) => tokens.push(token), - Err(error) => errors.push(error) + Err(error) => return Err(vec![error]) } } - if !errors.is_empty() { - return Err(errors) - } - + let mut errors = vec![]; let mut nodes = vec![]; for parse_result in ParseStream::new(tokens) { flog!(DEBUG, parse_result); @@ -59,7 +55,6 @@ impl ParsedSrc { Err(error) => errors.push(error) } } - flog!(DEBUG, errors); if !errors.is_empty() { return Err(errors) @@ -1108,7 +1103,7 @@ impl Iterator for ParseStream { flog!(DEBUG, "parsing"); flog!(DEBUG, self.tokens); // Empty token vector or only SOI/EOI tokens, nothing to do - if self.tokens.is_empty() || self.tokens.len() == 2 { + if self.tokens.is_empty() || self.tokens.len() == 1 { return None } while let Some(tk) = self.tokens.first() { diff --git a/src/state.rs b/src/state.rs index e85de88..0550f3a 100644 --- a/src/state.rs +++ b/src/state.rs @@ -184,6 +184,16 @@ impl VarTab { self.bpush_arg(arg); } } + pub fn update_exports(&mut self) { + for var_name in self.vars.keys() { + let var = self.vars.get(var_name).unwrap(); + if var.export { + env::set_var(var_name, &var.value); + } else { + env::set_var(var_name, ""); + } + } + } pub fn sh_argv(&self) -> &VecDeque { &self.sh_argv } @@ -379,6 +389,25 @@ pub fn set_status(code: i32) { write_vars(|v| v.set_param('?', &code.to_string())) } +/// Save the current state of the logic and variable table, and the working directory path +pub fn get_snapshots() -> (LogTab, VarTab, String) { + ( + read_logic(|l| l.clone()), + read_vars(|v| v.clone()), + env::var("PWD").unwrap_or_default() + ) +} + +pub fn restore_snapshot(snapshot: (LogTab, VarTab, String)) { + write_logic(|l| **l = snapshot.0); + write_vars(|v| { + **v = snapshot.1; + v.update_exports(); + }); + env::set_current_dir(&snapshot.2).unwrap(); + env::set_var("PWD", &snapshot.2); +} + pub fn source_rc() -> ShResult<()> { let path = if let Ok(path) = env::var("FERN_RC") { PathBuf::from(&path) diff --git a/src/tests/error.rs b/src/tests/error.rs index 6efd72c..5f4547c 100644 --- a/src/tests/error.rs +++ b/src/tests/error.rs @@ -10,6 +10,58 @@ fn cmd_not_found() { insta::assert_snapshot!(err_fmt) } +#[test] +fn unclosed_subsh() { + let input = "(foo"; + let token = LexStream::new(Arc::new(input.into()), LexFlags::empty()).skip(1).next().unwrap(); + let Err(err) = token else { + panic!("{:?}",token); + }; + + let err_fmt = format!("{err}"); + insta::assert_snapshot!(err_fmt) +} + +#[test] +fn unclosed_dquote() { + let input = "\"foo bar"; + let token = LexStream::new(Arc::new(input.into()), LexFlags::empty()).skip(1).next().unwrap(); + let Err(err) = token else { + panic!(); + }; + + let err_fmt = format!("{err}"); + insta::assert_snapshot!(err_fmt) +} + +#[test] +fn unclosed_squote() { + let input = "'foo bar"; + let token = LexStream::new(Arc::new(input.into()), LexFlags::empty()).skip(1).next().unwrap(); + let Err(err) = token else { + panic!(); + }; + + let err_fmt = format!("{err}"); + insta::assert_snapshot!(err_fmt) +} + +#[test] +fn unclosed_brc_grp() { + let input = "{ foo bar"; + let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty()) + .map(|tk| tk.unwrap()) + .collect::>(); + + let node = ParseStream::new(tokens).next().unwrap(); + let Err(err) = node else { + panic!(); + }; + + let err_fmt = format!("{err}"); + insta::assert_snapshot!(err_fmt) +} + #[test] fn if_no_fi() { let input = "if foo; then bar;"; diff --git a/src/tests/expand.rs b/src/tests/expand.rs index 7b81571..b09500c 100644 --- a/src/tests/expand.rs +++ b/src/tests/expand.rs @@ -5,7 +5,7 @@ use super::*; #[test] fn simple_expansion() { let varsub = "$foo"; - write_vars(|v| v.set_var("foo", "this is the value of the variable".into())); + write_vars(|v| v.set_var("foo", "this is the value of the variable".into(), false)); let mut tokens: Vec = LexStream::new(Arc::new(varsub.to_string()), LexFlags::empty()) .map(|tk| tk.unwrap()) @@ -14,7 +14,7 @@ fn simple_expansion() { let var_tk = tokens.pop().unwrap(); let var_span = var_tk.span.clone(); - let exp_tk = var_tk.expand(var_span, TkFlags::empty()).unwrap(); + let exp_tk = var_tk.expand().unwrap(); write_vars(|v| v.vars_mut().clear()); insta::assert_debug_snapshot!(exp_tk.get_words()) } diff --git a/src/tests/snapshots/fern__tests__error__case_no_in.snap b/src/tests/snapshots/fern__tests__error__case_no_in.snap index 11451a0..b1d3435 100644 --- a/src/tests/snapshots/fern__tests__error__case_no_in.snap +++ b/src/tests/snapshots/fern__tests__error__case_no_in.snap @@ -6,5 +6,5 @@ expression: err_fmt -> [1;1]  | 1 | case foo foo) bar;; bar) foo;; esac - | ^^^^^^^^^^^^^^^^^^^^ + | ^^^^^^^^^^^^^^^^^^^  | diff --git a/src/tests/snapshots/fern__tests__error__if_no_then.snap b/src/tests/snapshots/fern__tests__error__if_no_then.snap index 7885d13..7ea9a5d 100644 --- a/src/tests/snapshots/fern__tests__error__if_no_then.snap +++ b/src/tests/snapshots/fern__tests__error__if_no_then.snap @@ -6,5 +6,5 @@ expression: err_fmt -> [1;1]  | 1 | if foo; bar; fi - | ^^^^^^^^^^^^^ + | ^^^^^^^^^^^^  | diff --git a/src/tests/snapshots/fern__tests__error__loop_no_do.snap b/src/tests/snapshots/fern__tests__error__loop_no_do.snap index 4ff907e..4fccbfc 100644 --- a/src/tests/snapshots/fern__tests__error__loop_no_do.snap +++ b/src/tests/snapshots/fern__tests__error__loop_no_do.snap @@ -6,5 +6,5 @@ expression: err_fmt -> [1;1]  | 1 | while true; echo foo; done - | ^^^^^^^^^^^^^^^^^^^^^^ + | ^^^^^^^^^^^^^^^^^^^^^  | diff --git a/src/tests/snapshots/fern__tests__error__unclosed_brc_grp.snap b/src/tests/snapshots/fern__tests__error__unclosed_brc_grp.snap new file mode 100644 index 0000000..cd2bffd --- /dev/null +++ b/src/tests/snapshots/fern__tests__error__unclosed_brc_grp.snap @@ -0,0 +1,10 @@ +--- +source: src/tests/error.rs +expression: err_fmt +--- +Parse Error - Expected a closing brace for this brace group + -> [1;1] + | +1 | { foo bar + | ^^^^^^^^^ + | diff --git a/src/tests/snapshots/fern__tests__error__unclosed_dquote.snap b/src/tests/snapshots/fern__tests__error__unclosed_dquote.snap new file mode 100644 index 0000000..bb18194 --- /dev/null +++ b/src/tests/snapshots/fern__tests__error__unclosed_dquote.snap @@ -0,0 +1,10 @@ +--- +source: src/tests/error.rs +expression: err_fmt +--- +Parse Error - Unterminated quote + -> [1;1] + | +1 | "foo bar + | ^^^^^^^^ + | diff --git a/src/tests/snapshots/fern__tests__error__unclosed_squote.snap b/src/tests/snapshots/fern__tests__error__unclosed_squote.snap new file mode 100644 index 0000000..d121112 --- /dev/null +++ b/src/tests/snapshots/fern__tests__error__unclosed_squote.snap @@ -0,0 +1,10 @@ +--- +source: src/tests/error.rs +expression: err_fmt +--- +Parse Error - Unterminated quote + -> [1;1] + | +1 | 'foo bar + | ^^^^^^^^ + | diff --git a/src/tests/snapshots/fern__tests__error__unclosed_subsh.snap b/src/tests/snapshots/fern__tests__error__unclosed_subsh.snap new file mode 100644 index 0000000..a5f3979 --- /dev/null +++ b/src/tests/snapshots/fern__tests__error__unclosed_subsh.snap @@ -0,0 +1,10 @@ +--- +source: src/tests/error.rs +expression: err_fmt +--- +Parse Error - Unclosed subshell + -> [1;1] + | +1 | (foo + | ^ + |