aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/args.rs26
-rw-r--r--src/main.rs35
-rw-r--r--src/makefile/mod.rs444
-rw-r--r--src/makefile/token.rs236
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(&macro_value, subst2).to_string()
+ },
+ None => macro_value,
+ };
+ result.push_str(&macro_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."),
+ ]));
+ }
+}