diff --git a/Cargo.lock b/Cargo.lock index fe93503e..6189bd15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,8 +26,10 @@ dependencies = [ "heraclitus-compiler", "include_dir", "itertools", + "once_cell", "predicates", "pretty_assertions", + "regex", "similar-string", "tempfile", "test-generator", @@ -467,9 +469,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "pad" @@ -558,9 +560,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.5" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -570,9 +572,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -581,9 +583,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustix" diff --git a/Cargo.toml b/Cargo.toml index 808100d7..282aca4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,8 @@ glob = "0.3" heraclitus-compiler = "1.8.1" include_dir = "0.7.4" itertools = "0.13.0" +once_cell = "1.20.2" +regex = "1.11.1" similar-string = "1.4.2" test-generator = "0.3.1" wildmatch = "2.4.0" diff --git a/src/compiler.rs b/src/compiler.rs index aceebbb6..f6228379 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -12,11 +12,12 @@ use heraclitus_compiler::prelude::*; use itertools::Itertools; use wildmatch::WildMatchPattern; use std::env; +use std::ffi::OsStr; use std::fs; use std::fs::File; use std::io::{ErrorKind, Write}; use std::iter::once; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::{Command, ExitStatus}; use std::time::Instant; @@ -29,19 +30,23 @@ const AMBER_DEBUG_TIME: &str = "AMBER_DEBUG_TIME"; pub struct CompilerOptions { pub no_proc: Vec, pub minify: bool, + pub run_name: Option, } impl Default for CompilerOptions { fn default() -> Self { let no_proc = vec![String::from("*")]; - Self { no_proc, minify: false } + Self { no_proc, minify: false, run_name: None } } } impl CompilerOptions { - pub fn from_args(no_proc: &[String], minify: bool) -> Self { + pub fn from_args(no_proc: &[String], minify: bool, input: Option<&Path>) -> Self { let no_proc = no_proc.to_owned(); - Self { no_proc, minify } + let run_name = input.and_then(Path::file_name) + .and_then(OsStr::to_str) + .map(String::from); + Self { no_proc, minify, run_name } } } @@ -138,7 +143,7 @@ impl AmberCompiler { } } - pub fn get_sorted_ast_forest( + fn get_sorted_ast_forest( &self, block: Block, meta: &ParserMetadata, diff --git a/src/main.rs b/src/main.rs index cf3d90a8..6d8c121e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,6 +31,7 @@ struct Cli { /// Arguments passed to Amber script #[arg(trailing_var_arg = true)] + #[arg(allow_hyphen_values = true)] args: Vec, /// Disable a postprocessor @@ -70,6 +71,7 @@ struct RunCommand { /// Arguments passed to Amber script #[arg(trailing_var_arg = true)] + #[arg(allow_hyphen_values = true)] args: Vec, /// Disable a postprocessor @@ -134,17 +136,17 @@ fn main() -> Result<(), Box> { handle_eval(command)?; } CommandKind::Run(command) => { - let options = CompilerOptions::from_args(&command.no_proc, false); + let options = CompilerOptions::from_args(&command.no_proc, false, Some(&command.input)); let (code, messages) = compile_input(command.input, options); execute_output(code, command.args, messages)?; } CommandKind::Check(command) => { - let options = CompilerOptions::from_args(&command.no_proc, false); + let options = CompilerOptions::from_args(&command.no_proc, false, None); compile_input(command.input, options); } CommandKind::Build(command) => { let output = create_output(&command); - let options = CompilerOptions::from_args(&command.no_proc, command.minify); + let options = CompilerOptions::from_args(&command.no_proc, command.minify, None); let (code, _) = compile_input(command.input, options); write_output(output, code); } @@ -156,7 +158,7 @@ fn main() -> Result<(), Box> { } } } else if let Some(input) = cli.input { - let options = CompilerOptions::from_args(&cli.no_proc, false); + let options = CompilerOptions::from_args(&cli.no_proc, false, Some(&input)); let (code, messages) = compile_input(input, options); execute_output(code, cli.args, messages)?; } diff --git a/src/modules/builtin/cli/getopt.rs b/src/modules/builtin/cli/getopt.rs new file mode 100644 index 00000000..5be71258 --- /dev/null +++ b/src/modules/builtin/cli/getopt.rs @@ -0,0 +1,63 @@ +use crate::docs::module::DocumentationModule; +use crate::modules::builtin::cli::param::ParamImpl; +use crate::modules::builtin::cli::parser::ParserImpl; +use crate::modules::expression::expr::Expr; +use crate::modules::types::{Type, Typed}; +use crate::translate::module::TranslateModule; +use crate::utils::metadata::{ParserMetadata, TranslateMetadata}; +use heraclitus_compiler::prelude::*; +use std::cell::RefCell; +use std::rc::Rc; + +#[derive(Debug, Clone)] +pub struct GetoptCli { + parser: Option>>, + args: Box, +} + +impl Typed for GetoptCli { + fn get_type(&self) -> Type { + Type::Null + } +} + +impl SyntaxModule for GetoptCli { + syntax_name!("Getopt Invocation"); + + fn new() -> Self { + let args = Box::new(Expr::new()); + Self { parser: None, args } + } + + fn parse(&mut self, meta: &mut ParserMetadata) -> SyntaxResult { + token(meta, "getopt")?; + let mut parser = Expr::new(); + token(meta, "(")?; + let parser_tok = meta.get_current_token(); + syntax(meta, &mut parser)?; + token(meta, ",")?; + syntax(meta, &mut *self.args)?; + token(meta, ")")?; + let parser = match ParserImpl::find_parser(meta, &parser) { + Some(parser) => parser, + None => return error!(meta, parser_tok, "Expected parser object"), + }; + parser.borrow_mut().add_param(ParamImpl::help()); + self.parser = Some(parser); + Ok(()) + } +} + +impl TranslateModule for GetoptCli { + fn translate(&self, meta: &mut TranslateMetadata) -> String { + self.parser.as_ref() + .map(|parser| parser.borrow().translate(meta, &self.args)) + .unwrap_or_default() + } +} + +impl DocumentationModule for GetoptCli { + fn document(&self, _meta: &ParserMetadata) -> String { + String::new() + } +} diff --git a/src/modules/builtin/cli/mod.rs b/src/modules/builtin/cli/mod.rs new file mode 100644 index 00000000..5660712d --- /dev/null +++ b/src/modules/builtin/cli/mod.rs @@ -0,0 +1,3 @@ +pub mod getopt; +pub mod param; +pub mod parser; diff --git a/src/modules/builtin/cli/param.rs b/src/modules/builtin/cli/param.rs new file mode 100644 index 00000000..f662e16c --- /dev/null +++ b/src/modules/builtin/cli/param.rs @@ -0,0 +1,190 @@ +use crate::docs::module::DocumentationModule; +use crate::modules::builtin::cli::parser::ParserImpl; +use crate::modules::expression::expr::Expr; +use crate::modules::types::{Type, Typed}; +use crate::regex; +use crate::translate::module::TranslateModule; +use crate::utils::metadata::{ParserMetadata, TranslateMetadata}; +use crate::utils::payload::Payload; +use heraclitus_compiler::prelude::*; +use itertools::Itertools; +use std::cell::RefCell; +use std::rc::Rc; + +#[derive(Debug)] +pub enum ParamKind { + Positional(String), + Optional(Vec, Vec, bool), +} + +impl ParamKind { + fn from(option: String) -> Option { + let regex = regex!(r"^(?:(\w+)|-(\w)|--(\w+(?:-\w+)*))$"); + let mut names = Vec::new(); + let mut shorts = Vec::new(); + let mut longs = Vec::new(); + for token in option.split("|") { + if let Some(captures) = regex.captures(token) { + if let Some(name) = captures.get(1) { + let name = name.as_str().to_owned(); + names.push(name); + } else if let Some(short) = captures.get(2) { + let short = short.as_str().chars().next().unwrap(); + shorts.push(short); + } else if let Some(long) = captures.get(3) { + let long = long.as_str().to_owned(); + longs.push(long); + } else { + return None; + } + } else { + return None; + } + } + let positionals = names.len(); + let optionals = shorts.len() + longs.len(); + if positionals == 1 && optionals == 0 { + let name = names.into_iter().next().unwrap(); + Some(ParamKind::Positional(name)) + } else if positionals == 0 && optionals >= 1 { + Some(ParamKind::Optional(shorts, longs, false)) + } else { + None + } + } +} + +#[derive(Debug)] +pub struct ParamImpl { + pub name: String, + pub kind: ParamKind, + pub default: Expr, + pub help: String, +} + +impl ParamImpl { + pub fn new(kind: ParamKind, default: Expr, help: String) -> Rc> { + let name = String::new(); + let param = ParamImpl { name, kind, default, help }; + Rc::new(RefCell::new(param)) + } + + pub fn help() -> Rc> { + let kind = ParamKind::Optional(vec![], vec![String::from("help")], true); + let default = Expr::new(); + let help = String::from("Show help text"); + Self::new(kind, default, help) + } + + pub fn set_var_name(&mut self, name: &str, id: Option) { + self.name = match id { + Some(id) => format!("__{id}_{name}"), + None => name.to_string(), + }; + } + + pub fn describe_optional(shorts: &[char], longs: &[String]) -> String { + let shorts = shorts.iter().map(|short| format!("-{short}")); + let longs = longs.iter().map(|long| format!("--{long}")); + shorts.chain(longs).join("|") + } + + pub fn describe_help(&self) -> (String, String) { + let mut option = match &self.kind { + ParamKind::Positional(name) => name.to_uppercase(), + ParamKind::Optional(shorts, longs, _) => Self::describe_optional(shorts, longs), + }; + if self.default.kind != Type::Null { + let default = self.default.kind.to_string(); + option = format!("{option}: {default}"); + } + (option, self.help.clone()) + } + + pub fn invert_default_bool(&self) -> isize { + let value = self.default.get_integer_value().unwrap_or_default(); + if value == 0 { 1 } else { 0 } + } +} + +#[derive(Debug, Clone)] +pub struct ParamCli { + param: Option>>, +} + +impl ParamCli { + pub fn get_payload(&self) -> Option { + self.param.as_ref() + .map(|param| Payload::Param(Rc::clone(param))) + } +} + +impl Typed for ParamCli { + fn get_type(&self) -> Type { + self.param.as_ref() + .map(|param| param.borrow().default.kind.clone()) + .unwrap_or(Type::Null) + } +} + +impl SyntaxModule for ParamCli { + syntax_name!("Param Invocation"); + + fn new() -> Self { + Self { param: None } + } + + fn parse(&mut self, meta: &mut ParserMetadata) -> SyntaxResult { + token(meta, "param")?; + let mut parser = Expr::new(); + let mut option = Expr::new(); + let mut default = Expr::new(); + let mut help = Expr::new(); + token(meta, "(")?; + let parser_tok = meta.get_current_token(); + syntax(meta, &mut parser)?; + token(meta, ",")?; + let option_tok = meta.get_current_token(); + syntax(meta, &mut option)?; + token(meta, ",")?; + syntax(meta, &mut default)?; + token(meta, ",")?; + let help_tok = meta.get_current_token(); + syntax(meta, &mut help)?; + token(meta, ")")?; + let parser = match ParserImpl::find_parser(meta, &parser) { + Some(parser) => parser, + None => return error!(meta, parser_tok, "Expected parser object"), + }; + let option = match option.get_literal_text() { + Some(option) => option, + None => return error!(meta, option_tok, "Expected literal string"), + }; + let kind = match ParamKind::from(option) { + Some(kind) => kind, + None => return error!(meta, option_tok, "Expected option string"), + }; + let help = match help.get_literal_text() { + Some(help) => help, + None => return error!(meta, help_tok, "Expected literal string"), + }; + let param = ParamImpl::new(kind, default, help); + parser.borrow_mut().add_param(Rc::clone(¶m)); + self.param = Some(param); + Ok(()) + } +} + +impl TranslateModule for ParamCli { + fn translate(&self, meta: &mut TranslateMetadata) -> String { + self.param.as_ref() + .map(|param| param.borrow().default.translate(meta)) + .unwrap_or_default() + } +} + +impl DocumentationModule for ParamCli { + fn document(&self, _meta: &ParserMetadata) -> String { + String::new() + } +} diff --git a/src/modules/builtin/cli/parser.rs b/src/modules/builtin/cli/parser.rs new file mode 100644 index 00000000..91a6cf97 --- /dev/null +++ b/src/modules/builtin/cli/parser.rs @@ -0,0 +1,234 @@ +use crate::docs::module::DocumentationModule; +use crate::modules::builtin::cli::param::{ParamImpl, ParamKind}; +use crate::modules::expression::expr::Expr; +use crate::modules::types::{Type, Typed}; +use crate::translate::module::TranslateModule; +use crate::utils::metadata::{ParserMetadata, TranslateMetadata}; +use crate::utils::payload::Payload; +use heraclitus_compiler::prelude::*; +use std::cell::RefCell; +use std::rc::Rc; + +#[derive(Debug)] +pub struct ParserImpl { + about: String, + params: Vec>>, +} + +impl ParserImpl { + pub fn find_parser(meta: &ParserMetadata, parser: &Expr) -> Option>> { + if let Some(var) = meta.get_var_from_expr(parser) { + if let Some(Payload::Parser(parser)) = &var.payload { + return Some(Rc::clone(parser)); + } + } + None + } + + pub fn add_param(&mut self, param: Rc>) { + self.params.push(param); + } + + pub fn translate(&self, meta: &mut TranslateMetadata, args: &Expr) -> String { + let mut output = Vec::new(); + let indent = TranslateMetadata::single_indent(); + // Run getopt to parse command line + let getopt_id = meta.gen_value_id(); + let getopt_name = format!("__AMBER_GETOPT_{getopt_id}"); + let getopt_cmd = self.create_getopt(meta, args); + output.push(format!("{getopt_name}=$({getopt_cmd}) || exit")); + output.push(format!("eval set -- ${getopt_name}")); + output.push(String::from("while true; do")); + output.push(format!("{indent}case \"$1\" in")); + // Extract optional parameters + for param in &self.params { + let param = param.borrow(); + self.append_optional(&mut output, &indent, meta, args, ¶m); + } + // Stop at "--" or unexpected parameter + output.push(format!("{indent}--)")); + output.push(format!("{indent}{indent}shift")); + output.push(format!("{indent}{indent}break")); + output.push(format!("{indent}{indent};;")); + output.push(format!("{indent}*)")); + output.push(format!("{indent}{indent}exit 1")); + output.push(format!("{indent}{indent};;")); + output.push(format!("{indent}esac")); + output.push(String::from("done")); + // Skip "$0" in remaining parameters + output.push(String::from("shift")); + // Extract positional parameters + for param in &self.params { + let param = param.borrow(); + self.append_positional(&mut output, ¶m); + } + meta.stmt_queue.push_back(output.join("\n")); + String::new() + } + + fn create_getopt(&self, meta: &mut TranslateMetadata, args: &Expr) -> String { + let mut all_shorts = Vec::new(); + let mut all_longs = Vec::new(); + for param in &self.params { + let param = param.borrow(); + if let ParamKind::Optional(shorts, longs, _) = ¶m.kind { + let colon = match param.default.kind { + Type::Bool | Type::Null => "", + _ => ":", + }; + for short in shorts { + all_shorts.push(format!("{short}{colon}")); + } + for long in longs { + all_longs.push(format!("{long}{colon}")); + } + } + } + let shorts = all_shorts.join(""); + let longs = all_longs.join(","); + let args = args.translate(meta); + format!("getopt --options={shorts} --longoptions={longs} -- {args}") + } + + fn append_optional( + &self, + output: &mut Vec, + indent: &str, + meta: &TranslateMetadata, + args: &Expr, + param: &ParamImpl, + ) { + if let ParamKind::Optional(shorts, longs, help) = ¶m.kind { + let option = ParamImpl::describe_optional(shorts, longs); + output.push(format!("{indent}{option})")); + if *help { + let run_name = Self::create_run_name(meta, args); + self.append_help(output, indent, run_name); + } else { + let name = ¶m.name; + match param.default.kind { + Type::Null => { + output.push(format!("{indent}{indent}{name}=1")); + output.push(format!("{indent}{indent}shift")); + } + Type::Bool => { + let value = param.invert_default_bool(); + output.push(format!("{indent}{indent}{name}={value}")); + output.push(format!("{indent}{indent}shift")); + } + Type::Array(_) => { + // Optional array parameters with non-empty default + // values will *extend* not *replace* the default + // values here. We could code for this edge case, + // but I'm not sure it's worth it. + output.push(format!("{indent}{indent}{name}+=(\"$2\")")); + output.push(format!("{indent}{indent}shift")); + output.push(format!("{indent}{indent}shift")); + } + _ => { + output.push(format!("{indent}{indent}{name}=\"$2\"")); + output.push(format!("{indent}{indent}shift")); + output.push(format!("{indent}{indent}shift")); + } + } + } + output.push(format!("{indent}{indent};;")); + } + } + + fn append_positional(&self, output: &mut Vec, param: &ParamImpl) { + if let ParamKind::Positional(_) = ¶m.kind { + let name = ¶m.name; + if let Type::Array(_) = param.default.kind { + output.push(format!("[ -n \"$1\" ] && {name}=(\"$@\")")); + output.push(String::from("set --")); + } else { + output.push(format!("[ -n \"$1\" ] && {name}=\"$1\"")); + output.push(String::from("shift")); + } + } + } + + fn create_run_name(meta: &TranslateMetadata, args: &Expr) -> String { + if let Some(run_name) = &meta.run_name { + run_name.clone() + } else { + let name = args.get_translated_name().unwrap_or_default(); + format!("$(basename ${{{name}[0]}})") + } + } + + fn append_help(&self, output: &mut Vec, indent: &str, run_name: String) { + let params = self.params.iter() + .map(|param| param.borrow().describe_help()) + .collect::>(); + let width = params.iter() + .map(|(x, _)| x.len()) + .max() + .unwrap_or_default(); + output.push(format!("{indent}{indent}cat <>>, +} + +impl ParserCli { + pub fn get_payload(&self) -> Option { + self.parser.as_ref() + .map(|parser| Payload::Parser(Rc::clone(parser))) + } +} + +impl Typed for ParserCli { + fn get_type(&self) -> Type { + Type::Null + } +} + +impl SyntaxModule for ParserCli { + syntax_name!("Parser Invocation"); + + fn new() -> Self { + Self { parser: None } + } + + fn parse(&mut self, meta: &mut ParserMetadata) -> SyntaxResult { + token(meta, "parser")?; + let mut about = Expr::new(); + token(meta, "(")?; + let about_tok = meta.get_current_token(); + syntax(meta, &mut about)?; + token(meta, ")")?; + let about = match about.get_literal_text() { + Some(about) => about, + None => return error!(meta, about_tok, "Expected literal string"), + }; + let parser = ParserImpl { about, params: Vec::new() }; + self.parser = Some(Rc::new(RefCell::new(parser))); + Ok(()) + } +} + +impl TranslateModule for ParserCli { + fn translate(&self, _meta: &mut TranslateMetadata) -> String { + String::new() + } +} + +impl DocumentationModule for ParserCli { + fn document(&self, _meta: &ParserMetadata) -> String { + String::new() + } +} diff --git a/src/modules/builtin/mod.rs b/src/modules/builtin/mod.rs index 4ed3871b..f9700c08 100644 --- a/src/modules/builtin/mod.rs +++ b/src/modules/builtin/mod.rs @@ -1,7 +1,8 @@ pub mod cd; +pub mod cli; pub mod echo; -pub mod mv; -pub mod nameof; pub mod exit; pub mod len; pub mod lines; +pub mod mv; +pub mod nameof; diff --git a/src/modules/expression/expr.rs b/src/modules/expression/expr.rs index fd071418..32ebce82 100644 --- a/src/modules/expression/expr.rs +++ b/src/modules/expression/expr.rs @@ -49,6 +49,10 @@ use crate::modules::function::invocation::FunctionInvocation; use crate::modules::builtin::lines::LinesInvocation; use crate::modules::builtin::nameof::Nameof; use crate::{document_expression, parse_expr, parse_expr_group, translate_expression}; +use crate::modules::builtin::cli::getopt::GetoptCli; +use crate::modules::builtin::cli::param::ParamCli; +use crate::modules::builtin::cli::parser::ParserCli; +use crate::utils::payload::Payload; #[derive(Debug, Clone)] pub enum ExprType { @@ -73,6 +77,9 @@ pub enum ExprType { Neq(Neq), Not(Not), Ternary(Ternary), + ParserCli(ParserCli), + ParamCli(ParamCli), + GetoptCli(GetoptCli), LinesInvocation(LinesInvocation), FunctionInvocation(FunctionInvocation), Command(Command), @@ -101,14 +108,6 @@ impl Typed for Expr { } impl Expr { - pub fn get_integer_value(&self) -> Option { - match &self.value { - Some(ExprType::Number(value)) => value.get_integer_value(), - Some(ExprType::Neg(value)) => value.get_integer_value(), - _ => None, - } - } - pub fn get_position(&self, meta: &mut ParserMetadata) -> PositionInfo { let begin = meta.get_token_at(self.pos.0); let end = meta.get_token_at(self.pos.1); @@ -124,11 +123,43 @@ impl Expr { matches!(self.value, Some(ExprType::VariableGet(_))) } - // Get the variable name if the expression is a variable access - pub fn get_var_translated_name(&self) -> Option { + // Get the translated variable name for Bash output + pub fn get_translated_name(&self) -> Option { match &self.value { Some(ExprType::VariableGet(var)) => Some(var.get_translated_name()), - _ => None + _ => None, + } + } + + // Get the original variable name for variable lookup + pub fn get_original_name(&self) -> Option { + match &self.value { + Some(ExprType::VariableGet(var)) => Some(var.name.clone()), + _ => None, + } + } + + pub fn get_integer_value(&self) -> Option { + match &self.value { + Some(ExprType::Bool(value)) => value.get_integer_value(), + Some(ExprType::Number(value)) => value.get_integer_value(), + Some(ExprType::Neg(value)) => value.get_integer_value(), + _ => None, + } + } + + pub fn get_literal_text(&self) -> Option { + match &self.value { + Some(ExprType::Text(text)) => text.get_literal_text(), + _ => None, + } + } + + pub fn get_payload(&self) -> Option { + match &self.value { + Some(ExprType::ParserCli(parser)) => parser.get_payload(), + Some(ExprType::ParamCli(param)) => param.get_payload(), + _ => None, } } } @@ -160,6 +191,8 @@ impl SyntaxModule for Expr { // Literals Parentheses, Bool, Number, Text, Array, Null, Status, Nameof, + // Command line parser + ParserCli, ParamCli, GetoptCli, // Builtin invocation LinesInvocation, // Function invocation @@ -191,6 +224,8 @@ impl TranslateModule for Expr { // Literals Parentheses, Bool, Number, Text, Array, Null, Status, + // Command line parser + ParserCli, ParamCli, GetoptCli, // Builtin invocation LinesInvocation, // Function invocation @@ -219,6 +254,8 @@ impl DocumentationModule for Expr { // Literals Parentheses, Bool, Number, Text, Array, Null, Status, + // Command line parser + ParserCli, ParamCli, GetoptCli, // Builtin invocation LinesInvocation, // Function invocation diff --git a/src/modules/expression/literal/bool.rs b/src/modules/expression/literal/bool.rs index 97fb48a4..33e60c1d 100644 --- a/src/modules/expression/literal/bool.rs +++ b/src/modules/expression/literal/bool.rs @@ -8,6 +8,13 @@ pub struct Bool { value: bool } +impl Bool { + pub fn get_integer_value(&self) -> Option { + let value = if self.value { 1 } else { 0 }; + Some(value) + } +} + impl Typed for Bool { fn get_type(&self) -> Type { Type::Bool diff --git a/src/modules/expression/literal/number.rs b/src/modules/expression/literal/number.rs index b2deda3f..76e6da4d 100644 --- a/src/modules/expression/literal/number.rs +++ b/src/modules/expression/literal/number.rs @@ -7,6 +7,13 @@ pub struct Number { value: String } +impl Number { + pub fn get_integer_value(&self) -> Option { + let value = self.value.parse().unwrap_or_default(); + Some(value) + } +} + impl Typed for Number { fn get_type(&self) -> Type { Type::Num @@ -46,13 +53,6 @@ impl TranslateModule for Number { } } -impl Number { - pub fn get_integer_value(&self) -> Option { - let value = self.value.parse().unwrap_or_default(); - Some(value) - } -} - impl DocumentationModule for Number { fn document(&self, _meta: &ParserMetadata) -> String { "".to_string() diff --git a/src/modules/expression/literal/text.rs b/src/modules/expression/literal/text.rs index 54537383..735ec7c9 100644 --- a/src/modules/expression/literal/text.rs +++ b/src/modules/expression/literal/text.rs @@ -11,6 +11,16 @@ pub struct Text { interps: Vec, } +impl Text { + pub fn get_literal_text(&self) -> Option { + if self.strings.len() == 1 && self.interps.is_empty() { + self.strings.first().cloned() + } else { + None + } + } +} + impl Typed for Text { fn get_type(&self) -> Type { Type::Text diff --git a/src/modules/expression/macros.rs b/src/modules/expression/macros.rs index 06fdd662..4067f1dd 100644 --- a/src/modules/expression/macros.rs +++ b/src/modules/expression/macros.rs @@ -270,3 +270,11 @@ macro_rules! document_expression { } } } + +#[macro_export] +macro_rules! regex { + ($re:literal $(,)?) => {{ + static RE: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); + RE.get_or_init(|| regex::Regex::new($re).unwrap()) + }}; +} diff --git a/src/modules/expression/unop/neg.rs b/src/modules/expression/unop/neg.rs index 317e64d4..d63ad107 100644 --- a/src/modules/expression/unop/neg.rs +++ b/src/modules/expression/unop/neg.rs @@ -15,6 +15,20 @@ pub struct Neg { expr: Box } +impl Neg { + pub fn get_integer_value(&self) -> Option { + self.expr.get_integer_value().map(isize::neg) + } + + pub fn get_array_index(&self, meta: &mut TranslateMetadata) -> String { + if let Some(expr) = self.get_integer_value() { + expr.to_string() + } else { + self.translate(meta) + } + } +} + impl Typed for Neg { fn get_type(&self) -> Type { Type::Num @@ -57,20 +71,6 @@ impl TranslateModule for Neg { } } -impl Neg { - pub fn get_integer_value(&self) -> Option { - self.expr.get_integer_value().map(isize::neg) - } - - pub fn get_array_index(&self, meta: &mut TranslateMetadata) -> String { - if let Some(expr) = self.get_integer_value() { - expr.to_string() - } else { - self.translate(meta) - } - } -} - impl DocumentationModule for Neg { fn document(&self, _meta: &ParserMetadata) -> String { "".to_string() diff --git a/src/modules/function/invocation.rs b/src/modules/function/invocation.rs index a98f81eb..dc81b2bc 100644 --- a/src/modules/function/invocation.rs +++ b/src/modules/function/invocation.rs @@ -147,7 +147,7 @@ impl TranslateModule for FunctionInvocation { let silent = meta.gen_silent(); let args = izip!(self.args.iter(), self.refs.iter()).map(| (arg, is_ref) | { if *is_ref { - arg.get_var_translated_name().unwrap() + arg.get_translated_name().unwrap() } else { let translation = arg.translate_eval(meta, false); // If the argument is an array, we have to get just the "name[@]" part diff --git a/src/modules/loops/iter_loop.rs b/src/modules/loops/iter_loop.rs index 4fa0a7b2..3e31b370 100644 --- a/src/modules/loops/iter_loop.rs +++ b/src/modules/loops/iter_loop.rs @@ -49,9 +49,9 @@ impl SyntaxModule for IterLoop { token(meta, "{")?; // Create iterator variable meta.with_push_scope(|meta| { - meta.add_var(&self.iter_name, self.iter_type.clone(), false); + meta.add_var(&self.iter_name, self.iter_type.clone(), None, false); if let Some(index) = self.iter_index.as_ref() { - meta.add_var(index, Type::Num, false); + meta.add_var(index, Type::Num, None, false); } // Save loop context state and set it to true meta.with_context_fn(Context::set_is_loop_ctx, true, |meta| { diff --git a/src/modules/main.rs b/src/modules/main.rs index c2b0eac1..20e5650b 100644 --- a/src/modules/main.rs +++ b/src/modules/main.rs @@ -47,7 +47,7 @@ impl SyntaxModule for Main { meta.with_push_scope(|meta| { // Create variables for arg in self.args.iter() { - meta.add_var(arg, Type::Array(Box::new(Type::Text)), true); + meta.add_var(arg, Type::Array(Box::new(Type::Text)), None, true); } // Parse the block syntax(meta, &mut self.block)?; diff --git a/src/modules/variable/init.rs b/src/modules/variable/init.rs index bc377f82..f317e228 100644 --- a/src/modules/variable/init.rs +++ b/src/modules/variable/init.rs @@ -1,10 +1,11 @@ -use heraclitus_compiler::prelude::*; use crate::docs::module::DocumentationModule; -use crate::modules::types::{Typed, Type}; use crate::modules::expression::expr::Expr; +use crate::modules::types::{Type, Typed}; +use crate::modules::variable::handle_identifier_name; +use crate::modules::variable::variable_name_extensions; use crate::translate::module::TranslateModule; use crate::utils::metadata::{ParserMetadata, TranslateMetadata}; -use super::{variable_name_extensions, handle_identifier_name}; +use heraclitus_compiler::prelude::*; #[derive(Debug, Clone)] pub struct VariableInit { @@ -19,10 +20,18 @@ impl VariableInit { fn handle_add_variable( &mut self, meta: &mut ParserMetadata, - tok: Option + tok: Option, ) -> SyntaxResult { handle_identifier_name(meta, &self.name, tok)?; - self.global_id = meta.add_var(&self.name, self.expr.get_type(), self.is_const); + self.global_id = meta.add_var( + &self.name, + self.expr.get_type(), + self.expr.get_payload(), + self.is_const, + ); + if let Some(mut payload) = self.expr.get_payload() { + payload.set_var_name(&self.name, self.global_id); + } Ok(()) } } diff --git a/src/tests/mod.rs b/src/tests/mod.rs index aa2f41af..40e43da9 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -84,10 +84,22 @@ fn extract_output(code: impl Into) -> String { .skip_while(|line| !line.starts_with("// Output")) .skip(1) // skip "// Output" itself .take_while(|line| !line.is_empty() && line.starts_with("//")) - .map(|line| line.trim_start_matches("//").trim()) + .map(|line| trim_comment(line).trim_end()) .join("\n") } +fn trim_comment(line: &str) -> &str { + let mut chars = line.chars(); + if chars.next() == Some('/') && chars.next() == Some('/') { + return if chars.next() == Some(' ') { + &line[3..] + } else { + &line[2..] + } + } + line +} + /// Inner test logic for testing script output in case of success or failure pub fn script_test(input: &str, target: TestOutcomeTarget) { let code = fs::read_to_string(input) @@ -120,18 +132,21 @@ mod test { some header // some comment // Output -// expected -// output - -theres more code +// +//no space +// one space +// two spaces +// three spaces +// + +more code not output // Output // another output is invalid - "# ), - "expected\noutput" + "\nno space\none space\n two spaces\n three spaces\n" ); } } diff --git a/src/tests/validity/cli_parser_prints_help_text.ab b/src/tests/validity/cli_parser_prints_help_text.ab new file mode 100644 index 00000000..6580fe1b --- /dev/null +++ b/src/tests/validity/cli_parser_prints_help_text.ab @@ -0,0 +1,25 @@ +// Output +// Command line parser +// Syntax: script.ab [options] +// TEXT: Text ........... Positional text +// NUMBER: Num .......... Positional number +// ARRAY: [Text] ........ Positional array +// -t|--text: Text ...... Optional text +// -n|--number: Num ..... Optional number +// -f|--flag: Bool ...... Optional flag +// -g|--no-flag: Bool ... Optional invert +// -a|--array: [Num] .... Optional array +// --help ............... Show help text + +let parser = parser("Command line parser") +let positional_text = param(parser, "text", "Hello world", "Positional text") +let positional_number = param(parser, "number", 42, "Positional number") +let positional_array = param(parser, "array", ["foo", "bar", "baz"], "Positional array") +let optional_text = param(parser, "-t|--text", "Lorem ipsum", "Optional text") +let optional_number = param(parser, "-n|--number", 0.001, "Optional number") +let optional_false = param(parser, "-f|--flag", false, "Optional flag") +let optional_true = param(parser, "-g|--no-flag", true, "Optional invert") +let optional_array = param(parser, "-a|--array", [101, 102, 103], "Optional array") + +let args = ["./script.ab", "--help"] +getopt(parser, args) diff --git a/src/tests/validity/cli_parser_sets_default_values.ab b/src/tests/validity/cli_parser_sets_default_values.ab new file mode 100644 index 00000000..ceeada97 --- /dev/null +++ b/src/tests/validity/cli_parser_sets_default_values.ab @@ -0,0 +1,31 @@ +// Output +// Positional text: Hello world +// Positional number: 42 +// Positional array: foo bar baz +// Optional text: Lorem ipsum +// Optional number: 0.001 +// Optional flag: 0 +// Optional invert: 1 +// Optional array: 101 102 103 + +let parser = parser("Command line parser") +let positional_text = param(parser, "text", "Hello world", "Positional text") +let positional_number = param(parser, "number", 42, "Positional number") +let positional_array = param(parser, "array", ["foo", "bar", "baz"], "Positional array") +let optional_text = param(parser, "-t|--text", "Lorem ipsum", "Optional text") +let optional_number = param(parser, "-n|--number", 0.001, "Optional number") +let optional_false = param(parser, "-f|--flag", false, "Optional flag") +let optional_true = param(parser, "-g|--no-flag", true, "Optional invert") +let optional_array = param(parser, "-a|--array", [101, 102, 103], "Optional array") + +let args = ["./script.ab"] +getopt(parser, args) + +echo "Positional text: {positional_text}" +echo "Positional number: {positional_number}" +echo "Positional array: {positional_array}" +echo "Optional text: {optional_text}" +echo "Optional number: {optional_number}" +echo "Optional flag: {optional_false}" +echo "Optional invert: {optional_true}" +echo "Optional array: {optional_array}" diff --git a/src/tests/validity/cli_parser_sets_long_options.ab b/src/tests/validity/cli_parser_sets_long_options.ab new file mode 100644 index 00000000..b7b59090 --- /dev/null +++ b/src/tests/validity/cli_parser_sets_long_options.ab @@ -0,0 +1,44 @@ +// Output +// Positional text: Goodbye world +// Positional number: 999 +// Positional array: qux quux +// Optional text: dolor sit +// Optional number: -12.345 +// Optional flag: 1 +// Optional invert: 0 +// Optional array: 101 102 103 104 105 106 + +let parser = parser("Command line parser") +let positional_text = param(parser, "text", "Hello world", "Positional text") +let positional_number = param(parser, "number", 42, "Positional number") +let positional_array = param(parser, "array", ["foo", "bar", "baz"], "Positional array") +let optional_text = param(parser, "-t|--text", "Lorem ipsum", "Optional text") +let optional_number = param(parser, "-n|--number", 0.001, "Optional number") +let optional_false = param(parser, "-f|--flag", false, "Optional flag") +let optional_true = param(parser, "-g|--no-flag", true, "Optional invert") +let optional_array = param(parser, "-a|--array", [101, 102, 103], "Optional array") + +let args = [ + "./script.ab", + "Goodbye world", + "--text", "dolor sit", + "--number", "-12.345", + "--flag", + "--no-flag", + "--array", "104", + "--array", "105", + "--array", "106", + "999", + "qux", + "quux", +] +getopt(parser, args) + +echo "Positional text: {positional_text}" +echo "Positional number: {positional_number}" +echo "Positional array: {positional_array}" +echo "Optional text: {optional_text}" +echo "Optional number: {optional_number}" +echo "Optional flag: {optional_false}" +echo "Optional invert: {optional_true}" +echo "Optional array: {optional_array}" diff --git a/src/tests/validity/cli_parser_sets_short_options.ab b/src/tests/validity/cli_parser_sets_short_options.ab new file mode 100644 index 00000000..eda37649 --- /dev/null +++ b/src/tests/validity/cli_parser_sets_short_options.ab @@ -0,0 +1,43 @@ +// Output +// Positional text: Goodbye world +// Positional number: 999 +// Positional array: qux quux +// Optional text: dolor sit +// Optional number: -12.345 +// Optional flag: 1 +// Optional invert: 0 +// Optional array: 101 102 103 104 105 106 + +let parser = parser("Command line parser") +let positional_text = param(parser, "text", "Hello world", "Positional text") +let positional_number = param(parser, "number", 42, "Positional number") +let positional_array = param(parser, "array", ["foo", "bar", "baz"], "Positional array") +let optional_text = param(parser, "-t|--text", "Lorem ipsum", "Optional text") +let optional_number = param(parser, "-n|--number", 0.001, "Optional number") +let optional_false = param(parser, "-f|--flag", false, "Optional flag") +let optional_true = param(parser, "-g|--no-flag", true, "Optional invert") +let optional_array = param(parser, "-a|--array", [101, 102, 103], "Optional array") + +let args = [ + "./script.ab", + "Goodbye world", + "-t", "dolor sit", + "-n-12.345", + "-fg", + "-a104", + "-a105", + "-a106", + "999", + "qux", + "quux", +] +getopt(parser, args) + +echo "Positional text: {positional_text}" +echo "Positional number: {positional_number}" +echo "Positional array: {positional_array}" +echo "Optional text: {optional_text}" +echo "Optional number: {optional_number}" +echo "Optional flag: {optional_false}" +echo "Optional invert: {optional_true}" +echo "Optional array: {optional_array}" diff --git a/src/utils/context.rs b/src/utils/context.rs index 071472d1..751da5bb 100644 --- a/src/utils/context.rs +++ b/src/utils/context.rs @@ -1,6 +1,7 @@ use super::{cc_flags::CCFlags, function_interface::FunctionInterface}; use crate::modules::expression::expr::Expr; use crate::modules::types::Type; +use crate::utils::payload::Payload; use amber_meta::ContextHelper; use heraclitus_compiler::prelude::*; use std::collections::{HashMap, HashSet}; @@ -39,6 +40,7 @@ impl FunctionDecl { pub struct VariableDecl { pub name: String, pub kind: Type, + pub payload: Option, pub global_id: Option, pub is_ref: bool, pub is_const: bool, diff --git a/src/utils/metadata/parser.rs b/src/utils/metadata/parser.rs index e5665da4..1fc25d59 100644 --- a/src/utils/metadata/parser.rs +++ b/src/utils/metadata/parser.rs @@ -3,11 +3,13 @@ use std::collections::BTreeSet; use heraclitus_compiler::prelude::*; use amber_meta::ContextManager; use crate::modules::block::Block; +use crate::modules::expression::expr::Expr; use crate::modules::types::Type; use crate::utils::context::{Context, ScopeUnit, VariableDecl, FunctionDecl}; use crate::utils::function_interface::FunctionInterface; use crate::utils::import_cache::ImportCache; use crate::utils::function_cache::FunctionCache; +use crate::utils::payload::Payload; #[derive(Debug, ContextManager)] pub struct ParserMetadata { @@ -68,16 +70,24 @@ impl ParserMetadata { } /// Adds a variable to the current scope - pub fn add_var(&mut self, name: &str, kind: Type, is_const: bool) -> Option { + pub fn add_var( + &mut self, + name: &str, + kind: Type, + payload: Option, + is_const: bool, + ) -> Option { let global_id = (self.is_global_scope() || self.is_shadowing_prev_scope(name)).then(|| self.gen_var_id()); let scope = self.context.scopes.last_mut().unwrap(); - scope.add_var(VariableDecl { + let var = VariableDecl { name: name.to_string(), kind, + payload, global_id, is_ref: false, is_const, - }); + }; + scope.add_var(var); global_id } @@ -85,13 +95,15 @@ impl ParserMetadata { pub fn add_param(&mut self, name: &str, kind: Type, is_ref: bool) -> Option { let global_id = self.is_global_scope().then(|| self.gen_var_id()); let scope = self.context.scopes.last_mut().unwrap(); - scope.add_var(VariableDecl { + let var = VariableDecl { name: name.to_string(), kind, + payload: None, global_id, is_ref, is_const: false, - }); + }; + scope.add_var(var); global_id } @@ -112,6 +124,11 @@ impl ParserMetadata { self.context.scopes.iter().rev().find_map(|scope| scope.get_var(name)) } + /// Gets a variable from the current scope or any parent scope + pub fn get_var_from_expr(&self, expr: &Expr) -> Option<&VariableDecl> { + expr.get_original_name().and_then(|name| self.get_var(&name)) + } + /// Gets variable names pub fn get_var_names(&self) -> BTreeSet<&String> { self.context.scopes.iter().rev().flat_map(|scope| scope.get_var_names()).collect() diff --git a/src/utils/metadata/translate.rs b/src/utils/metadata/translate.rs index a53c6531..35372196 100644 --- a/src/utils/metadata/translate.rs +++ b/src/utils/metadata/translate.rs @@ -28,7 +28,9 @@ pub struct TranslateMetadata { /// The current indentation level. pub indent: i64, /// Determines if minify flag was set. - pub minify: bool + pub minify: bool, + /// Amber script name if running not building. + pub run_name: Option, } impl TranslateMetadata { @@ -43,6 +45,7 @@ impl TranslateMetadata { silenced: false, indent: -1, minify: options.minify, + run_name: options.run_name.clone(), } } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 93dd5dc1..54c9948a 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -5,5 +5,6 @@ pub mod function_interface; pub mod function_metadata; pub mod import_cache; pub mod metadata; +pub mod payload; pub use metadata::*; diff --git a/src/utils/payload.rs b/src/utils/payload.rs new file mode 100644 index 00000000..37ea35a1 --- /dev/null +++ b/src/utils/payload.rs @@ -0,0 +1,18 @@ +use crate::modules::builtin::cli::param::ParamImpl; +use crate::modules::builtin::cli::parser::ParserImpl; +use std::cell::RefCell; +use std::rc::Rc; + +#[derive(Clone, Debug)] +pub enum Payload { + Parser(Rc>), + Param(Rc>), +} + +impl Payload { + pub fn set_var_name(&mut self, name: &str, id: Option) { + if let Payload::Param(param) = self { + param.borrow_mut().set_var_name(name, id) + } + } +}