use crate::attr::{parse_attributes, Attributes, ParseAttrError};
use crate::error::ParseError;
use crate::include::IncludeContext;
use crate::lex::Token;
use crate::var::{builtin_scope, import_env, Precedence, Scope};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq)]
pub enum Stmt {
Rule(Rule),
Assign(Assign),
}
#[derive(Debug, Clone, PartialEq)]
pub struct Rule {
pub targets: Vec<String>,
pub prereqs: Vec<String>,
pub attributes: Attributes,
pub recipe: Option<String>,
pub is_metarule: bool,
pub is_regex: bool,
pub line: usize,
pub prog: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Assign {
pub name: String,
pub value: String,
}
pub fn parse(tokens: &[Token]) -> Result<Vec<Stmt>, ParseError> {
let mut scope = builtin_scope();
import_env(&mut scope);
parse_with_scope(tokens, &mut scope)
}
pub fn parse_with_scope(tokens: &[Token], scope: &mut Scope) -> Result<Vec<Stmt>, ParseError> {
parse_with_includes(
tokens,
&mut IncludeContext::new(),
&std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
scope,
)
}
fn parse_with_includes(
tokens: &[Token],
ctx: &mut IncludeContext,
base_dir: &Path,
scope: &mut Scope,
) -> Result<Vec<Stmt>, ParseError> {
let mut stmts = Vec::new();
let mut pos: usize = 0;
let mut line: usize = 1;
while pos < tokens.len() && tokens[pos] != Token::Eof {
match &tokens[pos] {
Token::Include => {
pos += 1;
if pos < tokens.len() && tokens[pos] == Token::Pipe {
pos += 1;
let mut cmd_parts = Vec::new();
while pos < tokens.len() && matches!(&tokens[pos], Token::Word(_)) {
if let Token::Word(ref w) = tokens[pos] {
cmd_parts.push(w.clone());
}
pos += 1;
}
let command = cmd_parts.join(" ");
match ctx.include_command(&command, base_dir, scope) {
Ok(included_stmts) => {
stmts.extend(included_stmts);
}
Err(e) => {
return Err(ParseError::UnexpectedToken {
expected: "valid command".into(),
got: e.to_string(),
line,
});
}
}
} else {
let mut path_parts = Vec::new();
while pos < tokens.len() && matches!(&tokens[pos], Token::Word(_)) {
if let Token::Word(ref w) = tokens[pos] {
path_parts.push(w.clone());
}
pos += 1;
}
let path = path_parts.join(" ");
match ctx.include_file(&path, base_dir, scope) {
Ok(included_stmts) => {
stmts.extend(included_stmts);
}
Err(e) => {
return Err(ParseError::UnexpectedToken {
expected: "valid include path".into(),
got: e.to_string(),
line,
});
}
}
}
if pos < tokens.len() && tokens[pos] == Token::Newline {
pos += 1;
line += 1;
}
}
Token::Newline => {
line += 1;
pos += 1;
}
Token::Indent | Token::Pipe => {
pos = skip_logical_line(tokens, pos, &mut line);
}
Token::Word(_) => {
if is_assignment(tokens, pos) {
let assign = parse_assign(tokens, &mut pos, &mut line, scope)?;
stmts.push(Stmt::Assign(assign));
} else {
let rule = parse_rule(tokens, &mut pos, &mut line, scope)?;
stmts.push(Stmt::Rule(rule));
}
}
_ => {
let rule = parse_rule(tokens, &mut pos, &mut line, scope)?;
stmts.push(Stmt::Rule(rule));
}
}
}
Ok(stmts)
}
fn is_assignment(tokens: &[Token], start: usize) -> bool {
let mut i = start;
while i < tokens.len() {
match &tokens[i] {
Token::Equals => return true,
Token::Colon => return false,
Token::Newline | Token::Eof => return false,
_ => i += 1,
}
}
false
}
fn skip_logical_line(tokens: &[Token], mut pos: usize, line: &mut usize) -> usize {
while pos < tokens.len() {
match &tokens[pos] {
Token::Newline => {
*line += 1;
return pos + 1;
}
Token::Eof => return pos,
_ => pos += 1,
}
}
pos
}
fn parse_assign(
tokens: &[Token],
pos: &mut usize,
line: &mut usize,
scope: &mut Scope,
) -> Result<Assign, ParseError> {
let name = match &tokens[*pos] {
Token::Word(s) => s.clone(),
_ => {
return Err(ParseError::UnexpectedToken {
expected: "variable name".into(),
got: token_name(&tokens[*pos]),
line: *line,
});
}
};
*pos += 1;
if !matches!(&tokens[*pos], Token::Equals) {
return Err(ParseError::UnexpectedToken {
expected: "=".into(),
got: token_name(&tokens[*pos]),
line: *line,
});
}
*pos += 1;
let mut parts: Vec<&str> = Vec::new();
while *pos < tokens.len() && matches!(&tokens[*pos], Token::Word(_)) {
if let Token::Word(s) = &tokens[*pos] {
parts.push(s);
}
*pos += 1;
}
let raw_value = parts.join(" ");
scope.set(&name, &raw_value, Precedence::Mkfile);
let expanded_value = scope.get(&name).unwrap_or(&raw_value).to_string();
if *pos < tokens.len() && matches!(&tokens[*pos], Token::Newline) {
*pos += 1;
*line += 1;
}
Ok(Assign {
name,
value: expanded_value,
})
}
fn parse_rule(
tokens: &[Token],
pos: &mut usize,
line: &mut usize,
scope: &mut Scope,
) -> Result<Rule, ParseError> {
let start_line = *line;
let mut raw_targets: Vec<String> = Vec::new();
while *pos < tokens.len() && matches!(&tokens[*pos], Token::Word(_)) {
if let Token::Word(s) = &tokens[*pos] {
raw_targets.push(s.clone());
}
*pos += 1;
}
if raw_targets.is_empty() {
return Err(ParseError::EmptyTarget { line: start_line });
}
if *pos >= tokens.len() || !matches!(&tokens[*pos], Token::Colon) {
return Err(ParseError::ExpectedColon { line: start_line });
}
*pos += 1;
let mut attrs = Attributes::new();
let mut prog: Option<String> = None;
if *pos < tokens.len()
&& matches!(&tokens[*pos], Token::Word(_))
&& *pos + 1 < tokens.len()
&& matches!(&tokens[*pos + 1], Token::Colon)
{
if let Token::Word(attr_str) = &tokens[*pos] {
let (clean_attr_str, extracted_prog) = extract_prog_from_attr(attr_str);
prog = extracted_prog;
attrs = parse_attributes(&clean_attr_str).map_err(|e| match e {
ParseAttrError::UnknownAttr(c) => ParseError::UnknownAttr {
attr: c,
line: start_line,
},
})?;
}
*pos += 2; }
let mut raw_prereqs: Vec<String> = Vec::new();
while *pos < tokens.len() && matches!(&tokens[*pos], Token::Word(_)) {
if let Token::Word(s) = &tokens[*pos] {
raw_prereqs.push(s.clone());
}
*pos += 1;
}
for prereq in &raw_prereqs {
if prereq.contains("$(") {
return Err(ParseError::UnexpectedToken {
expected: "mk glob pattern (e.g., '*.txt' or 'dir/*.c')".into(),
got: format!(
"GNU Make syntax $(...) in prereq '{}' is not supported",
prereq
),
line: start_line,
});
}
}
let targets: Vec<String> = expand_and_split(&raw_targets, scope);
let prereqs: Vec<String> = expand_and_split(&raw_prereqs, scope);
if *pos < tokens.len() && matches!(&tokens[*pos], Token::Newline) {
*pos += 1;
*line += 1;
}
let recipe = parse_recipe(tokens, pos, line);
let is_metarule = targets.iter().any(|t| t.contains('%') || t.contains('&'));
let is_regex = attrs.is_regex();
Ok(Rule {
targets,
prereqs,
attributes: attrs,
recipe,
is_metarule,
is_regex,
line: start_line,
prog,
})
}
const VALID_ATTR_CHARS: &[char] = &['V', 'Q', 'N', 'U', 'D', 'E', 'P', 'R', 'n'];
fn extract_prog_from_attr(attr_str: &str) -> (String, Option<String>) {
if let Some(p_pos) = attr_str.find('P') {
let before = &attr_str[..p_pos];
let after = &attr_str[p_pos + 1..];
let prog_end = after
.find(|c: char| VALID_ATTR_CHARS.contains(&c))
.unwrap_or(after.len());
let prog = &after[..prog_end];
let clean = format!("{}P{}", before, &after[prog_end..]);
let prog = if prog.is_empty() {
None
} else {
Some(prog.to_string())
};
(clean, prog)
} else {
(attr_str.to_string(), None)
}
}
fn expand_and_split(raw_words: &[String], scope: &mut Scope) -> Vec<String> {
let mut result = Vec::new();
for word in raw_words {
let expanded = scope.expand(word);
for part in expanded.split_whitespace() {
if !part.is_empty() {
result.push(part.to_string());
}
}
}
result
}
fn parse_recipe(tokens: &[Token], pos: &mut usize, line: &mut usize) -> Option<String> {
let mut recipe_lines: Vec<String> = Vec::new();
while *pos < tokens.len() && matches!(&tokens[*pos], Token::Indent) {
*pos += 1;
let mut words: Vec<&str> = Vec::new();
while *pos < tokens.len() && matches!(&tokens[*pos], Token::Word(_)) {
if let Token::Word(s) = &tokens[*pos] {
words.push(s);
}
*pos += 1;
}
if !words.is_empty() {
recipe_lines.push(words.join(" "));
}
if *pos < tokens.len() && matches!(&tokens[*pos], Token::Newline) {
*pos += 1;
*line += 1;
} else {
break;
}
}
if recipe_lines.is_empty() {
None
} else {
Some(recipe_lines.join("\n"))
}
}
fn token_name(tok: &Token) -> String {
match tok {
Token::Word(s) => format!("word '{s}'"),
Token::Colon => "colon".into(),
Token::Equals => "equals".into(),
Token::Include => "include".into(),
Token::Pipe => "pipe".into(),
Token::Indent => "indent".into(),
Token::Newline => "newline".into(),
Token::Eof => "end of file".into(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lex::{tokenize, ShellMode};
fn parse_str(input: &str) -> Result<Vec<Stmt>, ParseError> {
let tokens = tokenize(input, ShellMode::Sh).unwrap();
parse(&tokens)
}
#[test]
fn simple_rule() {
let stmts = parse_str("target: prereq\n").unwrap();
assert_eq!(stmts.len(), 1);
match &stmts[0] {
Stmt::Rule(r) => {
assert_eq!(r.targets, vec!["target"]);
assert_eq!(r.prereqs, vec!["prereq"]);
assert!(r.recipe.is_none());
assert!(!r.is_metarule);
}
_ => panic!("expected Rule"),
}
}
#[test]
fn rule_with_recipe() {
let stmts = parse_str("target: prereq\n\techo hello\n").unwrap();
match &stmts[0] {
Stmt::Rule(r) => {
assert_eq!(r.recipe, Some("echo hello".into()));
}
_ => panic!("expected Rule"),
}
}
#[test]
fn rule_multi_line_recipe() {
let stmts = parse_str("target: prereq\n\techo one\n\techo two\n").unwrap();
match &stmts[0] {
Stmt::Rule(r) => {
assert_eq!(r.recipe, Some("echo one\necho two".into()));
}
_ => panic!("expected Rule"),
}
}
#[test]
fn rule_without_prereqs() {
let stmts = parse_str("target:\n\techo hi\n").unwrap();
match &stmts[0] {
Stmt::Rule(r) => {
assert_eq!(r.prereqs, Vec::<String>::new());
assert_eq!(r.recipe, Some("echo hi".into()));
}
_ => panic!("expected Rule"),
}
}
#[test]
fn rule_with_virtual_attr() {
let stmts = parse_str("target:V: prereq\n").unwrap();
match &stmts[0] {
Stmt::Rule(r) => {
assert!(r.attributes.is_virtual());
assert!(!r.is_metarule);
}
_ => panic!("expected Rule"),
}
}
#[test]
fn rule_with_multiple_attrs() {
let stmts = parse_str("target:VQ: prereq\n").unwrap();
match &stmts[0] {
Stmt::Rule(r) => {
assert!(r.attributes.is_virtual());
assert!(r.attributes.is_quiet());
}
_ => panic!("expected Rule"),
}
}
#[test]
fn assignment() {
let stmts = parse_str("CC = gcc\n").unwrap();
match &stmts[0] {
Stmt::Assign(a) => {
assert_eq!(a.name, "CC");
assert_eq!(a.value, "gcc");
}
_ => panic!("expected Assign"),
}
}
#[test]
fn assignment_multi_word_value() {
let stmts = parse_str("CFLAGS = -Wall -O2\n").unwrap();
match &stmts[0] {
Stmt::Assign(a) => {
assert_eq!(a.name, "CFLAGS");
assert_eq!(a.value, "-Wall -O2");
}
_ => panic!("expected Assign"),
}
}
#[test]
fn multiple_rules() {
let stmts = parse_str("a: b\n\techo a\n\nc: d\n\techo c\n").unwrap();
assert_eq!(stmts.len(), 2);
}
#[test]
fn rule_with_multiple_targets() {
let stmts = parse_str("a b: c d\n").unwrap();
match &stmts[0] {
Stmt::Rule(r) => {
assert_eq!(r.targets, vec!["a", "b"]);
assert_eq!(r.prereqs, vec!["c", "d"]);
}
_ => panic!("expected Rule"),
}
}
#[test]
fn rule_with_multiple_prereqs() {
let stmts = parse_str("prog: a.o b.o c.o\n").unwrap();
match &stmts[0] {
Stmt::Rule(r) => {
assert_eq!(r.prereqs, vec!["a.o", "b.o", "c.o"]);
}
_ => panic!("expected Rule"),
}
}
#[test]
fn missing_colon() {
let result = parse_str("target prereq\n");
assert!(result.is_err());
}
#[test]
fn empty_target() {
let result = parse_str(": prereq\n");
assert!(result.is_err());
}
#[test]
fn empty_input() {
let stmts = parse_str("").unwrap();
assert!(stmts.is_empty());
}
#[test]
fn only_comments() {
let stmts = parse_str("# just a comment\n").unwrap();
assert!(stmts.is_empty());
}
#[test]
fn blank_lines_terminate_recipe() {
let input = "target: prereq\n\techo one\n\necho standalone\n";
assert!(parse_str(input).is_err());
let valid = "target: prereq\n\techo one\n";
let stmts = parse_str(valid).unwrap();
match &stmts[0] {
Stmt::Rule(r) => {
assert_eq!(r.recipe, Some("echo one".into()));
}
_ => panic!("expected Rule"),
}
}
#[test]
fn metarule_detection() {
let stmts = parse_str("%.o: %.c\n").unwrap();
match &stmts[0] {
Stmt::Rule(r) => {
assert!(r.is_metarule);
}
_ => panic!("expected Rule"),
}
}
#[test]
fn regex_rule_detection() {
let stmts = parse_str("foo:R: bar\n").unwrap();
match &stmts[0] {
Stmt::Rule(r) => {
assert!(r.is_regex);
}
_ => panic!("expected Rule"),
}
}
#[test]
fn rule_with_line_number() {
let stmts = parse_str("# comment\ntarget: prereq\n").unwrap();
match &stmts[0] {
Stmt::Rule(r) => {
assert_eq!(r.line, 2);
}
_ => panic!("expected Rule"),
}
}
#[test]
fn include_command_parses_stdout() {
let input = "<| echo \"target: prereq\"\n";
let tokens = tokenize(input, ShellMode::Sh).unwrap();
let stmts = parse(&tokens).unwrap();
assert_eq!(stmts.len(), 1);
match &stmts[0] {
Stmt::Rule(r) => {
assert_eq!(r.targets, vec!["target"]);
assert_eq!(r.prereqs, vec!["prereq"]);
}
_ => panic!("expected Rule"),
}
}
#[test]
fn p_attribute_with_program() {
let stmts = parse_str("target:Pcmp: prereq\n").unwrap();
match &stmts[0] {
Stmt::Rule(r) => {
assert!(r.attributes.has_comparison());
assert_eq!(r.prog, Some("cmp".into()));
}
_ => panic!("expected Rule"),
}
}
#[test]
fn p_attribute_no_program() {
let stmts = parse_str("target:P: prereq\n").unwrap();
match &stmts[0] {
Stmt::Rule(r) => {
assert!(r.attributes.has_comparison());
assert_eq!(r.prog, None);
}
_ => panic!("expected Rule"),
}
}
#[test]
fn p_attribute_with_trailing_attrs() {
let stmts = parse_str("target:PcmpQ: prereq\n").unwrap();
match &stmts[0] {
Stmt::Rule(r) => {
assert!(r.attributes.has_comparison());
assert!(r.attributes.is_quiet());
assert_eq!(r.prog, Some("cmp".into()));
}
_ => panic!("expected Rule"),
}
}
#[test]
fn p_attribute_with_leading_attrs() {
let stmts = parse_str("target:VPcmp: prereq\n").unwrap();
match &stmts[0] {
Stmt::Rule(r) => {
assert!(r.attributes.is_virtual());
assert!(r.attributes.has_comparison());
assert_eq!(r.prog, Some("cmp".into()));
}
_ => panic!("expected Rule"),
}
}
#[test]
fn p_attribute_pv_means_p_and_v_attrs_no_prog() {
let stmts = parse_str("target:PV: prereq\n").unwrap();
match &stmts[0] {
Stmt::Rule(r) => {
assert!(r.attributes.has_comparison());
assert!(r.attributes.is_virtual());
assert_eq!(r.prog, None);
}
_ => panic!("expected Rule"),
}
}
#[test]
fn rule_no_newline_at_eof() {
let stmts = parse_str("target: prereq").unwrap();
match &stmts[0] {
Stmt::Rule(r) => {
assert_eq!(r.targets, vec!["target"]);
assert_eq!(r.prereqs, vec!["prereq"]);
}
_ => panic!("expected Rule"),
}
}
#[test]
fn recipe_with_equals_parsed_correctly() {
let stmts = parse_str("test:V:\n\tconst x=1\n").unwrap();
match &stmts[0] {
Stmt::Rule(r) => {
assert_eq!(r.recipe, Some("const x=1".into()));
}
_ => panic!("expected Rule"),
}
}
#[test]
fn recipe_with_node_equals_parsed_correctly() {
let stmts = parse_str("test:V:\n\tlet x=1\n").unwrap();
match &stmts[0] {
Stmt::Rule(r) => {
assert_eq!(r.recipe, Some("let x=1".into()));
}
_ => panic!("expected Rule"),
}
}
#[test]
fn rejects_gnu_make_wildcard_syntax() {
let tokens = tokenize(
"target: $(wildcard /tmp/test/*.txt)\n\techo x\n",
ShellMode::Sh,
)
.unwrap();
let result = parse(&tokens);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("$(") || err.contains("$(wildcard"),
"error should mention $(...) syntax, got: {err}"
);
}
#[test]
fn rejects_dollar_paren_in_any_prereq() {
let tokens = tokenize("target: foo $(bar) baz\n\techo x\n", ShellMode::Sh).unwrap();
let result = parse(&tokens);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("$("),
"error should mention $(...) syntax, got: {err}"
);
}
#[test]
fn allows_dollar_without_paren_in_prereq() {
let stmts = parse_str("target: $prereq\n\techo x\n").unwrap();
match &stmts[0] {
Stmt::Rule(r) => {
assert_eq!(r.prereqs, Vec::<String>::new());
}
_ => panic!("expected Rule"),
}
}
#[test]
fn allows_parentheses_without_dollar_in_prereq() {
let stmts = parse_str("target: lib.a(foo.o)\n\techo x\n").unwrap();
match &stmts[0] {
Stmt::Rule(r) => {
assert_eq!(r.prereqs, vec!["lib.a(foo.o)"]);
}
_ => panic!("expected Rule"),
}
}
#[test]
fn f045_s1_read_time_expansion() {
let input = "TARG=early\ntarget: $TARG\nTARG=late\n";
let stmts = parse_str(input).unwrap();
let rule_stmts: Vec<_> = stmts
.iter()
.filter_map(|s| if let Stmt::Rule(r) = s { Some(r) } else { None })
.collect();
assert_eq!(rule_stmts.len(), 1);
assert_eq!(rule_stmts[0].prereqs, vec!["early"]);
}
#[test]
fn f045_s2_assign_rhs_expanded() {
let stmts = parse_str("A=world\nB=hello $A\n").unwrap();
let assigns: Vec<_> = stmts
.iter()
.filter_map(|s| {
if let Stmt::Assign(a) = s {
Some((a.name.as_str(), a.value.as_str()))
} else {
None
}
})
.collect();
assert_eq!(assigns, vec![("A", "world"), ("B", "hello world")]);
}
#[test]
fn f045_s4_var_is_word_list() {
let input = "SRCS=a.c b.c\ntarget: $SRCS\n";
let stmts = parse_str(input).unwrap();
let rule = stmts
.iter()
.find_map(|s| if let Stmt::Rule(r) = s { Some(r) } else { None })
.unwrap();
assert_eq!(rule.prereqs, vec!["a.c", "b.c"]);
}
#[test]
fn f045_s4_var_is_word_list_targets() {
let input = "TARGS=x y z\n$TARGS:\n";
let stmts = parse_str(input).unwrap();
let rule = stmts
.iter()
.find_map(|s| if let Stmt::Rule(r) = s { Some(r) } else { None })
.unwrap();
assert_eq!(rule.targets, vec!["x", "y", "z"]);
}
#[test]
fn f045_s5_namelist_in_header() {
let input = "SRC=a.c b.c\ntarget: ${SRC:%.c=%.o}\n";
let stmts = parse_str(input).unwrap();
let rule = stmts
.iter()
.find_map(|s| if let Stmt::Rule(r) = s { Some(r) } else { None })
.unwrap();
assert_eq!(rule.prereqs, vec!["a.o", "b.o"]);
}
#[test]
fn f045_s6_dollar_dollar_in_header() {
let input = "SRCS=a.c\ntarget: $$SRCS\n";
let stmts = parse_str(input).unwrap();
let rule = stmts
.iter()
.find_map(|s| if let Stmt::Rule(r) = s { Some(r) } else { None })
.unwrap();
assert_eq!(rule.prereqs, vec!["a.c"]);
}
#[test]
fn f045_s7_recipe_var_in_header_empties() {
let input = "target: $prereq $stem $target\n\techo hi\n";
let stmts = parse_str(input).unwrap();
let rule = stmts
.iter()
.find_map(|s| if let Stmt::Rule(r) = s { Some(r) } else { None })
.unwrap();
assert_eq!(rule.prereqs, Vec::<String>::new());
}
#[test]
fn f045_s9_var_target_before_attrs_no_attr_expand() {
let input = "objtype=x86\n${objtype}l.h:Q:\n";
let stmts = parse_str(input).unwrap();
let rule = stmts
.iter()
.find_map(|s| if let Stmt::Rule(r) = s { Some(r) } else { None })
.unwrap();
assert_eq!(rule.targets, vec!["x86l.h"]);
assert!(rule.attributes.is_quiet());
}
#[test]
fn f045_s13_var_to_metarule() {
let input = "PAT=%.o\n$PAT: %.c\n";
let stmts = parse_str(input).unwrap();
let rule = stmts
.iter()
.find_map(|s| if let Stmt::Rule(r) = s { Some(r) } else { None })
.unwrap();
assert!(
rule.is_metarule,
"variable expanding to %-pattern should be metarule"
);
assert_eq!(rule.targets, vec!["%.o"]);
}
#[test]
fn f045_s13_var_to_multi_target() {
let input = "NAMES=alpha beta\n$NAMES:\n";
let stmts = parse_str(input).unwrap();
let rule = stmts
.iter()
.find_map(|s| if let Stmt::Rule(r) = s { Some(r) } else { None })
.unwrap();
assert_eq!(rule.targets, vec!["alpha", "beta"]);
}
#[test]
fn f045_s11a_whole_word_var_many_targets() {
let input = "FILES=a.c b.c c.c\n$FILES: deps\n";
let stmts = parse_str(input).unwrap();
let rule = stmts
.iter()
.find_map(|s| if let Stmt::Rule(r) = s { Some(r) } else { None })
.unwrap();
assert_eq!(rule.targets, vec!["a.c", "b.c", "c.c"]);
}
#[test]
fn f045_s11b_literal_glue_prefix() {
let input = "PARTS=one two\ntarget: pre.$PARTS\n";
let stmts = parse_str(input).unwrap();
let rule = stmts
.iter()
.find_map(|s| if let Stmt::Rule(r) = s { Some(r) } else { None })
.unwrap();
assert_eq!(rule.prereqs, vec!["pre.one", "two"]);
}
#[test]
fn f045_assign_time_order_matters() {
let input = "GREETING=$FIRST world\nFIRST=hello\n";
let stmts = parse_str(input).unwrap();
let assigns: Vec<_> = stmts
.iter()
.filter_map(|s| {
if let Stmt::Assign(a) = s {
Some((a.name.as_str(), a.value.as_str()))
} else {
None
}
})
.collect();
assert_eq!(assigns, vec![("GREETING", " world"), ("FIRST", "hello"),]);
}
#[test]
fn f045_parse_with_scope_preserves_cli_vars() {
use crate::var::builtin_scope;
let mut scope = builtin_scope();
scope.set_raw("VAR", "cli_value", super::Precedence::CommandLine);
let tokens = tokenize("VAR=mkfile_value\ntarget: $VAR\n", ShellMode::Sh).unwrap();
let stmts = parse_with_scope(&tokens, &mut scope).unwrap();
let rule = stmts
.iter()
.find_map(|s| if let Stmt::Rule(r) = s { Some(r) } else { None })
.unwrap();
assert_eq!(rule.prereqs, vec!["cli_value"]);
}
#[test]
fn f063_rc_backtick_in_assignment() {
use crate::var::builtin_scope;
let mut scope = builtin_scope();
let tokens = tokenize("A = `{echo one two}\n", ShellMode::Sh).unwrap();
parse_with_scope(&tokens, &mut scope).unwrap();
assert_eq!(scope.get("A"), Some("one two"));
}
#[test]
fn f063_rc_backtick_in_prereq_indirect() {
use crate::var::builtin_scope;
let mut scope = builtin_scope();
let input = "A = `{echo x.c y.c}\nt: $A\n";
let tokens = tokenize(input, ShellMode::Sh).unwrap();
let stmts = parse_with_scope(&tokens, &mut scope).unwrap();
let rule = stmts
.iter()
.find_map(|s| if let Stmt::Rule(r) = s { Some(r) } else { None })
.unwrap();
assert_eq!(rule.prereqs, vec!["x.c", "y.c"]);
}
#[test]
fn f063_rc_backtick_end_to_end() {
use crate::var::builtin_scope;
let mut scope = builtin_scope();
let input = "DATA = `{echo a.toon b.toon}\nt: $DATA\n";
let tokens = tokenize(input, ShellMode::Sh).unwrap();
let stmts = parse_with_scope(&tokens, &mut scope).unwrap();
let rule = stmts
.iter()
.find_map(|s| if let Stmt::Rule(r) = s { Some(r) } else { None })
.unwrap();
assert_eq!(rule.prereqs, vec!["a.toon", "b.toon"]);
}
}