use crate::parser::functions::{FunResult, DEFAULT_FUNCTIONS};
use crate::types::{DynErrResult, TaskArgs};
use pest::error::{Error as PestError, ErrorVariant};
use pest::iterators::Pair;
use pest::Parser;
use pest_derive::Parser;
use serde_derive::Deserialize;
use std::cmp::{max, min};
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use std::{error, fmt};
mod functions;
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EscapeMode {
Always,
Spaces,
Never,
}
enum Slice {
Index(isize),
Range(Option<isize>, Option<isize>),
}
enum RealSlice {
Index(usize),
Range(usize, usize),
}
#[derive(Debug)]
struct IntParsingError(String);
impl Display for IntParsingError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "Error parsing `{}` as an integer", self.0)
}
}
impl error::Error for IntParsingError {}
fn custom_span_error(span: pest::Span, msg: String) -> PestError<Rule> {
PestError::new_from_span(ErrorVariant::CustomError { message: msg }, span)
}
#[derive(Parser)]
#[grammar = "parser/grammar.pest"]
struct ScriptParser;
fn rename_rules(rule: &Rule) -> String {
match rule {
Rule::WHITESPACE => "whitespace".to_string(),
Rule::RANGE_SEPARATOR => "..".to_string(),
Rule::digits => "integer".to_string(),
Rule::optional => "?".to_string(),
Rule::index => "integer".to_string(),
Rule::range_from => "integer".to_string(),
Rule::range_to => "integer".to_string(),
Rule::range => "range".to_string(),
Rule::slice => "slice".to_string(),
Rule::arg => "positional argument".to_string(),
Rule::all_args => "$@".to_string(),
Rule::kwarg_name => "keyword argument".to_string(),
Rule::kwarg => "keyword argument".to_string(),
Rule::env_var_name => "environment variable name".to_string(),
Rule::env_var => "environment variable".to_string(),
Rule::fun_name => "function identifier".to_string(),
Rule::expression_inner => "expression".to_string(),
Rule::expression => "expression".to_string(),
Rule::fun_params => "function parameters".to_string(),
Rule::fun => "function".to_string(),
Rule::tag => "tag".to_string(),
Rule::special_val => "valid escaped character".to_string(),
Rule::escape => "valid escaped value".to_string(),
Rule::string_content => "string".to_string(),
Rule::string => "string".to_string(),
Rule::esc_ob => "{{".to_string(),
Rule::esc_cb => "{{".to_string(),
Rule::literal_content => "literal".to_string(),
Rule::literal => "literal".to_string(),
Rule::comment => "comment".to_string(),
Rule::all => "comment, tag or literal".to_string(),
Rule::task_arg => "tag or literal".to_string(),
__other__ => format!("{:?}", __other__),
}
}
fn parse_int(s: &str) -> Result<isize, IntParsingError> {
s.parse::<isize>()
.map_err(|_| IntParsingError(s.to_string()))
}
fn get_slice_repr(slice: Pair<Rule>) -> DynErrResult<Slice> {
let mut slice_inner = slice.into_inner();
let val = slice_inner.next().unwrap();
match val.as_rule() {
Rule::index => Ok(Slice::Index(parse_int(val.as_str())?)),
Rule::range => {
let mut from = None;
let mut to = None;
let val_inner = val.into_inner();
for val in val_inner {
match val.as_rule() {
Rule::range_from => from = Some(parse_int(val.as_str())?),
Rule::range_to => to = Some(parse_int(val.as_str())?),
v => unreachable!("Unexpected rule {:?}", v),
}
}
Ok(Slice::Range(from, to))
}
v => unreachable!("Unexpected rule {:?}", v),
}
}
fn slice_string(val: String, slice: RealSlice) -> FunResult {
match slice {
RealSlice::Index(i) => {
if i >= val.len() {
return FunResult::String("".to_string());
}
FunResult::String(val.chars().nth(i).unwrap().to_string())
}
RealSlice::Range(from, to) => {
if from >= val.len() || from >= to {
return FunResult::String("".to_string());
}
FunResult::String(String::from(val.get(from..to).unwrap_or("")))
}
}
}
fn slice_vec(mut val: Vec<String>, slice: RealSlice) -> FunResult {
match slice {
RealSlice::Index(i) => FunResult::String(String::from(&val[i])),
RealSlice::Range(from, to) => {
if from >= val.len() || from >= to {
FunResult::Vec(vec![])
} else {
let result = val.drain(from..to).collect();
FunResult::Vec(result)
}
}
}
}
fn slice_val(val: FunResult, slice: RealSlice) -> FunResult {
match val {
FunResult::String(v) => slice_string(v, slice),
FunResult::Vec(v) => slice_vec(v, slice),
}
}
fn parse_expression_inner(
expression_inner: Pair<Rule>,
cli_args: &TaskArgs,
env: &HashMap<String, String>,
) -> DynErrResult<FunResult> {
let mut expression_inner = expression_inner.into_inner();
let param = expression_inner.next().unwrap();
match param.as_rule() {
Rule::fun => parse_fun(param, cli_args, env),
Rule::arg => parse_arg(param, cli_args),
Rule::kwarg => parse_kwargs(param, cli_args),
Rule::all_args => parse_all(cli_args),
Rule::env_var => parse_env_var(param, env),
Rule::string => parse_string(param),
v => unreachable!("Unexpected rule {:?}", v),
}
}
fn parse_slice(expression: Pair<Rule>, val: FunResult, optional: bool) -> DynErrResult<FunResult> {
let val_len = match val {
FunResult::String(ref v) => v.len(),
FunResult::Vec(ref v) => v.len(),
} as isize;
let span = expression.as_span();
let slice = get_slice_repr(expression)?;
match slice {
Slice::Index(i) => {
let real_index = if i < 0 { val_len + i } else { i };
if real_index >= val_len || real_index < 0 {
if !optional {
Err(custom_span_error(
span,
String::from("Index out of bounds for mandatory expression"),
)
.into())
} else {
Ok(FunResult::String("".to_string()))
}
} else {
Ok(slice_val(val, RealSlice::Index(real_index as usize)))
}
}
Slice::Range(from, to) => {
let from = from.unwrap_or(0);
let to = min(to.unwrap_or(val_len), val_len);
let real_from = if from < 0 { val_len + from } else { from };
let real_to = if to < 0 { val_len + to } else { to };
if real_from >= val_len || real_from < 0 || real_from > real_to {
if !optional {
Err(custom_span_error(
span,
String::from("Range out of bounds for mandatory expression"),
)
.into())
} else {
Ok(FunResult::Vec(vec![]))
}
} else {
Ok(slice_val(
val,
RealSlice::Range(real_from as usize, max(real_to, 0) as usize),
))
}
}
}
}
fn parse_expression(
expression: Pair<Rule>,
cli_args: &TaskArgs,
env: &HashMap<String, String>,
) -> DynErrResult<FunResult> {
let expression_copy = expression.clone();
let mut expression_inner_values = expression.into_inner();
let expression_inner = expression_inner_values.next().unwrap();
let span = expression_inner.as_span();
let mut val = match expression_inner.as_rule() {
Rule::expression_inner => parse_expression_inner(expression_inner, cli_args, env)?,
v => unreachable!("Unexpected rule {:?}", v),
};
let optional = match expression_copy.into_inner().last() {
Some(v) => v.as_rule() == Rule::optional,
None => false,
};
for slice_or_modifier in expression_inner_values {
match slice_or_modifier.as_rule() {
Rule::slice => {
val = parse_slice(slice_or_modifier, val, optional)?;
}
Rule::optional => (), v => unreachable!("Unexpected rule {:?}", v),
}
}
if !optional && val.is_empty() {
Err(custom_span_error(
span,
String::from("Mandatory expression did not return a value"),
)
.into())
} else {
Ok(val)
}
}
fn parse_fun(
function_pair: Pair<Rule>,
cli_args: &TaskArgs,
env: &HashMap<String, String>,
) -> DynErrResult<FunResult> {
let function_span = function_pair.as_span();
let mut function_inner = function_pair.into_inner();
let fun_name_pair = function_inner.next().unwrap();
let fun_name = fun_name_pair.as_str();
let arguments = function_inner.next();
let fun = match DEFAULT_FUNCTIONS.functions.get(fun_name) {
None => {
return Err(custom_span_error(
fun_name_pair.as_span(),
format!("Undefined function `{}`", fun_name_pair.as_str()),
)
.into())
}
Some(fun) => fun,
};
let arguments: Vec<FunResult> = match arguments {
None => {
vec![]
}
Some(arguments) => {
let mut arguments_list: Vec<FunResult> = vec![];
for param in arguments.into_inner() {
let param = parse_expression(param, cli_args, env)?;
arguments_list.push(param);
}
arguments_list
}
};
match fun(&arguments.iter().map(|v| v.as_val()).collect()) {
Ok(v) => Ok(v),
Err(e) => Err(custom_span_error(
function_span,
format!("Error running function `{}`: {}", fun_name, e),
)
.into()),
}
}
fn parse_string(tag: Pair<Rule>) -> DynErrResult<FunResult> {
let tag_inner = tag.into_inner();
let mut result = String::new();
for pair in tag_inner {
match pair.as_rule() {
Rule::string_content => result.push_str(pair.as_str()),
Rule::escape => {
let mut inner = pair.into_inner();
let val = inner.next().unwrap();
match val.as_str() {
"n" => result.push('\n'),
"r" => result.push('\r'),
"t" => result.push('\t'),
"\\" => result.push('\\'),
"0" => result.push('\0'),
"\"" => result.push('"'),
"'" => result.push('\''),
v => {
unreachable!("Unexpected escaped value {}", v)
}
}
if let Some(other) = inner.next() {
unreachable!("Unexpected pair {:?}", other)
}
}
v => unreachable!("Unexpected rule {:?}", v),
}
}
Ok(FunResult::String(result))
}
fn parse_arg(tag: Pair<Rule>, cli_args: &TaskArgs) -> DynErrResult<FunResult> {
let mut tag_inner = tag.into_inner();
let arg_index = tag_inner.next().unwrap().as_str();
let real_index: usize = usize::from_str(arg_index).unwrap() - 1;
let val: Option<&String> = cli_args.get("*").unwrap().get(real_index);
match val {
None => Ok(FunResult::String(String::from(""))),
Some(val) => Ok(FunResult::String(String::from(val))),
}
}
fn parse_kwargs(tag: Pair<Rule>, cli_args: &TaskArgs) -> DynErrResult<FunResult> {
let mut tag_inner = tag.into_inner();
let arg_name = tag_inner.next().unwrap().as_str();
let values = cli_args.get(arg_name);
match values {
None => Ok(FunResult::Vec(vec![])),
Some(values) => Ok(FunResult::Vec(values.clone())),
}
}
fn parse_env_var(tag: Pair<Rule>, env: &HashMap<String, String>) -> DynErrResult<FunResult> {
let mut tag_inner = tag.into_inner();
let env_var_name = tag_inner.next().unwrap();
let env_var = env.get(env_var_name.as_str());
match env_var {
None => Ok(FunResult::String(String::from(""))),
Some(val) => Ok(FunResult::String(val.clone())),
}
}
fn parse_all(cli_args: &TaskArgs) -> DynErrResult<FunResult> {
match cli_args.get("*") {
None => Ok(FunResult::Vec(vec![])),
Some(v) => Ok(FunResult::Vec(v.clone())),
}
}
fn parse_tag(
tag: Pair<Rule>,
cli_args: &TaskArgs,
env: &HashMap<String, String>,
) -> DynErrResult<FunResult> {
if let Some(tag) = tag.into_inner().next() {
return parse_expression(tag, cli_args, env);
}
unreachable!("tag should have inner values");
}
pub fn parse_script<S: AsRef<str>>(
script: S,
args: &TaskArgs,
env: &HashMap<String, String>,
escape_mode: &EscapeMode,
) -> DynErrResult<String> {
let tokens = ScriptParser::parse(Rule::all, script.as_ref());
let mut result = String::new();
let tokens = match tokens {
Ok(mut tokens) => tokens.next().unwrap().into_inner(),
Err(e) => return Err(e.renamed_rules(rename_rules).to_string().into()),
};
for token in tokens {
match token.as_rule() {
Rule::comment => {} Rule::literal => {
for literal in token.into_inner() {
match literal.as_rule() {
Rule::esc_ob => result.push('{'),
Rule::esc_cb => result.push('}'),
Rule::literal_content => result.push_str(literal.as_str()),
v => {
unreachable!("Unexpected rule {:?}", v);
}
}
}
}
Rule::tag => {
let tag_val = parse_tag(token, args, env)?;
match tag_val {
FunResult::String(val) => {
if !val.is_empty() {
let escape = match escape_mode {
EscapeMode::Always => true,
EscapeMode::Spaces => val.contains(' '),
EscapeMode::Never => false,
};
if escape {
result.push('"');
}
result.push_str(&val);
if escape {
result.push('"');
}
}
}
FunResult::Vec(values) => {
if !values.is_empty() {
let last_val_index = values.len() - 1;
for (i, val) in values.iter().enumerate() {
let escape = match escape_mode {
EscapeMode::Always => true,
EscapeMode::Spaces => val.contains(' '),
EscapeMode::Never => false,
};
if escape {
result.push('"');
}
result.push_str(val);
if escape {
result.push('"');
}
if i != last_val_index {
result.push(' ');
}
}
}
}
}
}
Rule::EOI => {
break;
}
v => {
unreachable!("Unexpected rule {:?}", v);
}
}
}
Ok(result)
}
fn parse_param(
param: &str,
args: &TaskArgs,
env: &HashMap<String, String>,
) -> DynErrResult<FunResult> {
let pairs = ScriptParser::parse(Rule::task_arg, param);
let mut pairs = match pairs {
Ok(mut tokens) => tokens.next().unwrap().into_inner(),
Err(e) => return Err(e.renamed_rules(rename_rules).to_string().into()),
};
match pairs.peek().unwrap().as_rule() {
Rule::tag => {
let tag = pairs.next().unwrap();
let next = pairs.next().unwrap();
match next.as_rule() {
Rule::EOI => (), v => {
unreachable!("Unexpected rule {:?}", v);
}
}
parse_tag(tag, args, env)
}
Rule::literal => {
let mut buffer = String::new();
for pair in pairs {
match pair.as_rule() {
Rule::EOI => (),
Rule::literal => {
for pair in pair.into_inner() {
match pair.as_rule() {
Rule::esc_ob => buffer.push('{'),
Rule::esc_cb => buffer.push('}'),
Rule::literal_content => buffer.push_str(pair.as_str()),
v => unreachable!("Unexpected rule {:?}", v),
}
}
}
v => unreachable!("Unexpected rule {:?}", v),
}
}
Ok(FunResult::String(buffer))
}
Rule::EOI => Ok(FunResult::String(String::new())),
v => unreachable!("Unexpected rule {:?}", v),
}
}
pub fn parse_params(
params: &Vec<String>,
args: &TaskArgs,
env: &HashMap<String, String>,
) -> DynErrResult<Vec<String>> {
let mut result = Vec::with_capacity(params.capacity());
for param in params {
match parse_param(param, args, env)? {
FunResult::String(val) => {
if !val.is_empty() {
result.push(val)
}
}
FunResult::Vec(values) => result.extend(values),
}
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_script() {
let mut vars = HashMap::<String, Vec<String>>::new();
let mut env = HashMap::new();
let script = "hello {$@?}";
let result = parse_script(script, &vars, &env, &EscapeMode::Never).unwrap();
assert_eq!(result, "hello ");
env.insert(
String::from("TEST_ENV_VARIABLE"),
String::from("sample_val"),
);
vars.insert(
String::from("*"),
vec![
String::from("positional"),
String::from("--key=val1"),
String::from("--key=val2"),
String::from("spaced value"),
],
);
vars.insert(
String::from("key"),
vec![String::from("val1"), String::from("val2")],
);
let script =
"Echo {{Hello}} {$@}{hello?} {key} {$1} {$2} {$5?} {$TEST_ENV_VARIABLE} {$TEST_ENV_VARIABLE2?}";
let result = parse_script(script, &vars, &env, &EscapeMode::Always).unwrap();
assert_eq!(
result,
"Echo {Hello} \"positional\" \"--key=val1\" \"--key=val2\" \"spaced value\" \"val1\" \"val2\" \"positional\" \"--key=val1\" \"sample_val\" "
);
let script = "Echo {{Hello}} {$@}";
let result = parse_script(script, &vars, &env, &EscapeMode::Spaces).unwrap();
assert_eq!(
result,
"Echo {Hello} positional --key=val1 --key=val2 \"spaced value\""
);
let script = r#"Echo {{map(Hello)}} {map("--f=\"%s.txt\"",key)}"#;
let result = parse_script(script, &vars, &env, &EscapeMode::Never).unwrap();
assert_eq!(
result,
"Echo {map(Hello)} --f=\"val1.txt\" --f=\"val2.txt\""
);
let script = r#"
print("hello world")
a = [{map("%s\n",jmap("\n '\\%s\\',",$@))}]
print("values are:", a)"#;
let expected = r#"
print("hello world")
a = [
'\positional\',
'\--key=val1\',
'\--key=val2\',
'\spaced value\',
]
print("values are:", a)"#;
let result = parse_script(script, &vars, &env, &EscapeMode::Never).unwrap();
assert_eq!(result, expected);
let script = "echo {$@[0]} {$@[-2]} {$@[-4:]} {key[:5]}{key[5]?}{key[5:]?}{key[5]?}{$1[15]?}{$1[10:]?}{key[2:0]?}";
let result = parse_script(script, &vars, &env, &EscapeMode::Never).unwrap();
assert_eq!(
result,
"echo positional --key=val2 positional --key=val1 --key=val2 spaced value val1 val2"
);
let script =
"echo {key[0][0]} {key[:5][0][1]} {key[0][2:3]} {key[0][3:]} {key[0][4]?} {key[:5][10:][1]?} {key[5:0]?} end";
let result = parse_script(script, &vars, &env, &EscapeMode::Never).unwrap();
assert_eq!(result, "echo v a l 1 end");
let script = "echo {key[3][0]}";
let result = parse_script(script, &vars, &env, &EscapeMode::Never).unwrap_err();
assert!(result
.to_string()
.ends_with("Index out of bounds for mandatory expression"));
let script = "echo {key[0][10]}";
let result = parse_script(script, &vars, &env, &EscapeMode::Never).unwrap_err();
assert!(result
.to_string()
.ends_with("Index out of bounds for mandatory expression"));
let script = "echo {key[0][-5]}";
let result = parse_script(script, &vars, &env, &EscapeMode::Never).unwrap_err();
assert!(result
.to_string()
.ends_with("Index out of bounds for mandatory expression"));
let script = "echo {key[5:0]}";
let result = parse_script(script, &vars, &env, &EscapeMode::Never).unwrap_err();
assert!(result
.to_string()
.ends_with("Range out of bounds for mandatory expression"));
let script = "echo {key[-10:5]}";
let result = parse_script(script, &vars, &env, &EscapeMode::Never).unwrap_err();
assert!(result
.to_string()
.ends_with("Range out of bounds for mandatory expression"));
}
#[test]
fn test_parse_script_errors() {
let vars = HashMap::<String, Vec<String>>::new();
let env = HashMap::new();
let script = "hello {$";
let result = parse_script(script, &vars, &env, &EscapeMode::Never).unwrap_err();
assert_eq!(result.to_string(), " --> 1:9\n |\n1 | hello {$\n | ^---\n |\n = expected integer or environment variable name");
}
#[test]
fn test_parse_escape_spaces() {
let mut vars = HashMap::<String, Vec<String>>::new();
let env = HashMap::new();
vars.insert(
String::from("*"),
vec![String::from("with spaces"), String::from("nospaces")],
);
let script = "{$@} {key?}end";
let result = parse_script(script, &vars, &env, &EscapeMode::Spaces).unwrap();
assert_eq!(result, "\"with spaces\" nospaces end");
}
#[test]
fn test_parse_escape_always() {
let mut vars = HashMap::<String, Vec<String>>::new();
let env = HashMap::new();
vars.insert(
String::from("*"),
vec![String::from("with spaces"), String::from("nospaces")],
);
let script = "{$@} {key?}end";
let result = parse_script(script, &vars, &env, &EscapeMode::Always).unwrap();
assert_eq!(result, "\"with spaces\" \"nospaces\" end");
}
#[test]
fn test_parse_escape_never() {
let mut vars = HashMap::<String, Vec<String>>::new();
let env = HashMap::new();
vars.insert(
String::from("*"),
vec![String::from("with spaces"), String::from("nospaces")],
);
let script = "{$@} {key?}end";
let result = parse_script(script, &vars, &env, &EscapeMode::Never).unwrap();
assert_eq!(result, "with spaces nospaces end");
}
#[test]
fn test_parse_params() {
let mut vars = HashMap::<String, Vec<String>>::new();
let mut env = HashMap::new();
env.insert(
String::from("TEST_ENV_VARIABLE"),
String::from("sample_val"),
);
vars.insert(
String::from("*"),
vec![
String::from("positional"),
String::from("--key=val1"),
String::from("--key=val2"),
],
);
vars.insert(
String::from("key"),
vec![String::from("val1"), String::from("val2")],
);
let params = vec![
"Echo",
"{{Hello}}\'",
"{$@}",
"{key}",
"{$1}",
"{$2}",
"{$5?}",
"{$TEST_ENV_VARIABLE}",
"{$TEST_ENV_VARIABLE2?}",
];
let result =
parse_params(¶ms.iter().map(|v| v.to_string()).collect(), &vars, &env).unwrap();
assert_eq!(
result,
vec![
"Echo",
"{Hello}'",
"positional",
"--key=val1",
"--key=val2",
"val1",
"val2",
"positional",
"--key=val1",
"sample_val"
]
);
let params = vec![
"Echo",
"{{map(Hello)}}",
r#"{ map("--f=\"%s.txt\"", key) }"#,
];
let result =
parse_params(¶ms.iter().map(|v| v.to_string()).collect(), &vars, &env).unwrap();
assert_eq!(
result,
vec![
"Echo",
"{map(Hello)}",
"--f=\"val1.txt\"",
"--f=\"val2.txt\""
]
);
let params = vec![
"Echo",
"{{jmap(Hello)}}",
r#"{ jmap("--f=\"%s.txt\" ", key) }"#,
];
let result =
parse_params(¶ms.iter().map(|v| v.to_string()).collect(), &vars, &env).unwrap();
assert_eq!(
result,
vec![
"Echo",
"{jmap(Hello)}",
"--f=\"val1.txt\" --f=\"val2.txt\" "
]
);
}
#[test]
fn test_parse_undef_function() {
let vars = HashMap::<String, Vec<String>>::new();
let env = HashMap::new();
let script = "echo {undef_function('hello')}";
let result = parse_script(script, &vars, &env, &EscapeMode::Never).unwrap_err();
assert!(result
.to_string()
.ends_with("Undefined function `undef_function`"));
}
#[test]
fn test_parse_function_error() {
let vars = HashMap::<String, Vec<String>>::new();
let env = HashMap::new();
let script = "echo {fmt('hello %', 'world')}";
let result = parse_script(script, &vars, &env, &EscapeMode::Never).unwrap_err();
assert!(result
.to_string()
.contains("Error running function `fmt`: Invalid format string:"));
}
#[test]
fn test_parse_function_out_required() {
let mut vars = HashMap::<String, Vec<String>>::new();
vars.insert(String::from("*"), vec![]);
let env = HashMap::new();
let script = "echo {fmt('%s', $1?)}";
let result = parse_script(script, &vars, &env, &EscapeMode::Never).unwrap_err();
assert!(result
.to_string()
.contains("Mandatory expression did not return a value"));
}
#[test]
fn test_parse_int_error() {
let mut vars = HashMap::<String, Vec<String>>::new();
vars.insert(String::from("*"), vec![]);
let env = HashMap::new();
let script = "echo {hello[999999999999999999999]}";
let result = parse_script(script, &vars, &env, &EscapeMode::Never).unwrap_err();
assert!(result
.to_string()
.contains("Error parsing `999999999999999999999` as an integer"));
}
}