1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-20 03:41:36 +03:00
wezterm/pty/src/cmdbuilder.rs

200 lines
6.3 KiB
Rust
Raw Normal View History

2019-03-25 21:45:52 +03:00
#[cfg(windows)]
use failure::{ensure, Error};
2019-06-09 01:37:55 +03:00
#[cfg(windows)]
use log::error;
#[cfg(feature = "serde_support")]
2019-06-09 18:15:37 +03:00
use serde_derive::*;
2019-03-25 17:16:58 +03:00
use std::ffi::{OsStr, OsString};
2019-03-25 21:45:52 +03:00
#[cfg(windows)]
use std::os::windows::ffi::OsStrExt;
2019-03-25 17:16:58 +03:00
/// `CommandBuilder` is used to prepare a command to be spawned into a pty.
/// The interface is intentionally similar to that of `std::process::Command`.
#[derive(Debug, PartialEq)]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
2019-03-25 17:16:58 +03:00
pub struct CommandBuilder {
args: Vec<OsString>,
2019-03-25 21:45:52 +03:00
envs: Vec<(OsString, OsString)>,
2019-03-25 17:16:58 +03:00
}
impl CommandBuilder {
/// Create a new builder instance with argv[0] set to the specified
/// program.
2019-03-25 17:16:58 +03:00
pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
Self {
args: vec![program.as_ref().to_owned()],
2019-03-25 21:45:52 +03:00
envs: vec![],
2019-03-25 17:16:58 +03:00
}
}
/// Append an argument to the current command line
2019-03-25 17:16:58 +03:00
pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) {
self.args.push(arg.as_ref().to_owned());
}
/// Append a sequence of arguments to the current command line
2019-03-25 17:16:58 +03:00
pub fn args<I, S>(&mut self, args: I)
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
for arg in args {
self.arg(arg);
}
}
/// Override the value of an environmental variable
2019-03-25 17:16:58 +03:00
pub fn env<K, V>(&mut self, key: K, val: V)
where
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
2019-03-25 21:45:52 +03:00
self.envs
.push((key.as_ref().to_owned(), val.as_ref().to_owned()));
#[cfg(windows)]
2019-06-09 01:37:55 +03:00
error!(
2019-03-25 17:16:58 +03:00
"ignoring env {:?}={:?} for child; FIXME: implement this!",
key.as_ref(),
val.as_ref()
);
}
#[cfg(feature = "ssh")]
pub(crate) fn iter_env_as_str(&self) -> impl Iterator<Item = (&str, &str)> {
self.envs.iter().filter_map(|(key, val)| {
let key = key.to_str()?;
let val = val.to_str()?;
Some((key, val))
})
}
#[cfg(feature = "ssh")]
pub(crate) fn as_unix_command_line(&self) -> failure::Fallible<String> {
let mut strs = vec![];
for arg in &self.args {
let s = arg
.to_str()
.ok_or_else(|| failure::err_msg("argument cannot be represented as utf8"))?;
strs.push(s);
}
Ok(shell_words::join(strs))
}
2019-03-25 21:45:52 +03:00
}
#[cfg(unix)]
impl CommandBuilder {
/// Convert the CommandBuilder to a `std::process::Command` instance.
pub(crate) fn as_command(&self) -> std::process::Command {
2019-03-25 21:45:52 +03:00
let mut cmd = std::process::Command::new(&self.args[0]);
cmd.args(&self.args[1..]);
for (key, val) in &self.envs {
cmd.env(key, val);
}
cmd
}
}
#[cfg(windows)]
impl CommandBuilder {
fn search_path(exe: &OsStr) -> OsString {
if let Some(path) = std::env::var_os("PATH") {
let extensions = std::env::var_os("PATHEXT").unwrap_or(".EXE".into());
for path in std::env::split_paths(&path) {
// Check for exactly the user's string in this path dir
let candidate = path.join(&exe);
if candidate.exists() {
return candidate.into_os_string();
}
// otherwise try tacking on some extensions.
// Note that this really replaces the extension in the
// user specified path, so this is potentially wrong.
for ext in std::env::split_paths(&extensions) {
// PATHEXT includes the leading `.`, but `with_extension`
// doesn't want that
let ext = ext.to_str().expect("PATHEXT entries must be utf8");
let path = path.join(&exe).with_extension(&ext[1..]);
if path.exists() {
return path.into_os_string();
}
}
}
}
exe.to_owned()
}
2019-03-25 17:16:58 +03:00
pub(crate) fn cmdline(&self) -> Result<(Vec<u16>, Vec<u16>), Error> {
2019-03-25 17:16:58 +03:00
let mut cmdline = Vec::<u16>::new();
let exe = Self::search_path(&self.args[0]);
Self::append_quoted(&exe, &mut cmdline);
// Ensure that we nul terminate the module name, otherwise we'll
// ask CreateProcessW to start something random!
let mut exe: Vec<u16> = exe.encode_wide().collect();
exe.push(0);
for arg in self.args.iter().skip(1) {
cmdline.push(' ' as u16);
ensure!(
!arg.encode_wide().any(|c| c == 0),
"invalid encoding for command line argument {:?}",
arg
);
Self::append_quoted(arg, &mut cmdline);
}
// Ensure that the command line is nul terminated too!
cmdline.push(0);
Ok((exe, cmdline))
}
// Borrowed from https://github.com/hniksic/rust-subprocess/blob/873dfed165173e52907beb87118b2c0c05d8b8a1/src/popen.rs#L1117
// which in turn was translated from ArgvQuote at http://tinyurl.com/zmgtnls
fn append_quoted(arg: &OsStr, cmdline: &mut Vec<u16>) {
if !arg.is_empty()
&& !arg.encode_wide().any(|c| {
c == ' ' as u16
|| c == '\t' as u16
|| c == '\n' as u16
|| c == '\x0b' as u16
|| c == '\"' as u16
})
{
cmdline.extend(arg.encode_wide());
return;
}
cmdline.push('"' as u16);
let arg: Vec<_> = arg.encode_wide().collect();
let mut i = 0;
while i < arg.len() {
let mut num_backslashes = 0;
while i < arg.len() && arg[i] == '\\' as u16 {
i += 1;
num_backslashes += 1;
}
if i == arg.len() {
for _ in 0..num_backslashes * 2 {
cmdline.push('\\' as u16);
}
break;
} else if arg[i] == b'"' as u16 {
for _ in 0..num_backslashes * 2 + 1 {
cmdline.push('\\' as u16);
}
cmdline.push(arg[i]);
} else {
for _ in 0..num_backslashes {
cmdline.push('\\' as u16);
}
cmdline.push(arg[i]);
}
i += 1;
}
cmdline.push('"' as u16);
}
}