diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index 27269dd..c6f44b3 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -12,8 +12,9 @@ pub mod jobctl; pub mod alias; pub mod flowctl; pub mod zoltraak; +pub mod shopt; -pub const BUILTINS: [&str;15] = [ +pub const BUILTINS: [&str;16] = [ "echo", "cd", "export", @@ -28,7 +29,8 @@ pub const BUILTINS: [&str;15] = [ "break", "continue", "exit", - "zoltraak" + "zoltraak", + "shopt" ]; /// Sets up a builtin command diff --git a/src/builtin/shopt.rs b/src/builtin/shopt.rs new file mode 100644 index 0000000..7974541 --- /dev/null +++ b/src/builtin/shopt.rs @@ -0,0 +1,30 @@ +use crate::{jobs::JobBldr, libsh::error::{ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}, state::write_shopts}; + +use super::setup_builtin; + +pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { + let NdRule::Command { assignments: _, argv } = node.class else { + unreachable!() + }; + + let (argv,io_frame) = setup_builtin(argv, job, Some((io_stack,node.redirs)))?; + + let mut io_frame = io_frame.unwrap(); + io_frame.redirect()?; + for (arg,span) in argv { + let Some(mut output) = write_shopts(|s| s.query(&arg)).blame(span)? else { + continue + }; + + let output_channel = borrow_fd(STDOUT_FILENO); + output.push('\n'); + + if let Err(e) = write(output_channel, output.as_bytes()) { + io_frame.restore()?; + return Err(e.into()) + } + } + io_frame.restore()?; + + Ok(()) +} diff --git a/src/builtin/zoltraak.rs b/src/builtin/zoltraak.rs index 9e3d402..d78e060 100644 --- a/src/builtin/zoltraak.rs +++ b/src/builtin/zoltraak.rs @@ -61,6 +61,7 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu match flag { 'r' => flags |= ZoltFlags::RECURSIVE, 'f' => flags |= ZoltFlags::FORCE, + 'v' => flags |= ZoltFlags::VERBOSE, _ => unreachable!() } } @@ -141,7 +142,7 @@ fn annihilate(path: &str, flags: ZoltFlags) -> ShResult<()> { fs::remove_file(path)?; if is_verbose { let stderr = borrow_fd(STDERR_FILENO); - write(stderr, format!("removed file '{path}'").as_bytes())?; + write(stderr, format!("shredded file '{path}'\n").as_bytes())?; } } else if path_buf.is_dir() { @@ -183,7 +184,7 @@ fn annihilate_recursive(dir: &str, flags: ZoltFlags) -> ShResult<()> { fs::remove_dir(dir)?; if is_verbose { let stderr = borrow_fd(STDERR_FILENO); - write(stderr, format!("removed directory '{dir}'").as_bytes())?; + write(stderr, format!("shredded directory '{dir}'\n").as_bytes())?; } Ok(()) } diff --git a/src/expand.rs b/src/expand.rs index 2091ec4..e981979 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -1,6 +1,6 @@ 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_logic, read_vars, write_meta, LogTab}}; +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}}; /// Variable substitution marker pub const VAR_SUB: char = '\u{fdd0}'; diff --git a/src/fern.rs b/src/fern.rs index 741a2a1..cce9d58 100644 --- a/src/fern.rs +++ b/src/fern.rs @@ -77,7 +77,12 @@ pub fn exec_input(input: String) -> ShResult<()> { let log_tab = read_logic(|l| l.clone()); let input = expand_aliases(input, HashSet::new(), &log_tab); let mut parser = ParsedSrc::new(Arc::new(input)); - parser.parse_src()?; + if let Err(errors) = parser.parse_src() { + for error in errors { + eprintln!("{error}"); + } + return Ok(()) + } let mut dispatcher = Dispatcher::new(parser.extract_nodes()); dispatcher.begin_dispatch() diff --git a/src/libsh/error.rs b/src/libsh/error.rs index 1e2acfa..669a421 100644 --- a/src/libsh/error.rs +++ b/src/libsh/error.rs @@ -135,8 +135,6 @@ impl ShErr { total_len += ch.len_utf8(); cur_line.push(ch); if ch == '\n' { - total_lines += 1; - if total_len > span.start { let line = ( total_lines, @@ -147,6 +145,8 @@ impl ShErr { if total_len >= span.end { break } + total_lines += 1; + cur_line.clear(); } } @@ -183,6 +183,23 @@ impl ShErr { } (lineno,colno) } + pub fn get_indicator_lines(&self) -> Option> { + match self { + ShErr::Simple { kind: _, msg: _, notes: _ } => None, + ShErr::Full { kind: _, msg: _, notes: _, span } => { + let text = span.as_str(); + let lines = text.lines(); + let mut indicator_lines = vec![]; + + for line in lines { + let indicator_line = "^".repeat(line.len()).styled(Style::Red | Style::Bold); + indicator_lines.push(indicator_line); + } + + Some(indicator_lines) + } + } + } } impl Display for ShErr { @@ -204,26 +221,34 @@ impl Display for ShErr { Self::Full { msg, kind, notes, span: _ } => { let window = self.get_window(); + let mut indicator_lines = self.get_indicator_lines().unwrap().into_iter(); let mut lineno_pad_count = 0; for (lineno,_) in window.clone() { if lineno.to_string().len() > lineno_pad_count { lineno_pad_count = lineno.to_string().len() + 1 } } - let (line,col) = self.get_line_col(); - let line = line.styled(Style::Cyan | Style::Bold); - let col = col.styled(Style::Cyan | Style::Bold); - let kind = kind.styled(Style::Red | Style::Bold); let padding = " ".repeat(lineno_pad_count); + writeln!(f)?; + + + let (line,col) = self.get_line_col(); + let line_fmt = line.styled(Style::Cyan | Style::Bold); + let col_fmt = col.styled(Style::Cyan | Style::Bold); + let kind = kind.styled(Style::Red | Style::Bold); let arrow = "->".styled(Style::Cyan | Style::Bold); writeln!(f, - "{padding}{arrow} [{line};{col}] - {kind}", + "{kind} - {msg}", + )?; + writeln!(f, + "{padding}{arrow} [{line_fmt};{col_fmt}]", )?; let mut bar = format!("{padding}|"); bar = bar.styled(Style::Cyan | Style::Bold); writeln!(f,"{bar}")?; + let mut first_ind_ln = true; for (lineno,line) in window { let lineno = lineno.to_string(); let line = line.trim(); @@ -231,18 +256,29 @@ impl Display for ShErr { prefix.replace_range(0..lineno.len(), &lineno); prefix = prefix.styled(Style::Cyan | Style::Bold); writeln!(f,"{prefix} {line}")?; + + if let Some(ind_ln) = indicator_lines.next() { + if first_ind_ln { + let ind_ln_padding = " ".repeat(col); + let ind_ln = format!("{ind_ln_padding}{ind_ln}"); + writeln!(f, "{bar}{ind_ln}")?; + first_ind_ln = false; + } else { + writeln!(f, "{bar} {ind_ln}")?; + } + } } - writeln!(f,"{bar}")?; + write!(f,"{bar}")?; + let bar_break = "-".styled(Style::Cyan | Style::Bold); - writeln!(f, - "{padding}{bar_break} {msg}", - )?; - + if !notes.is_empty() { + writeln!(f)?; + } for note in notes { - writeln!(f, + write!(f, "{padding}{bar_break} {note}" )?; } diff --git a/src/parse/execute.rs b/src/parse/execute.rs index 3cea2c2..025472c 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -1,7 +1,7 @@ use std::collections::VecDeque; -use crate::{builtin::{alias::alias, cd::cd, echo::echo, export::export, flowctl::flowctl, jobctl::{continue_job, jobs, JobBehavior}, pwd::pwd, shift::shift, source::source, zoltraak::zoltraak}, 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_vars, ShFunc, VarTab}}; +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}, 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_vars, ShFunc, VarTab}}; use super::{lex::{Span, Tk, TkFlags, KEYWORDS}, AssignKind, CaseNode, CondNode, ConjunctNode, ConjunctOp, LoopKind, NdFlags, NdRule, Node, ParsedSrc, Redir, RedirType}; @@ -121,7 +121,12 @@ impl Dispatcher { } let mut func_parser = ParsedSrc::new(Arc::new(body)); - func_parser.parse_src()?; // Parse the function + if let Err(errors) = func_parser.parse_src() { + for error in errors { + eprintln!("{error}"); + } + return Ok(()) + } let func = ShFunc::new(func_parser); write_logic(|l| l.insert_func(name, func)); // Store the AST @@ -364,6 +369,7 @@ impl Dispatcher { "continue" => flowctl(cmd, ShErrKind::LoopContinue(0)), "exit" => flowctl(cmd, ShErrKind::CleanExit(0)), "zoltraak" => zoltraak(cmd, io_stack_mut, curr_job_mut), + "shopt" => shopt(cmd, io_stack_mut, curr_job_mut), _ => unimplemented!("Have not yet added support for builtin '{}'", cmd_raw.span.as_str()) }; diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 8993911..c7d7358 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -37,16 +37,34 @@ impl ParsedSrc { pub fn new(src: Arc) -> Self { Self { src, ast: Ast::new(vec![]) } } - pub fn parse_src(&mut self) -> ShResult<()> { + pub fn parse_src(&mut self) -> Result<(),Vec> { let mut tokens = vec![]; - for token in LexStream::new(self.src.clone(), LexFlags::empty()) { - tokens.push(token?); + 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) + } + } + + if !errors.is_empty() { + return Err(errors) } let mut nodes = vec![]; - for result in ParseStream::new(tokens) { - nodes.push(result?); + for parse_result in ParseStream::new(tokens) { + flog!(DEBUG, parse_result); + match parse_result { + Ok(node) => nodes.push(node), + Err(error) => errors.push(error) + } } + flog!(DEBUG, errors); + + if !errors.is_empty() { + return Err(errors) + } + *self.ast.tree_mut() = nodes; Ok(()) } @@ -311,19 +329,11 @@ pub enum NdRule { #[derive(Debug)] pub struct ParseStream { pub tokens: Vec, - pub flags: ParseFlags -} - -bitflags! { - #[derive(Debug)] - pub struct ParseFlags: u32 { - const ERROR = 0b0000001; - } } impl ParseStream { pub fn new(tokens: Vec) -> Self { - Self { tokens, flags: ParseFlags::empty() } + Self { tokens } } fn next_tk_class(&self) -> &TkRule { if let Some(tk) = self.tokens.first() { @@ -444,7 +454,7 @@ impl ParseStream { /// 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 left-recursion issues in self.parse_pipeline() + /// The check_pipelines parameter is used to prevent left-recursion issues in self.parse_pipeln() fn parse_block(&mut self, check_pipelines: bool) -> ShResult> { try_match!(self.parse_func_def()?); try_match!(self.parse_brc_grp(false /* from_func_def */)?); @@ -452,7 +462,7 @@ impl ParseStream { try_match!(self.parse_loop()?); try_match!(self.parse_if()?); if check_pipelines { - try_match!(self.parse_pipeline()?); + try_match!(self.parse_pipeln()?); } else { try_match!(self.parse_cmd()?); } @@ -488,6 +498,14 @@ impl ParseStream { Ok(Some(node)) } + fn panic_mode(&mut self, node_tks: &mut Vec) { + while let Some(tk) = self.next_tk() { + node_tks.push(tk.clone()); + if tk.class == TkRule::Sep { + break + } + } + } fn parse_brc_grp(&mut self, from_func_def: bool) -> ShResult> { let mut node_tks: Vec = vec![]; let mut body: Vec = vec![]; @@ -508,6 +526,7 @@ impl ParseStream { body.push(node); } if !self.next_tk_is_some() { + self.panic_mode(&mut node_tks); return Err(parse_err_full( "Expected a closing brace for this brace group", &node_tks.get_span().unwrap() @@ -525,7 +544,6 @@ impl ParseStream { let path_tk = self.next_tk(); if path_tk.clone().is_none_or(|tk| tk.class == TkRule::EOI) { - self.flags |= ParseFlags::ERROR; return Err( ShErr::full( ShErrKind::ParseErr, @@ -541,7 +559,7 @@ impl ParseStream { let pathbuf = PathBuf::from(path_tk.span.as_str()); let Ok(file) = get_redir_file(redir_class, pathbuf) else { - self.flags |= ParseFlags::ERROR; + self.panic_mode(&mut node_tks); return Err(parse_err_full( "Error opening file for redirection", &path_tk.span @@ -579,6 +597,7 @@ impl ParseStream { node_tks.push(self.next_tk().unwrap()); let Some(pat_tk) = self.next_tk() else { + self.panic_mode(&mut node_tks); return Err( parse_err_full( "Expected a pattern after 'case' keyword", &node_tks.get_span().unwrap() @@ -596,6 +615,7 @@ impl ParseStream { node_tks.push(pattern.clone()); if !self.check_keyword("in") || !self.next_tk_is_some() { + self.panic_mode(&mut node_tks); return Err(parse_err_full("Expected 'in' after case variable name", &node_tks.get_span().unwrap())); } node_tks.push(self.next_tk().unwrap()); @@ -604,6 +624,7 @@ impl ParseStream { loop { if !self.check_case_pattern() || !self.next_tk_is_some() { + self.panic_mode(&mut node_tks); return Err(parse_err_full("Expected a case pattern here", &node_tks.get_span().unwrap())); } let case_pat_tk = self.next_tk().unwrap(); @@ -632,6 +653,7 @@ impl ParseStream { } if !self.next_tk_is_some() { + self.panic_mode(&mut node_tks); return Err(parse_err_full("Expected 'esac' after case block", &node_tks.get_span().unwrap())); } } @@ -666,6 +688,7 @@ impl ParseStream { "elif" }; let Some(cond) = self.parse_block(true)? else { + self.panic_mode(&mut node_tks); return Err(parse_err_full( &format!("Expected an expression after '{prefix_keywrd}'"), &node_tks.get_span().unwrap() @@ -674,6 +697,7 @@ impl ParseStream { node_tks.extend(cond.tokens.clone()); if !self.check_keyword("then") || !self.next_tk_is_some() { + self.panic_mode(&mut node_tks); return Err(parse_err_full( &format!("Expected 'then' after '{prefix_keywrd}' condition"), &node_tks.get_span().unwrap() @@ -688,6 +712,7 @@ impl ParseStream { body_blocks.push(body_block); } if body_blocks.is_empty() { + self.panic_mode(&mut node_tks); return Err(parse_err_full( "Expected an expression after 'then'", &node_tks.get_span().unwrap() @@ -711,6 +736,7 @@ impl ParseStream { else_block.push(block) } if else_block.is_empty() { + self.panic_mode(&mut node_tks); return Err(parse_err_full( "Expected an expression after 'else'", &node_tks.get_span().unwrap() @@ -719,6 +745,7 @@ impl ParseStream { } if !self.check_keyword("fi") || !self.next_tk_is_some() { + self.panic_mode(&mut node_tks); return Err(parse_err_full( "Expected 'fi' after if statement", &node_tks.get_span().unwrap() @@ -734,7 +761,6 @@ impl ParseStream { let path_tk = self.next_tk(); if path_tk.clone().is_none_or(|tk| tk.class == TkRule::EOI) { - self.flags |= ParseFlags::ERROR; return Err( ShErr::full( ShErrKind::ParseErr, @@ -750,7 +776,7 @@ impl ParseStream { let pathbuf = PathBuf::from(path_tk.span.as_str()); let Ok(file) = get_redir_file(redir_class, pathbuf) else { - self.flags |= ParseFlags::ERROR; + self.panic_mode(&mut node_tks); return Err(parse_err_full( "Error opening file for redirection", &path_tk.span @@ -792,6 +818,7 @@ impl ParseStream { self.catch_separator(&mut node_tks); let Some(cond) = self.parse_block(true)? else { + self.panic_mode(&mut node_tks); return Err(parse_err_full( &format!("Expected an expression after '{loop_kind}'"), // It also implements Display &node_tks.get_span().unwrap() @@ -800,6 +827,7 @@ impl ParseStream { node_tks.extend(cond.tokens.clone()); if !self.check_keyword("do") || !self.next_tk_is_some() { + self.panic_mode(&mut node_tks); return Err(parse_err_full( "Expected 'do' after loop condition", &node_tks.get_span().unwrap() @@ -814,6 +842,7 @@ impl ParseStream { body.push(block); } if body.is_empty() { + self.panic_mode(&mut node_tks); return Err(parse_err_full( "Expected an expression after 'do'", &node_tks.get_span().unwrap() @@ -821,6 +850,7 @@ impl ParseStream { }; if !self.check_keyword("done") || !self.next_tk_is_some() { + self.panic_mode(&mut node_tks); return Err(parse_err_full( "Expected 'done' after loop body", &node_tks.get_span().unwrap() @@ -838,7 +868,7 @@ impl ParseStream { }; Ok(Some(loop_node)) } - fn parse_pipeline(&mut self) -> ShResult> { + fn parse_pipeln(&mut self) -> ShResult> { let mut cmds = vec![]; let mut node_tks = vec![]; while let Some(cmd) = self.parse_block(false)? { @@ -868,7 +898,7 @@ impl ParseStream { } } fn parse_cmd(&mut self) -> ShResult> { - let tk_slice = self.tokens.as_slice(); + let tk_slice = self.tokens.clone(); let mut tk_iter = tk_slice.iter(); let mut node_tks = vec![]; let mut redirs = vec![]; @@ -876,19 +906,23 @@ impl ParseStream { let mut assignments = vec![]; while let Some(prefix_tk) = tk_iter.next() { - if prefix_tk.flags.contains(TkFlags::IS_CMD) { + let is_cmd = prefix_tk.flags.contains(TkFlags::IS_CMD); + let is_assignment = prefix_tk.flags.contains(TkFlags::ASSIGN); + let is_keyword = prefix_tk.flags.contains(TkFlags::KEYWORD); + + if is_cmd { node_tks.push(prefix_tk.clone()); argv.push(prefix_tk.clone()); break - } else if prefix_tk.flags.contains(TkFlags::ASSIGN) { + } else if is_assignment { let Some(assign) = self.parse_assignment(&prefix_tk) else { break }; node_tks.push(prefix_tk.clone()); assignments.push(assign) - } else if prefix_tk.flags.contains(TkFlags::KEYWORD) { + } else if is_keyword { return Ok(None) } } @@ -900,12 +934,12 @@ impl ParseStream { while let Some(tk) = tk_iter.next() { match tk.class { TkRule::EOI | - TkRule::Pipe | - TkRule::And | - TkRule::BraceGrpEnd | - TkRule::Or => { - break - } + TkRule::Pipe | + TkRule::And | + TkRule::BraceGrpEnd | + TkRule::Or => { + break + } TkRule::Sep => { node_tks.push(tk.clone()); break @@ -921,7 +955,6 @@ impl ParseStream { let path_tk = tk_iter.next(); if path_tk.is_none_or(|tk| tk.class == TkRule::EOI) { - self.flags |= ParseFlags::ERROR; return Err( ShErr::full( ShErrKind::ParseErr, @@ -937,7 +970,7 @@ impl ParseStream { let pathbuf = PathBuf::from(path_tk.span.as_str()); let Ok(file) = get_redir_file(redir_class, pathbuf) else { - self.flags |= ParseFlags::ERROR; + self.panic_mode(&mut node_tks); return Err(parse_err_full( "Error opening file for redirection", &path_tk.span @@ -1068,13 +1101,12 @@ impl ParseStream { impl Iterator for ParseStream { type Item = ShResult; fn next(&mut self) -> Option { + 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 { return None } - if self.flags.contains(ParseFlags::ERROR) { - return None - } while let Some(tk) = self.tokens.first() { if let TkRule::EOI = tk.class { return None @@ -1085,12 +1117,17 @@ impl Iterator for ParseStream { break } } - match self.parse_cmd_list() { + let result = self.parse_cmd_list(); + flog!(DEBUG, result); + flog!(DEBUG, self.tokens); + match result { Ok(Some(node)) => { return Some(Ok(node)); } Ok(None) => return None, - Err(e) => return Some(Err(e)) + Err(e) => { + return Some(Err(e)) + } } } } diff --git a/src/prompt/mod.rs b/src/prompt/mod.rs index c32bc38..15f1164 100644 --- a/src/prompt/mod.rs +++ b/src/prompt/mod.rs @@ -3,13 +3,40 @@ pub mod readline; use std::path::Path; use readline::FernReadline; -use rustyline::{error::ReadlineError, history::FileHistory, Editor}; +use rustyline::{error::ReadlineError, history::FileHistory, ColorMode, Config, Editor}; -use crate::{expand::expand_prompt, libsh::{error::ShResult, term::{Style, Styled}}, prelude::*}; +use crate::{expand::expand_prompt, libsh::{error::ShResult, term::{Style, Styled}}, prelude::*, state::read_shopts}; +/// Initialize the line editor fn init_rl() -> ShResult> { let rl = FernReadline::new(); - let mut editor = Editor::new()?; + + let tab_stop = read_shopts(|s| s.prompt.tab_stop); + let edit_mode = read_shopts(|s| s.prompt.edit_mode).into(); + let bell_style = read_shopts(|s| s.core.bell_style).into(); + let ignore_dups = read_shopts(|s| s.core.hist_ignore_dupes); + let comp_limit = read_shopts(|s| s.prompt.comp_limit); + let auto_hist = read_shopts(|s| s.core.auto_hist); + let max_hist = read_shopts(|s| s.core.max_hist); + let color_mode = match read_shopts(|s| s.prompt.prompt_highlight) { + true => ColorMode::Enabled, + false => ColorMode::Disabled, + }; + + let config = Config::builder() + .tab_stop(tab_stop) + .indent_size(1) + .edit_mode(edit_mode) + .bell_style(bell_style) + .color_mode(color_mode) + .history_ignore_dups(ignore_dups).unwrap() + .completion_prompt_limit(comp_limit) + .auto_add_history(auto_hist) + .max_history_size(max_hist).unwrap() + .build(); + + let mut editor = Editor::with_config(config).unwrap(); + editor.set_helper(Some(rl)); editor.load_history(&Path::new("/home/pagedmov/.fernhist"))?; Ok(editor) diff --git a/src/shopt.rs b/src/shopt.rs index 10f0e68..8a30c5d 100644 --- a/src/shopt.rs +++ b/src/shopt.rs @@ -1,18 +1,18 @@ -use std::str::FromStr; +use std::{collections::HashMap, fmt::Display, str::FromStr}; -use rustyline::EditMode; +use rustyline::{config::BellStyle, EditMode}; -use crate::{libsh::error::{ShErr, ShErrKind, ShResult}, prelude::*, state::LogTab}; +use crate::{libsh::error::{Note, ShErr, ShErrKind, ShResult}, state::ShFunc}; -#[derive(Clone, Debug)] -pub enum BellStyle { +#[derive(Clone, Copy, Debug)] +pub enum FernBellStyle { Audible, Visible, Disable, } -impl FromStr for BellStyle { +impl FromStr for FernBellStyle { type Err = ShErr; fn from_str(s: &str) -> Result { match s.to_ascii_uppercase().as_str() { @@ -29,7 +29,28 @@ impl FromStr for BellStyle { } } -#[derive(Clone, Debug)] +impl Into for FernBellStyle { + fn into(self) -> BellStyle { + match self { + FernBellStyle::Audible => BellStyle::Audible, + FernBellStyle::Visible => BellStyle::Visible, + FernBellStyle::Disable => BellStyle::None + } + } +} + + +impl Display for FernBellStyle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FernBellStyle::Audible => write!(f,"audible"), + FernBellStyle::Visible => write!(f,"visible"), + FernBellStyle::Disable => write!(f,"disable"), + } + } +} + +#[derive(Clone, Copy, Debug)] pub enum FernEditMode { Vi, Emacs @@ -60,46 +81,106 @@ impl FromStr for FernEditMode { } } +impl Display for FernEditMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FernEditMode::Vi => write!(f,"vi"), + FernEditMode::Emacs => write!(f,"emacs"), + } + } +} + #[derive(Clone, Debug)] pub struct ShOpts { - core: ShOptCore, - prompt: ShOptPrompt + pub core: ShOptCore, + pub prompt: ShOptPrompt } impl Default for ShOpts { fn default() -> Self { - let core = ShOptCore { - dotglob: false, - autocd: false, - hist_ignore_dupes: true, - max_hist: 1000, - int_comments: true, - auto_hist: true, - bell_style: BellStyle::Audible, - max_recurse_depth: 1000, - }; + let core = ShOptCore::default(); - let prompt = ShOptPrompt { - trunc_prompt_path: 3, - edit_mode: FernEditMode::Vi, - comp_limit: 100, - prompt_highlight: true, - tab_stop: 4, - custom: LogTab::new() - }; + let prompt = ShOptPrompt::default(); Self { core, prompt } } } impl ShOpts { - pub fn get(query: &str) -> ShResult { - todo!(); + pub fn query(&mut self, query: &str) -> ShResult> { + if let Some((opt,new_val)) = query.split_once('=') { + self.set(opt,new_val)?; + Ok(None) + } else { + self.get(query) + } + } + + pub fn set(&mut self, opt: &str, val: &str) -> ShResult<()> { + let mut query = opt.split('.'); + let Some(key) = query.next() else { + return Err( + ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: No option given" + ) + ) + }; + + let remainder = query.collect::>().join("."); + + match key { + "core" => self.core.set(&remainder, val)?, + "prompt" => self.prompt.set(&remainder, val)?, + _ => { + return Err( + ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: Expected 'core' or 'prompt' in shopt key" + ) + .with_note( + Note::new("'shopt' takes arguments separated by periods to denote namespaces") + .with_sub_notes(vec![ + "Example: 'shopt core.autocd=true'" + ]) + ) + ) + } + } + Ok(()) + } + + pub fn get(&self, query: &str) -> ShResult> { // TODO: handle escapes? let mut query = query.split('.'); - //let Some(key) = query.next() else { + let Some(key) = query.next() else { + return Err( + ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: No option given" + ) + ) + }; + let remainder = query.collect::>().join("."); - //}; + match key { + "core" => self.core.get(&remainder), + "prompt" => self.prompt.get(&remainder), + _ => { + return Err( + ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: Expected 'core' or 'prompt' in shopt key" + ) + .with_note( + Note::new("'shopt' takes arguments separated by periods to denote namespaces") + .with_sub_notes(vec![ + "Example: 'shopt core.autocd=true'" + ]) + ) + ) + } + } } } @@ -109,12 +190,243 @@ pub struct ShOptCore { pub autocd: bool, pub hist_ignore_dupes: bool, pub max_hist: usize, - pub int_comments: bool, + pub interactive_comments: bool, pub auto_hist: bool, - pub bell_style: BellStyle, + pub bell_style: FernBellStyle, pub max_recurse_depth: usize, } +impl ShOptCore { + pub fn set(&mut self, opt: &str, val: &str) -> ShResult<()> { + match opt { + "dotglob" => { + let Ok(val) = val.parse::() else { + return Err( + ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: expected 'true' or 'false' for dotglob value" + ) + ) + }; + self.dotglob = val; + } + "autocd" => { + let Ok(val) = val.parse::() else { + return Err( + ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: expected 'true' or 'false' for autocd value" + ) + ) + }; + self.autocd = val; + } + "hist_ignore_dupes" => { + let Ok(val) = val.parse::() else { + return Err( + ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: expected 'true' or 'false' for hist_ignore_dupes value" + ) + ) + }; + self.hist_ignore_dupes = val; + } + "max_hist" => { + let Ok(val) = val.parse::() else { + return Err( + ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: expected a positive integer for hist_ignore_dupes value" + ) + ) + }; + self.max_hist = val; + } + "interactive_comments" => { + let Ok(val) = val.parse::() else { + return Err( + ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: expected 'true' or 'false' for interactive_comments value" + ) + ) + }; + self.interactive_comments = val; + } + "auto_hist" => { + let Ok(val) = val.parse::() else { + return Err( + ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: expected 'true' or 'false' for auto_hist value" + ) + ) + }; + self.auto_hist = val; + } + "bell_style" => { + let Ok(val) = val.parse::() else { + return Err( + ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: expected a bell style for bell_style value" + ) + .with_note( + Note::new("bell_style takes these options as values") + .with_sub_notes(vec![ + "audible", + "visible", + "disable" + ]) + ) + ) + }; + self.bell_style = val; + } + "max_recurse_depth" => { + let Ok(val) = val.parse::() else { + return Err( + ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: expected a positive integer for max_recurse_depth value" + ) + ) + }; + self.max_recurse_depth = val; + } + _ => { + return Err( + ShErr::simple( + ShErrKind::SyntaxErr, + format!("shopt: Unexpected 'core' option '{opt}'") + ) + .with_note(Note::new("options can be accessed like 'core.option_name'")) + .with_note( + Note::new("'core' contains the following options") + .with_sub_notes(vec![ + "dotglob", + "autocd", + "hist_ignore_dupes", + "max_hist", + "interactive_comments", + "auto_hist", + "bell_style", + "max_recurse_depth", + ] + ) + ) + ) + } + } + Ok(()) + } + pub fn get(&self, query: &str) -> ShResult> { + if query.is_empty() { + return Ok(Some(format!("{self}"))) + } + + match query { + "dotglob" => { + let mut output = format!("Include hidden files in glob patterns\n"); + output.push_str(&format!("{}",self.dotglob)); + Ok(Some(output)) + } + "autocd" => { + let mut output = format!("Allow navigation to directories by passing the directory as a command directly\n"); + output.push_str(&format!("{}",self.autocd)); + Ok(Some(output)) + } + "hist_ignore_dupes" => { + let mut output = format!("Ignore consecutive duplicate command history entries\n"); + output.push_str(&format!("{}",self.hist_ignore_dupes)); + Ok(Some(output)) + } + "max_hist" => { + let mut output = format!("Maximum number of entries in the command history file (default '.fernhist')\n"); + output.push_str(&format!("{}",self.max_hist)); + Ok(Some(output)) + } + "interactive_comments" => { + let mut output = format!("Whether or not to allow comments in interactive mode\n"); + output.push_str(&format!("{}",self.interactive_comments)); + Ok(Some(output)) + } + "auto_hist" => { + let mut output = format!("Whether or not to automatically save commands to the command history file\n"); + output.push_str(&format!("{}",self.auto_hist)); + Ok(Some(output)) + } + "bell_style" => { + let mut output = format!("What type of bell style to use for the bell character\n"); + output.push_str(&format!("{}",self.bell_style)); + Ok(Some(output)) + } + "max_recurse_depth" => { + let mut output = format!("Maximum limit of recursive shell function calls\n"); + output.push_str(&format!("{}",self.max_recurse_depth)); + Ok(Some(output)) + } + _ => { + return Err( + ShErr::simple( + ShErrKind::SyntaxErr, + format!("shopt: Unexpected 'core' option '{query}'") + ) + .with_note(Note::new("options can be accessed like 'core.option_name'")) + .with_note( + Note::new("'core' contains the following options") + .with_sub_notes(vec![ + "dotglob", + "autocd", + "hist_ignore_dupes", + "max_hist", + "interactive_comments", + "auto_hist", + "bell_style", + "max_recurse_depth", + ] + ) + ) + ) + } + } + } +} + +impl Display for ShOptCore { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut output = vec![]; + output.push(format!("dotglob = {}",self.dotglob)); + output.push(format!("autocd = {}",self.autocd)); + output.push(format!("hist_ignore_dupes = {}",self.hist_ignore_dupes)); + output.push(format!("max_hist = {}",self.max_hist)); + output.push(format!("interactive_comments = {}",self.interactive_comments)); + output.push(format!("auto_hist = {}",self.auto_hist)); + output.push(format!("bell_style = {}",self.bell_style)); + output.push(format!("max_recurse_depth = {}",self.max_recurse_depth)); + + let final_output = output.join("\n"); + + writeln!(f,"{final_output}") + } +} + +impl Default for ShOptCore { + fn default() -> Self { + ShOptCore { + dotglob: false, + autocd: false, + hist_ignore_dupes: true, + max_hist: 1000, + interactive_comments: true, + auto_hist: true, + bell_style: FernBellStyle::Audible, + max_recurse_depth: 1000, + } + } +} + #[derive(Clone, Debug)] pub struct ShOptPrompt { pub trunc_prompt_path: usize, @@ -122,5 +434,191 @@ pub struct ShOptPrompt { pub comp_limit: usize, pub prompt_highlight: bool, pub tab_stop: usize, - pub custom: LogTab // Contains functions for prompt modules + pub custom: HashMap // Contains functions for prompt modules +} + +impl ShOptPrompt { + pub fn set(&mut self, opt: &str, val: &str) -> ShResult<()> { + match opt { + "trunc_prompt_path" => { + let Ok(val) = val.parse::() else { + return Err( + ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: expected a positive integer for trunc_prompt_path value" + ) + ) + }; + self.trunc_prompt_path = val; + } + "edit_mode" => { + let Ok(val) = val.parse::() else { + return Err( + ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: expected 'vi' or 'emacs' for edit_mode value" + ) + ) + }; + self.edit_mode = val; + } + "comp_limit" => { + let Ok(val) = val.parse::() else { + return Err( + ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: expected a positive integer for comp_limit value" + ) + ) + }; + self.comp_limit = val; + } + "prompt_highlight" => { + let Ok(val) = val.parse::() else { + return Err( + ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: expected 'true' or 'false' for prompt_highlight value" + ) + ) + }; + self.prompt_highlight = val; + } + "tab_stop" => { + let Ok(val) = val.parse::() else { + return Err( + ShErr::simple( + ShErrKind::SyntaxErr, + "shopt: expected a positive integer for tab_stop value" + ) + ) + }; + self.tab_stop = val; + } + "custom" => { + todo!() + } + _ => { + return Err( + ShErr::simple( + ShErrKind::SyntaxErr, + format!("shopt: Unexpected 'core' option '{opt}'") + ) + .with_note(Note::new("options can be accessed like 'core.option_name'")) + .with_note( + Note::new("'core' contains the following options") + .with_sub_notes(vec![ + "dotglob", + "autocd", + "hist_ignore_dupes", + "max_hist", + "interactive_comments", + "auto_hist", + "bell_style", + "max_recurse_depth", + ] + ) + ) + ) + } + } + Ok(()) + } + pub fn get(&self, query: &str) -> ShResult> { + if query.is_empty() { + return Ok(Some(format!("{self}"))) + } + + match query { + "trunc_prompt_path" => { + let mut output = format!("Maximum number of path segments used in the '\\W' prompt escape sequence\n"); + output.push_str(&format!("{}",self.trunc_prompt_path)); + Ok(Some(output)) + } + "edit_mode" => { + let mut output = format!("The style of editor shortcuts used in the line-editing of the prompt\n"); + output.push_str(&format!("{}",self.edit_mode)); + Ok(Some(output)) + } + "comp_limit" => { + let mut output = format!("Maximum number of completion candidates displayed upon pressing tab\n"); + output.push_str(&format!("{}",self.comp_limit)); + Ok(Some(output)) + } + "prompt_highlight" => { + let mut output = format!("Whether to enable or disable syntax highlighting on the prompt\n"); + output.push_str(&format!("{}",self.prompt_highlight)); + Ok(Some(output)) + } + "tab_stop" => { + let mut output = format!("The number of spaces used by the tab character '\\t'\n"); + output.push_str(&format!("{}",self.tab_stop)); + Ok(Some(output)) + } + "custom" => { + let mut output = format!("A table of custom 'modules' executed as shell functions for prompt scripting\n"); + output.push_str("Current modules: \n"); + for key in self.custom.keys() { + output.push_str(&format!(" - {key}\n")); + } + Ok(Some(output.trim().to_string())) + } + _ => { + return Err( + ShErr::simple( + ShErrKind::SyntaxErr, + format!("shopt: Unexpected 'core' option '{query}'") + ) + .with_note(Note::new("options can be accessed like 'core.option_name'")) + .with_note( + Note::new("'core' contains the following options") + .with_sub_notes(vec![ + "dotglob", + "autocd", + "hist_ignore_dupes", + "max_hist", + "interactive_comments", + "auto_hist", + "bell_style", + "max_recurse_depth", + ] + ) + ) + ) + } + } + } +} + +impl Display for ShOptPrompt { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut output = vec![]; + + output.push(format!("trunc_prompt_path = {}", self.trunc_prompt_path)); + output.push(format!("edit_mode = {}", self.edit_mode)); + output.push(format!("comp_limit = {}", self.comp_limit)); + output.push(format!("prompt_highlight = {}", self.prompt_highlight)); + output.push(format!("tab_stop = {}", self.tab_stop)); + output.push(format!("prompt modules: ")); + for key in self.custom.keys() { + output.push(format!(" - {key}")); + } + + let final_output = output.join("\n"); + + writeln!(f,"{final_output}") + } +} + +impl Default for ShOptPrompt { + fn default() -> Self { + ShOptPrompt { + trunc_prompt_path: 4, + edit_mode: FernEditMode::Vi, + comp_limit: 100, + prompt_highlight: true, + tab_stop: 4, + custom: HashMap::new() + } + } } diff --git a/src/state.rs b/src/state.rs index c4340cf..4ce8b4d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -314,6 +314,23 @@ pub fn write_logic) -> T>(f: F) -> T f(lock) } +pub fn read_shopts) -> T>(f: F) -> T { + let lock = SHOPTS.read().unwrap(); + f(lock) +} + +pub fn write_shopts) -> T>(f: F) -> T { + let lock = &mut SHOPTS.write().unwrap(); + f(lock) +} + +/// This function is used internally and ideally never sees user input +/// +/// It will panic if you give it an invalid path. +pub fn get_shopt(path: &str) -> String { + read_shopts(|s| s.get(path)).unwrap().unwrap() +} + pub fn get_status() -> i32 { read_vars(|v| v.get_param('?')).parse::().unwrap() } diff --git a/src/tests/mod.rs b/src/tests/mod.rs index b96c9f0..858ff22 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -pub use super::*; +use super::*; use crate::libsh::error::{ Note, ShErr, ShErrKind }; diff --git a/src/tests/snapshots/fern__tests__error__case_no_esac.snap b/src/tests/snapshots/fern__tests__error__case_no_esac.snap index 9cae343..b53486a 100644 --- a/src/tests/snapshots/fern__tests__error__case_no_esac.snap +++ b/src/tests/snapshots/fern__tests__error__case_no_esac.snap @@ -2,8 +2,9 @@ source: src/tests/error.rs expression: err_fmt --- - -> [1;1] - Parse Error +Parse Error - Expected 'esac' after case block + -> [1;1]  | 1 | case foo in foo) bar;; bar) foo;; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  | - - Expected 'esac' after case block 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 e959f17..11451a0 100644 --- a/src/tests/snapshots/fern__tests__error__case_no_in.snap +++ b/src/tests/snapshots/fern__tests__error__case_no_in.snap @@ -2,8 +2,9 @@ source: src/tests/error.rs expression: err_fmt --- - -> [1;1] - Parse Error +Parse Error - Expected 'in' after case variable name + -> [1;1]  | 1 | case foo foo) bar;; bar) foo;; esac + | ^^^^^^^^^^^^^^^^^^^^  | - - Expected 'in' after case variable name diff --git a/src/tests/snapshots/fern__tests__error__cmd_not_found.snap b/src/tests/snapshots/fern__tests__error__cmd_not_found.snap index d7884ae..05085be 100644 --- a/src/tests/snapshots/fern__tests__error__cmd_not_found.snap +++ b/src/tests/snapshots/fern__tests__error__cmd_not_found.snap @@ -2,8 +2,8 @@ source: src/tests/error.rs expression: err_fmt --- - -> [1;1] - Command not found: foo +Command not found: foo - + -> [1;1]  | 1 | foo  | - - diff --git a/src/tests/snapshots/fern__tests__error__if_no_fi.snap b/src/tests/snapshots/fern__tests__error__if_no_fi.snap index 7a651e2..1932844 100644 --- a/src/tests/snapshots/fern__tests__error__if_no_fi.snap +++ b/src/tests/snapshots/fern__tests__error__if_no_fi.snap @@ -2,8 +2,9 @@ source: src/tests/error.rs expression: err_fmt --- - -> [1;1] - Parse Error +Parse Error - Expected 'fi' after if statement + -> [1;1]  | 1 | if foo; then bar; + | ^^^^^^^^^^^^^^^^^  | - - Expected 'fi' after if statement 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 7163149..7885d13 100644 --- a/src/tests/snapshots/fern__tests__error__if_no_then.snap +++ b/src/tests/snapshots/fern__tests__error__if_no_then.snap @@ -2,8 +2,9 @@ source: src/tests/error.rs expression: err_fmt --- - -> [1;1] - Parse Error +Parse Error - Expected 'then' after 'if' condition + -> [1;1]  | 1 | if foo; bar; fi + | ^^^^^^^^^^^^^  | - - Expected 'then' after 'if' condition 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 fbf8ced..4ff907e 100644 --- a/src/tests/snapshots/fern__tests__error__loop_no_do.snap +++ b/src/tests/snapshots/fern__tests__error__loop_no_do.snap @@ -2,8 +2,9 @@ source: src/tests/error.rs expression: err_fmt --- - -> [1;1] - Parse Error +Parse Error - Expected 'do' after loop condition + -> [1;1]  | 1 | while true; echo foo; done + | ^^^^^^^^^^^^^^^^^^^^^^  | - - Expected 'do' after loop condition diff --git a/src/tests/snapshots/fern__tests__error__loop_no_done.snap b/src/tests/snapshots/fern__tests__error__loop_no_done.snap index 0e26f67..6f46636 100644 --- a/src/tests/snapshots/fern__tests__error__loop_no_done.snap +++ b/src/tests/snapshots/fern__tests__error__loop_no_done.snap @@ -2,8 +2,9 @@ source: src/tests/error.rs expression: err_fmt --- - -> [1;1] - Parse Error +Parse Error - Expected 'done' after loop body + -> [1;1]  | 1 | while true; do echo foo; + | ^^^^^^^^^^^^^^^^^^^^^^^^  | - - Expected 'done' after loop body