diff --git a/README.md b/README.md
index 4db681ca..367b79c4 100644
--- a/README.md
+++ b/README.md
@@ -58,9 +58,9 @@ Call/restart `my_server` when any file in the current directory (and all subdire
$ watchexec -r -s SIGKILL my_server
-Send a SIGHUP to the child process upon changes:
+Send a SIGHUP to the child process upon changes (Note: with using `-n | --no-shell` here, we're executing `my_server` directly, instead of wrapping it in a shell:
- $ watchexec -s SIGHUP my_server
+ $ watchexec -n -s SIGHUP my_server
Run `make` when any file changes, using the `.gitignore` file in the current directory to filter:
diff --git a/doc/watchexec.1 b/doc/watchexec.1
index c95a17da..393c6c03 100644
--- a/doc/watchexec.1
+++ b/doc/watchexec.1
@@ -34,6 +34,10 @@ Ignores modifications from paths that do not match \fIpattern\fR\. This option c
Sends the specified signal (e\.g\. \fBSIGKILL\fR) to the child process\. Defaults to \fBSIGTERM\fR\.
.
.TP
+\fB\-n\fR, \fB\-\-no\-shell\fR
+Execute command directly, do not wrap it in \fBsh -c\fR resp\. \fBcmd.exec /C\R\. This is especially useful in combination with \fB--signal\fR, as the signal is then send directly to the specified command\. While \fB--no-shell\fR is a little more performant than the default, it prevents using shell-features like pipes and redirects\.
+.
+.TP
\fB\-i\fR, \fB\-\-ignore\fR \fIpattern\fR
Ignores modifications from paths that match \fIpattern\fR\. This option can be specified multiple times, and a match on any pattern causes the path to be ignored\.
.
diff --git a/doc/watchexec.1.html b/doc/watchexec.1.html
index 071e13de..4fc28b7d 100644
--- a/doc/watchexec.1.html
+++ b/doc/watchexec.1.html
@@ -89,6 +89,7 @@
-e
, --exts
extensionsComma-separated list of file extensions to filter by. Leading dots are allowed (.rs) are allowed. (This is a shorthand for -f
).
-f
, --filter
patternIgnores modifications from paths that do not match pattern. This option can be specified multiple times, where a match on any given pattern causes the path to trigger command.
-s
, --signal
SIGNALSends the specified signal (e.g. SIGKILl
) to the child process. Defaults to SIGTERM
.
+-n
, --no-shell
Execute command directly, do not wrap it in sh -c
resp. cmd.exec /C
. This is especially useful in combination with --signal
, as the signal is then send directly to the specified command. While --no-shell
is a little more performant than the default, it prevents using shell-features like pipes and redirects.
-i
, --ignore
patternIgnores modifications from paths that match pattern. This option can be specified multiple times, and a match on any pattern causes the path to be ignored.
-w
, --watch
pathMonitor a specific path for changes. By default, the current working directory is watched. This may be specified multiple times, where a change in any watched directory (and subdirectories) causes command to be executed.
-r
, --restart
Terminates the child process group if it is still running when subsequent file modifications are detected. By default, sends SIGTERM
; use --kill
to send SIGKILL
.
diff --git a/doc/watchexec.1.ronn b/doc/watchexec.1.ronn
index 1b8bbe39..7a1cf344 100644
--- a/doc/watchexec.1.ronn
+++ b/doc/watchexec.1.ronn
@@ -25,6 +25,9 @@ Ignores modifications from paths that do not match . This option can be
* `-s`, `--signal`:
Sends the specified signal (e.g. `SIGKILL`) to the child process. Defaults to `SIGTERM`.
+* `-n`, `--no-shell`:
+Execute command directly, do not wrap it in `sh -c` resp. `cmd.exe /C`. This is especially useful in combination with `--signal`, as the signal is then send directly to the specified command. While `--no-shell` is a little more performant than the default, it prevents using shell-features like pipes and redirects.
+
* `-i`, `--ignore` :
Ignores modifications from paths that match . This option can be specified multiple times, and a match on any pattern causes the path to be ignored.
diff --git a/src/cli.rs b/src/cli.rs
index 3ef1a7fe..1dfe2924 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -14,6 +14,7 @@ pub struct Args {
pub restart: bool,
pub debug: bool,
pub run_initially: bool,
+ pub no_shell: bool,
pub no_vcs_ignore: bool,
pub once: bool,
pub poll: bool,
@@ -102,6 +103,10 @@ pub fn get_args() -> Args {
.help("Forces polling mode")
.long("force-poll")
.value_name("interval"))
+ .arg(Arg::with_name("no-shell")
+ .help("Do not wrap command in 'sh -c' resp. 'cmd.exe /C'")
+ .short("n")
+ .long("no-shell"))
.arg(Arg::with_name("once").short("1").hidden(true))
.get_matches();
@@ -111,9 +116,11 @@ pub fn get_args() -> Args {
let paths = values_t!(args.values_of("path"), String).unwrap_or(vec![String::from(".")]);
// Treat --kill as --signal SIGKILL (for compatibility with older syntax)
- let signal = match args.is_present("kill") {
- true => Some("SIGKILL".to_string()),
- false => args.value_of("signal").map(str::to_string), // Convert Option<&str> to Option
+ let signal = if args.is_present("kill") {
+ Some("SIGKILL".to_string())
+ } else {
+ // Convert Option<&str> to Option
+ args.value_of("signal").map(str::to_string)
};
let mut filters = values_t!(args.values_of("filter"), String).unwrap_or(vec![]);
@@ -144,13 +151,13 @@ pub fn get_args() -> Args {
if signal.is_some() && args.is_present("postpone") {
// TODO: Error::argument_conflict() might be the better fit, usage was unclear, though
- Error::value_validation_auto(format!("--postpone and --signal are mutually exclusive"))
+ Error::value_validation_auto("--postpone and --signal are mutually exclusive".to_string())
.exit();
}
if signal.is_some() && args.is_present("kill") {
// TODO: Error::argument_conflict() might be the better fit, usage was unclear, though
- Error::value_validation_auto(format!("--kill and --signal is ambiguous.\n Hint: Use only '--signal SIGKILL' without --kill"))
+ Error::value_validation_auto("--kill and --signal is ambiguous.\n Hint: Use only '--signal SIGKILL' without --kill".to_string())
.exit();
}
@@ -164,6 +171,7 @@ pub fn get_args() -> Args {
restart: args.is_present("restart"),
debug: args.is_present("debug"),
run_initially: !args.is_present("postpone"),
+ no_shell: args.is_present("no-shell"),
no_vcs_ignore: args.is_present("no-vcs-ignore"),
once: args.is_present("once"),
poll: args.occurrences_of("poll") > 0,
diff --git a/src/main.rs b/src/main.rs
index ed1ec3fe..bc5dfeb4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -107,7 +107,7 @@ fn main() {
}
let mut guard = child_process.write().unwrap();
- *guard = Some(process::spawn(&args.cmd, vec![]));
+ *guard = Some(process::spawn(&args.cmd, vec![], args.no_shell));
}
loop {
@@ -140,7 +140,7 @@ fn main() {
debug!("Launching child process");
{
let mut guard = child_process.write().unwrap();
- *guard = Some(process::spawn(&args.cmd, paths));
+ *guard = Some(process::spawn(&args.cmd, paths, args.no_shell));
}
}
@@ -158,7 +158,7 @@ fn main() {
debug!("Launching child process");
{
let mut guard = child_process.write().unwrap();
- *guard = Some(process::spawn(&args.cmd, paths));
+ *guard = Some(process::spawn(&args.cmd, paths, args.no_shell));
}
}
@@ -179,7 +179,7 @@ fn main() {
debug!("Launching child process");
{
let mut guard = child_process.write().unwrap();
- *guard = Some(process::spawn(&args.cmd, paths));
+ *guard = Some(process::spawn(&args.cmd, paths, args.no_shell));
}
}
}
diff --git a/src/process.rs b/src/process.rs
index c95ffc46..a5393e86 100644
--- a/src/process.rs
+++ b/src/process.rs
@@ -1,7 +1,7 @@
use std::path::PathBuf;
-pub fn spawn(cmd: &str, updated_paths: Vec) -> Process {
- self::imp::Process::new(cmd, updated_paths).expect("unable to spawn process")
+pub fn spawn(cmd: &str, updated_paths: Vec, no_shell: bool) -> Process {
+ self::imp::Process::new(cmd, updated_paths, no_shell).expect("unable to spawn process")
}
pub use self::imp::Process;
@@ -24,13 +24,28 @@ mod imp {
#[allow(unknown_lints)]
#[allow(mutex_atomic)]
impl Process {
- pub fn new(cmd: &str, updated_paths: Vec) -> Result {
+ pub fn new(cmd: &str, updated_paths: Vec, no_shell: bool) -> Result {
use nix::unistd::*;
use std::io;
use std::os::unix::process::CommandExt;
- let mut command = Command::new("sh");
- command.arg("-c").arg(cmd);
+ // Assemble command to run.
+ // This is either the first argument from cmd (if no_shell was given) or "sh".
+ // Using "sh -c" gives us features like supportin pipes and redirects,
+ // but is a little less performant and can cause trouble when using custom signals
+ // (e.g. --signal SIGHUP)
+ let mut command = if no_shell {
+ let mut split = cmd.split_whitespace();
+ let mut command = Command::new(split.next().unwrap());
+ command.args(split);
+ command
+ } else {
+ let mut command = Command::new("sh");
+ command.arg("-c").arg(cmd);
+ command
+ };
+
+ debug!("Assembled command {:?}", command);
if let Some(single_path) = super::get_single_updated_path(&updated_paths) {
command.env("WATCHEXEC_UPDATED_PATH", single_path);
@@ -119,7 +134,7 @@ mod imp {
}
impl Process {
- pub fn new(cmd: &str, updated_paths: Vec) -> Result {
+ pub fn new(cmd: &str, updated_paths: Vec, no_shell: bool) -> Result {
use std::os::windows::io::IntoRawHandle;
fn last_err() -> io::Error {
@@ -143,8 +158,23 @@ mod imp {
panic!("failed to set job info: {}", last_err());
}
- let mut command = Command::new("cmd.exe");
- command.arg("/C").arg(cmd);
+ let mut iter_args = cmd.split_whitespace();
+ let arg0 = match no_shell {
+ true => iter_args.next().unwrap(),
+ false => "cmd.exe",
+ };
+
+ // TODO: There might be a better way of doing this with &str.
+ // I've had to fall back to String, as I wasn't able to join(" ") a Vec<&str>
+ // into a &str
+ let args: Vec = match no_shell {
+ true => iter_args.map(str::to_string).collect(),
+ false => vec!["/C".to_string(), iter_args.collect::>().join(" ")],
+ };
+
+ let mut command = Command::new(arg0);
+ command.args(args);
+ debug!("Assembled command {:?}", command);
if let Some(single_path) = super::get_single_updated_path(&updated_paths) {
command.env("WATCHEXEC_UPDATED_PATH", single_path);
@@ -241,7 +271,7 @@ mod tests {
#[test]
fn test_start() {
- let _ = spawn("echo hi", vec![]);
+ let _ = spawn("echo hi", vec![], true);
}
#[test]