Adapt Shell command builder

This commit is contained in:
Félix Saparelli 2021-08-17 03:09:22 +12:00
parent 7053360187
commit f5e19a6e5f
No known key found for this signature in database
GPG Key ID: B948C4BAE44FC474
4 changed files with 218 additions and 0 deletions

14
Cargo.lock generated
View File

@ -80,6 +80,17 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "async-trait"
version = "0.1.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e"
dependencies = [
"proc-macro2",
"quote 1.0.9",
"syn 1.0.73",
]
[[package]]
name = "autocfg"
version = "1.0.1"
@ -254,7 +265,9 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "758ddf93da6b6b45c6e44a73d07362945b98814e8c9bb57a8c9353f921c18ba3"
dependencies = [
"async-trait",
"nix 0.22.0",
"tokio",
"winapi 0.3.9",
]
@ -2056,6 +2069,7 @@ version = "1.17.1"
dependencies = [
"chrono",
"color-eyre",
"command-group",
"dunce",
"miette",
"notify 5.0.0-pre.11",

View File

@ -22,6 +22,10 @@ thiserror = "1.0.26"
tracing = "0.1.26"
dunce = "1.0.2"
[dependencies.command-group]
version = "1.0.5"
features = ["with-tokio"]
[dependencies.tokio]
version = "1.10.0"
features = ["full"]

View File

@ -18,3 +18,6 @@
pub mod error;
pub mod event;
pub mod fs;
pub mod shell;
// the *action* is debounced, not the events

197
lib/src/shell.rs Normal file
View File

@ -0,0 +1,197 @@
use tokio::process::Command;
/// Shell to use to run commands.
///
/// `Cmd` and `Powershell` are special-cased because they have different calling
/// conventions. Also `Cmd` is only available in Windows, while `Powershell` is
/// also available on unices (provided the end-user has it installed, of course).
///
/// See [`Config.cmd`][crate::config::Config] for the semantics of `None` vs the
/// other options.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Shell {
/// Use no shell, and execute the command directly.
None,
/// Use the given string as a unix shell invocation.
///
/// This means two things:
/// - the program is invoked with `-c` followed by the command, and
/// - the string will be split on space, and the resulting vec used as
/// execvp(3) arguments: first is the shell program, rest are additional
/// arguments (which come before the `-c` mentioned above). This is a very
/// simplistic approach deliberately: it will not support quoted
/// arguments, for example. Use [`Shell::None`] with a custom command vec
/// if you want that.
Unix(String),
/// Use the Windows CMD.EXE shell.
///
/// This is invoked with `/C` followed by the command.
#[cfg(windows)]
Cmd,
/// Use Powershell, on Windows or elsewhere.
///
/// This is invoked with `-Command` followed by the command.
///
/// This is preferred over `Unix("pwsh")`, though that will also work
/// on unices due to Powershell supporting the `-c` short option.
Powershell,
}
impl Default for Shell {
#[cfg(windows)]
fn default() -> Self {
Self::Powershell
}
#[cfg(not(windows))]
fn default() -> Self {
Self::Unix("sh".into())
}
}
impl Shell {
/// Obtain a [`Command`] given the cmd vec from [`Config`][crate::config::Config].
///
/// Behaves as described in the enum documentation.
///
/// # Panics
///
/// - Panics if `cmd` is empty.
/// - Panics if the string in the `Unix` variant is empty or only whitespace.
pub fn to_command(&self, cmd: &[String]) -> Command {
assert!(!cmd.is_empty(), "cmd was empty");
match self {
Shell::None => {
// UNWRAP: checked by assert
#[allow(clippy::unwrap_used)]
let (first, rest) = cmd.split_first().unwrap();
let mut c = Command::new(first);
c.args(rest);
c
}
#[cfg(windows)]
Shell::Cmd => {
let mut c = Command::new("cmd.exe");
c.arg("/C").arg(cmd.join(" "));
c
}
Shell::Powershell if cfg!(windows) => {
let mut c = Command::new("powershell.exe");
c.arg("-Command").arg(cmd.join(" "));
c
}
Shell::Powershell => {
let mut c = Command::new("pwsh");
c.arg("-Command").arg(cmd.join(" "));
c
}
Shell::Unix(name) => {
assert!(!name.is_empty(), "shell program was empty");
let sh = name.split_ascii_whitespace().collect::<Vec<_>>();
// UNWRAP: checked by assert
#[allow(clippy::unwrap_used)]
let (shprog, shopts) = sh.split_first().unwrap();
let mut c = Command::new(shprog);
c.args(shopts);
c.arg("-c").arg(cmd.join(" "));
c
}
}
}
}
#[cfg(test)]
mod test {
use super::Shell;
use command_group::AsyncCommandGroup;
#[tokio::test]
#[cfg(unix)]
async fn unix_shell_default() -> Result<(), std::io::Error> {
assert!(Shell::default()
.to_command(&["echo".into(), "hi".into()])
.group_status().await?
.success());
Ok(())
}
#[tokio::test]
#[cfg(unix)]
async fn unix_shell_none() -> Result<(), std::io::Error> {
assert!(Shell::None
.to_command(&["echo".into(), "hi".into()])
.group_status().await?
.success());
Ok(())
}
#[tokio::test]
#[cfg(unix)]
async fn unix_shell_alternate() -> Result<(), std::io::Error> {
assert!(Shell::Unix("bash".into())
.to_command(&["echo".into(), "hi".into()])
.group_status().await?
.success());
Ok(())
}
#[tokio::test]
#[cfg(unix)]
async fn unix_shell_alternate_shopts() -> Result<(), std::io::Error> {
assert!(Shell::Unix("bash -o errexit".into())
.to_command(&["echo".into(), "hi".into()])
.group_status().await?
.success());
Ok(())
}
#[tokio::test]
#[cfg(windows)]
async fn windows_shell_default() -> Result<(), std::io::Error> {
assert!(Shell::default()
.to_command(&["echo".into(), "hi".into()])
.group_status().await?
.success());
Ok(())
}
#[tokio::test]
#[cfg(windows)]
async fn windows_shell_cmd() -> Result<(), std::io::Error> {
assert!(Shell::Cmd
.to_command(&["echo".into(), "hi".into()])
.group_status().await?
.success());
Ok(())
}
#[tokio::test]
#[cfg(windows)]
async fn windows_shell_powershell() -> Result<(), std::io::Error> {
assert!(Shell::Powershell
.to_command(&["echo".into(), "hi".into()])
.group_status().await?
.success());
Ok(())
}
#[tokio::test]
#[cfg(windows)]
async fn windows_shell_unix_style_powershell() -> Result<(), std::io::Error> {
assert!(Shell::Unix("powershell.exe".into())
.to_command(&["echo".into(), "hi".into()])
.group_status().await?
.success());
Ok(())
}
}