Implemented subshells and improved error handling

This commit is contained in:
2025-03-29 22:16:26 -04:00
parent 1eb19092cc
commit 80b6453597
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 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())

View File

@@ -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<Self> {
pub fn expand(self) -> ShResult<Self> {
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() {

View File

@@ -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<Node>, behavior: AssignBehavior) -> Vec<String> {
fn set_assignments(&self, assigns: Vec<Node>, behavior: AssignBehavior) -> Vec<String> {
let mut new_env_vars = vec![];
match behavior {
AssignBehavior::Export => {
@@ -483,9 +509,8 @@ pub fn prepare_argv(argv: Vec<Tk>) -> ShResult<Vec<(String,Span)>> {
let mut args = vec![];
for arg in argv {
let flags = arg.flags;
let span = arg.span.clone();
let expanded = arg.expand(span.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<Tk>) -> bool {
};
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() {
self.cursor = pos;
return Err(
ShErr::full(
ShErrKind::ParseErr,

View File

@@ -39,18 +39,14 @@ impl ParsedSrc {
}
pub fn parse_src(&mut self) -> Result<(),Vec<ShErr>> {
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() {

View File

@@ -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<String> {
&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)

View File

@@ -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::<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]
fn if_no_fi() {
let input = "if foo; then bar;";

View File

@@ -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<Tk> = 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())
}

View File

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

View File

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

View File

@@ -6,5 +6,5 @@ expression: err_fmt
-> [1;1]
 |
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
 | ^
 |