aboutsummaryrefslogtreecommitdiff
path: root/src/plugin_command.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugin_command.rs')
-rw-r--r--src/plugin_command.rs129
1 files changed, 129 insertions, 0 deletions
diff --git a/src/plugin_command.rs b/src/plugin_command.rs
new file mode 100644
index 00000000..ba0a1422
--- /dev/null
+++ b/src/plugin_command.rs
@@ -0,0 +1,129 @@
+#[cfg(any(windows, test))]
+use std::path::Path;
+#[cfg(windows)]
+use std::path::PathBuf;
+use std::process::Command;
+
+pub(crate) fn command_for_argv(program: &str, args: &[String]) -> Command {
+ let mut command = command_for_program(program);
+ command.args(args);
+ command
+}
+
+#[cfg(not(windows))]
+fn command_for_program(program: &str) -> Command {
+ Command::new(program)
+}
+
+#[cfg(windows)]
+fn command_for_program(program: &str) -> Command {
+ let resolved = resolve_windows_program(program);
+ let command_program = resolved.as_ref().map_or_else(
+ || std::ffi::OsString::from(program),
+ |path| path.as_os_str().to_os_string(),
+ );
+ if is_windows_batch_file_name(program)
+ || resolved
+ .as_ref()
+ .is_some_and(|path| is_windows_batch_path(path))
+ {
+ let shell =
+ std::env::var_os("ComSpec").unwrap_or_else(|| r"C:\Windows\System32\cmd.exe".into());
+ let mut command = Command::new(shell);
+ command.arg("/d").arg("/c").arg(command_program);
+ command
+ } else {
+ Command::new(command_program)
+ }
+}
+
+#[cfg(windows)]
+fn resolve_windows_program(program: &str) -> Option<PathBuf> {
+ if has_path_separator(program) {
+ return None;
+ }
+ let path = Path::new(program);
+ if path.extension().is_some() {
+ return std::env::var_os("PATH").and_then(|path_var| {
+ std::env::split_paths(&path_var)
+ .map(|dir| dir.join(program))
+ .find(|candidate| candidate.is_file())
+ });
+ }
+ let extensions = windows_path_extensions();
+ std::env::var_os("PATH").and_then(|path_var| {
+ std::env::split_paths(&path_var).find_map(|dir| {
+ extensions
+ .iter()
+ .map(|extension| dir.join(format!("{program}{extension}")))
+ .find(|candidate| candidate.is_file())
+ })
+ })
+}
+
+#[cfg(windows)]
+fn windows_path_extensions() -> Vec<String> {
+ std::env::var_os("PATHEXT")
+ .map(|value| {
+ value
+ .to_string_lossy()
+ .split(';')
+ .map(str::trim)
+ .filter(|part| !part.is_empty())
+ .map(|part| {
+ if part.starts_with('.') {
+ part.to_string()
+ } else {
+ format!(".{part}")
+ }
+ })
+ .collect::<Vec<_>>()
+ })
+ .filter(|extensions| !extensions.is_empty())
+ .unwrap_or_else(|| {
+ vec![
+ ".COM".to_string(),
+ ".EXE".to_string(),
+ ".BAT".to_string(),
+ ".CMD".to_string(),
+ ]
+ })
+}
+
+#[cfg(windows)]
+fn has_path_separator(program: &str) -> bool {
+ program.contains(['/', '\\'])
+}
+
+#[cfg(windows)]
+fn is_windows_batch_path(path: &Path) -> bool {
+ path.extension()
+ .and_then(std::ffi::OsStr::to_str)
+ .is_some_and(is_windows_batch_extension)
+}
+
+#[cfg(any(windows, test))]
+fn is_windows_batch_file_name(program: &str) -> bool {
+ Path::new(program)
+ .extension()
+ .and_then(std::ffi::OsStr::to_str)
+ .is_some_and(is_windows_batch_extension)
+}
+
+#[cfg(any(windows, test))]
+fn is_windows_batch_extension(extension: &str) -> bool {
+ extension.eq_ignore_ascii_case("cmd") || extension.eq_ignore_ascii_case("bat")
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn recognizes_windows_batch_extensions_case_insensitively() {
+ assert!(is_windows_batch_file_name("npm.cmd"));
+ assert!(is_windows_batch_file_name("script.BAT"));
+ assert!(!is_windows_batch_file_name("node.exe"));
+ assert!(!is_windows_batch_file_name("node"));
+ }
+}