added arithmetic substitution

This commit is contained in:
2025-05-13 13:08:53 -04:00
parent ec179a04be
commit c0743e473c
5 changed files with 366 additions and 48 deletions

View File

@@ -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<String> {
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);
}
ARITH_SUB => {
let mut body = String::new();
while let Some(arith_ch) = chars.next() {
match arith_ch {
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
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);
ARITH_SUB => break,
_ => body.push(arith_ch)
}
}
let expanded = expand_arithmetic(&body)?;
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),
}
}
if !var_name.is_empty() {
let var_val = read_vars(|v| v.get_var(&var_name));
result.push_str(&var_val);
var_name.clear();
}
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<Chars<'_>>) -> ShResult<String> {
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<String> {
let mut words = vec![];
@@ -171,6 +201,178 @@ pub fn expand_glob(raw: &str) -> ShResult<String> {
Ok(words.join(" "))
}
pub fn is_a_number(raw: &str) -> bool {
let trimmed = raw.trim();
trimmed.parse::<i32>().is_ok() || trimmed.parse::<f64>().is_ok()
}
enum ArithTk {
Num(f64),
Op(ArithOp),
LParen,
RParen
}
impl ArithTk {
pub fn tokenize(raw: &str) -> ShResult<Vec<Self>> {
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::<f64>() else {
panic!()
};
tokens.push(Self::Num(num));
}
'+' | '-' | '*' | '/' | '%' => {
let mut buf = String::new();
buf.push(ch);
tokens.push(Self::Op(buf.parse::<ArithOp>().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<ArithTk>) -> ShResult<Vec<ArithTk>> {
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<ArithTk>) -> ShResult<f64> {
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<Self, Self::Err> {
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<String> {
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<String> {
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;

View File

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

View File

@@ -64,6 +64,7 @@ impl ParsedSrc {
pub fn parse_src(&mut self) -> Result<(),Vec<ShErr>> {
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])

View File

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

View File

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