use std::env; use std::io::BufRead; use std::process::{Command, Stdio}; use eyre::{bail, Result, WrapErr}; use super::eval_context::DeferredEvalContext; use super::pattern::r#match; use super::r#macro::{Macro, Set as MacroSet}; use super::token::TokenString; use super::ItemSource; pub const NO_EVAL: Option<&mut DeferredEvalContext<&[u8]>> = None; #[allow(clippy::cognitive_complexity)] pub fn expand_call( name: &str, args: &[TokenString], macros: &MacroSet, mut eval_context: Option<&mut DeferredEvalContext>, ) -> Result { match name { "subst" => { assert_eq!(args.len(), 3); text::subst(macros, &args[0], &args[1], &args[2], eval_context) } "patsubst" => { assert_eq!(args.len(), 3); text::patsubst(macros, &args[0], &args[1], &args[2], eval_context) } "strip" => { assert_eq!(args.len(), 1); text::strip(macros, &args[0], eval_context) } "findstring" => { assert_eq!(args.len(), 2); text::findstring(macros, &args[0], &args[1], eval_context) } "filter" => { assert_eq!(args.len(), 2); text::filter(macros, &args[0], &args[1], eval_context) } "filter-out" => { assert_eq!(args.len(), 2); text::filter_out(macros, &args[0], &args[1], eval_context) } "sort" => { assert_eq!(args.len(), 1); text::sort(macros, &args[0], eval_context) } "word" => { assert_eq!(args.len(), 2); text::word(macros, &args[0], &args[1], eval_context) } "words" => { assert_eq!(args.len(), 1); text::words(macros, &args[0], eval_context) } "firstword" => { assert_eq!(args.len(), 1); text::firstword(macros, &args[0], eval_context) } "lastword" => { assert_eq!(args.len(), 1); text::lastword(macros, &args[0], eval_context) } "dir" => { assert_eq!(args.len(), 1); file_name::dir(macros, &args[0], eval_context) } "notdir" => { assert_eq!(args.len(), 1); file_name::notdir(macros, &args[0], eval_context) } "basename" => { assert_eq!(args.len(), 1); file_name::basename(macros, &args[0], eval_context) } "addsuffix" => { assert_eq!(args.len(), 2); file_name::addsuffix(macros, &args[0], &args[1], eval_context) } "addprefix" => { assert_eq!(args.len(), 2); file_name::addprefix(macros, &args[0], &args[1], eval_context) } "wildcard" => { assert_eq!(args.len(), 1); file_name::wildcard(macros, &args[0], eval_context) } "realpath" => { assert_eq!(args.len(), 1); file_name::realpath(macros, &args[0], eval_context) } "abspath" => { assert_eq!(args.len(), 1); file_name::abspath(macros, &args[0], eval_context) } "if" => { assert!(args.len() == 2 || args.len() == 3); conditional::r#if(macros, &args[0], &args[1], args.get(2), eval_context) } "or" => { assert!(!args.is_empty()); conditional::or(macros, args.iter(), eval_context) } "and" => { assert!(!args.is_empty()); conditional::and(macros, args.iter(), eval_context) } "foreach" => { assert_eq!(args.len(), 3); foreach(macros, &args[0], &args[1], &args[2], eval_context) } "call" => { assert!(!args.is_empty()); call(macros, args.iter(), eval_context) } "eval" => { assert_eq!(args.len(), 1); let should_eval = eval(macros, &args[0], eval_context.as_deref_mut())?; if let Some(eval_context) = eval_context { eval_context.eval(should_eval)?; } else { bail!("tried to eval something but no eval back-channel was available"); } Ok(String::new()) } "origin" => { assert_eq!(args.len(), 1); origin(macros, &args[0], eval_context) } "error" => { assert_eq!(args.len(), 1); meta::error(macros, &args[0], eval_context) } "shell" => { assert_eq!(args.len(), 1); shell(macros, &args[0], eval_context) } // fallback _ => bail!("function not implemented: {}", name), } } // Text Functions mod text { use super::*; pub fn subst( macros: &MacroSet, from: &TokenString, to: &TokenString, text: &TokenString, mut eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let from = macros.expand(from, eval_context.as_deref_mut())?; let to = macros.expand(to, eval_context.as_deref_mut())?; let text = macros.expand(text, eval_context)?; Ok(text.replace(&from, &to)) } pub fn patsubst( macros: &MacroSet, from: &TokenString, to: &TokenString, text: &TokenString, mut eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let from = macros.expand(from, eval_context.as_deref_mut())?; let to = macros.expand(to, eval_context.as_deref_mut())?; let text = macros.expand(text, eval_context)?; let words = text.split_whitespace() .map(|word| { let pattern_match = r#match(&from, word)?.and_then(|x| x.get(1)); Ok(pattern_match .map_or_else(|| word.to_owned(), |pm| to.replace('%', pm.as_str()))) }) .collect::>>()?; Ok(words.join(" ")) } pub fn strip( macros: &MacroSet, text: &TokenString, eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let text = macros.expand(text, eval_context)?; // TODO don't allocate this vec let words = text.split_whitespace().collect::>(); Ok(words.join(" ")) } pub fn findstring( macros: &MacroSet, needle: &TokenString, haystack: &TokenString, mut eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let needle = macros.expand(needle, eval_context.as_deref_mut())?; let haystack = macros.expand(haystack, eval_context)?; if haystack.contains(&needle) { Ok(needle) } else { Ok(String::new()) } } pub fn filter( macros: &MacroSet, patterns: &TokenString, text: &TokenString, mut eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let patterns = macros.expand(patterns, eval_context.as_deref_mut())?; let patterns = patterns.split_whitespace().collect::>(); let text = macros.expand(text, eval_context)?; let text = text.split_whitespace(); let mut result_pieces = vec![]; for word in text { if patterns .iter() .any(|pattern| r#match(pattern, word).map_or(false, |x| x.is_some())) { result_pieces.push(word); } } Ok(result_pieces.join(" ")) } pub fn filter_out( macros: &MacroSet, patterns: &TokenString, text: &TokenString, mut eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let patterns = macros.expand(patterns, eval_context.as_deref_mut())?; let patterns = patterns.split_whitespace().collect::>(); let text = macros.expand(text, eval_context)?; let text = text.split_whitespace(); let mut result_pieces = vec![]; for word in text { if patterns .iter() .all(|pattern| r#match(pattern, word).map_or(false, |x| x.is_none())) { result_pieces.push(word); } } Ok(result_pieces.join(" ")) } pub fn sort( macros: &MacroSet, words: &TokenString, eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let words = macros.expand(words, eval_context)?; let mut words = words.split_whitespace().collect::>(); words.sort_unstable(); words.dedup(); Ok(words.join(" ")) } pub fn word( macros: &MacroSet, n: &TokenString, text: &TokenString, mut eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let n = macros.expand(n, eval_context.as_deref_mut())?; let n: usize = n.parse().wrap_err("while calling `word`")?; let text = macros.expand(text, eval_context)?; Ok(text .split_whitespace() .nth(n.saturating_add(1)) .unwrap_or("") .to_owned()) } pub fn words( macros: &MacroSet, words: &TokenString, eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let words = macros.expand(words, eval_context)?; Ok(words.split_whitespace().count().to_string()) } pub fn firstword( macros: &MacroSet, words: &TokenString, eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let words = macros.expand(words, eval_context)?; Ok(words.split_whitespace().next().unwrap_or("").to_owned()) } pub fn lastword( macros: &MacroSet, words: &TokenString, eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let words = macros.expand(words, eval_context)?; Ok(words.split_whitespace().last().unwrap_or("").to_owned()) } } // File Name Functions mod file_name { use std::env; use std::ffi::OsStr; use std::fs; use std::io::BufRead; use std::path::{Path, MAIN_SEPARATOR}; use super::*; use crate::makefile::eval_context::DeferredEvalContext; use eyre::WrapErr; pub fn dir( macros: &MacroSet, words: &TokenString, eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let words = macros.expand(words, eval_context)?; let words = words .split_whitespace() .map(|word| { Path::new(word) .parent() .and_then(Path::to_str) .filter(|x| !x.is_empty()) .unwrap_or(".") }) .map(|x| format!("{}{}", x, MAIN_SEPARATOR)) .collect::>(); Ok(words.join(" ")) } pub fn notdir( macros: &MacroSet, words: &TokenString, eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let words = macros.expand(words, eval_context)?; let words = words .split_whitespace() .map(|word| { Path::new(word) .file_name() .and_then(OsStr::to_str) .unwrap_or("") }) .collect::>(); Ok(words.join(" ")) } pub fn basename( macros: &MacroSet, words: &TokenString, eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let words = macros.expand(words, eval_context)?; let words = words .split_whitespace() .map(|word| { Path::new(word) .with_extension("") .to_str() .map_or_else(String::new, ToString::to_string) }) .collect::>(); Ok(words.join(" ")) } pub fn addsuffix( macros: &MacroSet, suffix: &TokenString, targets: &TokenString, mut eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let suffix = macros.expand(suffix, eval_context.as_deref_mut())?; let targets = macros.expand(targets, eval_context)?; let results = targets .split_whitespace() .map(|t| format!("{}{}", t, suffix)) .collect::>(); Ok(results.join(" ")) } pub fn addprefix( macros: &MacroSet, prefix: &TokenString, targets: &TokenString, mut eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let prefix = macros.expand(prefix, eval_context.as_deref_mut())?; let targets = macros.expand(targets, eval_context)?; let results = targets .split_whitespace() .map(|t| format!("{}{}", prefix, t)) .collect::>(); Ok(results.join(" ")) } pub fn wildcard( macros: &MacroSet, pattern: &TokenString, eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let pattern = macros.expand(pattern, eval_context)?; let home_dir = env::var("HOME") .ok() .or_else(|| dirs::home_dir().and_then(|p| p.to_str().map(String::from))); let pattern = if let Some(home_dir) = home_dir { pattern.replace('~', &home_dir) } else { pattern }; let results = glob::glob(&pattern) .context("invalid glob pattern!")? .filter_map(|path| { path.ok() .map(|x| x.to_str().map(ToString::to_string).unwrap_or_default()) }) .collect::>(); Ok(results.join(" ")) } pub fn realpath( macros: &MacroSet, targets: &TokenString, eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let targets = macros.expand(targets, eval_context)?; let results = targets .split_whitespace() .map(|x| { fs::canonicalize(x) .map(|p| p.to_string_lossy().into_owned()) .unwrap_or_else(|_| x.to_owned()) }) .collect::>(); Ok(results.join(" ")) } pub fn abspath( macros: &MacroSet, targets: &TokenString, eval_context: Option<&mut DeferredEvalContext>, ) -> Result { // TODO don't resolve symlinks realpath(macros, targets, eval_context) } } // Functions for Conditionals mod conditional { use super::*; pub fn r#if( macros: &MacroSet, condition: &TokenString, if_true: &TokenString, if_false: Option<&TokenString>, mut eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let mut condition = condition.clone(); condition.trim_start(); condition.trim_end(); let condition = macros.expand(&condition, eval_context.as_deref_mut())?; if condition.is_empty() { if_false.map_or_else( || Ok(String::new()), |if_false| macros.expand(if_false, eval_context), ) } else { macros.expand(if_true, eval_context) } } pub fn or<'a>( macros: &MacroSet, args: impl Iterator, mut eval_context: Option<&mut DeferredEvalContext>, ) -> Result { for arg in args { let arg = macros.expand(arg, eval_context.as_deref_mut())?; if !arg.is_empty() { return Ok(arg); } } Ok(String::new()) } pub fn and<'a>( macros: &MacroSet, args: impl Iterator, mut eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let mut last = String::new(); for arg in args { last = macros.expand(arg, eval_context.as_deref_mut())?; if last.is_empty() { return Ok(String::new()); } } Ok(last) } } pub fn foreach( macros: &MacroSet, var: &TokenString, list: &TokenString, text: &TokenString, mut eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let var = macros.expand(var, eval_context.as_deref_mut())?; let list = macros.expand(list, eval_context.as_deref_mut())?; let words = list.split_whitespace(); let mut macros = macros.with_overlay(); let results = words .map(|word| { macros.set( var.clone(), Macro { source: ItemSource::FunctionCall, text: TokenString::text(word), #[cfg(feature = "full")] eagerly_expanded: false, }, ); macros.expand(text, eval_context.as_deref_mut()) }) .collect::, _>>()?; Ok(results.join(" ")) } pub fn call<'a>( macros: &MacroSet, args: impl Iterator, mut eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let args = args .map(|arg| macros.expand(arg, eval_context.as_deref_mut())) .collect::, _>>()?; let function = args[0].clone(); // TODO if function is a builtin, call the builtin instead let mut macros = macros.with_overlay(); for (i, x) in args.into_iter().enumerate() { macros.set( i.to_string(), Macro { source: ItemSource::FunctionCall, text: TokenString::text(x), #[cfg(feature = "full")] eagerly_expanded: false, }, ); } macros.expand(&TokenString::r#macro(function), eval_context) } // TODO consider bringing eval logic in here since we put the Vec in MacroSet IIRC pub fn eval( macros: &MacroSet, arg: &TokenString, eval_context: Option<&mut DeferredEvalContext>, ) -> Result { macros.expand(arg, eval_context) } pub fn origin( macros: &MacroSet, variable: &TokenString, eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let variable = macros.expand(variable, eval_context)?; Ok(macros.origin(&variable).to_owned()) } mod meta { use super::*; pub fn error( macros: &MacroSet, text: &TokenString, eval_context: Option<&mut DeferredEvalContext>, ) -> Result { let text = macros.expand(text, eval_context)?; bail!("{}", text); } } pub fn shell( macros: &MacroSet, command: &TokenString, eval_context: Option<&mut DeferredEvalContext>, ) -> Result { // TODO bring this in from command_line let command = macros.expand(command, eval_context)?; let (program, args) = if cfg!(windows) { let cmd = env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".into()); let args = vec!["/c", &command]; (cmd, args) } else { let sh = env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into()); let args = vec!["-e", "-c", &command]; (sh, args) }; let result = Command::new(program) .args(args) .stderr(Stdio::inherit()) .output()?; let _status = result.status; // TODO set .SHELLSTATUS Ok(String::from_utf8(result.stdout)? .replace("\r\n", "\n") .trim_end_matches('\n') .replace("\n", " ")) } #[cfg(test)] mod test { use super::*; type R = Result<()>; fn call(name: &str, args: &[TokenString], macros: &MacroSet) -> Result { super::expand_call(name, args, macros, NO_EVAL) } macro_rules! call { ($func:literal $($arg:literal),+) => { call($func, &[$(TokenString::text($arg)),+], &MacroSet::new())? }; ($func:ident $($arg:literal),+) => { call(stringify!($func), &[$(TokenString::text($arg)),+], &MacroSet::new())? }; } #[test] fn strip() -> R { let result = call!(strip " this is\tweirdly spaced text "); assert_eq!(result, "this is weirdly spaced text"); Ok(()) } #[test] fn findstring() -> R { assert_eq!(call!(findstring "hi", "thighs"), "hi"); assert_eq!(call!(findstring "hey", "hello there"), ""); Ok(()) } #[test] fn filter() -> R { let result = call!(filter "word", "this contains a word inside it"); assert_eq!(result, "word"); let result = call!(filter "%.c %.s", "foo.c bar.c baz.s ugh.h"); assert_eq!(result, "foo.c bar.c baz.s"); Ok(()) } #[test] fn filter_out() -> R { let result = call!("filter-out" "main1.o main2.o", "main1.o foo.o main2.o bar.o"); assert_eq!(result, "foo.o bar.o"); Ok(()) } #[test] fn sort() -> R { let result = call!(sort "foo bar lose foo"); assert_eq!(result, "bar foo lose"); Ok(()) } #[test] fn dir() -> R { let result = call!(dir "src/foo.c hacks"); assert_eq!(result, format!("src{0} .{0}", std::path::MAIN_SEPARATOR)); Ok(()) } #[test] fn notdir() -> R { let result = call!(notdir "src/foo.c hacks"); assert_eq!(result, "foo.c hacks"); Ok(()) } #[test] fn basename() -> R { let result = call!(basename "src/foo.c src-1.0/bar hacks"); assert_eq!(result, "src/foo src-1.0/bar hacks"); Ok(()) } #[test] fn addprefix() -> R { let result = call!(addprefix "src/", "foo bar"); assert_eq!(result, "src/foo src/bar"); Ok(()) } #[test] fn wildcard() -> R { use std::env::{set_current_dir, set_var}; use std::fs::write; use std::path::MAIN_SEPARATOR; let tempdir = tempfile::tempdir()?; write(tempdir.path().join("foo.c"), "")?; write(tempdir.path().join("bar.h"), "")?; write(tempdir.path().join("baz.txt"), "")?; write(tempdir.path().join("acab.c"), "ACAB")?; write(tempdir.path().join("based.txt"), "☭")?; set_current_dir(tempdir.path())?; set_var("HOME", tempdir.path()); let sort = |x: String| call("sort", &[TokenString::text(&x)], &MacroSet::new()); assert_eq!(sort(call!(wildcard "*.c"))?, "acab.c foo.c"); assert_eq!( sort(call!(wildcard "~/ba?.*"))?, format!( "{0}{1}bar.h {0}{1}baz.txt", tempdir.path().display(), MAIN_SEPARATOR ) ); Ok(()) } #[test] fn test_if() -> R { let mut macros = MacroSet::new(); macros.set( "test1".to_owned(), Macro { source: ItemSource::Builtin, text: TokenString::text("something"), #[cfg(feature = "full")] eagerly_expanded: false, }, ); macros.set( "test2".to_owned(), Macro { source: ItemSource::Builtin, text: TokenString::text(""), #[cfg(feature = "full")] eagerly_expanded: false, }, ); assert_eq!( call( "if", &[ TokenString::r#macro("test1"), TokenString::text("success"), TokenString::r#macro("failed"), ], ¯os )?, "success" ); assert_eq!( call( "if", &[ TokenString::r#macro("test2"), "$(error failed)".parse()?, TokenString::text("pass"), ], ¯os )?, "pass" ); Ok(()) } #[test] fn or() -> R { assert_eq!( call( "or", &[TokenString::text("yep"), TokenString::text("yeah")], &MacroSet::new() )?, "yep" ); assert_eq!( call( "or", &[ TokenString::text(""), TokenString::text("yeet"), "$(error fail)".parse()? ], &MacroSet::new() )?, "yeet" ); Ok(()) } #[test] fn and() -> R { assert_eq!( call( "and", &[TokenString::text("yep"), TokenString::text("yeah")], &MacroSet::new() )?, "yeah" ); assert_eq!( call( "and", &[ TokenString::text("maybe"), TokenString::text(""), "$(error fail)".parse()? ], &MacroSet::new() )?, "" ); Ok(()) } #[test] fn foreach() -> R { let mut macros = MacroSet::new(); macros.set( "test".to_owned(), Macro { source: ItemSource::Builtin, text: "worked for $(item).".parse()?, #[cfg(feature = "full")] eagerly_expanded: false, }, ); assert_eq!( call( "foreach", &[ TokenString::text("item"), TokenString::text("a b c d"), TokenString::r#macro("test") ], ¯os, )?, "worked for a. worked for b. worked for c. worked for d." ); Ok(()) } #[test] fn call_test() -> R { let mut macros = MacroSet::new(); macros.set( "reverse".to_owned(), Macro { source: ItemSource::Builtin, text: "$(2) $(1)".parse()?, #[cfg(feature = "full")] eagerly_expanded: false, }, ); assert_eq!( call( "call", &[ TokenString::text("reverse"), TokenString::text("a"), TokenString::text("b") ], ¯os, )?, "b a" ); Ok(()) } #[test] fn shell() -> R { let result = call!(shell "echo hi"); assert_eq!(result, "hi"); Ok(()) } }