diff --git a/src/expand.rs b/src/expand.rs index 6ebf9e6..ce623d9 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -1,5 +1,6 @@ use std::collections::HashSet; -use std::str::FromStr; +use std::iter::Peekable; +use std::str::{Chars, FromStr}; use glob::Pattern; use regex::Regex; @@ -22,6 +23,8 @@ pub const SNG_QUOTE: char = '\u{fdd2}'; pub const TILDE_SUB: char = '\u{fdd3}'; /// Subshell marker pub const SUBSH: char = '\u{fdd4}'; +/// Arithmetic substitution marker +pub const ARITH_SUB: char = '\u{fdd5}'; impl Tk { /// Create a new expanded token @@ -95,8 +98,6 @@ impl Expander { pub fn expand_raw(&self) -> ShResult { let mut chars = self.raw.chars().peekable(); let mut result = String::new(); - let mut var_name = String::new(); - let mut in_brace = false; flog!(INFO, self.raw); while let Some(ch) = chars.next() { @@ -105,51 +106,34 @@ impl Expander { let home = env::var("HOME").unwrap_or_default(); result.push_str(&home); } - VAR_SUB => { - while let Some(ch) = chars.next() { - flog!(INFO,ch); - flog!(INFO,var_name); - match ch { - SUBSH if var_name.is_empty() => { - let mut subsh_body = String::new(); - while let Some(ch) = chars.next() { - match ch { - SUBSH => { - break + ARITH_SUB => { + let mut body = String::new(); + while let Some(arith_ch) = chars.next() { + match arith_ch { + VAR_SUB => { + let expanded = expand_var(&mut chars)?; + if is_a_number(&expanded) { + body.push_str(&expanded); + } else { + return Err( + ShErr::Simple { + kind: ShErrKind::ParseErr, + msg: format!("Expected a number during substitution, found '{}'",&expanded), + notes: vec![], } - _ => subsh_body.push(ch) - } + ) } - let expanded = expand_cmd_sub(&subsh_body)?; - flog!(INFO, expanded); - result.push_str(&expanded); } - '{' if var_name.is_empty() => in_brace = true, - '}' if in_brace => { - flog!(DEBUG, var_name); - let var_val = perform_param_expansion(&var_name)?; - result.push_str(&var_val); - var_name.clear(); - break - } - _ if in_brace => { - var_name.push(ch) - } - _ if is_hard_sep(ch) || ch == DUB_QUOTE || ch == SUBSH || ch == '/' => { - let var_val = read_vars(|v| v.get_var(&var_name)); - result.push_str(&var_val); - result.push(ch); - var_name.clear(); - break - } - _ => var_name.push(ch), + ARITH_SUB => break, + _ => body.push(arith_ch) } } - if !var_name.is_empty() { - let var_val = read_vars(|v| v.get_var(&var_name)); - result.push_str(&var_val); - var_name.clear(); - } + let expanded = expand_arithmetic(&body)?; + result.push_str(&expanded); + } + VAR_SUB => { + let expanded = expand_var(&mut chars)?; + result.push_str(&expanded); } _ => result.push(ch) } @@ -158,6 +142,52 @@ impl Expander { } } +pub fn expand_var(chars: &mut Peekable>) -> ShResult { + let mut var_name = String::new(); + let mut in_brace = false; + while let Some(&ch) = chars.peek() { + match ch { + SUBSH if var_name.is_empty() => { + chars.next(); // now safe to consume + let mut subsh_body = String::new(); + while let Some(c) = chars.next() { + if c == SUBSH { break } + subsh_body.push(c); + } + let expanded = expand_cmd_sub(&subsh_body)?; + return Ok(expanded); + } + '{' if var_name.is_empty() => { + chars.next(); // consume the brace + in_brace = true; + } + '}' if in_brace => { + chars.next(); // consume the brace + let val = perform_param_expansion(&var_name)?; + return Ok(val); + } + ch if in_brace => { + chars.next(); // safe to consume + var_name.push(ch); + } + ch if is_hard_sep(ch) || ch == ARITH_SUB || ch == DUB_QUOTE || ch == SUBSH || ch == '/' => { + let val = read_vars(|v| v.get_var(&var_name)); + return Ok(val); + } + _ => { + chars.next(); + var_name.push(ch); + } + } + } + if !var_name.is_empty() { + let var_val = read_vars(|v| v.get_var(&var_name)); + Ok(var_val) + } else { + Ok(String::new()) + } +} + pub fn expand_glob(raw: &str) -> ShResult { let mut words = vec![]; @@ -171,6 +201,178 @@ pub fn expand_glob(raw: &str) -> ShResult { Ok(words.join(" ")) } +pub fn is_a_number(raw: &str) -> bool { + let trimmed = raw.trim(); + trimmed.parse::().is_ok() || trimmed.parse::().is_ok() +} + +enum ArithTk { + Num(f64), + Op(ArithOp), + LParen, + RParen +} + +impl ArithTk { + pub fn tokenize(raw: &str) -> ShResult> { + let mut tokens = Vec::new(); + let mut chars = raw.chars().peekable(); + + while let Some(&ch) = chars.peek() { + match ch { + ' ' | '\t' => { chars.next(); } + '0'..='9' | '.' => { + let mut num = String::new(); + while let Some(&digit) = chars.peek() { + if digit.is_ascii_digit() || digit == '.' { + num.push(digit); + chars.next(); + } else { + break; + } + } + let Ok(num) = num.parse::() else { + panic!() + }; + tokens.push(Self::Num(num)); + } + '+' | '-' | '*' | '/' | '%' => { + let mut buf = String::new(); + buf.push(ch); + tokens.push(Self::Op(buf.parse::().unwrap())); + chars.next(); + } + '(' => { tokens.push(Self::LParen); chars.next(); } + ')' => { tokens.push(Self::RParen); chars.next(); } + _ => return Err(ShErr::Simple { kind: ShErrKind::ParseErr, msg: "Invalid character in arithmetic substitution".into(), notes: vec![] }) + } + } + + Ok(tokens) + } + + fn to_rpn(tokens: Vec) -> ShResult> { + let mut output = Vec::new(); + let mut ops = Vec::new(); + + fn precedence(op: &ArithOp) -> usize { + match op { + ArithOp::Add | + ArithOp::Sub => 1, + ArithOp::Mul | + ArithOp::Div | + ArithOp::Mod => 2, + } + } + + for token in tokens { + match token { + ArithTk::Num(_) => output.push(token), + ArithTk::Op(op1) => { + while let Some(ArithTk::Op(op2)) = ops.last() { + if precedence(op2) >= precedence(&op1) { + output.push(ops.pop().unwrap()); + } else { + break; + } + } + ops.push(ArithTk::Op(op1)); + } + ArithTk::LParen => ops.push(ArithTk::LParen), + ArithTk::RParen => { + while let Some(top) = ops.pop() { + if let ArithTk::LParen = top { + break; + } else { + output.push(top); + } + } + } + } + } + + while let Some(op) = ops.pop() { + output.push(op); + } + + Ok(output) + } + pub fn eval_rpn(tokens: Vec) -> ShResult { + let mut stack = Vec::new(); + + for token in tokens { + match token { + ArithTk::Num(n) => stack.push(n), + ArithTk::Op(op) => { + let rhs = stack.pop().ok_or(ShErr::Simple { + kind: ShErrKind::ParseErr, + msg: "Missing right-hand operand".into(), + notes: vec![], + })?; + let lhs = stack.pop().ok_or(ShErr::Simple { + kind: ShErrKind::ParseErr, + msg: "Missing left-hand operand".into(), + notes: vec![], + })?; + let result = match op { + ArithOp::Add => lhs + rhs, + ArithOp::Sub => lhs - rhs, + ArithOp::Mul => lhs * rhs, + ArithOp::Div => lhs / rhs, + ArithOp::Mod => lhs % rhs, + }; + stack.push(result); + } + _ => return Err(ShErr::Simple { + kind: ShErrKind::ParseErr, + msg: "Unexpected token during evaluation".into(), + notes: vec![], + }), + } + } + + if stack.len() != 1 { + return Err(ShErr::Simple { + kind: ShErrKind::ParseErr, + msg: "Invalid arithmetic expression".into(), + notes: vec![], + }); + } + + Ok(stack[0]) + } +} + +enum ArithOp { + Add, + Sub, + Mul, + Div, + Mod, +} + +impl FromStr for ArithOp { + type Err = ShErr; + fn from_str(s: &str) -> Result { + assert!(s.len() == 1); + match s.chars().next().unwrap() { + '+' => Ok(Self::Add), + '-' => Ok(Self::Sub), + '*' => Ok(Self::Mul), + '/' => Ok(Self::Div), + '%' => Ok(Self::Mod), + _ => Err(ShErr::Simple { kind: ShErrKind::ParseErr, msg: "Invalid arithmetic operator".into(), notes: vec![] }) + } + } +} + +pub fn expand_arithmetic(raw: &str) -> ShResult { + let tokens = ArithTk::tokenize(raw)?; + let rpn = ArithTk::to_rpn(tokens)?; + let result = ArithTk::eval_rpn(rpn)?; + Ok(result.to_string()) +} + /// Get the command output of a given command input as a String pub fn expand_cmd_sub(raw: &str) -> ShResult { flog!(DEBUG, "in expand_cmd_sub"); @@ -229,6 +431,63 @@ pub fn unescape_str(raw: &str) -> String { result.push(next_ch) } } + '`' => { + result.push(ARITH_SUB); + let mut closed = false; + while let Some(arith_ch) = chars.next() { + match arith_ch { + '\\' => { + result.push(arith_ch); + if let Some(next_ch) = chars.next() { + result.push(next_ch) + } + } + '`' => { + closed = true; + result.push(ARITH_SUB); + break + } + '$' if chars.peek() == Some(&'(') => { + result.push(VAR_SUB); + result.push(SUBSH); + chars.next(); + let mut cmdsub_count = 1; + while let Some(cmdsub_ch) = chars.next() { + flog!(DEBUG, cmdsub_count); + flog!(DEBUG, cmdsub_ch); + match cmdsub_ch { + '\\' => { + result.push(cmdsub_ch); + if let Some(next_ch) = chars.next() { + result.push(next_ch) + } + } + '$' if chars.peek() == Some(&'(') => { + cmdsub_count += 1; + result.push(cmdsub_ch); + result.push(chars.next().unwrap()); + } + ')' => { + cmdsub_count -= 1; + flog!(DEBUG, cmdsub_count); + if cmdsub_count == 0 { + result.push(SUBSH); + break + } else { + result.push(cmdsub_ch); + } + } + _ => result.push(cmdsub_ch) + } + } + } + '$' => { + result.push(VAR_SUB); + } + _ => result.push(arith_ch) + } + } + } '(' => { result.push(SUBSH); let mut paren_count = 1; diff --git a/src/parse/lex.rs b/src/parse/lex.rs index 0d85b51..15dab8c 100644 --- a/src/parse/lex.rs +++ b/src/parse/lex.rs @@ -2,7 +2,7 @@ use std::{collections::VecDeque, fmt::Display, iter::Peekable, ops::{Bound, Dere use bitflags::bitflags; -use crate::{builtin::BUILTINS, libsh::{error::{ShErr, ShErrKind, ShResult}, utils::CharDequeUtils}, prelude::*}; +use crate::{builtin::BUILTINS, libsh::{error::{ShErr, ShErrKind, ShResult}, utils::CharDequeUtils}, parse::parse_err_full, prelude::*}; pub const KEYWORDS: [&str;16] = [ "if", @@ -327,6 +327,7 @@ impl LexStream { } while let Some(ch) = chars.next() { + flog!(DEBUG, "we are in the loop"); match ch { _ if self.flags.contains(LexFlags::RAW) => { if ch.is_whitespace() { @@ -341,6 +342,61 @@ impl LexStream { pos += ch.len_utf8(); } } + '`' => { + let arith_pos = pos; + pos += 1; + let mut closed = false; + while let Some(arith_ch) = chars.next() { + match arith_ch { + '\\' => { + pos += 1; + if let Some(ch) = chars.next() { + pos += ch.len_utf8(); + } + } + '`' => { + pos += 1; + closed = true; + break + } + '$' if chars.peek() == Some(&'(') => { + pos += 2; + chars.next(); + let mut cmdsub_count = 1; + while let Some(cmdsub_ch) = chars.next() { + match cmdsub_ch { + '$' if chars.peek() == Some(&'(') => { + pos += 2; + chars.next(); + cmdsub_count += 1; + } + ')' => { + pos += 1; + cmdsub_count -= 1; + if cmdsub_count == 0 { + break + } + } + _ => pos += cmdsub_ch.len_utf8() + } + } + } + _ => pos += arith_ch.len_utf8() + } + } + flog!(DEBUG, "we have left the loop"); + flog!(DEBUG, closed); + if !closed && !self.flags.contains(LexFlags::LEX_UNFINISHED) { + self.cursor = pos; + return Err( + ShErr::full( + ShErrKind::ParseErr, + "Unclosed arithmetic substitution", + Span::new(arith_pos..arith_pos + 1, self.source.clone()) + ) + ) + } + } '$' if chars.peek() == Some(&'{') => { pos += 2; chars.next(); @@ -396,6 +452,7 @@ impl LexStream { } } if !paren_count == 0 && !self.flags.contains(LexFlags::LEX_UNFINISHED) { + self.cursor = pos; return Err( ShErr::full( ShErrKind::ParseErr, @@ -718,7 +775,10 @@ impl Iterator for LexStream { } else { match self.read_string() { Ok(tk) => tk, - Err(e) => return Some(Err(e)) + Err(e) => { + flog!(ERROR, e); + return Some(Err(e)) + } } } } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index af069a2..40e28e5 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -64,6 +64,7 @@ impl ParsedSrc { pub fn parse_src(&mut self) -> Result<(),Vec> { let mut tokens = vec![]; for lex_result in LexStream::new(self.src.clone(), LexFlags::empty()) { + flog!(DEBUG, lex_result); match lex_result { Ok(token) => tokens.push(token), Err(error) => return Err(vec![error]) diff --git a/src/prompt/highlight.rs b/src/prompt/highlight.rs index 521b4da..a19dadf 100644 --- a/src/prompt/highlight.rs +++ b/src/prompt/highlight.rs @@ -1,5 +1,5 @@ use std::{env, os::unix::fs::PermissionsExt, path::{Path, PathBuf}, sync::Arc}; -use crate::{builtin::BUILTINS, prelude::*}; +use crate::builtin::BUILTINS; use rustyline::highlight::Highlighter; use crate::{libsh::term::{Style, StyleSet, Styled}, parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}, state::read_logic}; diff --git a/src/prompt/readline.rs b/src/prompt/readline.rs index 5ff6c98..4050835 100644 --- a/src/prompt/readline.rs +++ b/src/prompt/readline.rs @@ -1,5 +1,3 @@ -use std::borrow::Cow; - use rustyline::{completion::Completer, hint::{Hint, Hinter}, history::SearchDirection, validate::{ValidationResult, Validator}, Helper}; use crate::{libsh::term::{Style, Styled}, parse::{lex::{LexFlags, LexStream}, ParseStream}};