use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use tokio::process::Command;
use super::windows_shim_args::{bat_command_line, validate_powershell_args, WindowsShimArgError};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum CliInvocationKind {
Direct,
CmdScript,
PowerShellScript,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct CliExecutable {
program: PathBuf,
kind: CliInvocationKind,
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub(crate) enum CliExecutableError {
#[error(transparent)]
WindowsShim(#[from] WindowsShimArgError),
}
impl CliExecutable {
pub(crate) fn resolve(program: &str) -> Option<Self> {
if program.contains('/') || program.contains('\\') {
let path = PathBuf::from(program);
return path.is_file().then(|| Self::from_path(path));
}
if let Some(path) = crate::executable::find_on_path(program) {
return Some(Self::from_path(path));
}
#[cfg(windows)]
{
for candidate in [format!("{program}.cmd"), format!("{program}.ps1")] {
if let Some(path) = crate::executable::find_on_path(&candidate) {
return Some(Self::from_path(path));
}
}
}
None
}
pub(crate) fn from_path(path: impl Into<PathBuf>) -> Self {
let program = path.into();
let kind = classify_program(&program);
Self { program, kind }
}
pub(crate) fn display(&self) -> String {
crate::paths::display(&self.program)
}
pub(crate) fn path(&self) -> &Path {
&self.program
}
#[cfg(test)]
pub(crate) fn kind(&self) -> CliInvocationKind {
self.kind
}
pub(crate) fn plan<I, S>(&self, args: I) -> Result<CliArgv, CliExecutableError>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let args: Vec<OsString> = args
.into_iter()
.map(|arg| arg.as_ref().to_os_string())
.collect();
match self.kind {
CliInvocationKind::Direct => Ok(CliArgv {
program: self.program.clone(),
args,
}),
CliInvocationKind::CmdScript => {
let _ = bat_command_line(&self.program, &args)?;
Ok(CliArgv {
program: self.program.clone(),
args,
})
}
CliInvocationKind::PowerShellScript => {
validate_powershell_args(&args)?;
let mut argv = vec![
OsString::from("-NoProfile"),
OsString::from("-NonInteractive"),
OsString::from("-ExecutionPolicy"),
OsString::from("Bypass"),
OsString::from("-File"),
self.program.as_os_str().to_os_string(),
];
argv.extend(args);
Ok(CliArgv {
program: PathBuf::from("powershell.exe"),
args: argv,
})
}
}
}
pub(crate) fn try_command<I, S>(&self, args: I) -> Result<Command, CliExecutableError>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
Ok(self.plan(args)?.command())
}
}
fn classify_program(path: &Path) -> CliInvocationKind {
let ext = path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("")
.to_ascii_lowercase();
match ext.as_str() {
"cmd" | "bat" => CliInvocationKind::CmdScript,
"ps1" => CliInvocationKind::PowerShellScript,
_ => CliInvocationKind::Direct,
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct CliArgv {
pub(crate) program: PathBuf,
pub(crate) args: Vec<OsString>,
}
impl CliArgv {
fn command(&self) -> Command {
let mut command = Command::new(&self.program);
command.args(&self.args);
command
}
}
#[cfg(test)]
#[path = "executable_tests.rs"]
mod tests;