Implemented subshells and improved error handling

This commit is contained in:
2025-03-29 22:16:26 -04:00
parent a16ad981bc
commit ed05e34379
15 changed files with 222 additions and 35 deletions

View File

@@ -9,7 +9,7 @@ pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> {
let (argv,_) = setup_builtin(argv,job,None)?; 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) PathBuf::from(arg)
} else { } else {
PathBuf::from(env::var("HOME").unwrap()) PathBuf::from(env::var("HOME").unwrap())

View File

@@ -1,6 +1,12 @@
use std::collections::HashSet; 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 /// Variable substitution marker
pub const VAR_SUB: char = '\u{fdd0}'; 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}'; pub const SNG_QUOTE: char = '\u{fdd2}';
/// Tilde sub marker /// Tilde sub marker
pub const TILDE_SUB: char = '\u{fdd3}'; pub const TILDE_SUB: char = '\u{fdd3}';
/// Subshell marker
pub const SUBSH: char = '\u{fdd4}';
impl Tk { impl Tk {
/// Create a new expanded token /// Create a new expanded token
@@ -18,7 +26,9 @@ impl Tk {
/// tokens: A vector of raw tokens lexed from the expansion result /// tokens: A vector of raw tokens lexed from the expansion result
/// span: The span of the original token that is being expanded /// span: The span of the original token that is being expanded
/// flags: some TkFlags /// flags: some TkFlags
pub fn expand(self, span: Span, flags: TkFlags) -> ShResult<Self> { pub fn expand(self) -> ShResult<Self> {
let flags = self.flags;
let span = self.span.clone();
let exp = Expander::new(self).expand()?; let exp = Expander::new(self).expand()?;
let class = TkRule::Expanded { exp }; let class = TkRule::Expanded { exp };
Ok(Self { class, span, flags, }) Ok(Self { class, span, flags, })
@@ -57,7 +67,9 @@ impl Expander {
'outer: while let Some(ch) = chars.next() { 'outer: while let Some(ch) = chars.next() {
match ch { match ch {
DUB_QUOTE | SNG_QUOTE => { DUB_QUOTE |
SNG_QUOTE |
SUBSH => {
while let Some(q_ch) = chars.next() { while let Some(q_ch) = chars.next() {
match q_ch { match q_ch {
_ if q_ch == ch => continue 'outer, // Isn't rust cool _ if q_ch == ch => continue 'outer, // Isn't rust cool
@@ -119,7 +131,7 @@ impl Expander {
var_name.clear(); var_name.clear();
break 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)); let var_val = read_vars(|v| v.get_var(&var_name));
result.push_str(&var_val); result.push_str(&var_val);
result.push(ch); result.push(ch);
@@ -211,6 +223,35 @@ pub fn unescape_str(raw: &str) -> String {
result.push(next_ch) 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); result.push(DUB_QUOTE);
while let Some(q_ch) = chars.next() { while let Some(q_ch) = chars.next() {

View File

@@ -1,7 +1,7 @@
use std::collections::{HashSet, VecDeque}; 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}; 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) self.exec_builtin(node)
} else if is_func(node.get_command().cloned()) { } else if is_func(node.get_command().cloned()) {
self.exec_func(node) self.exec_func(node)
} else if is_subsh(node.get_command().cloned()) {
self.exec_subsh(node)
} else { } else {
self.exec_cmd(node) self.exec_cmd(node)
} }
@@ -151,7 +153,29 @@ impl Dispatcher {
write_logic(|l| l.insert_func(name, func)); // Store the AST write_logic(|l| l.insert_func(name, func)); // Store the AST
Ok(()) 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 blame = func.get_span().clone();
let NdRule::Command { assignments, mut argv } = func.class else { let NdRule::Command { assignments, mut argv } = func.class else {
unreachable!() unreachable!()
@@ -163,7 +187,7 @@ impl Dispatcher {
let func_name = argv.remove(0).span.as_str().to_string(); let func_name = argv.remove(0).span.as_str().to_string();
if let Some(func) = read_logic(|l| l.get_func(&func_name)) { 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 // Set up the inner scope
write_vars(|v| { write_vars(|v| {
**v = VarTab::new(); **v = VarTab::new();
@@ -174,19 +198,21 @@ impl Dispatcher {
}); });
if let Err(e) = self.exec_brc_grp((*func).clone()) { if let Err(e) = self.exec_brc_grp((*func).clone()) {
write_vars(|v| **v = scope_snapshot); restore_snapshot(snapshot);
match e.kind() { match e.kind() {
ShErrKind::FuncReturn(code) => { ShErrKind::FuncReturn(code) => {
state::set_status(*code); state::set_status(*code);
return Ok(()) return Ok(())
} }
_ => return Err(e.into()) _ => return {
Err(e.into())
}
} }
} }
// Return to the outer scope // Return to the outer scope
write_vars(|v| **v = scope_snapshot); restore_snapshot(snapshot);
Ok(()) Ok(())
} else { } else {
Err( 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 { let NdRule::BraceGrp { body } = brc_grp.class else {
unreachable!() unreachable!()
}; };
@@ -213,7 +239,7 @@ impl Dispatcher {
Ok(()) 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 { let NdRule::CaseNode { pattern, case_blocks } = case_stmt.class else {
unreachable!() unreachable!()
}; };
@@ -221,7 +247,7 @@ impl Dispatcher {
self.io_stack.append_to_frame(case_stmt.redirs); self.io_stack.append_to_frame(case_stmt.redirs);
flog!(DEBUG,pattern.span.as_str()); 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 let pattern_raw = exp_pattern
.get_words() .get_words()
.first() .first()
@@ -246,7 +272,7 @@ impl Dispatcher {
Ok(()) 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 { let NdRule::LoopNode { kind, cond_node } = loop_stmt.class else {
unreachable!(); unreachable!();
}; };
@@ -297,7 +323,7 @@ impl Dispatcher {
Ok(()) 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 { let NdRule::IfNode { cond_nodes, else_block } = if_stmt.class else {
unreachable!(); unreachable!();
}; };
@@ -337,7 +363,7 @@ impl Dispatcher {
Ok(()) 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 { let NdRule::Pipeline { cmds, pipe_err: _ } = pipeline.class else {
unreachable!() unreachable!()
}; };
@@ -362,7 +388,7 @@ impl Dispatcher {
dispatch_job(job, is_bg)?; dispatch_job(job, is_bg)?;
Ok(()) 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 { let NdRule::Command { ref mut assignments, argv: _ } = &mut cmd.class else {
unreachable!() unreachable!()
}; };
@@ -402,7 +428,7 @@ impl Dispatcher {
} }
Ok(()) 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 { let NdRule::Command { assignments, argv } = cmd.class else {
unreachable!() unreachable!()
}; };
@@ -438,7 +464,7 @@ impl Dispatcher {
Ok(()) Ok(())
} }
pub fn set_assignments(&self, assigns: Vec<Node>, behavior: AssignBehavior) -> Vec<String> { fn set_assignments(&self, assigns: Vec<Node>, behavior: AssignBehavior) -> Vec<String> {
let mut new_env_vars = vec![]; let mut new_env_vars = vec![];
match behavior { match behavior {
AssignBehavior::Export => { AssignBehavior::Export => {
@@ -483,9 +509,8 @@ pub fn prepare_argv(argv: Vec<Tk>) -> ShResult<Vec<(String,Span)>> {
let mut args = vec![]; let mut args = vec![];
for arg in argv { for arg in argv {
let flags = arg.flags;
let span = arg.span.clone(); let span = arg.span.clone();
let expanded = arg.expand(span.clone(), flags)?; let expanded = arg.expand()?;
for exp in expanded.get_words() { for exp in expanded.get_words() {
args.push((exp,span.clone())) args.push((exp,span.clone()))
} }
@@ -602,3 +627,7 @@ pub fn is_func(tk: Option<Tk>) -> bool {
}; };
read_logic(|l| l.get_func(&tk.to_string())).is_some() read_logic(|l| l.get_func(&tk.to_string())).is_some()
} }
pub fn is_subsh(tk: Option<Tk>) -> bool {
tk.is_some_and(|tk| tk.flags.contains(TkFlags::IS_SUBSH))
}

View File

@@ -401,6 +401,7 @@ impl LexStream {
} }
} }
if !paren_stack.is_empty() { if !paren_stack.is_empty() {
self.cursor = pos;
return Err( return Err(
ShErr::full( ShErr::full(
ShErrKind::ParseErr, ShErrKind::ParseErr,

View File

@@ -39,18 +39,14 @@ impl ParsedSrc {
} }
pub fn parse_src(&mut self) -> Result<(),Vec<ShErr>> { pub fn parse_src(&mut self) -> Result<(),Vec<ShErr>> {
let mut tokens = vec![]; let mut tokens = vec![];
let mut errors = vec![];
for lex_result in LexStream::new(self.src.clone(), LexFlags::empty()) { for lex_result in LexStream::new(self.src.clone(), LexFlags::empty()) {
match lex_result { match lex_result {
Ok(token) => tokens.push(token), Ok(token) => tokens.push(token),
Err(error) => errors.push(error) Err(error) => return Err(vec![error])
} }
} }
if !errors.is_empty() { let mut errors = vec![];
return Err(errors)
}
let mut nodes = vec![]; let mut nodes = vec![];
for parse_result in ParseStream::new(tokens) { for parse_result in ParseStream::new(tokens) {
flog!(DEBUG, parse_result); flog!(DEBUG, parse_result);
@@ -59,7 +55,6 @@ impl ParsedSrc {
Err(error) => errors.push(error) Err(error) => errors.push(error)
} }
} }
flog!(DEBUG, errors);
if !errors.is_empty() { if !errors.is_empty() {
return Err(errors) return Err(errors)
@@ -1108,7 +1103,7 @@ impl Iterator for ParseStream {
flog!(DEBUG, "parsing"); flog!(DEBUG, "parsing");
flog!(DEBUG, self.tokens); flog!(DEBUG, self.tokens);
// Empty token vector or only SOI/EOI tokens, nothing to do // 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 return None
} }
while let Some(tk) = self.tokens.first() { while let Some(tk) = self.tokens.first() {

View File

@@ -184,6 +184,16 @@ impl VarTab {
self.bpush_arg(arg); 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<String> { pub fn sh_argv(&self) -> &VecDeque<String> {
&self.sh_argv &self.sh_argv
} }
@@ -379,6 +389,25 @@ pub fn set_status(code: i32) {
write_vars(|v| v.set_param('?', &code.to_string())) 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<()> { pub fn source_rc() -> ShResult<()> {
let path = if let Ok(path) = env::var("FERN_RC") { let path = if let Ok(path) = env::var("FERN_RC") {
PathBuf::from(&path) PathBuf::from(&path)

View File

@@ -10,6 +10,58 @@ fn cmd_not_found() {
insta::assert_snapshot!(err_fmt) 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::<Vec<_>>();
let node = ParseStream::new(tokens).next().unwrap();
let Err(err) = node else {
panic!();
};
let err_fmt = format!("{err}");
insta::assert_snapshot!(err_fmt)
}
#[test] #[test]
fn if_no_fi() { fn if_no_fi() {
let input = "if foo; then bar;"; let input = "if foo; then bar;";

View File

@@ -5,7 +5,7 @@ use super::*;
#[test] #[test]
fn simple_expansion() { fn simple_expansion() {
let varsub = "$foo"; 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<Tk> = LexStream::new(Arc::new(varsub.to_string()), LexFlags::empty()) let mut tokens: Vec<Tk> = LexStream::new(Arc::new(varsub.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
@@ -14,7 +14,7 @@ fn simple_expansion() {
let var_tk = tokens.pop().unwrap(); let var_tk = tokens.pop().unwrap();
let var_span = var_tk.span.clone(); 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()); write_vars(|v| v.vars_mut().clear());
insta::assert_debug_snapshot!(exp_tk.get_words()) insta::assert_debug_snapshot!(exp_tk.get_words())
} }

View File

@@ -6,5 +6,5 @@ expression: err_fmt
-> [1;1] -> [1;1]
 |  |
1 | case foo foo) bar;; bar) foo;; esac 1 | case foo foo) bar;; bar) foo;; esac
 | ^^^^^^^^^^^^^^^^^^^^  | ^^^^^^^^^^^^^^^^^^^
 |  |

View File

@@ -6,5 +6,5 @@ expression: err_fmt
-> [1;1] -> [1;1]
 |  |
1 | if foo; bar; fi 1 | if foo; bar; fi
 | ^^^^^^^^^^^^^  | ^^^^^^^^^^^^
 |  |

View File

@@ -6,5 +6,5 @@ expression: err_fmt
-> [1;1] -> [1;1]
 |  |
1 | while true; echo foo; done 1 | while true; echo foo; done
 | ^^^^^^^^^^^^^^^^^^^^^^  | ^^^^^^^^^^^^^^^^^^^^^
 |  |

View File

@@ -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
 | ^^^^^^^^^
 |

View File

@@ -0,0 +1,10 @@
---
source: src/tests/error.rs
expression: err_fmt
---
Parse Error - Unterminated quote
-> [1;1]
 |
1 | "foo bar
 | ^^^^^^^^
 |

View File

@@ -0,0 +1,10 @@
---
source: src/tests/error.rs
expression: err_fmt
---
Parse Error - Unterminated quote
-> [1;1]
 |
1 | 'foo bar
 | ^^^^^^^^
 |

View File

@@ -0,0 +1,10 @@
---
source: src/tests/error.rs
expression: err_fmt
---
Parse Error - Unclosed subshell
-> [1;1]
 |
1 | (foo
 | ^
 |