1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
use std::{
ffi::{OsStr, OsString},
mem::take,
path::PathBuf,
};
use clap::{
builder::TypedValueParser,
error::{Error, ErrorKind},
Parser, ValueEnum, ValueHint,
};
use miette::{IntoDiagnostic, Result};
use tracing::{info, warn};
use watchexec_signals::Signal;
use crate::socket::{SocketSpec, SocketSpecValueParser};
use super::{TimeSpan, OPTSET_COMMAND};
#[derive(Debug, Clone, Parser)]
pub struct CommandArgs {
/// Use a different shell
///
/// By default, Watchexec will use '$SHELL' if it's defined or a default of 'sh' on Unix-likes,
/// and either 'pwsh', 'powershell', or 'cmd' (CMD.EXE) on Windows, depending on what Watchexec
/// detects is the running shell.
///
/// With this option, you can override that and use a different shell, for example one with more
/// features or one which has your custom aliases and functions.
///
/// If the value has spaces, it is parsed as a command line, and the first word used as the
/// shell program, with the rest as arguments to the shell.
///
/// The command is run with the '-c' flag (except for 'cmd' on Windows, where it's '/C').
///
/// The special value 'none' can be used to disable shell use entirely. In that case, the
/// command provided to Watchexec will be parsed, with the first word being the executable and
/// the rest being the arguments, and executed directly. Note that this parsing is rudimentary,
/// and may not work as expected in all cases.
///
/// Using 'none' is a little more efficient and can enable a stricter interpretation of the
/// input, but it also means that you can't use shell features like globbing, redirection,
/// control flow, logic, or pipes.
///
/// Examples:
///
/// Use without shell:
///
/// $ watchexec -n -- zsh -x -o shwordsplit scr
///
/// Use with powershell core:
///
/// $ watchexec --shell=pwsh -- Test-Connection localhost
///
/// Use with CMD.exe:
///
/// $ watchexec --shell=cmd -- dir
///
/// Use with a different unix shell:
///
/// $ watchexec --shell=bash -- 'echo $BASH_VERSION'
///
/// Use with a unix shell and options:
///
/// $ watchexec --shell='zsh -x -o shwordsplit' -- scr
#[arg(
long,
help_heading = OPTSET_COMMAND,
value_name = "SHELL",
display_order = 190,
)]
pub shell: Option<String>,
/// Shorthand for '--shell=none'
#[arg(
short = 'n',
help_heading = OPTSET_COMMAND,
display_order = 140,
)]
pub no_shell: bool,
/// Deprecated shorthand for '--emit-events=none'
///
/// This is the old way to disable event emission into the environment. See '--emit-events' for
/// more. Will be removed at next major release.
#[arg(
long,
help_heading = OPTSET_COMMAND,
hide = true, // deprecated
)]
pub no_environment: bool,
/// Add env vars to the command
///
/// This is a convenience option for setting environment variables for the command, without
/// setting them for the Watchexec process itself.
///
/// Use key=value syntax. Multiple variables can be set by repeating the option.
#[arg(
long,
short = 'E',
help_heading = OPTSET_COMMAND,
value_name = "KEY=VALUE",
value_parser = EnvVarValueParser,
display_order = 50,
)]
pub env: Vec<EnvVar>,
/// Don't use a process group
///
/// By default, Watchexec will run the command in a process group, so that signals and
/// terminations are sent to all processes in the group. Sometimes that's not what you want, and
/// you can disable the behaviour with this option.
///
/// Deprecated, use '--wrap-process=none' instead.
#[arg(
long,
help_heading = OPTSET_COMMAND,
display_order = 141,
)]
pub no_process_group: bool,
/// Configure how the process is wrapped
///
/// By default, Watchexec will run the command in a session on Mac, in a process group in Unix,
/// and in a Job Object in Windows.
///
/// Some Unix programs prefer running in a session, while others do not work in a process group.
///
/// Use 'group' to use a process group, 'session' to use a process session, and 'none' to run
/// the command directly. On Windows, either of 'group' or 'session' will use a Job Object.
///
/// If you find you need to specify this frequently for different kinds of programs, file an
/// issue at <https://github.com/watchexec/watchexec/issues>. As errors of this nature are hard to
/// debug and can be highly environment-dependent, reports from *multiple affected people* are
/// more likely to be actioned promptly. Ask your friends/colleagues!
#[arg(
long,
help_heading = OPTSET_COMMAND,
value_name = "MODE",
default_value = WRAP_DEFAULT,
display_order = 231,
)]
pub wrap_process: WrapMode,
/// Signal to send to stop the command
///
/// This is used by 'restart' and 'signal' modes of '--on-busy-update' (unless '--signal' is
/// provided). The restart behaviour is to send the signal, wait for the command to exit, and if
/// it hasn't exited after some time (see '--timeout-stop'), forcefully terminate it.
///
/// The default on unix is "SIGTERM".
///
/// Input is parsed as a full signal name (like "SIGTERM"), a short signal name (like "TERM"),
/// or a signal number (like "15"). All input is case-insensitive.
///
/// On Windows this option is technically supported but only supports the "KILL" event, as
/// Watchexec cannot yet deliver other events. Windows doesn't have signals as such; instead it
/// has termination (here called "KILL" or "STOP") and "CTRL+C", "CTRL+BREAK", and "CTRL+CLOSE"
/// events. For portability the unix signals "SIGKILL", "SIGINT", "SIGTERM", and "SIGHUP" are
/// respectively mapped to these.
#[arg(
long,
help_heading = OPTSET_COMMAND,
value_name = "SIGNAL",
display_order = 191,
)]
pub stop_signal: Option<Signal>,
/// Time to wait for the command to exit gracefully
///
/// This is used by the 'restart' mode of '--on-busy-update'. After the graceful stop signal
/// is sent, Watchexec will wait for the command to exit. If it hasn't exited after this time,
/// it is forcefully terminated.
///
/// Takes a unit-less value in seconds, or a time span value such as "5min 20s".
/// Providing a unit-less value is deprecated and will warn; it will be an error in the future.
///
/// The default is 10 seconds. Set to 0 to immediately force-kill the command.
///
/// This has no practical effect on Windows as the command is always forcefully terminated; see
/// '--stop-signal' for why.
#[arg(
long,
help_heading = OPTSET_COMMAND,
default_value = "10s",
hide_default_value = true,
value_name = "TIMEOUT",
display_order = 192,
)]
pub stop_timeout: TimeSpan,
/// Kill the command if it runs longer than this duration
///
/// Takes a time span value such as "30s", "5min", or "1h 30m".
///
/// When the timeout is reached, the command is gracefully stopped using --stop-signal, then
/// forcefully terminated after --stop-timeout if still running.
///
/// Each run of the command has its own independent timeout.
#[arg(
long,
help_heading = OPTSET_COMMAND,
value_name = "TIMEOUT",
display_order = 193,
)]
pub timeout: Option<TimeSpan>,
/// Sleep before running the command
///
/// This option will cause Watchexec to sleep for the specified amount of time before running
/// the command, after an event is detected. This is like using "sleep 5 && command" in a shell,
/// but portable and slightly more efficient.
///
/// Takes a unit-less value in seconds, or a time span value such as "2min 5s".
/// Providing a unit-less value is deprecated and will warn; it will be an error in the future.
#[arg(
long,
help_heading = OPTSET_COMMAND,
value_name = "DURATION",
display_order = 40,
)]
pub delay_run: Option<TimeSpan>,
/// Set the working directory
///
/// By default, the working directory of the command is the working directory of Watchexec. You
/// can change that with this option. Note that paths may be less intuitive to use with this.
#[arg(
long,
help_heading = OPTSET_COMMAND,
value_hint = ValueHint::DirPath,
value_name = "DIRECTORY",
display_order = 230,
)]
pub workdir: Option<PathBuf>,
/// Provide a socket to the command
///
/// This implements the systemd socket-passing protocol, like with `systemfd`: sockets are
/// opened from the watchexec process, and then passed to the commands it runs. This lets you
/// keep sockets open and avoid address reuse issues or dropping packets.
///
/// This option can be supplied multiple times, to open multiple sockets.
///
/// The value can be either of `PORT` (opens a TCP listening socket at that port), `HOST:PORT`
/// (specify a host IP address; IPv6 addresses can be specified `[bracketed]`), `TYPE::PORT` or
/// `TYPE::HOST:PORT` (specify a socket type, `tcp` / `udp`).
///
/// This integration only provides basic support, if you want more control you should use the
/// `systemfd` tool from <https://github.com/mitsuhiko/systemfd>, upon which this is based. The
/// syntax here and the spawning behaviour is identical to `systemfd`, and both watchexec and
/// systemfd are compatible implementations of the systemd socket-activation protocol.
///
/// Watchexec does _not_ set the `LISTEN_PID` variable on unix, which means any child process of
/// your command could accidentally bind to the sockets, unless the `LISTEN_*` variables are
/// removed from the environment.
#[arg(
long,
help_heading = OPTSET_COMMAND,
value_name = "PORT",
value_parser = SocketSpecValueParser,
display_order = 60,
)]
pub socket: Vec<SocketSpec>,
}
impl CommandArgs {
pub(crate) async fn normalise(&mut self) -> Result<()> {
if self.no_process_group {
warn!("--no-process-group is deprecated");
self.wrap_process = WrapMode::None;
}
let workdir = if let Some(w) = take(&mut self.workdir) {
w
} else {
let curdir = std::env::current_dir().into_diagnostic()?;
dunce::canonicalize(curdir).into_diagnostic()?
};
info!(path=?workdir, "effective working directory");
self.workdir = Some(workdir);
debug_assert!(self.workdir.is_some());
Ok(())
}
}
#[derive(Clone, Copy, Debug, Default, ValueEnum)]
pub enum WrapMode {
#[default]
Group,
Session,
None,
}
pub const WRAP_DEFAULT: &str = if cfg!(target_os = "macos") {
"session"
} else {
"group"
};
#[derive(Clone, Debug)]
pub struct EnvVar {
pub key: String,
pub value: OsString,
}
#[derive(Clone)]
pub(crate) struct EnvVarValueParser;
impl TypedValueParser for EnvVarValueParser {
type Value = EnvVar;
fn parse_ref(
&self,
_cmd: &clap::Command,
_arg: Option<&clap::Arg>,
value: &OsStr,
) -> Result<Self::Value, Error> {
let value = value
.to_str()
.ok_or_else(|| Error::raw(ErrorKind::ValueValidation, "invalid UTF-8"))?;
let (key, value) = value
.split_once('=')
.ok_or_else(|| Error::raw(ErrorKind::ValueValidation, "missing = separator"))?;
Ok(EnvVar {
key: key.into(),
value: value.into(),
})
}
}