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
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
use std::{
ffi::{OsStr, OsString},
mem::take,
path::PathBuf,
};
use clap::{
builder::BoolishValueParser,
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};
/// Returns the default quoting value for the current platform.
///
/// On Windows this is false, and on Unix this is true.
/// This is to allow Unix and Windows to separately work as expected by default,
/// with logic elsewhere to allow opting in to/out of quoting.
const fn get_platform_default_quoting() -> bool {
#[cfg(windows)]
{
false
}
#[cfg(unix)]
{
true
}
}
#[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 (mode 'auto'), 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.
/// Additionally, on Unix in 'auto' mode, Watchexec will detect when a program needs to be the
/// foreground process to handle terminal input, and grant that temporarily. This is the case
/// for many pagers and interactive programs. To opt-out, specify an explicit mode as needed.
///
/// 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 = "auto",
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>,
/// Set whether or not to quote the command symbols when passing them to the shell.
///
/// On Windows by default this is treated as false, as CMD and PowerShell do not work correctly
/// when the symbols are quoted.
/// For git-bash and nushell on Windows, as well as other shells that won't work correctly
/// without quoting, opt in to quoting using this option.
///
/// On Linux and MacOS this is ignored and always treated as true, because they do not allow
/// raw values to be passed.
///
/// The difference between the two operating systems is to allow both to work as expected by
/// default for their built in shells, and to allow changing the default behavior for
/// non-default shells if necessary.
///
/// If you wish to write shell scripts that work on Linux/MacOS as well as Windows via git-bash,
/// you should set this option to true so that the behavior is the same on all platforms.
/// The same goes for Nushell, and possibly other newer shells.
#[arg(
long,
short = 'Q',
help_heading = OPTSET_COMMAND,
default_value_t = get_platform_default_quoting(),
value_parser = BoolishValueParser::new(),
display_order = 170,
)]
pub quote: bool,
}
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()?;
crate::dirs::canonicalize(curdir).await.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]
Auto,
Group,
Session,
None,
}
#[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(),
})
}
}