diff --git a/docs/tasks/toml-tasks.md b/docs/tasks/toml-tasks.md index 171bfbd85..6a016b8e1 100644 --- a/docs/tasks/toml-tasks.md +++ b/docs/tasks/toml-tasks.md @@ -27,6 +27,8 @@ dir = "{{cwd}}" # run in user's cwd, default is the project's base directory [tasks.lint] description = 'Lint with clippy' env = {RUST_BACKTRACE = '1'} # env vars for the script +# specify a shell command to run the script with (default is 'sh -c') +shell = 'bash -c' # you can specify a multiline script instead of individual commands run = """ #!/usr/bin/env bash diff --git a/schema/mise.json b/schema/mise.json index 3f2b3acda..c8e7ac167 100644 --- a/schema/mise.json +++ b/schema/mise.json @@ -554,6 +554,10 @@ } ] }, + "shell": { + "description": "shell command to run script with (e.g. sh -c)", + "type": "string" + }, "sources": { "description": "files that this task depends on", "items": { diff --git a/src/cli/config/snapshots/mise__cli__config__get__tests__toml_get.snap b/src/cli/config/snapshots/mise__cli__config__get__tests__toml_get.snap index 1515ceceb..56437d54f 100644 --- a/src/cli/config/snapshots/mise__cli__config__get__tests__toml_get.snap +++ b/src/cli/config/snapshots/mise__cli__config__get__tests__toml_get.snap @@ -21,5 +21,17 @@ run = 'echo "configtask:"' [tasks.lint] run = 'echo "linting!"' +[tasks.shell] +run = ''' +#MISE outputs=["$MISE_PROJECT_ROOT/test/test-build-output.txt"] +cd "$MISE_PROJECT_ROOT" || exit 1 +echo "using shell $0" > test-build-output.txt +''' +shell = "bash -c" + +[tasks."shell invalid"] +run = 'echo "invalid shell"' +shell = "bash" + [tasks.test] run = 'echo "testing!"' diff --git a/src/cli/run.rs b/src/cli/run.rs index 57ecd908d..daa4f1c15 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -297,21 +297,44 @@ impl Run { let filename = file.display().to_string(); self.exec(&filename, args, task, env, prefix) } else { + let shell = self.get_shell(task); + trace!("using shell: {} {}", shell.0, shell.1); #[cfg(windows)] { let script = format!("{} {}", script, args.join(" ")); - let args = vec!["/c".to_string(), script]; - self.exec("cmd", &args, task, env, prefix) + let args = vec![shell.1, script]; + self.exec(shell.0.as_str(), &args, task, env, prefix) } #[cfg(unix)] { let script = format!("{} {}", script, shell_words::join(args)); - let args = vec!["-c".to_string(), script]; - self.exec("sh", &args, task, env, prefix) + let args = vec![shell.1, script]; + self.exec(shell.0.as_str(), &args, task, env, prefix) } } } + fn get_shell(&self, task: &Task) -> (String, String) { + let default_shell = if cfg!(windows) { + ("cmd".to_string(), "/c".to_string()) + } else { + ("sh".to_string(), "-c".to_string()) + }; + + if let Some(shell) = task.shell.clone() { + let shell_cmd = shell + .split_whitespace() + .map(|s| s.to_string()) + .collect_tuple() + .unwrap_or_else(|| { + warn!("invalid shell '{shell}', expected ' ' (e.g. sh -c)"); + default_shell + }); + return shell_cmd; + } + default_shell + } + fn exec_file( &self, file: &Path, @@ -660,4 +683,27 @@ mod tests { TEST_BUILDSCRIPT_ENV_VAR: VALID "###); } + + #[test] + fn test_task_custom_shell() { + reset(); + file::remove_all("test-build-output.txt").unwrap(); + assert_cli_snapshot!( + "r", + "shell", + @""); + let body = file::read_to_string("test-build-output.txt").unwrap(); + assert_snapshot!(body, @r###" + using shell bash + "###); + } + + #[test] + fn test_task_custom_shell_invalid() { + reset(); + assert_cli_snapshot!( + "r", + "shell invalid", + @"mise invalid shell 'bash', expected ' ' (e.g. sh -c)"); + } } diff --git a/src/cli/tasks/deps.rs b/src/cli/tasks/deps.rs index d07462550..a5ecafe65 100644 --- a/src/cli/tasks/deps.rs +++ b/src/cli/tasks/deps.rs @@ -166,14 +166,16 @@ mod tests { #[test] fn test_tasks_deps_tree() { reset(); - assert_cli_snapshot!("tasks", "deps", @r###" + assert_cli_snapshot!("tasks", "deps", @r#" configtask filetask ├── test └── lint lint + shell + shell invalid test - "### + "# ); } @@ -193,16 +195,18 @@ mod tests { #[test] fn test_tasks_deps_dot() { reset(); - assert_cli_snapshot!("tasks", "deps", "--dot", @r###" + assert_cli_snapshot!("tasks", "deps", "--dot", @r#" digraph { 0 [ label = "configtask"] 1 [ label = "filetask"] 2 [ label = "lint"] - 3 [ label = "test"] + 3 [ label = "shell"] + 4 [ label = "shell invalid"] + 5 [ label = "test"] 1 -> 2 [ ] - 1 -> 3 [ ] + 1 -> 5 [ ] } - "### + "# ); } } diff --git a/src/cli/tasks/ls.rs b/src/cli/tasks/ls.rs index 2188692eb..d9051c2a4 100644 --- a/src/cli/tasks/ls.rs +++ b/src/cli/tasks/ls.rs @@ -194,10 +194,12 @@ mod tests { fn test_task_ls() { reset(); assert_cli_snapshot!("t", "--no-headers", @r#" - configtask ~/config/config.toml - filetask This is a test build script ~/cwd/.mise/tasks/filetask - lint ~/config/config.toml - test ~/config/config.toml + configtask ~/config/config.toml + filetask This is a test build script ~/cwd/.mise/tasks/filetask + lint ~/config/config.toml + shell ~/config/config.toml + shell invalid ~/config/config.toml + test ~/config/config.toml "#); } diff --git a/src/cli/tasks/snapshots/mise__cli__tasks__ls__tests__task_ls_json.snap b/src/cli/tasks/snapshots/mise__cli__tasks__ls__tests__task_ls_json.snap index d99471372..b6cf8ceb4 100644 --- a/src/cli/tasks/snapshots/mise__cli__tasks__ls__tests__task_ls_json.snap +++ b/src/cli/tasks/snapshots/mise__cli__tasks__ls__tests__task_ls_json.snap @@ -18,6 +18,16 @@ expression: output "name": "lint", "source": "~/config/config.toml" }, + { + "description": "", + "name": "shell", + "source": "~/config/config.toml" + }, + { + "description": "", + "name": "shell invalid", + "source": "~/config/config.toml" + }, { "description": "", "name": "test", diff --git a/src/cli/tasks/snapshots/mise__cli__tasks__ls__tests__task_ls_json_extended.snap b/src/cli/tasks/snapshots/mise__cli__tasks__ls__tests__task_ls_json_extended.snap index 363145a92..bda6d9553 100644 --- a/src/cli/tasks/snapshots/mise__cli__tasks__ls__tests__task_ls_json_extended.snap +++ b/src/cli/tasks/snapshots/mise__cli__tasks__ls__tests__task_ls_json_extended.snap @@ -21,6 +21,18 @@ expression: output "name": "lint", "source": "~/config/config.toml" }, + { + "alias": "", + "description": "", + "name": "shell", + "source": "~/config/config.toml" + }, + { + "alias": "", + "description": "", + "name": "shell invalid", + "source": "~/config/config.toml" + }, { "alias": "", "description": "", diff --git a/src/cli/use.rs b/src/cli/use.rs index c0d82d20d..84292b4d7 100644 --- a/src/cli/use.rs +++ b/src/cli/use.rs @@ -347,7 +347,7 @@ mod tests { mise ~/config/config.toml tools: tiny@2.1.0 mise tiny is defined in ~/cwd/.test-tool-versions which overrides the global config (~/config/config.toml) "###); - assert_snapshot!(file::read_to_string(&cf_path).unwrap(), @r###" + assert_snapshot!(file::read_to_string(&cf_path).unwrap(), @r##" [env] TEST_ENV_VAR = 'test-123' @@ -360,6 +360,16 @@ mod tests { run = 'echo "linting!"' [tasks.test] run = 'echo "testing!"' + [tasks."shell invalid"] + shell = 'bash' + run = 'echo "invalid shell"' + [tasks.shell] + shell = 'bash -c' + run = ''' + #MISE outputs=["$MISE_PROJECT_ROOT/test/test-build-output.txt"] + cd "$MISE_PROJECT_ROOT" || exit 1 + echo "using shell $0" > test-build-output.txt + ''' [settings] always_keep_download= true always_keep_install= true @@ -369,7 +379,7 @@ mod tests { [tools] tiny = "2" - "###); + "##); file::write(&cf_path, orig).unwrap(); } diff --git a/src/task/mod.rs b/src/task/mod.rs index 93c8c3bec..3e7a9decd 100644 --- a/src/task/mod.rs +++ b/src/task/mod.rs @@ -50,6 +50,8 @@ pub struct Task { pub sources: Vec, #[serde(default)] pub outputs: Vec, + #[serde(default)] + pub shell: Option, // normal type #[serde(default, deserialize_with = "deserialize_arr")] @@ -136,6 +138,7 @@ impl Task { dir: p.parse_str("dir")?, env: p.parse_env("env")?.unwrap_or_default(), file: Some(path.to_path_buf()), + shell: p.parse_str("shell")?, ..Task::new(name_from_path(config_root, path)?, path.to_path_buf()) }; Ok(task) diff --git a/src/test.rs b/src/test.rs index aee5cc955..f86c956fe 100644 --- a/src/test.rs +++ b/src/test.rs @@ -159,6 +159,16 @@ pub fn reset() { run = 'echo "linting!"' [tasks.test] run = 'echo "testing!"' + [tasks."shell invalid"] + shell = 'bash' + run = 'echo "invalid shell"' + [tasks.shell] + shell = 'bash -c' + run = ''' + #MISE outputs=["$MISE_PROJECT_ROOT/test/test-build-output.txt"] + cd "$MISE_PROJECT_ROOT" || exit 1 + echo "using shell $0" > test-build-output.txt + ''' [settings] always_keep_download= true always_keep_install= true diff --git a/test/config/config.toml b/test/config/config.toml index f5dbd71df..d3cbadba1 100644 --- a/test/config/config.toml +++ b/test/config/config.toml @@ -10,6 +10,16 @@ run = 'echo "configtask:"' run = 'echo "linting!"' [tasks.test] run = 'echo "testing!"' +[tasks."shell invalid"] +shell = 'bash' +run = 'echo "invalid shell"' +[tasks.shell] +shell = 'bash -c' +run = ''' +#MISE outputs=["$MISE_PROJECT_ROOT/test/test-build-output.txt"] +cd "$MISE_PROJECT_ROOT" || exit 1 +echo "using shell $0" > test-build-output.txt +''' [settings] always_keep_download= true always_keep_install= true