diff --git a/src/eval.rs b/src/eval.rs index ff03a08121..adc4795054 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, fmt::Display}; use crate::{ parser::Operator, Block, BlockId, Call, Expr, Expression, ParserState, Span, Statement, VarId, @@ -20,6 +20,24 @@ pub enum Value { Block(BlockId), Unknown, } + +impl Display for Value { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Value::Bool { val, .. } => { + write!(f, "{}", val) + } + Value::Int { val, .. } => { + write!(f, "{}", val) + } + Value::String { val, .. } => write!(f, "{}", val), + Value::List(..) => write!(f, ""), + Value::Block(..) => write!(f, ""), + Value::Unknown => write!(f, ""), + } + } +} + impl Value { pub fn add(&self, rhs: &Value) -> Result { match (self, rhs) { @@ -138,6 +156,18 @@ fn eval_call(state: &State, stack: &mut Stack, call: &Call) -> Result Err(ShellError::Mismatch("bool".into(), Span::unknown())), } + } else if decl.signature.name == "build-string" { + let mut output = vec![]; + + for expr in &call.positional { + let val = eval_expression(state, stack, expr)?; + + output.push(val.to_string()); + } + Ok(Value::String { + val: output.join(""), + span: call.head, + }) } else { Ok(Value::Unknown) } diff --git a/src/main.rs b/src/main.rs index c2ddd36986..95c0d594b8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,6 +49,9 @@ fn main() -> std::io::Result<()> { ); working_set.add_decl(sig.into()); + let sig = Signature::build("build-string").rest(SyntaxShape::String, "list of string"); + working_set.add_decl(sig.into()); + let sig = Signature::build("def") .required("def_name", SyntaxShape::String, "definition name") .required("params", SyntaxShape::Signature, "parameters") diff --git a/src/parser.rs b/src/parser.rs index d9d81b7ee9..e8cf87db99 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -894,7 +894,144 @@ impl<'a> ParserWorkingSet<'a> { } pub(crate) fn parse_dollar_expr(&mut self, span: Span) -> (Expression, Option) { - self.parse_variable_expr(span) + let contents = self.get_span_contents(span); + + if contents.starts_with(b"$\"") { + self.parse_string_interpolation(span) + } else { + self.parse_variable_expr(span) + } + } + + pub fn parse_string_interpolation(&mut self, span: Span) -> (Expression, Option) { + #[derive(PartialEq, Eq, Debug)] + enum InterpolationMode { + String, + Expression, + } + let mut error = None; + + let contents = self.get_span_contents(span); + + let start = if contents.starts_with(b"$\"") { + span.start + 2 + } else { + span.start + }; + + let end = if contents.ends_with(b"\"") && contents.len() > 2 { + span.end - 1 + } else { + span.end + }; + + let inner_span = Span { start, end }; + let contents = self.get_span_contents(inner_span).to_vec(); + + let mut output = vec![]; + let mut mode = InterpolationMode::String; + let mut token_start = start; + let mut depth = 0; + + let mut b = start; + + #[allow(clippy::needless_range_loop)] + while b != end { + if contents[b - start] == b'(' && mode == InterpolationMode::String { + depth = 1; + mode = InterpolationMode::Expression; + if token_start < b { + let span = Span { + start: token_start, + end: b, + }; + let str_contents = self.get_span_contents(span); + output.push(Expression { + expr: Expr::String(String::from_utf8_lossy(str_contents).to_string()), + span, + ty: Type::String, + }); + } + token_start = b; + } else if contents[b - start] == b'(' && mode == InterpolationMode::Expression { + depth += 1; + } else if contents[b - start] == b')' && mode == InterpolationMode::Expression { + match depth { + 0 => {} + 1 => { + mode = InterpolationMode::String; + + if token_start < b { + let span = Span { + start: token_start, + end: b + 1, + }; + + let (expr, err) = self.parse_full_column_path(span); + error = error.or(err); + output.push(expr); + } + + token_start = b + 1; + } + _ => depth -= 1, + } + } + b += 1; + } + + match mode { + InterpolationMode::String => { + if token_start < end { + let span = Span { + start: token_start, + end, + }; + let str_contents = self.get_span_contents(span); + output.push(Expression { + expr: Expr::String(String::from_utf8_lossy(str_contents).to_string()), + span, + ty: Type::String, + }); + } + } + InterpolationMode::Expression => { + if token_start < end { + let span = Span { + start: token_start, + end, + }; + + let (expr, err) = self.parse_full_column_path(span); + error = error.or(err); + output.push(expr); + } + } + } + + if let Some(decl_id) = self.find_decl(b"build-string") { + ( + Expression { + expr: Expr::Call(Box::new(Call { + head: Span { + start: span.start, + end: span.start + 2, + }, + named: vec![], + positional: output, + decl_id, + })), + span, + ty: Type::String, + }, + error, + ) + } else { + ( + Expression::garbage(span), + Some(ParseError::UnknownCommand(span)), + ) + } } pub fn parse_variable_expr(&mut self, span: Span) -> (Expression, Option) { diff --git a/src/signature.rs b/src/signature.rs index 8e12543a0c..a678982c4c 100644 --- a/src/signature.rs +++ b/src/signature.rs @@ -102,6 +102,17 @@ impl Signature { self } + pub fn rest(mut self, shape: impl Into, desc: impl Into) -> Signature { + self.rest_positional = Some(PositionalArg { + name: "rest".into(), + desc: desc.into(), + shape: shape.into(), + var_id: None, + }); + + self + } + /// Add an optional named flag argument to the signature pub fn named( mut self,