Add macro definition and substitution support
This commit is contained in:
parent
6180a8f7e9
commit
0c9d4a27ce
38
src/main.rs
38
src/main.rs
|
|
@ -299,6 +299,8 @@ fn run_one(
|
|||
let mut user_env: Vec<(String, String)> = vec![];
|
||||
let mut setup_commands: Vec<Command> = vec![];
|
||||
let mut teardown_commands: Vec<Command> = parsed.teardown_commands.clone();
|
||||
let mut setup_defs: Vec<(String, String)> = vec![];
|
||||
let mut user_defs: Vec<(String, String)> = vec![];
|
||||
|
||||
for d in &parsed.directives {
|
||||
match d {
|
||||
|
|
@ -322,9 +324,15 @@ fn run_one(
|
|||
}
|
||||
};
|
||||
for sd in &setup_parsed.directives {
|
||||
if let Directive::Env { key, value, .. } = sd {
|
||||
match sd {
|
||||
Directive::Env { key, value, .. } => {
|
||||
setup_env.push((key.clone(), value.clone()));
|
||||
}
|
||||
Directive::Def { name, body, .. } => {
|
||||
setup_defs.push((name.clone(), body.clone()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
setup_commands.extend(setup_parsed.commands);
|
||||
teardown_commands.extend(setup_parsed.teardown_commands);
|
||||
|
|
@ -332,8 +340,20 @@ fn run_one(
|
|||
Directive::Env { key, value, .. } => {
|
||||
user_env.push((key.clone(), value.clone()));
|
||||
}
|
||||
Directive::Def { name, body, .. } => {
|
||||
user_defs.push((name.clone(), body.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge defs: setup first, then user overrides
|
||||
let mut defs: std::collections::HashMap<String, String> = std::collections::HashMap::new();
|
||||
for (name, body) in &setup_defs {
|
||||
defs.insert(name.clone(), body.clone());
|
||||
}
|
||||
for (name, body) in &user_defs {
|
||||
defs.insert(name.clone(), body.clone());
|
||||
}
|
||||
|
||||
// Merge env: setup first, then user overrides
|
||||
let mut env_vars: Vec<(String, String)> = vec![];
|
||||
|
|
@ -349,10 +369,22 @@ fn run_one(
|
|||
env_vars.push(("PORT".to_string(), port.to_string()));
|
||||
}
|
||||
|
||||
// Apply macro substitutions
|
||||
let apply_defs = |commands: &mut Vec<Command>| {
|
||||
for cmd in commands.iter_mut() {
|
||||
if let Some(body) = defs.get(&cmd.command) {
|
||||
cmd.command = body.clone();
|
||||
}
|
||||
}
|
||||
};
|
||||
let mut user_commands = parsed.commands.clone();
|
||||
apply_defs(&mut setup_commands);
|
||||
apply_defs(&mut user_commands);
|
||||
|
||||
// Merge commands: setup + user + teardown
|
||||
let mut merged_commands = Vec::new();
|
||||
merged_commands.extend(setup_commands.clone());
|
||||
merged_commands.extend(parsed.commands.clone());
|
||||
merged_commands.extend(user_commands.clone());
|
||||
merged_commands.extend(teardown_commands.clone());
|
||||
|
||||
let merged = ShoutFile {
|
||||
|
|
@ -363,7 +395,7 @@ fn run_one(
|
|||
};
|
||||
|
||||
let setup_len = setup_commands.len();
|
||||
let user_len = parsed.commands.len();
|
||||
let user_len = user_commands.len();
|
||||
|
||||
let run_opts = RunOptions {
|
||||
clean_env: opts.clean_env,
|
||||
|
|
|
|||
46
src/parse.rs
46
src/parse.rs
|
|
@ -25,6 +25,7 @@ pub enum ExitCode {
|
|||
pub enum Directive {
|
||||
Setup { path: String, line: usize },
|
||||
Env { key: String, value: String, line: usize },
|
||||
Def { name: String, body: String, line: usize },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -109,6 +110,29 @@ fn parse_env_directive(path: &str, line: &str, line_num: usize) -> Result<(Strin
|
|||
}
|
||||
}
|
||||
|
||||
/// Parse `@def name body` with backslash continuation support.
|
||||
/// Returns (name, body, number_of_lines_consumed).
|
||||
fn parse_def_directive(path: &str, rest: &str, line_num: usize, lines: &[&str]) -> Result<(String, String, usize), ParseError> {
|
||||
let rest = rest.trim();
|
||||
let space = rest.find(' ').ok_or_else(|| {
|
||||
ParseError(format!("{}:{}: @def requires a name and body", path, line_num))
|
||||
})?;
|
||||
let name = rest[..space].to_string();
|
||||
let mut body = rest[space + 1..].to_string();
|
||||
let mut consumed = 1;
|
||||
|
||||
// Handle backslash continuation
|
||||
while body.ends_with('\\') {
|
||||
body.pop(); // remove trailing backslash
|
||||
consumed += 1;
|
||||
if consumed <= lines.len() {
|
||||
body.push_str(lines[consumed - 1].trim_start());
|
||||
}
|
||||
}
|
||||
|
||||
Ok((name, body, consumed))
|
||||
}
|
||||
|
||||
fn finalize_command(cmd: &mut Command) {
|
||||
let trimmed = trim_trailing_empty(&cmd.expected);
|
||||
let (expected, exit_code) = parse_exit_code(&trimmed);
|
||||
|
|
@ -128,10 +152,13 @@ pub fn parse_setup(path: &str, content: &str) -> Result<ShoutFile, ParseError> {
|
|||
let mut teardown_commands = Vec::new();
|
||||
let mut directives = Vec::new();
|
||||
|
||||
for (i, &line) in raw_lines.iter().enumerate() {
|
||||
let mut i = 0;
|
||||
while i < raw_lines.len() {
|
||||
let line = raw_lines[i];
|
||||
let line_num = i + 1;
|
||||
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -139,6 +166,11 @@ pub fn parse_setup(path: &str, content: &str) -> Result<ShoutFile, ParseError> {
|
|||
let _ = rest; // parsed below
|
||||
let (key, value) = parse_env_directive(path, line, line_num)?;
|
||||
directives.push(Directive::Env { key, value, line: line_num });
|
||||
} else if let Some(rest) = line.strip_prefix("@def ") {
|
||||
let (name, body, lines_consumed) = parse_def_directive(path, rest, line_num, &raw_lines[i..])?;
|
||||
directives.push(Directive::Def { name, body, line: line_num });
|
||||
i += lines_consumed;
|
||||
continue;
|
||||
} else if let Some(rest) = line.strip_prefix("@teardown ") {
|
||||
if rest.trim().is_empty() {
|
||||
return Err(ParseError(format!("{}:{}: @teardown requires a command", path, line_num)));
|
||||
|
|
@ -163,6 +195,7 @@ pub fn parse_setup(path: &str, content: &str) -> Result<ShoutFile, ParseError> {
|
|||
exit_code: ExitCode::Default,
|
||||
});
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
Ok(ShoutFile {
|
||||
|
|
@ -187,7 +220,9 @@ pub fn parse(path: &str, content: &str) -> Result<ShoutFile, ParseError> {
|
|||
let mut current: Option<Command> = None;
|
||||
let mut seen_command = false;
|
||||
|
||||
for (i, &line) in raw_lines.iter().enumerate() {
|
||||
let mut i = 0;
|
||||
while i < raw_lines.len() {
|
||||
let line = raw_lines[i];
|
||||
let line_num = i + 1;
|
||||
|
||||
// Directives (before first command)
|
||||
|
|
@ -198,6 +233,11 @@ pub fn parse(path: &str, content: &str) -> Result<ShoutFile, ParseError> {
|
|||
return Err(ParseError(format!("{}:{}: @setup requires a file path", path, line_num)));
|
||||
}
|
||||
directives.push(Directive::Setup { path: setup_path.to_string(), line: line_num });
|
||||
} else if let Some(rest) = line.strip_prefix("@def ") {
|
||||
let (name, body, lines_consumed) = parse_def_directive(path, rest, line_num, &raw_lines[i..])?;
|
||||
directives.push(Directive::Def { name, body, line: line_num });
|
||||
i += lines_consumed;
|
||||
continue;
|
||||
} else if let Some(rest) = line.strip_prefix("@teardown ") {
|
||||
if rest.trim().is_empty() {
|
||||
return Err(ParseError(format!("{}:{}: @teardown requires a command", path, line_num)));
|
||||
|
|
@ -215,6 +255,7 @@ pub fn parse(path: &str, content: &str) -> Result<ShoutFile, ParseError> {
|
|||
} else {
|
||||
return Err(ParseError(format!("{}:{}: unknown directive: {}", path, line_num, line)));
|
||||
}
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -244,6 +285,7 @@ pub fn parse(path: &str, content: &str) -> Result<ShoutFile, ParseError> {
|
|||
} else if let Some(ref mut cmd) = current {
|
||||
cmd.expected.push(line.to_string());
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if let Some(ref mut cmd) = current {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user