Improved error reporting and fully implemented the shopt command
This commit is contained in:
@@ -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
30
src/builtin/shopt.rs
Normal 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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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}';
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}"
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
};
|
||||
|
||||
|
||||
113
src/parse/mod.rs
113
src/parse/mod.rs
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
566
src/shopt.rs
566
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<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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
src/state.rs
17
src/state.rs
@@ -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()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use super::*;
|
||||
use super::*;
|
||||
use crate::libsh::error::{
|
||||
Note, ShErr, ShErrKind
|
||||
};
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
source: src/tests/error.rs
|
||||
expression: err_fmt
|
||||
---
|
||||
[36m[1m->[0m [[36m[1m1[0m;[36m[1m1[0m] - [31m[1mParse Error[0m
|
||||
[31m[1mParse Error[0m - Expected 'esac' after case block
|
||||
[36m[1m->[0m [[36m[1m1[0m;[36m[1m1[0m]
|
||||
[36m[1m |[0m
|
||||
[36m[1m1 |[0m case foo in foo) bar;; bar) foo;;
|
||||
[36m[1m |[0m [31m[1m^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^[0m
|
||||
[36m[1m |[0m
|
||||
[36m[1m-[0m Expected 'esac' after case block
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
source: src/tests/error.rs
|
||||
expression: err_fmt
|
||||
---
|
||||
[36m[1m->[0m [[36m[1m1[0m;[36m[1m1[0m] - [31m[1mParse Error[0m
|
||||
[31m[1mParse Error[0m - Expected 'in' after case variable name
|
||||
[36m[1m->[0m [[36m[1m1[0m;[36m[1m1[0m]
|
||||
[36m[1m |[0m
|
||||
[36m[1m1 |[0m case foo foo) bar;; bar) foo;; esac
|
||||
[36m[1m |[0m [31m[1m^^^^^^^^^^^^^^^^^^^^[0m
|
||||
[36m[1m |[0m
|
||||
[36m[1m-[0m Expected 'in' after case variable name
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
source: src/tests/error.rs
|
||||
expression: err_fmt
|
||||
---
|
||||
[36m[1m->[0m [[36m[1m1[0m;[36m[1m1[0m] - [31m[1mCommand not found: foo[0m
|
||||
[31m[1mCommand not found: foo[0m -
|
||||
[36m[1m->[0m [[36m[1m1[0m;[36m[1m1[0m]
|
||||
[36m[1m |[0m
|
||||
[36m[1m1 |[0m foo
|
||||
[36m[1m |[0m
|
||||
[36m[1m-[0m
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
source: src/tests/error.rs
|
||||
expression: err_fmt
|
||||
---
|
||||
[36m[1m->[0m [[36m[1m1[0m;[36m[1m1[0m] - [31m[1mParse Error[0m
|
||||
[31m[1mParse Error[0m - Expected 'fi' after if statement
|
||||
[36m[1m->[0m [[36m[1m1[0m;[36m[1m1[0m]
|
||||
[36m[1m |[0m
|
||||
[36m[1m1 |[0m if foo; then bar;
|
||||
[36m[1m |[0m [31m[1m^^^^^^^^^^^^^^^^^[0m
|
||||
[36m[1m |[0m
|
||||
[36m[1m-[0m Expected 'fi' after if statement
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
source: src/tests/error.rs
|
||||
expression: err_fmt
|
||||
---
|
||||
[36m[1m->[0m [[36m[1m1[0m;[36m[1m1[0m] - [31m[1mParse Error[0m
|
||||
[31m[1mParse Error[0m - Expected 'then' after 'if' condition
|
||||
[36m[1m->[0m [[36m[1m1[0m;[36m[1m1[0m]
|
||||
[36m[1m |[0m
|
||||
[36m[1m1 |[0m if foo; bar; fi
|
||||
[36m[1m |[0m [31m[1m^^^^^^^^^^^^^[0m
|
||||
[36m[1m |[0m
|
||||
[36m[1m-[0m Expected 'then' after 'if' condition
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
source: src/tests/error.rs
|
||||
expression: err_fmt
|
||||
---
|
||||
[36m[1m->[0m [[36m[1m1[0m;[36m[1m1[0m] - [31m[1mParse Error[0m
|
||||
[31m[1mParse Error[0m - Expected 'do' after loop condition
|
||||
[36m[1m->[0m [[36m[1m1[0m;[36m[1m1[0m]
|
||||
[36m[1m |[0m
|
||||
[36m[1m1 |[0m while true; echo foo; done
|
||||
[36m[1m |[0m [31m[1m^^^^^^^^^^^^^^^^^^^^^^[0m
|
||||
[36m[1m |[0m
|
||||
[36m[1m-[0m Expected 'do' after loop condition
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
source: src/tests/error.rs
|
||||
expression: err_fmt
|
||||
---
|
||||
[36m[1m->[0m [[36m[1m1[0m;[36m[1m1[0m] - [31m[1mParse Error[0m
|
||||
[31m[1mParse Error[0m - Expected 'done' after loop body
|
||||
[36m[1m->[0m [[36m[1m1[0m;[36m[1m1[0m]
|
||||
[36m[1m |[0m
|
||||
[36m[1m1 |[0m while true; do echo foo;
|
||||
[36m[1m |[0m [31m[1m^^^^^^^^^^^^^^^^^^^^^^^^[0m
|
||||
[36m[1m |[0m
|
||||
[36m[1m-[0m Expected 'done' after loop body
|
||||
|
||||
Reference in New Issue
Block a user