Skip to main content

watchexec_cli/
args.rs

1use std::{
2	ffi::{OsStr, OsString},
3	str::FromStr,
4	time::Duration,
5};
6
7use clap::{Parser, ValueEnum, ValueHint};
8use miette::{IntoDiagnostic, Result, WrapErr};
9use tracing::{debug, info, warn};
10use tracing_appender::non_blocking::WorkerGuard;
11
12pub(crate) mod command;
13pub(crate) mod events;
14pub(crate) mod filtering;
15pub(crate) mod logging;
16pub(crate) mod output;
17
18const OPTSET_COMMAND: &str = "Command";
19const OPTSET_DEBUGGING: &str = "Debugging";
20const OPTSET_EVENTS: &str = "Events";
21const OPTSET_FILTERING: &str = "Filtering";
22const OPTSET_OUTPUT: &str = "Output";
23
24include!(env!("BOSION_PATH"));
25
26/// Execute commands when watched files change.
27///
28/// Recursively monitors the current directory for changes, executing the command when a filesystem
29/// change is detected (among other event sources). By default, watchexec uses efficient
30/// kernel-level mechanisms to watch for changes.
31///
32/// At startup, the specified command is run once, and watchexec begins monitoring for changes.
33///
34/// Events are debounced and checked using a variety of mechanisms, which you can control using
35/// the flags in the **Filtering** section. The order of execution is: internal prioritisation
36/// (signals come before everything else, and SIGINT/SIGTERM are processed even more urgently),
37/// then file event kind (`--fs-events`), then files explicitly watched with `-w`, then ignores
38/// (`--ignore` and co), then filters (which includes `--exts`), then filter programs.
39///
40/// Examples:
41///
42/// Rebuild a project when source files change:
43///
44///   $ watchexec make
45///
46/// Watch all HTML, CSS, and JavaScript files for changes:
47///
48///   $ watchexec -e html,css,js make
49///
50/// Run tests when source files change, clearing the screen each time:
51///
52///   $ watchexec -c make test
53///
54/// Launch and restart a node.js server:
55///
56///   $ watchexec -r node app.js
57///
58/// Watch lib and src directories for changes, rebuilding each time:
59///
60///   $ watchexec -w lib -w src make
61#[derive(Debug, Clone, Parser)]
62#[command(
63	name = "watchexec",
64	bin_name = "watchexec",
65	author,
66	version,
67	long_version = Bosion::LONG_VERSION,
68	after_help = "Want more detail? Try the long '--help' flag!",
69	after_long_help = "Use @argfile as first argument to load arguments from the file 'argfile' (one argument per line) which will be inserted in place of the @argfile (further arguments on the CLI will override or add onto those in the file).\n\nDidn't expect this much output? Use the short '-h' flag to get short help.",
70	hide_possible_values = true,
71)]
72pub struct Args {
73	/// Command (program and arguments) to run on changes
74	///
75	/// It's run when events pass filters and the debounce period (and once at startup unless
76	/// '--postpone' is given). If you pass flags to the command, you should separate it with --
77	/// though that is not strictly required.
78	///
79	/// Examples:
80	///
81	///   $ watchexec -w src npm run build
82	///
83	///   $ watchexec -w src -- rsync -a src dest
84	///
85	/// Take care when using globs or other shell expansions in the command. Your shell may expand
86	/// them before ever passing them to Watchexec, and the results may not be what you expect.
87	/// Compare:
88	///
89	///   $ watchexec echo src/*.rs
90	///
91	///   $ watchexec echo 'src/*.rs'
92	///
93	///   $ watchexec --shell=none echo 'src/*.rs'
94	///
95	/// Behaviour depends on the value of '--shell': for all except 'none', every part of the
96	/// command is joined together into one string with a single ascii space character, and given to
97	/// the shell as described in the help for '--shell'. For 'none', each distinct element the
98	/// command is passed as per the execvp(3) convention: first argument is the program, as a path
99	/// or searched for in the 'PATH' environment variable, rest are arguments.
100	#[arg(
101		trailing_var_arg = true,
102		num_args = 1..,
103		value_hint = ValueHint::CommandString,
104		value_name = "COMMAND",
105		required_unless_present_any = ["completions", "manual", "only_emit_events"],
106	)]
107	pub program: Vec<String>,
108
109	/// Show the manual page
110	///
111	/// This shows the manual page for Watchexec, if the output is a terminal and the 'man' program
112	/// is available. If not, the manual page is printed to stdout in ROFF format (suitable for
113	/// writing to a watchexec.1 file).
114	#[arg(
115		long,
116		conflicts_with_all = ["program", "completions", "only_emit_events"],
117		display_order = 130,
118	)]
119	pub manual: bool,
120
121	/// Generate a shell completions script
122	///
123	/// Provides a completions script or configuration for the given shell. If Watchexec is not
124	/// distributed with pre-generated completions, you can use this to generate them yourself.
125	///
126	/// Supported shells: bash, elvish, fish, nu, powershell, zsh.
127	#[arg(
128		long,
129		value_name = "SHELL",
130		conflicts_with_all = ["program", "manual", "only_emit_events"],
131		display_order = 30,
132	)]
133	pub completions: Option<ShellCompletion>,
134
135	/// Only emit events to stdout, run no commands.
136	///
137	/// This is a convenience option for using Watchexec as a file watcher, without running any
138	/// commands. It is almost equivalent to using `cat` as the command, except that it will not
139	/// spawn a new process for each event.
140	///
141	/// This option implies `--emit-events-to=json-stdio`; you may also use the text mode by
142	/// specifying `--emit-events-to=stdio`.
143	#[arg(
144		long,
145		conflicts_with_all = ["program", "completions", "manual"],
146		display_order = 150,
147	)]
148	pub only_emit_events: bool,
149
150	/// Testing only: exit Watchexec after the first run and return the command's exit code
151	#[arg(short = '1', hide = true)]
152	pub once: bool,
153
154	#[command(flatten)]
155	pub command: command::CommandArgs,
156
157	#[command(flatten)]
158	pub events: events::EventsArgs,
159
160	#[command(flatten)]
161	pub filtering: filtering::FilteringArgs,
162
163	#[command(flatten)]
164	pub logging: logging::LoggingArgs,
165
166	#[command(flatten)]
167	pub output: output::OutputArgs,
168}
169
170#[derive(Clone, Copy, Debug)]
171pub struct TimeSpan<const UNITLESS_NANOS_MULTIPLIER: u64 = { 1_000_000_000 }>(pub Duration);
172
173impl<const UNITLESS_NANOS_MULTIPLIER: u64> FromStr for TimeSpan<UNITLESS_NANOS_MULTIPLIER> {
174	type Err = humantime::DurationError;
175
176	fn from_str(s: &str) -> Result<Self, Self::Err> {
177		s.parse::<u64>()
178			.map_or_else(
179				|_| humantime::parse_duration(s),
180				|unitless| {
181					if unitless != 0 {
182						eprintln!("Warning: unitless non-zero time span values are deprecated and will be removed in an upcoming version");
183					}
184					Ok(Duration::from_nanos(unitless * UNITLESS_NANOS_MULTIPLIER))
185				},
186			)
187			.map(TimeSpan)
188	}
189}
190
191fn expand_args_up_to_doubledash(args: impl IntoIterator<Item = OsString>) -> Result<Vec<OsString>> {
192	use argfile::Argument;
193	use std::collections::VecDeque;
194
195	let args = args.into_iter();
196	let mut expanded_args = Vec::with_capacity(args.size_hint().0);
197
198	let mut todo: VecDeque<_> = args.map(|a| Argument::parse(a, argfile::PREFIX)).collect();
199	while let Some(next) = todo.pop_front() {
200		match next {
201			Argument::PassThrough(arg) => {
202				expanded_args.push(arg.clone());
203				if arg == "--" {
204					break;
205				}
206			}
207			Argument::Path(path) => {
208				let content = std::fs::read_to_string(&path)
209					.into_diagnostic()
210					.wrap_err_with(|| format!("while expanding @argfile {}", path.display()))?;
211				let new_args = argfile::parse_fromfile(&content, argfile::PREFIX);
212				todo.reserve(new_args.len());
213				for (i, arg) in new_args.into_iter().enumerate() {
214					todo.insert(i, arg);
215				}
216			}
217		}
218	}
219
220	while let Some(next) = todo.pop_front() {
221		expanded_args.push(match next {
222			Argument::PassThrough(arg) => arg,
223			Argument::Path(path) => {
224				let path = path.as_os_str();
225				let mut restored = OsString::with_capacity(path.len() + 1);
226				restored.push(OsStr::new("@"));
227				restored.push(path);
228				restored
229			}
230		});
231	}
232	Ok(expanded_args)
233}
234
235#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
236pub enum ShellCompletion {
237	Bash,
238	Elvish,
239	Fish,
240	Nu,
241	Powershell,
242	Zsh,
243}
244
245#[derive(Debug, Default)]
246pub struct Guards {
247	_log: Option<WorkerGuard>,
248}
249
250pub async fn get_args() -> Result<(Args, Guards)> {
251	let prearg_logs = logging::preargs();
252	if prearg_logs {
253		warn!(
254			"⚠ WATCHEXEC_LOG environment variable set or hardcoded, logging options have no effect"
255		);
256	}
257
258	debug!("expanding @argfile arguments if any");
259	let args = expand_args_up_to_doubledash(std::env::args_os())?;
260
261	debug!("parsing arguments");
262	let mut args = Args::parse_from(args);
263
264	let _log = if !prearg_logs {
265		logging::postargs(&args.logging).await?
266	} else {
267		None
268	};
269
270	args.output.normalise()?;
271	args.command.normalise().await?;
272	args.filtering.normalise(&args.command).await?;
273	args.events
274		.normalise(&args.command, &args.filtering, args.only_emit_events)?;
275
276	info!(?args, "got arguments");
277	Ok((args, Guards { _log }))
278}
279
280#[test]
281fn verify_cli() {
282	use clap::CommandFactory;
283	Args::command().debug_assert()
284}
285
286#[cfg(test)]
287fn argfile_arg(path: &std::path::Path) -> OsString {
288	let mut arg = OsString::from("@");
289	arg.push(path);
290	arg
291}
292
293#[test]
294fn argfile_is_expanded_up_to_doubledash() {
295	let dir = tempfile::tempdir().unwrap();
296	let argfile = dir.path().join("argfile");
297	std::fs::write(&argfile, "-1\n--postpone\n").unwrap();
298
299	let expanded = expand_args_up_to_doubledash([
300		"watchexec".into(),
301		argfile_arg(&argfile),
302		"--".into(),
303		argfile_arg(&argfile),
304	])
305	.unwrap();
306
307	assert_eq!(
308		expanded,
309		[
310			OsString::from("watchexec"),
311			"-1".into(),
312			"--postpone".into(),
313			"--".into(),
314			argfile_arg(&argfile),
315		]
316	);
317}
318
319#[test]
320fn unreadable_argfile_is_an_error() {
321	let dir = tempfile::tempdir().unwrap();
322	let missing = dir.path().join("missing");
323	let not_utf8 = dir.path().join("not-utf8");
324	std::fs::write(&not_utf8, b"\xff\xfe\n").unwrap();
325
326	for path in [missing, not_utf8] {
327		let err = expand_args_up_to_doubledash([
328			"watchexec".into(),
329			argfile_arg(&path),
330			"--".into(),
331			"echo".into(),
332		])
333		.expect_err("an unreadable argfile should be an error, not a panic");
334		assert_eq!(
335			err.to_string(),
336			format!("while expanding @argfile {}", path.display())
337		);
338	}
339}