diff --git a/Cargo.lock b/Cargo.lock index c184b84d2e..fc37198f6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3993,9 +3993,9 @@ checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "usage-lib" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e94c6959099cfbc672b68392f9b9fe7fde12447c4550f5a36af1516a0277807" +checksum = "27ec81814b1b6f0170de712a56989f5156f95e7021368e21a05806898edb3b43" dependencies = [ "clap", "heck 0.5.0", @@ -4007,6 +4007,7 @@ dependencies = [ "once_cell", "regex", "serde", + "shell-words", "strum", "tera", "thiserror 2.0.6", @@ -4500,9 +4501,9 @@ dependencies = [ [[package]] name = "xx" -version = "2.0.0" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c507cfdc3944bf23d0ef0fc39894bd05b566828bb961dbd1dc42e013d99f99" +checksum = "53ba8e1329637bfde9509cf8be1b3b9eed7c0130e60408a45f6d514a11f7974b" dependencies = [ "bzip2", "duct", diff --git a/e2e/run_test b/e2e/run_test index 7d2ec2045f..00299b6f7a 100755 --- a/e2e/run_test +++ b/e2e/run_test @@ -58,6 +58,7 @@ within_isolated_env() { GITHUB_TOKEN="${GITHUB_TOKEN:-}" \ GOPROXY="${GOPROXY:-}" \ HOME="$TEST_HOME" \ + LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}" \ LLVM_PROFILE_FILE="${LLVM_PROFILE_FILE:-}" \ MISE_CACHE_DIR="$MISE_CACHE_DIR" \ MISE_CACHE_PRUNE_AGE="0" \ diff --git a/e2e/tasks/test_task_shell b/e2e/tasks/test_task_shell index eca4d9999b..53c4dbbc51 100644 --- a/e2e/tasks/test_task_shell +++ b/e2e/tasks/test_task_shell @@ -11,3 +11,11 @@ EOF assert "mise run shell" "using shell bash" assert_fail "mise run shell-invalid" "invalid shell" + +cat <mise.toml +tasks.escapeme = "echo {{arg(name='a')}}" +tasks.escapeme_var = "echo {{arg(name='a', var=true)}}" +EOF + +assert "mise run escapeme 'hello world'" "hello world" +assert "mise run escapeme_var hello 'world of mise'" "hello world of mise" diff --git a/src/cli/run.rs b/src/cli/run.rs index 6104e1d179..9d3463ebb4 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -437,8 +437,7 @@ impl Run { file::make_executable(&file)?; self.exec(&file, args, task, env, prefix) } else { - let default_shell = self.clone_default_inline_shell()?; - let (program, args) = self.get_cmd_program_and_args(script, task, args, default_shell); + let (program, args) = self.get_cmd_program_and_args(script, task, args)?; self.exec_program(&program, &args, task, env, prefix) } } @@ -460,8 +459,7 @@ impl Run { } return Ok((display, args.to_vec())); } - let default_shell = self.clone_default_file_shell()?; - let shell = self.get_shell(task, default_shell); + let shell = task.shell().unwrap_or(SETTINGS.default_file_shell()?); trace!("using shell: {}", shell.join(" ")); let mut full_args = shell.clone(); full_args.push(display); @@ -476,9 +474,8 @@ impl Run { script: &str, task: &Task, args: &[String], - default_shell: Vec, - ) -> (String, Vec) { - let shell = self.get_shell(task, default_shell); + ) -> Result<(String, Vec)> { + let shell = task.shell().unwrap_or(self.clone_default_inline_shell()?); trace!("using shell: {}", shell.join(" ")); let mut full_args = shell.clone(); let mut script = script.to_string(); @@ -493,46 +490,15 @@ impl Run { } } full_args.push(script); - (full_args[0].clone(), full_args[1..].to_vec()) + Ok((full_args[0].clone(), full_args[1..].to_vec())) } fn clone_default_inline_shell(&self) -> Result> { if let Some(shell) = &self.shell { Ok(shell_words::split(shell)?) - } else if cfg!(windows) { - Ok(shell_words::split( - &SETTINGS.windows_default_inline_shell_args, - )?) - } else { - Ok(shell_words::split( - &SETTINGS.unix_default_inline_shell_args, - )?) - } - } - - fn clone_default_file_shell(&self) -> Result> { - if cfg!(windows) { - Ok(shell_words::split( - &SETTINGS.windows_default_file_shell_args, - )?) } else { - Ok(shell_words::split(&SETTINGS.unix_default_file_shell_args)?) - } - } - - fn get_shell(&self, task: &Task, default_shell: Vec) -> Vec { - let default_shell = default_shell.clone(); - if let Some(shell) = task.shell.clone() { - let shell_cmd = shell - .split_whitespace() - .map(|s| s.to_string()) - .collect::>(); - if !shell_cmd.is_empty() && !shell_cmd[0].trim().is_empty() { - return shell_cmd; - } - warn!("invalid shell '{shell}', expected ' ' (e.g. sh -c)"); + SETTINGS.default_inline_shell() } - default_shell } fn exec_file( diff --git a/src/config/settings.rs b/src/config/settings.rs index feb34dfcae..2eeee2e92c 100644 --- a/src/config/settings.rs +++ b/src/config/settings.rs @@ -393,6 +393,24 @@ impl Settings { let table = toml::from_str(&s)?; Ok(table) } + + pub fn default_inline_shell(&self) -> Result> { + let sa = if cfg!(windows) { + &SETTINGS.windows_default_inline_shell_args + } else { + &SETTINGS.unix_default_inline_shell_args + }; + Ok(shell_words::split(sa)?) + } + + pub fn default_file_shell(&self) -> Result> { + let sa = if cfg!(windows) { + &SETTINGS.windows_default_file_shell_args + } else { + &SETTINGS.unix_default_file_shell_args + }; + Ok(shell_words::split(sa)?) + } } impl Display for Settings { diff --git a/src/shell/mod.rs b/src/shell/mod.rs index 706750a170..b3564cca60 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -1,7 +1,7 @@ +use crate::env; use std::fmt::{Display, Formatter}; use std::path::Path; - -use crate::env; +use std::str::FromStr; mod bash; mod elvish; @@ -22,22 +22,11 @@ pub enum ShellType { impl ShellType { pub fn load() -> Option { - let shell = env::var("MISE_SHELL").or(env::var("SHELL")).ok()?; - if shell.ends_with("bash") { - Some(ShellType::Bash) - } else if shell.ends_with("elvish") { - Some(ShellType::Elvish) - } else if shell.ends_with("fish") { - Some(ShellType::Fish) - } else if shell.ends_with("nu") { - Some(ShellType::Nu) - } else if shell.ends_with("xonsh") { - Some(ShellType::Xonsh) - } else if shell.ends_with("zsh") { - Some(ShellType::Zsh) - } else { - None - } + env::var("MISE_SHELL") + .or(env::var("SHELL")) + .ok()? + .parse() + .ok() } pub fn as_shell(&self) -> Box { @@ -65,6 +54,24 @@ impl Display for ShellType { } } +impl FromStr for ShellType { + type Err = String; + + fn from_str(s: &str) -> Result { + let s = s.to_lowercase(); + let s = s.rsplit_once('/').map(|(_, s)| s).unwrap_or(&s); + match s { + "bash" | "sh" => Ok(Self::Bash), + "elvish" => Ok(Self::Elvish), + "fish" => Ok(Self::Fish), + "nu" => Ok(Self::Nu), + "xonsh" => Ok(Self::Xonsh), + "zsh" => Ok(Self::Zsh), + _ => Err(format!("unsupported shell type: {s}")), + } + } +} + pub trait Shell: Display { fn activate(&self, exe: &Path, flags: String) -> String; fn deactivate(&self) -> String; diff --git a/src/task/mod.rs b/src/task/mod.rs index 7a3731bb42..11fe6085ab 100644 --- a/src/task/mod.rs +++ b/src/task/mod.rs @@ -272,7 +272,7 @@ impl Task { let (spec, scripts) = self.parse_usage_spec(cwd)?; if has_any_args_defined(&spec) { Ok( - replace_template_placeholders_with_args(&spec, &scripts, args)? + replace_template_placeholders_with_args(self, &spec, &scripts, args)? .into_iter() .map(|s| (s, vec![])) .collect(), @@ -347,6 +347,21 @@ impl Task { pub fn cf<'a>(&self, config: &'a Config) -> Option<&'a Box> { config.config_files.get(&self.config_source) } + + pub fn shell(&self) -> Option> { + self.shell.as_ref().and_then(|shell| { + let shell_cmd = shell + .split_whitespace() + .map(|s| s.to_string()) + .collect::>(); + if shell_cmd.is_empty() || shell_cmd[0].trim().is_empty() { + warn!("invalid shell '{shell}', expected ' ' (e.g. sh -c)"); + None + } else { + Some(shell_cmd) + } + }) + } } fn name_from_path(prefix: impl AsRef, path: impl AsRef) -> Result { diff --git a/src/task/task_script_parser.rs b/src/task/task_script_parser.rs index fdfe2fd5cd..39cb3c3122 100644 --- a/src/task/task_script_parser.rs +++ b/src/task/task_script_parser.rs @@ -1,4 +1,6 @@ -use crate::config::Config; +use crate::config::{Config, SETTINGS}; +use crate::shell::ShellType; +use crate::task::Task; use crate::tera::{get_tera, BASE_CONTEXT}; use eyre::Result; use indexmap::IndexMap; @@ -6,6 +8,7 @@ use itertools::Itertools; use std::collections::HashMap; use std::path::PathBuf; use std::sync::{Arc, Mutex}; +use usage::parse::ParseValue; use xx::regex; pub struct TaskScriptParser { @@ -292,10 +295,26 @@ impl TaskScriptParser { } pub fn replace_template_placeholders_with_args( + task: &Task, spec: &usage::Spec, scripts: &[String], args: &[String], ) -> Result> { + let shell_type: Option = task.shell().unwrap_or(SETTINGS.default_inline_shell()?)[0] + .parse() + .ok(); + let escape = |v: &ParseValue| match v { + ParseValue::MultiString(_) => { + // these are already escaped + v.to_string() + } + _ => match shell_type { + Some(ShellType::Zsh | ShellType::Bash | ShellType::Fish) => { + shell_words::quote(&v.to_string()).to_string() + } + _ => v.to_string(), + }, + }; let args = vec!["".to_string()] .into_iter() .chain(args.iter().cloned()) @@ -308,13 +327,13 @@ pub fn replace_template_placeholders_with_args( for (arg, value) in &m.args { script = script.replace( &format!("MISE_TASK_ARG:{}:MISE_TASK_ARG", arg.name), - &value.to_string(), + &escape(value), ); } for (flag, value) in &m.flags { script = script.replace( &format!("MISE_TASK_ARG:{}:MISE_TASK_ARG", flag.name), - &value.to_string(), + &escape(value), ); } script = re.replace_all(&script, "").to_string(); @@ -333,6 +352,7 @@ mod tests { #[test] fn test_task_parse_arg() { + let task = Task::default(); let parser = TaskScriptParser::new(None); let scripts = vec!["echo {{ arg(i=0, name='foo') }}".to_string()]; let (scripts, spec) = parser.parse_run_scripts(&None, &scripts).unwrap(); @@ -341,12 +361,14 @@ mod tests { assert_eq!(arg0.name, "foo"); let scripts = - replace_template_placeholders_with_args(&spec, &scripts, &["abc".to_string()]).unwrap(); + replace_template_placeholders_with_args(&task, &spec, &scripts, &["abc".to_string()]) + .unwrap(); assert_eq!(scripts, vec!["echo abc"]); } #[test] fn test_task_parse_arg_var() { + let task = Task::default(); let parser = TaskScriptParser::new(None); let scripts = vec!["echo {{ arg(var=true) }}".to_string()]; let (scripts, spec) = parser.parse_run_scripts(&None, &scripts).unwrap(); @@ -355,6 +377,7 @@ mod tests { assert_eq!(arg0.name, "0"); let scripts = replace_template_placeholders_with_args( + &task, &spec, &scripts, &["abc".to_string(), "def".to_string()], @@ -365,6 +388,7 @@ mod tests { #[test] fn test_task_parse_flag() { + let task = Task::default(); let parser = TaskScriptParser::new(None); let scripts = vec!["echo {{ flag(name='foo') }}".to_string()]; let (scripts, spec) = parser.parse_run_scripts(&None, &scripts).unwrap(); @@ -373,13 +397,14 @@ mod tests { assert_eq!(&flag.name, "foo"); let scripts = - replace_template_placeholders_with_args(&spec, &scripts, &["--foo".to_string()]) + replace_template_placeholders_with_args(&task, &spec, &scripts, &["--foo".to_string()]) .unwrap(); assert_eq!(scripts, vec!["echo true"]); } #[test] fn test_task_parse_option() { + let task = Task::default(); let parser = TaskScriptParser::new(None); let scripts = vec!["echo {{ option(name='foo') }}".to_string()]; let (scripts, spec) = parser.parse_run_scripts(&None, &scripts).unwrap(); @@ -388,6 +413,7 @@ mod tests { assert_eq!(&option.name, "foo"); let scripts = replace_template_placeholders_with_args( + &task, &spec, &scripts, &["--foo".to_string(), "abc".to_string()],