diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/args.rs | 26 | ||||
-rw-r--r-- | src/main.rs | 35 | ||||
-rw-r--r-- | src/makefile/mod.rs | 444 | ||||
-rw-r--r-- | src/makefile/token.rs | 236 |
4 files changed, 726 insertions, 15 deletions
diff --git a/src/args.rs b/src/args.rs index 1fe6731..963c01e 100644 --- a/src/args.rs +++ b/src/args.rs @@ -4,13 +4,13 @@ use std::path::PathBuf; use structopt::StructOpt; -#[derive(StructOpt, Debug, PartialEq, Eq)] +#[derive(StructOpt, Debug, PartialEq, Eq, Clone)] #[structopt(author, about)] pub struct Args { /// Cause environment variables, including those with null values, to override macro /// assignments within makefiles. #[structopt(short, long)] - environment_overrides: bool, + pub environment_overrides: bool, /// Specify a different makefile (or '-' for standard input). /// @@ -20,20 +20,20 @@ pub struct Args { /// specified. The effect of specifying the same option-argument more than once is /// unspecified. #[structopt(short = "f", long = "file", visible_alias = "makefile", number_of_values = 1, parse(from_os_str))] - makefile: Vec<PathBuf>, + pub makefile: Vec<PathBuf>, /// Ignore error codes returned by invoked commands. /// /// This mode is the same as if the special target .IGNORE were specified without /// prerequisites. #[structopt(short, long)] - ignore_errors: bool, + pub ignore_errors: bool, /// Continue to update other targets that do not depend on the current target if a /// non-ignored error occurs while executing the commands to bring a target /// up-to-date. #[structopt(short, long)] - keep_going: bool, + pub keep_going: bool, /// Write commands that would be executed on standard output, but do not execute them /// (but execute lines starting with '+'). @@ -42,14 +42,14 @@ pub struct Args { /// lines with an at-sign ( '@' ) character prefix shall be written to standard /// output. #[structopt(short = "n", long, visible_alias = "just-print", visible_alias = "recon")] - dry_run: bool, + pub dry_run: bool, /// Write to standard output the complete set of macro definitions and target /// descriptions. /// /// The output format is unspecified. #[structopt(short, long, visible_alias = "print-data-base")] - print_everything: bool, + pub print_everything: bool, /// Return a zero exit value if the target file is up-to-date; otherwise, return an /// exit value of 1. @@ -58,11 +58,11 @@ pub struct Args { /// command line (associated with the targets) with a <plus-sign> ( '+' ) prefix /// shall be executed. #[structopt(short, long)] - question: bool, + pub question: bool, /// Clear the suffix list and do not use the built-in rules. #[structopt(short = "r", long)] - no_builtin_rules: bool, + pub no_builtin_rules: bool, /// Terminate make if an error occurs while executing the commands to bring a target /// up-to-date (default behavior, required by POSIX to be also a flag for some @@ -70,7 +70,7 @@ pub struct Args { /// /// This shall be the default and the opposite of -k. #[structopt(short = "S", long, visible_alias = "stop", hidden = true, overrides_with="keep-going")] - no_keep_going: bool, + pub no_keep_going: bool, /// Do not write makefile command lines or touch messages to standard output before /// executing. @@ -78,7 +78,7 @@ pub struct Args { /// This mode shall be the same as if the special target .SILENT were specified /// without prerequisites. #[structopt(short, long, visible_alias = "quiet")] - silent: bool, + pub silent: bool, /// Update the modification time of each target as though a touch target had been /// executed. @@ -89,14 +89,14 @@ pub struct Args { /// the makefile command lines associated with each target are not executed. However, /// a command line with a <plus-sign> ( '+' ) prefix shall be executed. #[structopt(short, long)] - touch: bool, + pub touch: bool, /// Target names or macro definitions. /// /// If no target is specified, while make is processing the makefiles, the first /// target that make encounters that is not a special target or an inference rule /// shall be used. - targets_or_macros: Vec<String>, + pub targets_or_macros: Vec<String>, } impl Args { diff --git a/src/main.rs b/src/main.rs index 2316546..4048886 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,40 @@ +use std::fs::metadata; +use std::io::stdin; +use std::path::PathBuf; mod args; +mod makefile; use args::Args; +use makefile::Makefile; fn main() { - let args = Args::from_env_and_args(); - dbg!(args); + let mut args = Args::from_env_and_args(); + // If no makefile is specified, try some options. + if args.makefile.is_empty() { + if metadata("./makefile").is_ok() { + args.makefile = vec!["./makefile".into()]; + } else if metadata("./Makefile").is_ok() { + args.makefile = vec!["./Makefile".into()]; + } else { + // TODO handle error gracefully + panic!("no makefile found"); + } + } + // Read in the makefile(s) specified. + // TODO dump command-line args into MAKEFLAGS + // TODO dump command-line macros into environment + // TODO add SHELL macro + let mut makefile = Makefile::new(args.clone()); + if !args.no_builtin_rules { + makefile.add_builtins(); + } + makefile.add_env(); + for filename in &args.makefile { + if filename == &PathBuf::from("-") { + makefile.and_read(stdin().lock()); + } else { + makefile.and_read_file(filename); + }; + } } diff --git a/src/makefile/mod.rs b/src/makefile/mod.rs new file mode 100644 index 0000000..75873d2 --- /dev/null +++ b/src/makefile/mod.rs @@ -0,0 +1,444 @@ +use std::collections::HashMap; +use std::env; +use std::fs::{File, metadata}; +use std::io::{BufRead, BufReader}; +use std::path::Path; +use std::time::SystemTime; + +use lazy_static::lazy_static; +use regex::Regex; + +use crate::args::Args; + +mod token; + +use token::{tokenize, Token, TokenString}; + +pub enum RuleType { + Inference, + Target, +} + +#[derive(PartialEq, Eq, Clone)] +pub struct Rule { + name: String, + prerequisites: Vec<String>, + commands: Vec<CommandLine>, +} + +impl Rule { + pub fn r#type(&self) -> RuleType { + if self.name.contains(".") && !self.name.contains("/") { + RuleType::Inference + } else { + RuleType::Target + } + } + + fn execute_commands(&self, file: &Makefile, target: &Target) { + for command in &self.commands { + command.execute(file, target); + } + } +} + +#[derive(PartialEq, Eq, Clone)] +pub struct Target { + name: String, + prerequisites: Vec<String>, + rule: Option<Rule>, + already_updated: bool, +} + +impl Target { + fn modified_time(&self) -> Option<SystemTime> { + metadata(&self.name) + .and_then(|metadata| metadata.modified()) + .ok() + } + + fn newer_than(&self, other: &Target) -> Option<bool> { + Some(match (self.modified_time(), other.modified_time()) { + (Some(self_mtime), Some(other_mtime)) => self_mtime >= other_mtime, + // per POSIX: "If the target does not exist after the target has been + // successfully made up-to-date, the target shall be treated as being + // newer than any target for which it is a prerequisite." + (None, _) if self.already_updated && other.prerequisites.contains(&self.name) => true, + (_, None) if other.already_updated && self.prerequisites.contains(&other.name) => false, + _ => return None, + }) + } + + fn is_up_to_date(&self, file: &mut Makefile) -> bool { + if self.already_updated { + return true; + } + let exists = metadata(&self.name).is_ok(); + if exists && self.rule.is_none() { + return true; + } + let newer_than_all_dependencies = self.prerequisites + .iter() + .all(|t| self.newer_than(&file.get_target(t)).unwrap_or(false)); + if exists && newer_than_all_dependencies { + return true; + } + false + } + + fn update(&mut self, file: &mut Makefile) { + for prereq in &self.prerequisites { + file.update_target(prereq); + } + if !self.is_up_to_date(file) { + match &self.rule { + Some(rule) => rule.execute_commands(file, self), + None => panic!("target doesn't exist & no rule to make it"), // TODO handle this error well + } + } + self.already_updated = true; + } +} + +#[derive(PartialEq, Eq, Clone)] +pub struct CommandLine { + /// If the command prefix contains a <hyphen-minus>, or the -i option is present, or + /// the special target .IGNORE has either the current target as a prerequisite or has + /// no prerequisites, any error found while executing the command shall be ignored. + ignore_errors: bool, + /// If the command prefix contains an at-sign and the make utility command line -n + /// option is not specified, or the -s option is present, or the special target + /// .SILENT has either the current target as a prerequisite or has no prerequisites, + /// the command shall not be written to standard output before it is executed. + silent: bool, + /// If the command prefix contains a <plus-sign>, this indicates a makefile command + /// line that shall be executed even if -n, -q, or -t is specified. + always_execute: bool, + execution_line: TokenString, +} + +impl CommandLine { + fn from(mut line: TokenString) -> Self { + let mut ignore_errors = false; + let mut silent = false; + let mut always_execute = false; + + if let Token::Text(text) = line.first_token_mut() { + let mut text_chars = text.chars().peekable(); + loop { + match text_chars.peek() { + Some('-') | Some('@') | Some('+') => match text_chars.next() { + Some('-') => ignore_errors = true, + Some('@') => silent = true, + Some('+') => always_execute = true, + _ => unreachable!() + }, + _ => break, + } + } + *text = text_chars.collect(); + } + + CommandLine { + ignore_errors, + silent, + always_execute, + execution_line: line, + } + } + + fn execute(&self, file: &Makefile, target: &Target) { + let avoid_execution = file.args.dry_run || file.args.question || file.args.touch; + if avoid_execution && !self.always_execute { + return; + } + + let execution_line = file.expand_macros(&self.execution_line); + + let self_silent = self.silent && !file.args.dry_run; + let special_target_silent = file.rules.get(".SILENT") + .map_or(false, |silent_target| { + silent_target.prerequisites.is_empty() || silent_target.prerequisites.contains(&target.name) + }); + let silent = self_silent || file.args.silent || special_target_silent; + if !silent { + println!("{}", execution_line); + } + + let special_target_ignore = file.rules.get(".IGNORE") + .map_or(false, |ignore_target| { + ignore_target.prerequisites.is_empty() || ignore_target.prerequisites.contains(&target.name) + }); + let ignore_error = self.ignore_errors || file.args.ignore_errors || special_target_ignore; + + // TODO don't fuck this up + let execution_line = ::std::ffi::CString::new(execution_line.as_bytes()) + .expect("execution line shouldn't have a null in the middle"); + // TODO pass shell "-e" if errors are not ignored + let return_value = unsafe { libc::system(execution_line.as_ptr()) }; + if return_value != 0 { + // apparently there was an error. do we care? + if !ignore_error { + // TODO handle this error gracefully + panic!("error from command execution!"); + } + } + } +} + +enum MacroSource { + File, + CommandLineOrMAKEFLAGS, + Environment, + Builtin, +} + +pub struct Makefile { + rules: HashMap<String, Rule>, + macros: HashMap<String, (MacroSource, TokenString)>, + targets: HashMap<String, Target>, + args: Args, +} + +impl Makefile { + pub fn new(args: Args) -> Makefile { + Makefile { + rules: HashMap::new(), + macros: HashMap::new(), + targets: HashMap::new(), + args, + } + } + + pub fn add_builtins(&mut self) -> &mut Makefile { + self.rules.extend(BUILTIN_RULES.iter().map(|(name, rule)| (name.to_string(), rule.clone()))); + self + } + + pub fn add_env(&mut self) -> &mut Makefile { + self.macros.extend(env::vars() + .filter_map(|(name, value)| { + if name == "MAKEFLAGS" || name == "SHELL" { + None + } else { + Some((name, (MacroSource::Environment, TokenString::from(vec![Token::Text(value)])))) + } + }) + ); + self + } + + pub fn and_read_file(&mut self, path: impl AsRef<Path>) -> &mut Makefile { + let file = File::open(path); + // TODO handle errors + let file = file.expect("couldn't open makefile!"); + let file_reader = BufReader::new(file); + self.and_read(file_reader) + } + + pub fn and_read(&mut self, source: impl BufRead) -> &mut Makefile { + let mut lines_iter = source.lines().peekable(); + while lines_iter.peek().is_some() { + let line = match lines_iter.next() { + Some(x) => x, + // fancy Rust trick: break-with-an-argument to return a value from a + // `loop` expression + None => break, + }; + // TODO handle I/O errors at all + let mut line = line.expect("failed to read line of makefile!"); + + // handle escaped newlines (TODO exception for command lines) + while line.ends_with(r"\") { + let next_line = match lines_iter.next() { + Some(x) => x, + None => Ok("".into()), + }; + let next_line = next_line.expect("failed to read line of makefile!"); + let next_line = next_line.trim_start(); + line.push(' '); + line.push_str(next_line); + } + // handle comments + lazy_static! { + static ref COMMENT: Regex = Regex::new("#.*$").unwrap(); + } + let line = COMMENT.replace(&line, "").into_owned(); + + // handle include lines + if let Some(line) = line.strip_prefix("include ") { + // remove extra leading space + let line = line.trim_start(); + let line = self.expand_macros(&tokenize(line)); + let fields = line.split_whitespace(); + // POSIX says we only have to handle a single filename, but GNU make + // handles arbitrarily many filenames, and it's not like that's more work + // TODO have some way of linting for non-portable constructs + for field in fields { + self.and_read_file(field); + } + } else if line.trim().is_empty() { + // handle blank lines + continue; + } else { + // unfortunately, rules vs macros can't be determined until after + // macro tokenizing. so that's suboptimal. + // TODO errors + let line_tokens: TokenString = line.parse().unwrap(); + + enum LineType { + Rule, + Macro, + Unknown, + } + + fn get_line_type(line_tokens: &TokenString) -> LineType { + for token in line_tokens.tokens() { + if let Token::Text(text) = token { + let colon_idx = text.find(":"); + let equals_idx = text.find("="); + match (colon_idx, equals_idx) { + (Some(_), None) => { + return LineType::Rule; + } + (Some(c), Some(e)) if c < e => { + return LineType::Rule; + } + (None, Some(_)) => { + return LineType::Macro; + } + (Some(c), Some(e)) if e < c => { + return LineType::Macro; + } + _ => {} + } + } + } + LineType::Unknown + } + + let line_type = get_line_type(&line_tokens); + + match line_type { + LineType::Rule => { + let (targets, not_targets) = line_tokens.split_once(':').unwrap(); + let targets = self.expand_macros(&targets); + let targets = targets.split_whitespace().map(|x| x.into()).collect::<Vec<String>>(); + let (prerequisites, mut commands) = match not_targets.split_once(';') { + Some((prerequisites, commands)) => (prerequisites, vec![commands]), + None => (not_targets, vec![]), + }; + let prerequisites = self.expand_macros(&prerequisites); + let prerequisites = prerequisites.split_whitespace().map(|x| x.into()).collect::<Vec<String>>(); + + while lines_iter.peek().and_then(|x| x.as_ref().ok()).map_or(false, |line| line.starts_with('\t')) { + let line = lines_iter.next().unwrap().unwrap(); + let line = line.strip_prefix("\t").unwrap(); + commands.push(line.parse().unwrap()); + } + + let commands = commands.into_iter() + .map(CommandLine::from) + .collect::<Vec<_>>(); + + for target in targets { + match self.rules.get_mut(&target) { + Some(old_rule) if commands.is_empty() => { + old_rule.prerequisites.extend(prerequisites.clone()); + } + _ => { + self.rules.insert(target.clone(), Rule { + name: target, + prerequisites: prerequisites.clone(), + commands: commands.clone(), + }); + } + } + } + }, + LineType::Macro => { + let (name, value) = line_tokens.split_once('=').unwrap(); + let name = self.expand_macros(&name); + match self.macros.get(&name) { + // We always let command line or MAKEFLAGS macros override macros from the file. + Some((MacroSource::CommandLineOrMAKEFLAGS, _)) => continue, + // We let environment variables override macros from the file only if the command-line argument to do that was given + Some((MacroSource::Environment, _)) if self.args.environment_overrides => continue, + _ => {} + } + self.macros.insert(name, (MacroSource::File, value)); + } + LineType::Unknown => { + panic!("Unknown line {:?}", line_tokens); + } + } + } + } + self + } + + fn get_target(&mut self, name: impl Into<String>) -> &mut Target { + let name = name.into(); + { + let rules_get_name = self.rules.get(&name); + let make_target = || { + if let Some(target_rule) = rules_get_name { + return Target { + name: name.clone(), + prerequisites: target_rule.prerequisites.clone(), + already_updated: false, + rule: Some(target_rule.clone()) + }; + } + panic!("uhhhhh i don't even know anymore bro"); + }; + self.targets + .entry(name.clone()) + .or_insert_with(make_target); + } + self.targets.get_mut(&name).unwrap() + } + + fn update_target(&mut self, name: impl Into<String>) { + // This is the dumbest fucking thing I've ever had to do. + // We can't leave it in the map, because then we have overlapping mutable borrows of self, + // so we have to remove it from the map, do the work, and then re-insert it into the map. + // Fuck this so much. + // Why the goddamn hell do I even write Rust. + let name = name.into(); + { + let _ = self.get_target(name.clone()); + } + let mut target = self.targets.remove(&name).unwrap(); + target.update(self); + self.targets.insert(name.clone(), target); + } + + fn expand_macros(&self, text: &TokenString) -> String { + let mut result = String::new(); + for token in text.tokens() { + match token { + Token::Text(t) => result.push_str(t), + Token::MacroExpansion { name, replacement } => { + let (_, macro_value) = &self.macros[name]; + let macro_value = self.expand_macros(macro_value); + let macro_value = match replacement { + Some((subst1, subst2)) => { + let subst1 = self.expand_macros(subst1); + let subst1_suffix = regex::escape(&subst1); + let subst1_suffix = Regex::new(&format!(r"{}\b", subst1_suffix)).unwrap(); + let subst2 = self.expand_macros(subst2); + subst1_suffix.replace_all(¯o_value, subst2).to_string() + }, + None => macro_value, + }; + result.push_str(¯o_value); + } + } + } + return result; + } +} + +const BUILTIN_RULES: &'static [(&'static str, Rule)] = &[]; +const BUILTIN_SUFFIX_LIST: &'static [&'static str] = &[]; diff --git a/src/makefile/token.rs b/src/makefile/token.rs new file mode 100644 index 0000000..3f69b50 --- /dev/null +++ b/src/makefile/token.rs @@ -0,0 +1,236 @@ +use std::str::FromStr; + +use nom::{ + Finish, IResult, + branch::alt, + bytes::complete::{tag, take_till1, take_while1}, + character::complete::anychar, + combinator::{all_consuming, map, opt, verify}, + multi::many1, + sequence::{delimited, pair, preceded, separated_pair}, +}; + +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct TokenString(Vec<Token>); + +impl TokenString { + pub fn tokens(&self) -> impl Iterator<Item=&Token> { + self.0.iter() + } + + pub fn first_token_mut(&mut self) -> &mut Token { + &mut self.0[0] + } + + pub fn split_once(&self, delimiter: char) -> Option<(TokenString, TokenString)> { + let mut result0 = vec![]; + let mut iter = self.0.iter(); + while let Some(t) = iter.next() { + match t { + Token::Text(text) if text.contains(delimiter) => { + let split_text = text.splitn(2, delimiter); + let pieces = split_text.collect::<Vec<_>>(); + assert_eq!(pieces.len(), 2, "wrong number of pieces!"); + result0.push(Token::Text(pieces[0].into())); + let mut result1 = vec![Token::Text(pieces[1].into())]; + result1.extend(iter.cloned()); + return Some((TokenString(result0), TokenString(result1))); + } + _ => result0.push(t.clone()), + } + } + None + } +} + +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum Token { + Text(String), + MacroExpansion { + name: String, + replacement: Option<(TokenString, TokenString)>, + }, +} + +fn macro_name(input: &str) -> IResult<&str, &str> { + // POSIX says "periods, underscores, digits, and alphabetics from the portable character set" + take_while1(|c: char| { + c == '.' || c == '_' || c.is_alphanumeric() + })(input) +} + +fn macro_expansion_body<'a>(end: char) -> impl FnMut(&'a str) -> IResult<&'a str, Token> { + let subst = preceded(tag(":"), separated_pair(tokens_but_not('='), tag("="), tokens_but_not(end))); + map( + pair(macro_name, opt(subst)), + |(name, replacement)| Token::MacroExpansion { name: name.into(), replacement }, + ) +} + +fn parens_macro_expansion(input: &str) -> IResult<&str, Token> { + delimited(tag("$("), macro_expansion_body(')'), tag(")"))(input) +} + +fn braces_macro_expansion(input: &str) -> IResult<&str, Token> { + delimited(tag("${"), macro_expansion_body('}'), tag("}"))(input) +} + +fn tiny_macro_expansion(input: &str) -> IResult<&str, Token> { + let raw = preceded(tag("$"), verify(anychar, |&c| c != '(' && c != '{')); + map(raw, |c| { + if c == '$' { + Token::Text("$".into()) + } else { + Token::MacroExpansion { + name: c.to_string(), + replacement: None, + } + } + })(input) +} + +fn macro_expansion(input: &str) -> IResult<&str, Token> { + alt((tiny_macro_expansion, parens_macro_expansion, braces_macro_expansion))(input) +} + +fn text(input: &str) -> IResult<&str, Token> { + map(take_till1(|c| c == '$'), |x: &str| Token::Text(x.into()))(input) +} + +fn text_but_not<'a>(end: char) -> impl FnMut(&'a str) -> IResult<&'a str, Token> { + map(take_till1(move |c| c == '$' || c == end), |x: &str| Token::Text(x.into())) +} + +fn single_token(input: &str) -> IResult<&str, Token> { + alt((text, macro_expansion))(input) +} + +fn single_token_but_not<'a>(end: char) -> impl FnMut(&'a str) -> IResult<&'a str, Token> { + alt((text_but_not(end), macro_expansion)) +} + +fn empty_tokens(input: &str) -> IResult<&str, TokenString> { + map(tag(""), |_| TokenString(vec![Token::Text(String::new())]))(input) +} + +fn tokens(input: &str) -> IResult<&str, TokenString> { + alt((map(many1(single_token), TokenString), empty_tokens))(input) +} + +fn tokens_but_not<'a>(end: char) -> impl FnMut(&'a str) -> IResult<&'a str, TokenString> { + alt((map(many1(single_token_but_not(end)), TokenString), empty_tokens)) +} + +fn full_text_tokens(input: &str) -> IResult<&str, TokenString> { + all_consuming(tokens)(input) +} + +pub fn tokenize(input: &str) -> TokenString { + // TODO handle errors gracefully + let (_, result) = full_text_tokens(input).expect("couldn't parse"); + result +} + +impl FromStr for TokenString { + // TODO figure out how to get nom errors working (Error<&str> doesn't work because lifetimes) + type Err = (); + + fn from_str(s: &str) -> Result<Self, Self::Err> { + full_text_tokens(s).finish() + .map(|(_, x)| x) + .map_err(|_| ()) + } +} + +#[cfg(test)] +mod test { + use super::{Token, TokenString, tokenize}; + + impl From<Vec<Token>> for TokenString { + fn from(x: Vec<Token>) -> Self { + TokenString(x) + } + } + + fn token_text(text: impl Into<String>) -> Token { + Token::Text(text.into()) + } + + fn token_macro_expansion(name: impl Into<String>) -> Token { + Token::MacroExpansion { name: name.into(), replacement: None } + } + + fn token_macro_expansion_replacement(name: impl Into<String>, + subst1: impl Into<TokenString>, + subst2: impl Into<TokenString>) -> Token { + Token::MacroExpansion { name: name.into(), replacement: Some((subst1.into(), subst2.into())) } + } + + #[test] + fn no_macros() { + let text = "This is an example sentence! There aren't macros in it at all!"; + let tokens = tokenize(text); + assert_eq!(tokens, TokenString(vec![token_text(text)])); + } + + #[test] + fn no_replacement() { + let text = "This is a $Q sentence! There are $(BORING) macros in it at ${YEET}!"; + let tokens = tokenize(text); + assert_eq!(tokens, TokenString(vec![ + token_text("This is a "), + token_macro_expansion("Q"), + token_text(" sentence! There are "), + token_macro_expansion("BORING"), + token_text(" macros in it at "), + token_macro_expansion("YEET"), + token_text("!"), + ])); + } + + #[test] + fn escaped() { + let text = "This costs $$2 to run, which isn't ideal"; + let tokens = tokenize(text); + assert_eq!(tokens, TokenString(vec![ + token_text("This costs "), + token_text("$"), + token_text("2 to run, which isn't ideal"), + ])); + } + + #[test] + fn replacement() { + let text = "Can I get a $(DATA:.c=.oof) in this ${SWAG:.yolo=}"; + let tokens = tokenize(text); + assert_eq!(tokens, TokenString(vec![ + token_text("Can I get a "), + token_macro_expansion_replacement("DATA", vec![token_text(".c")], vec![token_text(".oof")]), + token_text(" in this "), + token_macro_expansion_replacement("SWAG", vec![token_text(".yolo")], vec![token_text("")]), + ])); + } + + #[test] + fn hell() { + let text = "$(OOF:${ouch:hi=hey} there=$(owie:$(my)=${bones})), bro."; + let tokens = tokenize(text); + assert_eq!(tokens, TokenString(vec![ + token_macro_expansion_replacement( + "OOF", + vec![ + token_macro_expansion_replacement("ouch", vec![token_text("hi")], vec![token_text("hey")]), + token_text(" there"), + ], + vec![ + token_macro_expansion_replacement( + "owie", + vec![token_macro_expansion("my")], + vec![token_macro_expansion("bones")], + ), + ], + ), + token_text(", bro."), + ])); + } +} |