From 61265bdab776a3675b587b99abfa71ee2a7a691a Mon Sep 17 00:00:00 2001 From: Jeff Dickey <216188+jdx@users.noreply.github.com> Date: Wed, 18 Sep 2024 21:48:33 -0500 Subject: [PATCH] feat: add arguments to file tasks --- .mise/tasks/filetask | 15 +- Cargo.lock | 8 +- Cargo.toml | 2 +- docs/tasks/script-tasks.md | 23 ++ e2e/cli/test_run | 11 + src/cli/alias/ls.rs | 4 +- src/cli/alias/set.rs | 4 +- src/cli/alias/unset.rs | 4 +- src/cli/plugins/link.rs | 5 +- ..._cli__plugins__ls__tests__plugin_list.snap | 1 - src/cli/run.rs | 34 ++- src/main.rs | 3 +- src/output.rs | 17 +- src/{task.rs => task/mod.rs} | 46 +++ src/task/task_script_parser.rs | 265 +++++++++++++++++ src/task_parser.rs | 267 ------------------ src/test.rs | 5 +- 17 files changed, 394 insertions(+), 320 deletions(-) rename src/{task.rs => task/mod.rs} (89%) create mode 100644 src/task/task_script_parser.rs delete mode 100644 src/task_parser.rs diff --git a/.mise/tasks/filetask b/.mise/tasks/filetask index 1c25bb9b8..36030a263 100755 --- a/.mise/tasks/filetask +++ b/.mise/tasks/filetask @@ -1,13 +1,22 @@ #!/usr/bin/env bash -# mise description="This is a test build script" +# shellcheck disable=SC2154 +#USAGE flag "-f --force" help="Overwrite existing " +#USAGE flag "-u --user " help="User to run as" +#USAGE arg "" help="The file to write" default="file.txt" + +# mise description="This is a test build script" # mise depends=["lint", "build"] # mise sources=[".test-tool-versions"] # mise outputs=["$MISE_PROJECT_ROOT/test/test-build-output.txt"] # mise env={TEST_BUILDSCRIPT_ENV_VAR = "VALID"} # mise dir="{{config_root}}" -set -euxo pipefail +set -exo pipefail cd "$MISE_PROJECT_ROOT" || exit 1 echo "running test-build script" -echo "TEST_BUILDSCRIPT_ENV_VAR: $TEST_BUILDSCRIPT_ENV_VAR" > test/test-build-output.txt +echo "TEST_BUILDSCRIPT_ENV_VAR: $TEST_BUILDSCRIPT_ENV_VAR" >test/test-build-output.txt echo "ARGS:" "$@" + +echo "usage_force: $usage_force" +echo "usage_user: $usage_user" +echo "usage_file: $usage_file" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 252639b3f..888fca256 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3226,9 +3226,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -4106,9 +4106,9 @@ checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "usage-lib" -version = "0.3.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e782a5477c492c1c900deafb948024689d2e169c603b780e269f782cd03b7b" +checksum = "53b81f1b763b6ccdd225d7609165fbd90b1407e7134d1037ef203632805a8b93" dependencies = [ "clap", "heck 0.5.0", diff --git a/Cargo.toml b/Cargo.toml index d49a310df..e9985fae5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,7 +115,7 @@ tokio = { version = "1.37.0", features = [ toml = { version = "0.8", features = ["parse"] } toml_edit = { version = "0.22", features = ["parse"] } url = "2.5.0" -usage-lib = { version = "0.3", features = ["clap"] } +usage-lib = { version = "0.5", features = ["clap"] } versions = { version = "6.2.0", features = ["serde"] } vfox = "0.1" walkdir = "2.5.0" diff --git a/docs/tasks/script-tasks.md b/docs/tasks/script-tasks.md index 2a7f1d10b..21dd5a84b 100644 --- a/docs/tasks/script-tasks.md +++ b/docs/tasks/script-tasks.md @@ -66,3 +66,26 @@ build .../.mise/tasks/build test:integration .../.mise/tasks/test/integration test:units .../.mise/tasks/test/units ``` + +### Argument parsing with usage + +[usage](https://usage.jdx.dev) spec can be used within these files to provide argument parsing, autocompletion, and help documentation. + +Here is an example of a script task that builds a Rust CLI using some of the features of usage: + +```bash +#!/usr/bin/env -S usage bash +set -e + +#USAGE flag "-c --clean" help="Clean the build directory before building" +#USAGE flag "-p --profile " help="Build with the specified profile" +#USAGE arg "" help="The target to build" + +if [ "$usage_clean" = "true" ]; then + cargo clean +fi + +cargo build --profile "${usage_profile:-debug}" --target "$usage_target" +``` + +(Note that autocompletion and help are not yet implemented in mise as of this writing but that is planned.) diff --git a/e2e/cli/test_run b/e2e/cli/test_run index 9ba644c3d..668bfd775 100644 --- a/e2e/cli/test_run +++ b/e2e/cli/test_run @@ -7,6 +7,8 @@ run = 'echo "configtask:"' run = 'echo "linting!"' [tasks.test] run = 'echo "testing!"' +[tasks.test-with-args] +run = 'echo "{{arg()}} {{flag(name="force")}} {{option(name="user")}}"' EOF mkdir -p .mise/tasks @@ -32,3 +34,12 @@ assert "cat test-e2e/test-build-output.txt" "TEST_BUILDSCRIPT_ENV_VAR: VALID ARGS: arg1 arg2" assert "mise run test arg1 arg2 arg3" "testing! arg1 arg2 arg3" +assert "mise run test-with-args foo --force --user=user" "foo true user" + +cat <<'EOF' >.mise/tasks/filetask +#!/usr/bin/env bash +#USAGE flag "-u --user " help="User to run as" + +echo "user=$usage_user" +EOF +assert "mise run filetask --user=jdx" "user=jdx" diff --git a/src/cli/alias/ls.rs b/src/cli/alias/ls.rs index 30477e9da..1b1c91536 100644 --- a/src/cli/alias/ls.rs +++ b/src/cli/alias/ls.rs @@ -76,7 +76,7 @@ mod tests { #[test] fn test_alias_ls() { reset(); - assert_cli_snapshot!("aliases", @r###" + assert_cli_snapshot!("aliases", @r#" java lts 21 node lts 20 node lts-argon 4 @@ -91,7 +91,7 @@ mod tests { tiny lts 3.1.0 tiny lts-prev 2.0.0 tiny my/alias 3.0 - "###); + "#); } #[test] diff --git a/src/cli/alias/set.rs b/src/cli/alias/set.rs index ab036a5cb..157972340 100644 --- a/src/cli/alias/set.rs +++ b/src/cli/alias/set.rs @@ -42,7 +42,7 @@ pub mod tests { reset(); assert_cli!("alias", "set", "tiny", "my/alias", "3.0"); - assert_cli_snapshot!("aliases", @r###" + assert_cli_snapshot!("aliases", @r#" java lts 21 node lts 20 node lts-argon 4 @@ -57,7 +57,7 @@ pub mod tests { tiny lts 3.1.0 tiny lts-prev 2.0.0 tiny my/alias 3.0 - "###); + "#); reset(); } } diff --git a/src/cli/alias/unset.rs b/src/cli/alias/unset.rs index 57d1947e7..25f0230d6 100644 --- a/src/cli/alias/unset.rs +++ b/src/cli/alias/unset.rs @@ -40,7 +40,7 @@ mod tests { reset(); assert_cli!("alias", "unset", "tiny", "my/alias"); - assert_cli_snapshot!("aliases", @r###" + assert_cli_snapshot!("aliases", @r#" java lts 21 node lts 20 node lts-argon 4 @@ -54,7 +54,7 @@ mod tests { node lts-iron 20 tiny lts 3.1.0 tiny lts-prev 2.0.0 - "###); + "#); reset(); } diff --git a/src/cli/plugins/link.rs b/src/cli/plugins/link.rs index ec43385d0..82ba12478 100644 --- a/src/cli/plugins/link.rs +++ b/src/cli/plugins/link.rs @@ -86,12 +86,11 @@ mod tests { fn test_plugin_link() { reset(); assert_cli_snapshot!("plugin", "link", "-f", "tiny-link", "../data/plugins/tiny", @""); - assert_cli_snapshot!("plugins", "ls", @r###" + assert_cli_snapshot!("plugins", "ls", @r#" dummy tiny tiny-link - mise hint see available plugins with mise registry - "###); + "#); assert_cli_snapshot!("plugin", "uninstall", "tiny-link", @""); } } diff --git a/src/cli/plugins/snapshots/mise__cli__plugins__ls__tests__plugin_list.snap b/src/cli/plugins/snapshots/mise__cli__plugins__ls__tests__plugin_list.snap index 07b6667d4..751d679e5 100644 --- a/src/cli/plugins/snapshots/mise__cli__plugins__ls__tests__plugin_list.snap +++ b/src/cli/plugins/snapshots/mise__cli__plugins__ls__tests__plugin_list.snap @@ -4,4 +4,3 @@ expression: output --- dummy tiny -mise hint see available plugins with mise registry diff --git a/src/cli/run.rs b/src/cli/run.rs index 5a1bef879..e50a10ed9 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -24,7 +24,6 @@ use crate::errors::Error; use crate::errors::Error::ScriptFailed; use crate::file::display_path; use crate::task::{Deps, GetMatchingExt, Task}; -use crate::task_parser::TaskParser; use crate::toolset::{InstallOptions, ToolsetBuilder}; use crate::ui::{ctrlc, style}; use crate::{env, file, ui}; @@ -206,6 +205,9 @@ impl Run { }; let rx = tasks.lock().unwrap().subscribe(); while let Some(task) = rx.recv().unwrap() { + if exit_status.lock().unwrap().is_some() { + break; + } run(&task); } }); @@ -252,20 +254,8 @@ impl Run { if let Some(file) = &task.file { self.exec_file(file, task, &env, &prefix)?; } else { - let parser = TaskParser::new(self.cd.clone()).parse_run_scripts(&task.run)?; - if parser.has_any_args_defined() { - for script in parser.render(&self.args) { - self.exec_script(&script, &[], task, &env, &prefix)?; - } - } else { - for (i, script) in task.run.iter().enumerate() { - // only pass args to the last script if no formal args are defined - let args = match i == task.run.len() - 1 { - true => task.args.iter().cloned().collect_vec(), - false => vec![], - }; - self.exec_script(script, &args, task, &env, &prefix)?; - } + for (script, args) in task.render_run_scripts_with_args(self.cd.clone(), &task.args)? { + self.exec_script(&script, &args, task, &env, &prefix)?; } } @@ -327,14 +317,23 @@ impl Run { env: &BTreeMap, prefix: &str, ) -> Result<()> { + let mut env = env.clone(); let command = file.to_string_lossy().to_string(); let args = task.args.iter().cloned().collect_vec(); + let (spec, _) = task.parse_usage_spec(self.cd.clone())?; + if !spec.cmd.args.is_empty() || !spec.cmd.flags.is_empty() { + let args = once(command.clone()).chain(args.clone()).collect_vec(); + let po = usage::cli::parse(&spec, &args).map_err(|err| eyre!(err))?; + for (k, v) in po.as_env() { + env.insert(k, v); + } + } let cmd = format!("{} {}", display_path(file), args.join(" ")); let cmd = style::ebold(format!("$ {cmd}")).bright().to_string(); info_unprefix_trunc!("{prefix} {cmd}"); - self.exec(&command, &args, task, env, prefix) + self.exec(&command, &args, task, &env, prefix) } fn exec( @@ -624,8 +623,7 @@ mod tests { assert_cli_snapshot!( "r", "filetask", - "arg1", - "arg2", + "--user=jdx", ":::", "configtask", "arg3", diff --git a/src/main.rs b/src/main.rs index ef14a4160..fcd21f788 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,8 +53,7 @@ mod runtime_symlinks; mod shell; mod shims; mod shorthands; -mod task; -mod task_parser; +pub(crate) mod task; pub(crate) mod tera; pub(crate) mod timeout; mod toml; diff --git a/src/output.rs b/src/output.rs index 14842f985..bd268fb46 100644 --- a/src/output.rs +++ b/src/output.rs @@ -45,19 +45,6 @@ macro_rules! miseprint { }} } -#[cfg(test)] -#[macro_export] -macro_rules! hint { - ($id:expr, $message:expr, $example_cmd:expr) => {{ - let mut stderr = $crate::output::tests::STDERR.lock().unwrap(); - if !$crate::output::should_display_hint($id) { - let prefix = console::style("mise hint").dim().for_stderr(); - let cmd = console::style($example_cmd).bold().for_stderr(); - stderr.push(format!("{} {} {}", prefix, format!($message), cmd)); - } - }}; -} - #[cfg(test)] #[macro_export] macro_rules! info { @@ -107,6 +94,9 @@ macro_rules! debug { } pub fn should_display_hint(id: &str) -> bool { + if cfg!(test) { + return false; + } if SETTINGS .disable_hints .iter() @@ -127,7 +117,6 @@ pub fn should_display_hint(id: &str) -> bool { } } -#[cfg(not(test))] #[macro_export] macro_rules! hint { ($id:expr, $message:expr, $example_cmd:expr) => {{ diff --git a/src/task.rs b/src/task/mod.rs similarity index 89% rename from src/task.rs rename to src/task/mod.rs index 68c5f14fe..feb2d9093 100644 --- a/src/task.rs +++ b/src/task/mod.rs @@ -18,9 +18,14 @@ use serde_derive::Deserialize; use crate::config::config_file::toml::{deserialize_arr, TomlParser}; use crate::config::Config; use crate::file; +use crate::task::task_script_parser::{ + has_any_args_defined, replace_template_placeholders_with_args, TaskScriptParser, +}; use crate::tera::{get_tera, BASE_CONTEXT}; use crate::ui::tree::TreeItem; +mod task_script_parser; + #[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)] pub struct Task { #[serde(skip)] @@ -157,6 +162,47 @@ impl Task { .filter_ok(|t| t.name != self.name) .collect() } + + pub fn parse_usage_spec(&self, cwd: Option) -> Result<(usage::Spec, Vec)> { + if let Some(file) = &self.file { + let spec = usage::Spec::parse_script(file) + .inspect_err(|e| debug!("failed to parse task file with usage: {e}")) + .unwrap_or_default(); + Ok((spec, vec![])) + } else { + let (scripts, spec) = TaskScriptParser::new(cwd).parse_run_scripts(&self.run)?; + Ok((spec, scripts)) + } + } + + pub fn render_run_scripts_with_args( + &self, + cwd: Option, + args: &[String], + ) -> Result)>> { + let (spec, scripts) = self.parse_usage_spec(cwd)?; + if has_any_args_defined(&spec) { + Ok( + replace_template_placeholders_with_args(&spec, &scripts, args) + .into_iter() + .map(|s| (s, vec![])) + .collect(), + ) + } else { + Ok(self + .run + .iter() + .enumerate() + .map(|(i, script)| { + // only pass args to the last script if no formal args are defined + match i == self.run.len() - 1 { + true => (script.clone(), args.iter().cloned().collect_vec()), + false => (script.clone(), vec![]), + } + }) + .collect()) + } + } } fn name_from_path(root: impl AsRef, path: impl AsRef) -> Result { diff --git a/src/task/task_script_parser.rs b/src/task/task_script_parser.rs new file mode 100644 index 000000000..cf32a8d49 --- /dev/null +++ b/src/task/task_script_parser.rs @@ -0,0 +1,265 @@ +use crate::tera::{get_tera, BASE_CONTEXT}; +use eyre::Result; +use itertools::Itertools; +use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; + +pub struct TaskScriptParser { + dir: Option, + ctx: tera::Context, +} + +impl TaskScriptParser { + pub fn new(dir: Option) -> Self { + TaskScriptParser { + dir, + ctx: BASE_CONTEXT.clone(), + } + } + + fn get_tera(&self) -> tera::Tera { + get_tera(self.dir.as_deref()) + } + + pub fn parse_run_scripts(&self, scripts: &[String]) -> Result<(Vec, usage::Spec)> { + let mut tera = self.get_tera(); + let input_args = Arc::new(Mutex::new(vec![])); + let template_key = |name| format!("MISE_TASK_ARG:{name}:MISE_TASK_ARG"); + tera.register_function("arg", { + { + let input_args = input_args.clone(); + move |args: &HashMap| -> tera::Result { + let i = args + .get("i") + .map(|i| i.as_i64().unwrap() as usize) + .unwrap_or_else(|| input_args.lock().unwrap().len()); + let required = args + .get("required") + .map(|r| r.as_bool().unwrap()) + .unwrap_or(true); + let var = args + .get("var") + .map(|r| r.as_bool().unwrap()) + .unwrap_or(false); + let name = args + .get("name") + .map(|n| n.as_str().unwrap().to_string()) + .unwrap_or(i.to_string()); + // let default = args.get("default").map(|d| d.as_str().unwrap().to_string()); + let arg = usage::SpecArg { + name: name.clone(), + required, + var, + // default, + ..Default::default() + }; + input_args.lock().unwrap().push((i, arg)); + Ok(tera::Value::String(template_key(name))) + } + } + }); + let input_flags = Arc::new(Mutex::new(vec![])); + tera.register_function("option", { + { + let input_flags = input_flags.clone(); + move |args: &HashMap| -> tera::Result { + let name = args + .get("name") + .map(|n| n.as_str().unwrap().to_string()) + .unwrap(); + let var = args + .get("var") + .map(|r| r.as_bool().unwrap()) + .unwrap_or(false); + // let default = args.get("default").map(|d| d.as_str().unwrap().to_string()); + let flag = usage::SpecFlag { + name: name.clone(), + var, + arg: Some(usage::SpecArg { + name: name.clone(), + var, + ..Default::default() + }), + // default, + ..Default::default() + }; + input_flags.lock().unwrap().push(flag); + Ok(tera::Value::String(template_key(name))) + } + } + }); + tera.register_function("flag", { + { + let input_flags = input_flags.clone(); + move |args: &HashMap| -> tera::Result { + let name = args + .get("name") + .map(|n| n.as_str().unwrap().to_string()) + .unwrap(); + // let default = args.get("default").map(|d| d.as_str().unwrap().to_string()); + let flag = usage::SpecFlag { + name: name.clone(), + // default, + ..Default::default() + }; + input_flags.lock().unwrap().push(flag); + Ok(tera::Value::String(template_key(name))) + } + } + }); + let scripts = scripts + .iter() + .map(|s| tera.render_str(s, &self.ctx).unwrap()) + .collect(); + let spec = usage::Spec { + cmd: usage::SpecCommand { + // TODO: ensure no gaps in args, e.g.: 1,2,3,4,5 + args: input_args + .lock() + .unwrap() + .iter() + .cloned() + .sorted_by_key(|(i, _)| *i) + .map(|(_, arg)| arg) + .collect(), + flags: input_flags.lock().unwrap().clone(), + ..Default::default() + }, + ..Default::default() + }; + + Ok((scripts, spec)) + } +} + +pub fn replace_template_placeholders_with_args( + spec: &usage::Spec, + scripts: &[String], + args: &[String], +) -> Vec { + let mut cmd = clap::Command::new("mise-task"); + for arg in &spec.cmd.args { + cmd = cmd.arg( + clap::Arg::new(arg.name.clone()) + .required(arg.required) + .action(if arg.var { + clap::ArgAction::Append + } else { + clap::ArgAction::Set + }), + ); + } + let mut flags = HashSet::new(); + for flag in &spec.cmd.flags { + if flag.arg.is_some() { + cmd = cmd.arg( + clap::Arg::new(flag.name.clone()) + .long(flag.name.clone()) + .action(if flag.var { + clap::ArgAction::Append + } else { + clap::ArgAction::Set + }), + ); + } else { + flags.insert(flag.name.as_str()); + cmd = cmd.arg( + clap::Arg::new(flag.name.clone()) + .long(flag.name.clone()) + .action(clap::ArgAction::SetTrue), + ); + } + } + let matches = cmd.get_matches_from(["mise-task".to_string()].iter().chain(args.iter())); + let mut out = vec![]; + for script in scripts { + let mut script = script.clone(); + for id in matches.ids() { + let value = if flags.contains(id.as_str()) { + matches.get_one::(id.as_str()).unwrap().to_string() + } else { + matches.get_many::(id.as_str()).unwrap().join(" ") + }; + script = script.replace(&format!("MISE_TASK_ARG:{id}:MISE_TASK_ARG"), &value); + } + out.push(script); + } + out +} + +pub fn has_any_args_defined(spec: &usage::Spec) -> bool { + !spec.cmd.args.is_empty() || !spec.cmd.flags.is_empty() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test::reset; + + #[test] + fn test_task_parse_arg() { + reset(); + let parser = TaskScriptParser::new(None); + let scripts = vec!["echo {{ arg(i=0, name='foo') }}".to_string()]; + let (scripts, spec) = parser.parse_run_scripts(&scripts).unwrap(); + assert_eq!(scripts, vec!["echo MISE_TASK_ARG:foo:MISE_TASK_ARG"]); + let arg0 = spec.cmd.args.first().unwrap(); + assert_eq!(arg0.name, "foo"); + + let scripts = + replace_template_placeholders_with_args(&spec, &scripts, &["abc".to_string()]); + assert_eq!(scripts, vec!["echo abc"]); + } + + #[test] + fn test_task_parse_arg_var() { + reset(); + let parser = TaskScriptParser::new(None); + let scripts = vec!["echo {{ arg(var=true) }}".to_string()]; + let (scripts, spec) = parser.parse_run_scripts(&scripts).unwrap(); + assert_eq!(scripts, vec!["echo MISE_TASK_ARG:0:MISE_TASK_ARG"]); + let arg0 = spec.cmd.args.first().unwrap(); + assert_eq!(arg0.name, "0"); + + let scripts = replace_template_placeholders_with_args( + &spec, + &scripts, + &["abc".to_string(), "def".to_string()], + ); + assert_eq!(scripts, vec!["echo abc def"]); + } + + #[test] + fn test_task_parse_flag() { + reset(); + let parser = TaskScriptParser::new(None); + let scripts = vec!["echo {{ flag(name='foo') }}".to_string()]; + let (scripts, spec) = parser.parse_run_scripts(&scripts).unwrap(); + assert_eq!(scripts, vec!["echo MISE_TASK_ARG:foo:MISE_TASK_ARG"]); + let flag = spec.cmd.flags.iter().find(|f| &f.name == "foo").unwrap(); + assert_eq!(&flag.name, "foo"); + + let scripts = + replace_template_placeholders_with_args(&spec, &scripts, &["--foo".to_string()]); + assert_eq!(scripts, vec!["echo true"]); + } + + #[test] + fn test_task_parse_option() { + reset(); + let parser = TaskScriptParser::new(None); + let scripts = vec!["echo {{ option(name='foo') }}".to_string()]; + let (scripts, spec) = parser.parse_run_scripts(&scripts).unwrap(); + assert_eq!(scripts, vec!["echo MISE_TASK_ARG:foo:MISE_TASK_ARG"]); + let option = spec.cmd.flags.iter().find(|f| &f.name == "foo").unwrap(); + assert_eq!(&option.name, "foo"); + + let scripts = replace_template_placeholders_with_args( + &spec, + &scripts, + &["--foo".to_string(), "abc".to_string()], + ); + assert_eq!(scripts, vec!["echo abc"]); + } +} diff --git a/src/task_parser.rs b/src/task_parser.rs deleted file mode 100644 index 5f73237d0..000000000 --- a/src/task_parser.rs +++ /dev/null @@ -1,267 +0,0 @@ -use crate::tera::{get_tera, BASE_CONTEXT}; -use clap::Arg; -use eyre::Result; -use itertools::Itertools; -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::{Arc, Mutex}; - -#[derive(Debug, Default, Clone)] -pub struct TaskParseArg { - i: usize, - name: String, - required: bool, - var: bool, - // default: Option, - // var_min: Option, - // var_max: Option, - // choices: Vec, -} - -#[derive(Debug, Default)] -pub struct TaskParseResults { - scripts: Vec, - args: Vec, - flags: HashMap, - options: HashMap, -} - -impl TaskParseResults { - pub fn render(&self, args: &[String]) -> Vec { - let mut cmd = clap::Command::new("mise-task"); - for arg in &self.args { - cmd = cmd.arg( - Arg::new(arg.name.clone()) - .required(arg.required) - .action(if arg.var { - clap::ArgAction::Append - } else { - clap::ArgAction::Set - }), - ); - } - for flag in self.flags.values() { - cmd = cmd.arg( - Arg::new(flag.name.clone()) - .long(flag.name.clone()) - .action(clap::ArgAction::SetTrue), - ); - } - for option in self.options.values() { - cmd = cmd.arg( - Arg::new(option.name.clone()) - .long(option.name.clone()) - .action(if option.var { - clap::ArgAction::Append - } else { - clap::ArgAction::Set - }), - ); - } - let matches = cmd.get_matches_from(["mise-task".to_string()].iter().chain(args.iter())); - let mut out = vec![]; - for script in &self.scripts { - let mut script = script.clone(); - for id in matches.ids() { - let value = if self.flags.contains_key(id.as_str()) { - matches.get_one::(id.as_str()).unwrap().to_string() - } else { - matches.get_many::(id.as_str()).unwrap().join(" ") - }; - script = script.replace(&format!("MISE_TASK_ARG:{id}:MISE_TASK_ARG"), &value); - } - out.push(script); - } - out - } - - pub fn has_any_args_defined(&self) -> bool { - !self.args.is_empty() || !self.flags.is_empty() || !self.options.is_empty() - } -} - -pub struct TaskParser { - dir: Option, - ctx: tera::Context, -} - -impl TaskParser { - pub fn new(dir: Option) -> Self { - TaskParser { - dir, - ctx: BASE_CONTEXT.clone(), - } - } - - fn get_tera(&self) -> tera::Tera { - get_tera(self.dir.as_deref()) - } - - pub fn parse_run_scripts(&self, scripts: &[String]) -> Result { - let mut tera = self.get_tera(); - let input_args = Arc::new(Mutex::new(vec![])); - let template_key = |name| format!("MISE_TASK_ARG:{name}:MISE_TASK_ARG"); - tera.register_function("arg", { - { - let input_args = input_args.clone(); - move |args: &HashMap| -> tera::Result { - let i = args - .get("i") - .map(|i| i.as_i64().unwrap() as usize) - .unwrap_or_else(|| input_args.lock().unwrap().len()); - let required = args - .get("required") - .map(|r| r.as_bool().unwrap()) - .unwrap_or(true); - let var = args - .get("var") - .map(|r| r.as_bool().unwrap()) - .unwrap_or(false); - let name = args - .get("name") - .map(|n| n.as_str().unwrap().to_string()) - .unwrap_or(i.to_string()); - // let default = args.get("default").map(|d| d.as_str().unwrap().to_string()); - let arg = TaskParseArg { - i, - name: name.clone(), - required, - var, - // default, - }; - input_args.lock().unwrap().push(arg); - Ok(tera::Value::String(template_key(name))) - } - } - }); - let input_options = Arc::new(Mutex::new(HashMap::new())); - tera.register_function("option", { - { - let input_options = input_options.clone(); - move |args: &HashMap| -> tera::Result { - let name = args - .get("name") - .map(|n| n.as_str().unwrap().to_string()) - .unwrap(); - let var = args - .get("var") - .map(|r| r.as_bool().unwrap()) - .unwrap_or(false); - // let default = args.get("default").map(|d| d.as_str().unwrap().to_string()); - let flag = TaskParseArg { - name: name.clone(), - var, - // default, - ..Default::default() - }; - input_options.lock().unwrap().insert(name.clone(), flag); - Ok(tera::Value::String(template_key(name))) - } - } - }); - let input_flags = Arc::new(Mutex::new(HashMap::new())); - tera.register_function("flag", { - { - let input_flags = input_flags.clone(); - move |args: &HashMap| -> tera::Result { - let name = args - .get("name") - .map(|n| n.as_str().unwrap().to_string()) - .unwrap(); - // let default = args.get("default").map(|d| d.as_str().unwrap().to_string()); - let flag = TaskParseArg { - name: name.clone(), - // default, - ..Default::default() - }; - input_flags.lock().unwrap().insert(name.clone(), flag); - Ok(tera::Value::String(template_key(name))) - } - } - }); - let out = TaskParseResults { - scripts: scripts - .iter() - .map(|s| tera.render_str(s, &self.ctx).unwrap()) - .collect(), - args: input_args - .lock() - .unwrap() - .iter() - .cloned() - .sorted_by_key(|a| a.i) - .collect(), - flags: input_flags.lock().unwrap().clone(), - options: input_options.lock().unwrap().clone(), - }; - // TODO: ensure no gaps in args, e.g.: 1,2,3,4,5 - - Ok(out) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_task_parse_arg() { - let parser = TaskParser::new(None); - let scripts = vec!["echo {{ arg(i=0, name='foo') }}".to_string()]; - let results = parser.parse_run_scripts(&scripts).unwrap(); - assert_eq!( - results.scripts, - vec!["echo MISE_TASK_ARG:foo:MISE_TASK_ARG"] - ); - let arg0 = results.args.first().unwrap(); - assert_eq!(arg0.name, "foo"); - - let scripts = results.render(&["abc".to_string()]); - assert_eq!(scripts, vec!["echo abc"]); - } - - #[test] - fn test_task_parse_arg_var() { - let parser = TaskParser::new(None); - let scripts = vec!["echo {{ arg(var=true) }}".to_string()]; - let results = parser.parse_run_scripts(&scripts).unwrap(); - assert_eq!(results.scripts, vec!["echo MISE_TASK_ARG:0:MISE_TASK_ARG"]); - let arg0 = results.args.first().unwrap(); - assert_eq!(arg0.name, "0"); - - let scripts = results.render(&["abc".to_string(), "def".to_string()]); - assert_eq!(scripts, vec!["echo abc def"]); - } - - #[test] - fn test_task_parse_flag() { - let parser = TaskParser::new(None); - let scripts = vec!["echo {{ flag(name='foo') }}".to_string()]; - let results = parser.parse_run_scripts(&scripts).unwrap(); - assert_eq!( - results.scripts, - vec!["echo MISE_TASK_ARG:foo:MISE_TASK_ARG"] - ); - let flag = results.flags.get("foo").unwrap(); - assert_eq!(flag.name, "foo"); - - let scripts = results.render(&["--foo".to_string()]); - assert_eq!(scripts, vec!["echo true"]); - } - - #[test] - fn test_task_parse_option() { - let parser = TaskParser::new(None); - let scripts = vec!["echo {{ option(name='foo') }}".to_string()]; - let results = parser.parse_run_scripts(&scripts).unwrap(); - assert_eq!( - results.scripts, - vec!["echo MISE_TASK_ARG:foo:MISE_TASK_ARG"] - ); - let option = results.options.get("foo").unwrap(); - assert_eq!(option.name, "foo"); - - let scripts = results.render(&["--foo".to_string(), "abc".to_string()]); - assert_eq!(scripts, vec!["echo abc"]); - } -} diff --git a/src/test.rs b/src/test.rs index fef5bb8d9..60775ca89 100644 --- a/src/test.rs +++ b/src/test.rs @@ -109,11 +109,14 @@ pub fn reset() { # mise sources=[".test-tool-versions"] # mise outputs=["$MISE_PROJECT_ROOT/test/test-build-output.txt"] # mise env={TEST_BUILDSCRIPT_ENV_VAR = "VALID"} + + #USAGE flag "--user " help="The user to run as" - set -euxo pipefail + set -exo pipefail cd "$MISE_PROJECT_ROOT" || exit 1 echo "running test-build script" echo "TEST_BUILDSCRIPT_ENV_VAR: $TEST_BUILDSCRIPT_ENV_VAR" > test-build-output.txt + echo "user=$usage_user" "#}, ) .unwrap();