use super::pattern::r#match; use super::r#macro::{MacroSet, MacroSource}; use super::token::TokenString; pub(crate) fn expand_call( name: &str, args: &[TokenString], macros: &MacroSet, ) -> anyhow::Result { match name { "filter" => { assert_eq!(args.len(), 2); text::filter(macros, &args[0], &args[1]) } "filter-out" => { assert_eq!(args.len(), 2); text::filter_out(macros, &args[0], &args[1]) } "sort" => { assert_eq!(args.len(), 1); text::sort(macros, &args[0]) } "notdir" => { assert_eq!(args.len(), 1); file_name::notdir(macros, &args[0]) } "basename" => { assert_eq!(args.len(), 1); file_name::basename(macros, &args[0]) } "addprefix" => { assert_eq!(args.len(), 2); file_name::addprefix(macros, &args[0], &args[1]) } "wildcard" => { assert_eq!(args.len(), 1); file_name::wildcard(macros, &args[0]) } // foreach "foreach" => { assert_eq!(args.len(), 3); foreach::foreach(macros, &args[0], &args[1], &args[2]) } // call "call" => { assert!(!args.is_empty()); call::call(macros, args.iter()) } // eval "eval" => todo!(), // shell "shell" => todo!(), // fallback _ => panic!("function not implemented: {}", name), } } // Text Functions mod text { use super::r#match; use super::MacroSet; use super::TokenString; pub(crate) fn filter( macros: &MacroSet, patterns: &TokenString, text: &TokenString, ) -> anyhow::Result { let patterns = macros.expand(patterns)?; let patterns = patterns.split_whitespace().collect::>(); let text = macros.expand(text)?; 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(crate) fn filter_out( macros: &MacroSet, patterns: &TokenString, text: &TokenString, ) -> anyhow::Result { let patterns = macros.expand(patterns)?; let patterns = patterns.split_whitespace().collect::>(); let text = macros.expand(text)?; 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(crate) fn sort(macros: &MacroSet, words: &TokenString) -> anyhow::Result { let words = macros.expand(words)?; let mut words = words.split_whitespace().collect::>(); words.sort_unstable(); words.dedup(); Ok(words.join(" ")) } } // File Name Functions mod file_name { use std::env; use std::ffi::OsStr; use std::path::Path; use anyhow::Context; use super::MacroSet; use super::TokenString; pub(crate) fn notdir(macros: &MacroSet, words: &TokenString) -> anyhow::Result { let words = macros.expand(words)?; let words = words .split_whitespace() .map(|word| { Path::new(word) .file_name() .and_then(OsStr::to_str) .unwrap_or("") }) .collect::>(); Ok(words.join(" ")) } pub(crate) fn basename(macros: &MacroSet, words: &TokenString) -> anyhow::Result { let words = macros.expand(words)?; 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(crate) fn addprefix( macros: &MacroSet, prefix: &TokenString, targets: &TokenString, ) -> anyhow::Result { let prefix = macros.expand(prefix)?; let targets = macros.expand(targets)?; let results = targets .split_whitespace() .map(|t| format!("{}{}", prefix, t)) .collect::>(); Ok(results.join(" ")) } pub(crate) fn wildcard(macros: &MacroSet, pattern: &TokenString) -> anyhow::Result { let pattern = macros.expand(pattern)?; 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(" ")) } } // foreach mod foreach { use super::MacroSet; use super::MacroSource; use super::TokenString; pub(crate) fn foreach( macros: &MacroSet, var: &TokenString, list: &TokenString, text: &TokenString, ) -> anyhow::Result { let var = macros.expand(var)?; let list = macros.expand(list)?; let words = list.split_whitespace(); let mut macros = macros.with_overlay(); let results = words .map(|word| { macros.set(var.clone(), MacroSource::File, TokenString::text(word)); macros.expand(text) }) .collect::, _>>()?; Ok(results.join(" ")) } } // call mod call { use super::MacroSet; use super::MacroSource; use super::TokenString; pub(crate) fn call<'a>( macros: &MacroSet, args: impl Iterator, ) -> anyhow::Result { let args = args .map(|arg| macros.expand(arg)) .collect::, _>>()?; let function = args[0].clone(); let mut macros = macros.with_overlay(); for (i, x) in args.into_iter().enumerate() { macros.set(i.to_string(), MacroSource::File, TokenString::text(x)); } macros.expand(&TokenString::r#macro(function)) } } #[cfg(test)] mod test { use super::*; use crate::makefile::r#macro::{MacroSet, MacroSource}; fn call(name: &str, args: &[TokenString], macros: &MacroSet) -> anyhow::Result { super::expand_call(name, args, macros) } 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 filter() { 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"); } #[test] fn filter_out() { let result = call!("filter-out" "main1.o main2.o", "main1.o foo.o main2.o bar.o"); assert_eq!(result, "foo.o bar.o"); } #[test] fn sort() { let result = call!(sort "foo bar lose foo"); assert_eq!(result, "bar foo lose"); } #[test] fn notdir() { let result = call!(notdir "src/foo.c hacks"); assert_eq!(result, "foo.c hacks"); } #[test] fn basename() { let result = call!(basename "src/foo.c src-1.0/bar hacks"); assert_eq!(result, "src/foo src-1.0/bar hacks"); } #[test] fn addprefix() { let result = call!(addprefix "src/", "foo bar"); assert_eq!(result, "src/foo src/bar"); } #[test] fn wildcard() { use std::env::{set_current_dir, set_var}; use std::fs::write; use std::path::MAIN_SEPARATOR; let tempdir = tempfile::tempdir().unwrap(); write(tempdir.path().join("foo.c"), "").unwrap(); write(tempdir.path().join("bar.h"), "").unwrap(); write(tempdir.path().join("baz.txt"), "").unwrap(); write(tempdir.path().join("acab.c"), "ACAB").unwrap(); write(tempdir.path().join("based.txt"), "☭").unwrap(); set_current_dir(tempdir.path()).unwrap(); set_var("HOME", tempdir.path().to_str().unwrap()); 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().to_str().unwrap(), MAIN_SEPARATOR ) ); } #[test] fn foreach() { let mut macros = MacroSet::new(); macros.set( "test".to_string(), MacroSource::File, "worked for $(item).".parse().unwrap(), ); 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." ); } #[test] fn call_test() { let mut macros = MacroSet::new(); macros.set( "reverse".to_string(), MacroSource::File, "$(2) $(1)".parse().unwrap(), ); assert_eq!( call( "call", &[ TokenString::text("reverse"), TokenString::text("a"), TokenString::text("b") ], ¯os, ), "b a" ); } }