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::Result;
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() -> Result<Vec<OsString>, std::io::Error> {
192	use argfile::Argument;
193	use std::collections::VecDeque;
194
195	let args = std::env::args_os();
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				let new_args = argfile::parse_fromfile(&content, argfile::PREFIX);
210				todo.reserve(new_args.len());
211				for (i, arg) in new_args.into_iter().enumerate() {
212					todo.insert(i, arg);
213				}
214			}
215		}
216	}
217
218	while let Some(next) = todo.pop_front() {
219		expanded_args.push(match next {
220			Argument::PassThrough(arg) => arg,
221			Argument::Path(path) => {
222				let path = path.as_os_str();
223				let mut restored = OsString::with_capacity(path.len() + 1);
224				restored.push(OsStr::new("@"));
225				restored.push(path);
226				restored
227			}
228		});
229	}
230	Ok(expanded_args)
231}
232
233#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
234pub enum ShellCompletion {
235	Bash,
236	Elvish,
237	Fish,
238	Nu,
239	Powershell,
240	Zsh,
241}
242
243#[derive(Debug, Default)]
244pub struct Guards {
245	_log: Option<WorkerGuard>,
246}
247
248pub async fn get_args() -> Result<(Args, Guards)> {
249	let prearg_logs = logging::preargs();
250	if prearg_logs {
251		warn!(
252			"⚠ WATCHEXEC_LOG environment variable set or hardcoded, logging options have no effect"
253		);
254	}
255
256	debug!("expanding @argfile arguments if any");
257	let args = expand_args_up_to_doubledash().expect("while expanding @argfile");
258
259	debug!("parsing arguments");
260	let mut args = Args::parse_from(args);
261
262	let _log = if !prearg_logs {
263		logging::postargs(&args.logging).await?
264	} else {
265		None
266	};
267
268	args.output.normalise()?;
269	args.command.normalise().await?;
270	args.filtering.normalise(&args.command).await?;
271	args.events
272		.normalise(&args.command, &args.filtering, args.only_emit_events)?;
273
274	info!(?args, "got arguments");
275	Ok((args, Guards { _log }))
276}
277
278#[test]
279fn verify_cli() {
280	use clap::CommandFactory;
281	Args::command().debug_assert()
282}