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;
use super::{ItemSource, MacroScopeStack, MacroSet, TokenString};
pub const NO_EVAL: Option<&mut DeferredEvalContext<&[u8]>> = None;
#[allow(clippy::cognitive_complexity)]
pub fn expand_call(
name: &str,
args: &[TokenString],
stack: &MacroScopeStack,
mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
match name {
"subst" => {
assert_eq!(args.len(), 3);
text::subst(stack, &args[0], &args[1], &args[2], eval_context)
}
"patsubst" => {
assert_eq!(args.len(), 3);
text::patsubst(stack, &args[0], &args[1], &args[2], eval_context)
}
"strip" => {
assert_eq!(args.len(), 1);
text::strip(stack, &args[0], eval_context)
}
"findstring" => {
assert_eq!(args.len(), 2);
text::findstring(stack, &args[0], &args[1], eval_context)
}
"filter" => {
assert_eq!(args.len(), 2);
text::filter(stack, &args[0], &args[1], eval_context)
}
"filter-out" => {
assert_eq!(args.len(), 2);
text::filter_out(stack, &args[0], &args[1], eval_context)
}
"sort" => {
assert_eq!(args.len(), 1);
text::sort(stack, &args[0], eval_context)
}
"word" => {
assert_eq!(args.len(), 2);
text::word(stack, &args[0], &args[1], eval_context)
}
"words" => {
assert_eq!(args.len(), 1);
text::words(stack, &args[0], eval_context)
}
"firstword" => {
assert_eq!(args.len(), 1);
text::firstword(stack, &args[0], eval_context)
}
"lastword" => {
assert_eq!(args.len(), 1);
text::lastword(stack, &args[0], eval_context)
}
"dir" => {
assert_eq!(args.len(), 1);
file_name::dir(stack, &args[0], eval_context)
}
"notdir" => {
assert_eq!(args.len(), 1);
file_name::notdir(stack, &args[0], eval_context)
}
"basename" => {
assert_eq!(args.len(), 1);
file_name::basename(stack, &args[0], eval_context)
}
"addsuffix" => {
assert_eq!(args.len(), 2);
file_name::addsuffix(stack, &args[0], &args[1], eval_context)
}
"addprefix" => {
assert_eq!(args.len(), 2);
file_name::addprefix(stack, &args[0], &args[1], eval_context)
}
"wildcard" => {
assert_eq!(args.len(), 1);
file_name::wildcard(stack, &args[0], eval_context)
}
"realpath" => {
assert_eq!(args.len(), 1);
file_name::realpath(stack, &args[0], eval_context)
}
"abspath" => {
assert_eq!(args.len(), 1);
file_name::abspath(stack, &args[0], eval_context)
}
"if" => {
assert!(args.len() == 2 || args.len() == 3);
conditional::r#if(stack, &args[0], &args[1], args.get(2), eval_context)
}
"or" => {
assert!(!args.is_empty());
conditional::or(stack, args.iter(), eval_context)
}
"and" => {
assert!(!args.is_empty());
conditional::and(stack, args.iter(), eval_context)
}
"foreach" => {
assert_eq!(args.len(), 3);
foreach(stack, &args[0], &args[1], &args[2], eval_context)
}
"call" => {
assert!(!args.is_empty());
call(stack, args.iter(), eval_context)
}
"eval" => {
assert_eq!(args.len(), 1);
let should_eval = eval(stack, &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(stack, &args[0], eval_context)
}
"error" => {
assert_eq!(args.len(), 1);
meta::error(stack, &args[0], eval_context)
}
"shell" => {
assert_eq!(args.len(), 1);
shell(stack, &args[0], eval_context)
}
_ => bail!("function not implemented: {}", name),
}
}
mod text {
use super::*;
pub fn subst(
stack: &MacroScopeStack,
from: &TokenString,
to: &TokenString,
text: &TokenString,
mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let from = stack.expand(from, eval_context.as_deref_mut())?;
let to = stack.expand(to, eval_context.as_deref_mut())?;
let text = stack.expand(text, eval_context)?;
Ok(text.replace(&from, &to))
}
pub fn patsubst(
stack: &MacroScopeStack,
from: &TokenString,
to: &TokenString,
text: &TokenString,
mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let from = stack.expand(from, eval_context.as_deref_mut())?;
let to = stack.expand(to, eval_context.as_deref_mut())?;
let text = stack.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::<Result<Vec<_>>>()?;
Ok(words.join(" "))
}
pub fn strip(
stack: &MacroScopeStack,
text: &TokenString,
eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let text = stack.expand(text, eval_context)?;
let words = text.split_whitespace().collect::<Vec<_>>();
Ok(words.join(" "))
}
pub fn findstring(
stack: &MacroScopeStack,
needle: &TokenString,
haystack: &TokenString,
mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let needle = stack.expand(needle, eval_context.as_deref_mut())?;
let haystack = stack.expand(haystack, eval_context)?;
if haystack.contains(&needle) {
Ok(needle)
} else {
Ok(String::new())
}
}
pub fn filter(
stack: &MacroScopeStack,
patterns: &TokenString,
text: &TokenString,
mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let patterns = stack.expand(patterns, eval_context.as_deref_mut())?;
let patterns = patterns.split_whitespace().collect::<Vec<_>>();
let text = stack.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(
stack: &MacroScopeStack,
patterns: &TokenString,
text: &TokenString,
mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let patterns = stack.expand(patterns, eval_context.as_deref_mut())?;
let patterns = patterns.split_whitespace().collect::<Vec<_>>();
let text = stack.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(
stack: &MacroScopeStack,
words: &TokenString,
eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let words = stack.expand(words, eval_context)?;
let mut words = words.split_whitespace().collect::<Vec<_>>();
words.sort_unstable();
words.dedup();
Ok(words.join(" "))
}
pub fn word(
stack: &MacroScopeStack,
n: &TokenString,
text: &TokenString,
mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let n = stack.expand(n, eval_context.as_deref_mut())?;
let n: usize = n.parse().wrap_err("while calling `word`")?;
let text = stack.expand(text, eval_context)?;
Ok(text
.split_whitespace()
.nth(n.saturating_add(1))
.unwrap_or("")
.to_owned())
}
pub fn words(
stack: &MacroScopeStack,
words: &TokenString,
eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let words = stack.expand(words, eval_context)?;
Ok(words.split_whitespace().count().to_string())
}
pub fn firstword(
stack: &MacroScopeStack,
words: &TokenString,
eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let words = stack.expand(words, eval_context)?;
Ok(words.split_whitespace().next().unwrap_or("").to_owned())
}
pub fn lastword(
stack: &MacroScopeStack,
words: &TokenString,
eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let words = stack.expand(words, eval_context)?;
Ok(words.split_whitespace().last().unwrap_or("").to_owned())
}
}
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(
stack: &MacroScopeStack,
words: &TokenString,
eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let words = stack.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::<Vec<_>>();
Ok(words.join(" "))
}
pub fn notdir(
stack: &MacroScopeStack,
words: &TokenString,
eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let words = stack.expand(words, eval_context)?;
let words = words
.split_whitespace()
.map(|word| {
Path::new(word)
.file_name()
.and_then(OsStr::to_str)
.unwrap_or("")
})
.collect::<Vec<_>>();
Ok(words.join(" "))
}
pub fn basename(
stack: &MacroScopeStack,
words: &TokenString,
eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let words = stack.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::<Vec<_>>();
Ok(words.join(" "))
}
pub fn addsuffix(
stack: &MacroScopeStack,
suffix: &TokenString,
targets: &TokenString,
mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let suffix = stack.expand(suffix, eval_context.as_deref_mut())?;
let targets = stack.expand(targets, eval_context)?;
let results = targets
.split_whitespace()
.map(|t| format!("{}{}", t, suffix))
.collect::<Vec<_>>();
Ok(results.join(" "))
}
pub fn addprefix(
stack: &MacroScopeStack,
prefix: &TokenString,
targets: &TokenString,
mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let prefix = stack.expand(prefix, eval_context.as_deref_mut())?;
let targets = stack.expand(targets, eval_context)?;
let results = targets
.split_whitespace()
.map(|t| format!("{}{}", prefix, t))
.collect::<Vec<_>>();
Ok(results.join(" "))
}
pub fn wildcard(
stack: &MacroScopeStack,
pattern: &TokenString,
eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let pattern = stack.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::<Vec<_>>();
Ok(results.join(" "))
}
pub fn realpath(
stack: &MacroScopeStack,
targets: &TokenString,
eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let targets = stack.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::<Vec<_>>();
Ok(results.join(" "))
}
pub fn abspath(
stack: &MacroScopeStack,
targets: &TokenString,
eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
realpath(stack, targets, eval_context)
}
}
mod conditional {
use super::*;
pub fn r#if(
stack: &MacroScopeStack,
condition: &TokenString,
if_true: &TokenString,
if_false: Option<&TokenString>,
mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let mut condition = condition.clone();
condition.trim_start();
condition.trim_end();
let condition = stack.expand(&condition, eval_context.as_deref_mut())?;
if condition.is_empty() {
if_false.map_or_else(
|| Ok(String::new()),
|if_false| stack.expand(if_false, eval_context),
)
} else {
stack.expand(if_true, eval_context)
}
}
pub fn or<'a>(
stack: &MacroScopeStack,
args: impl Iterator<Item = &'a TokenString>,
mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
for arg in args {
let arg = stack.expand(arg, eval_context.as_deref_mut())?;
if !arg.is_empty() {
return Ok(arg);
}
}
Ok(String::new())
}
pub fn and<'a>(
stack: &MacroScopeStack,
args: impl Iterator<Item = &'a TokenString>,
mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let mut last = String::new();
for arg in args {
last = stack.expand(arg, eval_context.as_deref_mut())?;
if last.is_empty() {
return Ok(String::new());
}
}
Ok(last)
}
}
pub fn foreach(
stack: &MacroScopeStack,
var: &TokenString,
list: &TokenString,
text: &TokenString,
mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let var = stack.expand(var, eval_context.as_deref_mut())?;
let list = stack.expand(list, eval_context.as_deref_mut())?;
let words = list.split_whitespace();
let results = words
.map(|word| {
let mut macros = MacroSet::new();
macros.set(
var.clone(),
Macro {
source: ItemSource::FunctionCall,
text: TokenString::text(word),
#[cfg(feature = "full")]
eagerly_expanded: false,
},
);
stack
.with_scope(¯os)
.expand(text, eval_context.as_deref_mut())
})
.collect::<Result<Vec<_>, _>>()?;
Ok(results.join(" "))
}
pub fn call<'a>(
stack: &MacroScopeStack,
args: impl Iterator<Item = &'a TokenString>,
mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let args = args
.map(|arg| stack.expand(arg, eval_context.as_deref_mut()))
.collect::<Result<Vec<_>, _>>()?;
let function = args[0].clone();
let mut macros = MacroSet::new();
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,
},
);
}
stack
.with_scope(¯os)
.expand(&TokenString::r#macro(function), eval_context)
}
pub fn eval(
stack: &MacroScopeStack,
arg: &TokenString,
eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
stack.expand(arg, eval_context)
}
pub fn origin(
stack: &MacroScopeStack,
variable: &TokenString,
eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let variable = stack.expand(variable, eval_context)?;
Ok(stack.origin(&variable).to_owned())
}
mod meta {
use super::*;
pub fn error(
stack: &MacroScopeStack,
text: &TokenString,
eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let text = stack.expand(text, eval_context)?;
bail!("{}", text);
}
}
pub fn shell(
stack: &MacroScopeStack,
command: &TokenString,
eval_context: Option<&mut DeferredEvalContext<impl BufRead>>,
) -> Result<String> {
let command = stack.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;
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<String> {
let stack = MacroScopeStack::default().with_scope(macros);
expand_call(name, args, &stack, 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(())
}
}