From bb6a5ae8914c67dbfde41c64b005211ba57f55d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Wed, 21 Jul 2021 23:37:24 +1200 Subject: [PATCH] Split process.rs into shell and paths (now that the bulk of process code is gone) --- lib/src/config.rs | 2 +- lib/src/lib.rs | 5 +- lib/src/{process.rs => paths.rs} | 208 +------------------------------ lib/src/shell.rs | 197 +++++++++++++++++++++++++++++ 4 files changed, 205 insertions(+), 207 deletions(-) rename lib/src/{process.rs => paths.rs} (50%) create mode 100644 lib/src/shell.rs diff --git a/lib/src/config.rs b/lib/src/config.rs index eeb5221..f7e14a7 100644 --- a/lib/src/config.rs +++ b/lib/src/config.rs @@ -15,8 +15,8 @@ use std::{path::PathBuf, time::Duration}; -use crate::process::Shell; use crate::run::OnBusyUpdate; +use crate::Shell; /// Arguments to the watcher #[derive(Builder, Clone, Debug)] diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 3b9e0c9..3f153be 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -21,10 +21,11 @@ mod gitignore; mod ignore; mod notification_filter; pub mod pathop; -mod process; +mod paths; pub mod run; +mod shell; mod signal; mod watcher; -pub use process::Shell; pub use run::{run, watch, Handler}; +pub use shell::Shell; diff --git a/lib/src/process.rs b/lib/src/paths.rs similarity index 50% rename from lib/src/process.rs rename to lib/src/paths.rs index 18ac55f..9984fac 100644 --- a/lib/src/process.rs +++ b/lib/src/paths.rs @@ -1,147 +1,9 @@ -#![allow(unsafe_code)] - -use crate::error::Result; use crate::pathop::PathOp; -use command_group::{CommandGroup, GroupChild}; use std::{ collections::{HashMap, HashSet}, path::PathBuf, - 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::>(); - - // 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 - } - } - } -} - -pub fn spawn( - cmd: &[String], - updated_paths: &[PathOp], - shell: Shell, - environment: bool, -) -> Result { - let mut command = shell.to_command(&cmd); - debug!("Assembled command {:?}", command); - - let command_envs = if !environment { - Vec::new() - } else { - collect_path_env_vars(updated_paths) - }; - - for (name, val) in &command_envs { - command.env(name, val); - } - - let child = command.group_spawn()?; - Ok(child) -} - /// Collect `PathOp` details into op-categories to pass onto the exec'd command as env-vars /// /// `WRITTEN` -> `notify::ops::WRITE`, `notify::ops::CLOSE_WRITE` @@ -149,7 +11,7 @@ pub fn spawn( /// `REMOVED` -> `notify::ops::REMOVE` /// `CREATED` -> `notify::ops::CREATE` /// `RENAMED` -> `notify::ops::RENAME` -fn collect_path_env_vars(pathops: &[PathOp]) -> Vec<(String, String)> { +pub fn collect_path_env_vars(pathops: &[PathOp]) -> Vec<(String, String)> { #[cfg(target_family = "unix")] const ENV_SEP: &str = ":"; #[cfg(not(target_family = "unix"))] @@ -205,7 +67,7 @@ fn collect_path_env_vars(pathops: &[PathOp]) -> Vec<(String, String)> { vars } -fn get_longest_common_path(paths: &[PathBuf]) -> Option { +pub fn get_longest_common_path(paths: &[PathBuf]) -> Option { match paths.len() { 0 => return None, 1 => return paths[0].to_str().map(ToString::to_string), @@ -238,48 +100,16 @@ fn get_longest_common_path(paths: &[PathBuf]) -> Option { } #[cfg(test)] -#[cfg(target_family = "unix")] mod tests { - use super::Shell; use crate::pathop::PathOp; use std::collections::HashSet; use std::path::PathBuf; use super::collect_path_env_vars; use super::get_longest_common_path; - use super::spawn; - - #[test] - fn test_shell_default() { - let _ = spawn(&["echo".into(), "hi".into()], &[], Shell::default(), false); - } - - #[test] - fn test_shell_none() { - let _ = spawn(&["echo".into(), "hi".into()], &[], Shell::None, false); - } - - #[test] - fn test_shell_alternate() { - let _ = spawn( - &["echo".into(), "hi".into()], - &[], - Shell::Unix("bash".into()), - false, - ); - } - - #[test] - fn test_shell_alternate_shopts() { - let _ = spawn( - &["echo".into(), "hi".into()], - &[], - Shell::Unix("bash -o errexit".into()), - false, - ); - } #[test] + #[cfg(unix)] fn longest_common_path_should_return_correct_value() { let single_path = vec![PathBuf::from("/tmp/random/")]; let single_result = @@ -315,6 +145,7 @@ mod tests { } #[test] + #[cfg(unix)] fn pathops_collect_to_env_vars() { let pathops = vec![ PathOp::new( @@ -348,34 +179,3 @@ mod tests { ); } } - -#[cfg(test)] -#[cfg(target_family = "windows")] -mod tests { - use super::{spawn, Shell}; - - #[test] - fn test_shell_default() { - let _ = spawn(&["echo".into(), "hi".into()], &[], Shell::default(), false); - } - - #[test] - fn test_shell_cmd() { - let _ = spawn(&["echo".into(), "hi".into()], &[], Shell::Cmd, false); - } - - #[test] - fn test_shell_powershell() { - let _ = spawn(&["echo".into(), "hi".into()], &[], Shell::Powershell, false); - } - - #[test] - fn test_shell_bash() { - let _ = spawn( - &["echo".into(), "hi".into()], - &[], - Shell::Unix("bash".into()), - false, - ); - } -} diff --git a/lib/src/shell.rs b/lib/src/shell.rs new file mode 100644 index 0000000..4566681 --- /dev/null +++ b/lib/src/shell.rs @@ -0,0 +1,197 @@ +use std::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::>(); + + // 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::CommandGroup; + + #[test] + #[cfg(unix)] + fn unix_shell_default() -> Result<(), std::io::Error> { + assert!(Shell::default() + .to_command(&["echo".into(), "hi".into()]) + .group_status()? + .success()); + Ok(()) + } + + #[test] + #[cfg(unix)] + fn unix_shell_none() -> Result<(), std::io::Error> { + assert!(Shell::None + .to_command(&["echo".into(), "hi".into()]) + .group_status()? + .success()); + Ok(()) + } + + #[test] + #[cfg(unix)] + fn unix_shell_alternate() -> Result<(), std::io::Error> { + assert!(Shell::Unix("bash".into()) + .to_command(&["echo".into(), "hi".into()]) + .group_status()? + .success()); + Ok(()) + } + + #[test] + #[cfg(unix)] + fn unix_shell_alternate_shopts() -> Result<(), std::io::Error> { + assert!(Shell::Unix("bash -o errexit".into()) + .to_command(&["echo".into(), "hi".into()]) + .group_status()? + .success()); + Ok(()) + } + + #[test] + #[cfg(windows)] + fn windows_shell_default() -> Result<(), std::io::Error> { + assert!(Shell::default() + .to_command(&["echo".into(), "hi".into()]) + .group_status()? + .success()); + Ok(()) + } + + #[test] + #[cfg(windows)] + fn windows_shell_cmd() -> Result<(), std::io::Error> { + assert!(Shell::Cmd + .to_command(&["echo".into(), "hi".into()]) + .group_status()? + .success()); + Ok(())(&["echo".into(), "hi".into()], &[], Shell::Cmd, false); + } + + #[test] + #[cfg(windows)] + fn windows_shell_powershell() -> Result<(), std::io::Error> { + assert!(Shell::Powershell + .to_command(&["echo".into(), "hi".into()]) + .group_status()? + .success()); + Ok(()) + } + + #[test] + #[cfg(windows)] + fn windows_shell_bash() -> Result<(), std::io::Error> { + assert!(Shell::Unix("bash") + .to_command(&["echo".into(), "hi".into()]) + .group_status()? + .success()); + Ok(()) + } +}