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 user_env: Vec<(String, String)> = vec![];
|
||||||
let mut setup_commands: Vec<Command> = vec![];
|
let mut setup_commands: Vec<Command> = vec![];
|
||||||
let mut teardown_commands: Vec<Command> = parsed.teardown_commands.clone();
|
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 {
|
for d in &parsed.directives {
|
||||||
match d {
|
match d {
|
||||||
|
|
@ -322,9 +324,15 @@ fn run_one(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
for sd in &setup_parsed.directives {
|
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()));
|
setup_env.push((key.clone(), value.clone()));
|
||||||
}
|
}
|
||||||
|
Directive::Def { name, body, .. } => {
|
||||||
|
setup_defs.push((name.clone(), body.clone()));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setup_commands.extend(setup_parsed.commands);
|
setup_commands.extend(setup_parsed.commands);
|
||||||
teardown_commands.extend(setup_parsed.teardown_commands);
|
teardown_commands.extend(setup_parsed.teardown_commands);
|
||||||
|
|
@ -332,8 +340,20 @@ fn run_one(
|
||||||
Directive::Env { key, value, .. } => {
|
Directive::Env { key, value, .. } => {
|
||||||
user_env.push((key.clone(), value.clone()));
|
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
|
// Merge env: setup first, then user overrides
|
||||||
let mut env_vars: Vec<(String, String)> = vec![];
|
let mut env_vars: Vec<(String, String)> = vec![];
|
||||||
|
|
@ -349,10 +369,22 @@ fn run_one(
|
||||||
env_vars.push(("PORT".to_string(), port.to_string()));
|
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
|
// Merge commands: setup + user + teardown
|
||||||
let mut merged_commands = Vec::new();
|
let mut merged_commands = Vec::new();
|
||||||
merged_commands.extend(setup_commands.clone());
|
merged_commands.extend(setup_commands.clone());
|
||||||
merged_commands.extend(parsed.commands.clone());
|
merged_commands.extend(user_commands.clone());
|
||||||
merged_commands.extend(teardown_commands.clone());
|
merged_commands.extend(teardown_commands.clone());
|
||||||
|
|
||||||
let merged = ShoutFile {
|
let merged = ShoutFile {
|
||||||
|
|
@ -363,7 +395,7 @@ fn run_one(
|
||||||
};
|
};
|
||||||
|
|
||||||
let setup_len = setup_commands.len();
|
let setup_len = setup_commands.len();
|
||||||
let user_len = parsed.commands.len();
|
let user_len = user_commands.len();
|
||||||
|
|
||||||
let run_opts = RunOptions {
|
let run_opts = RunOptions {
|
||||||
clean_env: opts.clean_env,
|
clean_env: opts.clean_env,
|
||||||
|
|
|
||||||
46
src/parse.rs
46
src/parse.rs
|
|
@ -25,6 +25,7 @@ pub enum ExitCode {
|
||||||
pub enum Directive {
|
pub enum Directive {
|
||||||
Setup { path: String, line: usize },
|
Setup { path: String, line: usize },
|
||||||
Env { key: String, value: String, line: usize },
|
Env { key: String, value: String, line: usize },
|
||||||
|
Def { name: String, body: String, line: usize },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[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) {
|
fn finalize_command(cmd: &mut Command) {
|
||||||
let trimmed = trim_trailing_empty(&cmd.expected);
|
let trimmed = trim_trailing_empty(&cmd.expected);
|
||||||
let (expected, exit_code) = parse_exit_code(&trimmed);
|
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 teardown_commands = Vec::new();
|
||||||
let mut directives = 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;
|
let line_num = i + 1;
|
||||||
|
|
||||||
if line.is_empty() || line.starts_with('#') {
|
if line.is_empty() || line.starts_with('#') {
|
||||||
|
i += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,6 +166,11 @@ pub fn parse_setup(path: &str, content: &str) -> Result<ShoutFile, ParseError> {
|
||||||
let _ = rest; // parsed below
|
let _ = rest; // parsed below
|
||||||
let (key, value) = parse_env_directive(path, line, line_num)?;
|
let (key, value) = parse_env_directive(path, line, line_num)?;
|
||||||
directives.push(Directive::Env { key, value, 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 ") {
|
} else if let Some(rest) = line.strip_prefix("@teardown ") {
|
||||||
if rest.trim().is_empty() {
|
if rest.trim().is_empty() {
|
||||||
return Err(ParseError(format!("{}:{}: @teardown requires a command", path, line_num)));
|
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,
|
exit_code: ExitCode::Default,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
i += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ShoutFile {
|
Ok(ShoutFile {
|
||||||
|
|
@ -187,7 +220,9 @@ pub fn parse(path: &str, content: &str) -> Result<ShoutFile, ParseError> {
|
||||||
let mut current: Option<Command> = None;
|
let mut current: Option<Command> = None;
|
||||||
let mut seen_command = false;
|
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;
|
let line_num = i + 1;
|
||||||
|
|
||||||
// Directives (before first command)
|
// 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)));
|
return Err(ParseError(format!("{}:{}: @setup requires a file path", path, line_num)));
|
||||||
}
|
}
|
||||||
directives.push(Directive::Setup { path: setup_path.to_string(), line: 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 ") {
|
} else if let Some(rest) = line.strip_prefix("@teardown ") {
|
||||||
if rest.trim().is_empty() {
|
if rest.trim().is_empty() {
|
||||||
return Err(ParseError(format!("{}:{}: @teardown requires a command", path, line_num)));
|
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 {
|
} else {
|
||||||
return Err(ParseError(format!("{}:{}: unknown directive: {}", path, line_num, line)));
|
return Err(ParseError(format!("{}:{}: unknown directive: {}", path, line_num, line)));
|
||||||
}
|
}
|
||||||
|
i += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -244,6 +285,7 @@ pub fn parse(path: &str, content: &str) -> Result<ShoutFile, ParseError> {
|
||||||
} else if let Some(ref mut cmd) = current {
|
} else if let Some(ref mut cmd) = current {
|
||||||
cmd.expected.push(line.to_string());
|
cmd.expected.push(line.to_string());
|
||||||
}
|
}
|
||||||
|
i += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref mut cmd) = current {
|
if let Some(ref mut cmd) = current {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user