From 45fa49be5dc2630187e78c8adec498b751d9481e Mon Sep 17 00:00:00 2001 From: Melody Horn Date: Fri, 26 Mar 2021 19:43:40 -0600 Subject: overhaul target handling --- src/main.rs | 18 +- src/makefile/mod.rs | 555 ++++++++++++++++++++++++++++++++++++++------------ src/makefile/token.rs | 49 +++++ 3 files changed, 487 insertions(+), 135 deletions(-) diff --git a/src/main.rs b/src/main.rs index 4048886..2764e97 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,10 +26,6 @@ fn main() { // 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()); @@ -37,4 +33,18 @@ fn main() { makefile.and_read_file(filename); }; } + + if args.print_everything { + println!("{}", &makefile); + } + + let targets = if args.targets().count() == 0 { + vec![makefile.first_non_special_target.clone().expect("couldn't find a target!")] + } else { + args.targets().cloned().collect() + }; + + for target in targets { + makefile.update_target(&target); + } } diff --git a/src/makefile/mod.rs b/src/makefile/mod.rs index 75873d2..e8adac9 100644 --- a/src/makefile/mod.rs +++ b/src/makefile/mod.rs @@ -1,8 +1,11 @@ +use std::cell::{Cell, RefCell}; use std::collections::HashMap; use std::env; +use std::fmt; use std::fs::{File, metadata}; use std::io::{BufRead, BufReader}; use std::path::Path; +use std::rc::Rc; use std::time::SystemTime; use lazy_static::lazy_static; @@ -14,31 +17,22 @@ mod token; use token::{tokenize, Token, TokenString}; -pub enum RuleType { - Inference, - Target, -} - #[derive(PartialEq, Eq, Clone)] -pub struct Rule { - name: String, - prerequisites: Vec, +pub struct InferenceRule { + /// POSIX calls this ".s1" but that's not useful. + product: String, + /// POSIX calls this ".s2" but that's not useful. + prereq: String, commands: Vec, } -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) { +impl fmt::Display for InferenceRule { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "{}{}:", &self.prereq, &self.product)?; for command in &self.commands { - command.execute(file, target); + writeln!(f, "\t{}", command)?; } + Ok(()) } } @@ -46,8 +40,8 @@ impl Rule { pub struct Target { name: String, prerequisites: Vec, - rule: Option, - already_updated: bool, + commands: Vec, + already_updated: Cell, } impl Target { @@ -58,45 +52,55 @@ impl Target { } fn newer_than(&self, other: &Target) -> Option { + let self_updated = self.already_updated.get(); + let other_updated = other.already_updated.get(); 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, + (None, _) if self_updated && other.prerequisites.contains(&self.name) => true, + (_, None) if other_updated && self.prerequisites.contains(&other.name) => false, _ => return None, }) } - fn is_up_to_date(&self, file: &mut Makefile) -> bool { - if self.already_updated { + fn is_up_to_date(&self, file: &Makefile) -> bool { + if self.already_updated.get() { 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 + .all(|t| self.newer_than(&file.get_target(t).borrow()).unwrap_or(false)); + exists && newer_than_all_dependencies } - fn update(&mut self, file: &mut Makefile) { + pub fn update(&self, file: &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.execute_commands(file); + } + self.already_updated.set(true); + } + + fn execute_commands(&self, file: &Makefile) { + for command in &self.commands { + command.execute(file, self); } - self.already_updated = true; + } +} + +impl fmt::Display for Target { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let prereqs = self.prerequisites.join(" "); + writeln!(f, "{}: {}", &self.name, prereqs)?; + for command in &self.commands { + writeln!(f, "\t{}", command)?; + } + Ok(()) } } @@ -148,28 +152,29 @@ impl CommandLine { } fn execute(&self, file: &Makefile, target: &Target) { + let ignore_error = self.ignore_errors || + file.args.ignore_errors || + file.special_target_has_prereq(".IGNORE", &target.name); + let silent = (self.silent && !file.args.dry_run) || + file.args.silent || + file.special_target_has_prereq(".SILENT", &target.name); + + let execution_line = file.expand_macros(&self.execution_line, Some(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; + let should_execute = self.always_execute || + !(file.args.dry_run || file.args.question || file.args.touch); + if !should_execute { + return; + } // TODO don't fuck this up let execution_line = ::std::ffi::CString::new(execution_line.as_bytes()) @@ -186,6 +191,24 @@ impl CommandLine { } } +impl fmt::Display for CommandLine { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.ignore_errors { + write!(f, "-")?; + } + if self.silent { + write!(f, "@")?; + } + if self.always_execute { + write!(f, "+")?; + } + let execution_line = format!("{}", &self.execution_line); + let execution_line = execution_line.replace("\n", "↵\n"); + writeln!(f, "{}", execution_line)?; + Ok(()) + } +} + enum MacroSource { File, CommandLineOrMAKEFLAGS, @@ -194,38 +217,53 @@ enum MacroSource { } pub struct Makefile { - rules: HashMap, + inference_rules: Vec, macros: HashMap, - targets: HashMap, + targets: RefCell>>>, + pub first_non_special_target: Option, args: Args, + // TODO borrow warnings from Python version } impl Makefile { pub fn new(args: Args) -> Makefile { - Makefile { - rules: HashMap::new(), - macros: HashMap::new(), - targets: HashMap::new(), - args, + let mut inference_rules = vec![]; + let mut macros = HashMap::new(); + let mut targets = HashMap::new(); + let first_non_special_target = None; + + if !args.no_builtin_rules { + inference_rules.extend(builtin_inference_rules()); + for (k, v) in builtin_macros() { + macros.insert(k.into(), (MacroSource::Builtin, v)); + } + targets.extend(builtin_targets().into_iter() + .map(|t| (t.name.clone(), Rc::new(RefCell::new(t))))); } - } - pub fn add_builtins(&mut self) -> &mut Makefile { - self.rules.extend(BUILTIN_RULES.iter().map(|(name, rule)| (name.to_string(), rule.clone()))); - self - } + for (k, v) in env::vars() { + if k != "MAKEFLAGS" && k != "SHELL" { + macros.insert(k, (MacroSource::Environment, TokenString::text(v))); + } + } - 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)])))) + for r#macro in args.macros() { + let pieces = r#macro.splitn(2, '=').collect::>(); + match *pieces { + [name, value] => { + macros.insert(name.into(), (MacroSource::CommandLineOrMAKEFLAGS, TokenString::text(value))); } - }) - ); - self + _ => {} + } + } + + Makefile { + inference_rules, + macros, + targets: RefCell::new(targets), + first_non_special_target, + args, + } } pub fn and_read_file(&mut self, path: impl AsRef) -> &mut Makefile { @@ -248,7 +286,7 @@ impl Makefile { // 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) + // handle escaped newlines while line.ends_with(r"\") { let next_line = match lines_iter.next() { Some(x) => x, @@ -256,9 +294,11 @@ impl Makefile { }; let next_line = next_line.expect("failed to read line of makefile!"); let next_line = next_line.trim_start(); + line.pop(); line.push(' '); line.push_str(next_line); } + // handle comments lazy_static! { static ref COMMENT: Regex = Regex::new("#.*$").unwrap(); @@ -269,11 +309,10 @@ impl Makefile { 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 line = self.expand_macros(&tokenize(line), None); 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); } @@ -322,18 +361,34 @@ impl Makefile { match line_type { LineType::Rule => { let (targets, not_targets) = line_tokens.split_once(':').unwrap(); - let targets = self.expand_macros(&targets); + let targets = self.expand_macros(&targets, None); let targets = targets.split_whitespace().map(|x| x.into()).collect::>(); let (prerequisites, mut commands) = match not_targets.split_once(';') { - Some((prerequisites, commands)) => (prerequisites, vec![commands]), + Some((prerequisites, mut command)) => { + while command.ends_with(r"\") && lines_iter.peek().is_some() { + command.strip_suffix(r"\"); + command.extend(tokenize(&lines_iter.next().unwrap().unwrap())); + } + (prerequisites, vec![command]) + } None => (not_targets, vec![]), }; - let prerequisites = self.expand_macros(&prerequisites); + let prerequisites = self.expand_macros(&prerequisites, None); let prerequisites = prerequisites.split_whitespace().map(|x| x.into()).collect::>(); 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(); + let mut line: String = line.strip_prefix("\t").unwrap().into(); + while line.ends_with('\\') { + match lines_iter.next() { + Some(Ok(next_line)) => { + let next_line = next_line.strip_prefix("\t").unwrap_or(&next_line); + line.push('\n'); + line.push_str(next_line); + } + _ => break + } + } commands.push(line.parse().unwrap()); } @@ -341,32 +396,110 @@ impl Makefile { .map(CommandLine::from) .collect::>(); - for target in targets { - match self.rules.get_mut(&target) { - Some(old_rule) if commands.is_empty() => { - old_rule.prerequisites.extend(prerequisites.clone()); + if targets.is_empty() { + continue; + } + + // we don't know yet if it's a target rule or an inference rule + lazy_static! { + static ref INFERENCE_RULE: Regex = Regex::new(r"^(?P(\.[^/.]+)?)(?P\.[^/.]+)$").unwrap(); + static ref SPECIAL_TARGET: Regex = Regex::new(r"^\.[A-Z]+$").unwrap(); + } + + let inference_match = INFERENCE_RULE.captures(&targets[0]); + let special_target_match = SPECIAL_TARGET.captures(&targets[0]); + + let inference_rule = targets.len() == 1 && prerequisites.len() == 0 && inference_match.is_some() && special_target_match.is_none(); + if inference_rule { + let inference_match = inference_match.unwrap(); + let new_rule = InferenceRule { + product: inference_match.name("s1").unwrap().as_str().to_string(), + prereq: inference_match.name("s2").unwrap().as_str().to_string(), + commands, + }; + + self.inference_rules.retain(|existing_rule| + (&existing_rule.prereq, &existing_rule.product) != (&new_rule.prereq, &new_rule.product)); + self.inference_rules.push(new_rule); + } else { + for target in targets { + if self.first_non_special_target.is_none() && !target.starts_with('.') { + self.first_non_special_target = Some(target.clone()); } - _ => { - self.rules.insert(target.clone(), Rule { - name: target, - prerequisites: prerequisites.clone(), - commands: commands.clone(), - }); + let mut targets = self.targets.borrow_mut(); + match targets.get_mut(&target) { + Some(old_target) if commands.is_empty() && !(target == ".SUFIXES" && prerequisites.is_empty()) => { + let mut old_target = old_target.borrow_mut(); + let new_prerequisites = prerequisites.iter() + .filter(|x| !old_target.prerequisites.contains(x)) + .cloned() + .collect::>(); + old_target.prerequisites.extend(new_prerequisites); + } + _ => { + let new_target = Target { + name: target.clone(), + prerequisites: prerequisites.clone(), + commands: commands.clone(), + already_updated: Cell::new(false), + }; + targets.insert(target.clone(), Rc::new(RefCell::new(new_target))); + } } } } }, LineType::Macro => { - let (name, value) = line_tokens.split_once('=').unwrap(); - let name = self.expand_macros(&name); - match self.macros.get(&name) { + let (name, mut value) = line_tokens.split_once('=').unwrap(); + let name = self.expand_macros(&name, None); + // GNUisms are annoying, but popular + let mut expand_value = false; + let mut skip_if_defined = false; + let mut append = false; + let name = if let Some(real_name) = name.strip_suffix("::") { + expand_value = true; + real_name + } else if let Some(real_name) = name.strip_suffix(":") { + expand_value = true; + real_name + } else if let Some(real_name) = name.strip_suffix("?") { + skip_if_defined = true; + real_name + } else if let Some(real_name) = name.strip_suffix("+") { + append = true; + real_name + } else { + &name + }; + + let name = name.trim_end(); + value.trim_start(); + + let value = if expand_value { + TokenString::text(self.expand_macros(&value, None)) + } else { + value + }; + + 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, + _ if skip_if_defined => continue, _ => {} } - self.macros.insert(name, (MacroSource::File, value)); + + let value = match self.macros.remove(name) { + Some((_, mut old_value)) if append => { + // TODO eagerly expand if appending to eagerly-expanded macro + old_value.extend(TokenString::text(" ")); + old_value.extend(value); + old_value + }, + _ => value + }; + self.macros.insert(name.into(), (MacroSource::File, value)); } LineType::Unknown => { panic!("Unknown line {:?}", line_tokens); @@ -377,57 +510,182 @@ impl Makefile { self } - fn get_target(&mut self, name: impl Into) -> &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()) + fn special_target_has_prereq(&self, target: &str, name: &str) -> bool { + let targets = self.targets.borrow(); + match targets.get(target) { + Some(target) => { + let target = target.borrow(); + target.prerequisites.len() == 0 || target.prerequisites.contains(&name.into()) + }, + None => false, + } + } + + pub fn get_target(&self, name: &str) -> Rc> { + // TODO implement .POSIX + let follow_gnu = true; + + let vpath_options = match self.macros.get("VPATH") { + Some((_, vpath)) if follow_gnu => { + let vpath = self.expand_macros(vpath, None); + env::split_paths(&vpath).collect() + } + _ => vec![] + }; + + let mut targets = self.targets.borrow_mut(); + let exists_but_infer_anyway = if follow_gnu { + match targets.get(name) { + Some(target) => { + let target = target.borrow(); + target.commands.is_empty() + } + None => false, + } + } else { + false + }; + if !targets.contains_key(name) || exists_but_infer_anyway { + // When no target rule is found to update a target, the inference rules shall + // be checked. The suffix of the target to be built... + let suffix = Path::new(name).extension().map_or_else(String::new, |ext| ext.to_string_lossy().into()); + // is compared to the list of suffixes specified by the .SUFFIXES special + // targets. If the .s1 suffix is found in .SUFFIXES... + if self.special_target_has_prereq(".SUFFIXES", &suffix) || suffix.is_empty() { + // the inference rules shall be searched in the order defined... + 'rules: for rule in &self.inference_rules { + // for the first .s2.s1 rule... + if rule.product == suffix { + // whose prerequisite file ($*.s2) exists. + let prereq_path = Path::new(name).with_extension(rule.prereq.clone()); + let prereq_path_options = if prereq_path.is_absolute() { + vec![prereq_path] + } else { + let mut options = vec![prereq_path.clone()]; + options.extend(vpath_options.iter().map(|vpath| vpath.join(&prereq_path))); + options + }; + for prereq in prereq_path_options { + if prereq.exists() { + let new_target = Target { + name: name.into(), + prerequisites: vec![prereq.to_string_lossy().into()], + commands: rule.commands.clone(), + already_updated: Cell::new(false), + }; + targets.insert(name.into(), Rc::new(RefCell::new(new_target))); + break 'rules; + } + } + } + } + } + } + + if !targets.contains_key(name) { + // well, inference didn't work. is there a default? + if let Some(default) = targets.get(".DEFAULT") { + let default = default.borrow(); + let commands = default.commands.clone(); + drop(default); + let new_target = Target { + name: name.into(), + prerequisites: vec![], + commands, + already_updated: Cell::new(false), + }; + targets.insert(name.into(), Rc::new(RefCell::new(new_target))); + } else { + // if it already exists, it counts as up-to-date + if Path::new(name).exists() { + let new_target = Target { + name: name.into(), + prerequisites: vec![], + commands: vec![], + already_updated: Cell::new(true), }; + targets.insert(name.into(), Rc::new(RefCell::new(new_target))); } - 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() + + drop(targets); + let targets = self.targets.borrow(); + + targets.get(name).expect("Target not found!").clone() } - fn update_target(&mut self, name: impl Into) { - // 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(); + pub fn update_target(&self, name: &str) { + let target = self.get_target(name); + let target = target.borrow(); target.update(self); - self.targets.insert(name.clone(), target); } - fn expand_macros(&self, text: &TokenString) -> String { + fn expand_macros(&self, text: &TokenString, target: Option<&Target>) -> 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 internal_macro_names = &['@', '?', '<', '*'][..]; + let internal_macro_suffices = &['D', 'F'][..]; + let just_internal = name.len() == 1 && name.starts_with(internal_macro_names); + let suffixed_internal = name.len() == 2 && name.starts_with(internal_macro_names) && name.ends_with(internal_macro_suffices); + let macro_value = if just_internal || suffixed_internal { + let target = target.expect("internal macro but no current target!"); + let macro_pieces = if name.starts_with('@') { + // The $@ shall evaluate to the full target name of the + // current target. + vec![target.name.clone()] + } else if name.starts_with('?') { + // The $? macro shall evaluate to the list of prerequisites + // that are newer than the current target. + target.prerequisites + .iter() + .filter(|prereq| self.get_target(prereq).borrow().newer_than(target).unwrap_or(false)) + .cloned() + .collect() + } else if name.starts_with('<') { + // In an inference rule, the $< macro shall evaluate to the + // filename whose existence allowed the inference rule to be + // chosen for the target. In the .DEFAULT rule, the $< macro + // shall evaluate to the current target name. + target.prerequisites.clone() + } else if name.starts_with('*') { + // The $* macro shall evaluate to the current target name with + // its suffix deleted. + vec![Path::new(name).with_extension("").to_string_lossy().into()] + } else { + unreachable!() + }; + + let macro_pieces = if name.ends_with('D') { + macro_pieces.into_iter() + .map(|x| Path::new(&x).parent().expect("no parent").to_string_lossy().into()) + .collect() + } else if name.ends_with('F') { + macro_pieces.into_iter() + .map(|x| Path::new(&x).file_name().expect("no filename").to_string_lossy().into()) + .collect() + } else { + macro_pieces + }; + + macro_pieces.join(" ") + } else { + match self.macros.get(name) { + Some((_, macro_value)) => self.expand_macros(¯o_value, target), + None => { + String::new() + } + } + }; let macro_value = match replacement { Some((subst1, subst2)) => { - let subst1 = self.expand_macros(subst1); + let subst1 = self.expand_macros(subst1, target); let subst1_suffix = regex::escape(&subst1); let subst1_suffix = Regex::new(&format!(r"{}\b", subst1_suffix)).unwrap(); - let subst2 = self.expand_macros(subst2); + let subst2 = self.expand_macros(subst2, target); subst1_suffix.replace_all(¯o_value, subst2).to_string() }, None => macro_value, @@ -440,5 +698,40 @@ impl Makefile { } } -const BUILTIN_RULES: &'static [(&'static str, Rule)] = &[]; -const BUILTIN_SUFFIX_LIST: &'static [&'static str] = &[]; +impl fmt::Display for Makefile { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let header = |f: &mut fmt::Formatter, t: &str| { + writeln!(f, "{}\n{:=^width$}", t, "", width = t.len()) + }; + header(f, "Inference Rules")?; + for rule in &self.inference_rules { + writeln!(f, "{}", rule)?; + } + writeln!(f)?; + + header(f, "Macros")?; + for (k, (_, v)) in &self.macros { + writeln!(f, "{}={}", k, v)?; + } + writeln!(f)?; + + header(f, "Targets")?; + let targets = self.targets.borrow(); + for (_, target) in &*targets { + let target = target.borrow(); + writeln!(f, "{}", target)?; + } + + Ok(()) + } +} + +fn builtin_inference_rules() -> Vec { + todo!() +} +fn builtin_macros() -> Vec<(&'static str, TokenString)> { + todo!() +} +fn builtin_targets() -> Vec { + todo!() +} diff --git a/src/makefile/token.rs b/src/makefile/token.rs index 3f69b50..4943010 100644 --- a/src/makefile/token.rs +++ b/src/makefile/token.rs @@ -1,3 +1,4 @@ +use std::fmt; use std::str::FromStr; use nom::{ @@ -14,6 +15,10 @@ use nom::{ pub struct TokenString(Vec); impl TokenString { + pub fn text(text: impl Into) -> Self { + Self(vec![Token::Text(text.into())]) + } + pub fn tokens(&self) -> impl Iterator { self.0.iter() } @@ -41,6 +46,40 @@ impl TokenString { } None } + + pub fn ends_with(&self, pattern: &str) -> bool { + match self.0.last() { + Some(Token::Text(t)) => t.ends_with(pattern), + _ => false + } + } + + pub fn strip_suffix(&mut self, suffix: &str) { + if let Some(Token::Text(t)) = self.0.last_mut() { + if let Some(x) = t.strip_suffix(suffix) { + *t = x.into() + } + } + } + + pub fn extend(&mut self, other: TokenString) { + self.0.extend(other.0); + } + + pub fn trim_start(&mut self) { + if let Some(Token::Text(t)) = self.0.last_mut() { + *t = t.trim_start().into(); + } + } +} + +impl fmt::Display for TokenString { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for t in &self.0 { + write!(f, "{}", t)?; + } + Ok(()) + } } #[derive(PartialEq, Eq, Clone, Debug)] @@ -52,6 +91,16 @@ pub enum Token { }, } +impl fmt::Display for Token { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Token::Text(t) => write!(f, "{}", t), + Token::MacroExpansion { name, replacement: None } => write!(f, "$({})", name), + Token::MacroExpansion { name, replacement: Some((r1, r2)) } => write!(f, "$({}:{}={})", name, r1, r2), + } + } +} + fn macro_name(input: &str) -> IResult<&str, &str> { // POSIX says "periods, underscores, digits, and alphabetics from the portable character set" take_while1(|c: char| { -- cgit v1.2.3