Improved error reporting and fully implemented the shopt command

This commit is contained in:
2025-03-26 23:41:19 -04:00
parent 30cd3c0b73
commit 1854578d49
19 changed files with 776 additions and 111 deletions

View File

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

30
src/builtin/shopt.rs Normal file
View File

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

View File

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

View File

@@ -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}';

View File

@@ -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()

View File

@@ -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<Vec<String>> {
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}"
)?;
}

View File

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

View File

@@ -37,16 +37,34 @@ impl ParsedSrc {
pub fn new(src: Arc<String>) -> Self {
Self { src, ast: Ast::new(vec![]) }
}
pub fn parse_src(&mut self) -> ShResult<()> {
pub fn parse_src(&mut self) -> Result<(),Vec<ShErr>> {
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<Tk>,
pub flags: ParseFlags
}
bitflags! {
#[derive(Debug)]
pub struct ParseFlags: u32 {
const ERROR = 0b0000001;
}
}
impl ParseStream {
pub fn new(tokens: Vec<Tk>) -> 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<Option<Node>> {
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<Tk>) {
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<Option<Node>> {
let mut node_tks: Vec<Tk> = vec![];
let mut body: Vec<Node> = 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<Option<Node>> {
fn parse_pipeln(&mut self) -> ShResult<Option<Node>> {
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<Option<Node>> {
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<Node>;
fn next(&mut self) -> Option<Self::Item> {
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))
}
}
}
}

View File

@@ -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<Editor<FernReadline,FileHistory>> {
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)

View File

@@ -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<Self, Self::Err> {
match s.to_ascii_uppercase().as_str() {
@@ -29,7 +29,28 @@ impl FromStr for BellStyle {
}
}
#[derive(Clone, Debug)]
impl Into<BellStyle> 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<String> {
todo!();
pub fn query(&mut self, query: &str) -> ShResult<Option<String>> {
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::<Vec<_>>().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<Option<String>> {
// 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::<Vec<_>>().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::<bool>() else {
return Err(
ShErr::simple(
ShErrKind::SyntaxErr,
"shopt: expected 'true' or 'false' for dotglob value"
)
)
};
self.dotglob = val;
}
"autocd" => {
let Ok(val) = val.parse::<bool>() 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::<bool>() 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::<usize>() 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::<bool>() 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::<bool>() 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::<FernBellStyle>() 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::<usize>() 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<Option<String>> {
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<String,ShFunc> // 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::<usize>() 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::<FernEditMode>() 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::<usize>() 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::<bool>() 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::<usize>() 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<Option<String>> {
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()
}
}
}

View File

@@ -314,6 +314,23 @@ pub fn write_logic<T, F: FnOnce(&mut RwLockWriteGuard<LogTab>) -> T>(f: F) -> T
f(lock)
}
pub fn read_shopts<T, F: FnOnce(RwLockReadGuard<ShOpts>) -> T>(f: F) -> T {
let lock = SHOPTS.read().unwrap();
f(lock)
}
pub fn write_shopts<T, F: FnOnce(&mut RwLockWriteGuard<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::<i32>().unwrap()
}

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
pub use super::*;
use super::*;
use crate::libsh::error::{
Note, ShErr, ShErrKind
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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