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/// Examples:
35///
36/// Rebuild a project when source files change:
37///
38///   $ watchexec make
39///
40/// Watch all HTML, CSS, and JavaScript files for changes:
41///
42///   $ watchexec -e html,css,js make
43///
44/// Run tests when source files change, clearing the screen each time:
45///
46///   $ watchexec -c make test
47///
48/// Launch and restart a node.js server:
49///
50///   $ watchexec -r node app.js
51///
52/// Watch lib and src directories for changes, rebuilding each time:
53///
54///   $ watchexec -w lib -w src make
55#[derive(Debug, Clone, Parser)]
56#[command(
57	name = "watchexec",
58	bin_name = "watchexec",
59	author,
60	version,
61	long_version = Bosion::LONG_VERSION,
62	after_help = "Want more detail? Try the long '--help' flag!",
63	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.",
64	hide_possible_values = true,
65)]
66pub struct Args {
67	/// Command (program and arguments) to run on changes
68	///
69	/// It's run when events pass filters and the debounce period (and once at startup unless
70	/// '--postpone' is given). If you pass flags to the command, you should separate it with --
71	/// though that is not strictly required.
72	///
73	/// Examples:
74	///
75	///   $ watchexec -w src npm run build
76	///
77	///   $ watchexec -w src -- rsync -a src dest
78	///
79	/// Take care when using globs or other shell expansions in the command. Your shell may expand
80	/// them before ever passing them to Watchexec, and the results may not be what you expect.
81	/// Compare:
82	///
83	///   $ watchexec echo src/*.rs
84	///
85	///   $ watchexec echo 'src/*.rs'
86	///
87	///   $ watchexec --shell=none echo 'src/*.rs'
88	///
89	/// Behaviour depends on the value of '--shell': for all except 'none', every part of the
90	/// command is joined together into one string with a single ascii space character, and given to
91	/// the shell as described in the help for '--shell'. For 'none', each distinct element the
92	/// command is passed as per the execvp(3) convention: first argument is the program, as a path
93	/// or searched for in the 'PATH' environment variable, rest are arguments.
94	#[arg(
95		trailing_var_arg = true,
96		num_args = 1..,
97		value_hint = ValueHint::CommandString,
98		value_name = "COMMAND",
99		required_unless_present_any = ["completions", "manual", "only_emit_events"],
100	)]
101	pub program: Vec<String>,
102
103	/// Show the manual page
104	///
105	/// This shows the manual page for Watchexec, if the output is a terminal and the 'man' program
106	/// is available. If not, the manual page is printed to stdout in ROFF format (suitable for
107	/// writing to a watchexec.1 file).
108	#[arg(
109		long,
110		conflicts_with_all = ["program", "completions", "only_emit_events"],
111		display_order = 130,
112	)]
113	pub manual: bool,
114
115	/// Generate a shell completions script
116	///
117	/// Provides a completions script or configuration for the given shell. If Watchexec is not
118	/// distributed with pre-generated completions, you can use this to generate them yourself.
119	///
120	/// Supported shells: bash, elvish, fish, nu, powershell, zsh.
121	#[arg(
122		long,
123		value_name = "SHELL",
124		conflicts_with_all = ["program", "manual", "only_emit_events"],
125		display_order = 30,
126	)]
127	pub completions: Option<ShellCompletion>,
128
129	/// Only emit events to stdout, run no commands.
130	///
131	/// This is a convenience option for using Watchexec as a file watcher, without running any
132	/// commands. It is almost equivalent to using `cat` as the command, except that it will not
133	/// spawn a new process for each event.
134	///
135	/// This option implies `--emit-events-to=json-stdio`; you may also use the text mode by
136	/// specifying `--emit-events-to=stdio`.
137	#[arg(
138		long,
139		conflicts_with_all = ["program", "completions", "manual"],
140		display_order = 150,
141	)]
142	pub only_emit_events: bool,
143
144	/// Testing only: exit Watchexec after the first run and return the command's exit code
145	#[arg(short = '1', hide = true)]
146	pub once: bool,
147
148	#[command(flatten)]
149	pub command: command::CommandArgs,
150
151	#[command(flatten)]
152	pub events: events::EventsArgs,
153
154	#[command(flatten)]
155	pub filtering: filtering::FilteringArgs,
156
157	#[command(flatten)]
158	pub logging: logging::LoggingArgs,
159
160	#[command(flatten)]
161	pub output: output::OutputArgs,
162}
163
164#[derive(Clone, Copy, Debug)]
165pub struct TimeSpan<const UNITLESS_NANOS_MULTIPLIER: u64 = { 1_000_000_000 }>(pub Duration);
166
167impl<const UNITLESS_NANOS_MULTIPLIER: u64> FromStr for TimeSpan<UNITLESS_NANOS_MULTIPLIER> {
168	type Err = humantime::DurationError;
169
170	fn from_str(s: &str) -> Result<Self, Self::Err> {
171		s.parse::<u64>()
172			.map_or_else(
173				|_| humantime::parse_duration(s),
174				|unitless| {
175					if unitless != 0 {
176						eprintln!("Warning: unitless non-zero time span values are deprecated and will be removed in an upcoming version");
177					}
178					Ok(Duration::from_nanos(unitless * UNITLESS_NANOS_MULTIPLIER))
179				},
180			)
181			.map(TimeSpan)
182	}
183}
184
185fn expand_args_up_to_doubledash() -> Result<Vec<OsString>, std::io::Error> {
186	use argfile::Argument;
187	use std::collections::VecDeque;
188
189	let args = std::env::args_os();
190	let mut expanded_args = Vec::with_capacity(args.size_hint().0);
191
192	let mut todo: VecDeque<_> = args.map(|a| Argument::parse(a, argfile::PREFIX)).collect();
193	while let Some(next) = todo.pop_front() {
194		match next {
195			Argument::PassThrough(arg) => {
196				expanded_args.push(arg.clone());
197				if arg == "--" {
198					break;
199				}
200			}
201			Argument::Path(path) => {
202				let content = std::fs::read_to_string(path)?;
203				let new_args = argfile::parse_fromfile(&content, argfile::PREFIX);
204				todo.reserve(new_args.len());
205				for (i, arg) in new_args.into_iter().enumerate() {
206					todo.insert(i, arg);
207				}
208			}
209		}
210	}
211
212	while let Some(next) = todo.pop_front() {
213		expanded_args.push(match next {
214			Argument::PassThrough(arg) => arg,
215			Argument::Path(path) => {
216				let path = path.as_os_str();
217				let mut restored = OsString::with_capacity(path.len() + 1);
218				restored.push(OsStr::new("@"));
219				restored.push(path);
220				restored
221			}
222		});
223	}
224	Ok(expanded_args)
225}
226
227#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
228pub enum ShellCompletion {
229	Bash,
230	Elvish,
231	Fish,
232	Nu,
233	Powershell,
234	Zsh,
235}
236
237#[derive(Debug, Default)]
238pub struct Guards {
239	_log: Option<WorkerGuard>,
240}
241
242pub async fn get_args() -> Result<(Args, Guards)> {
243	let prearg_logs = logging::preargs();
244	if prearg_logs {
245		warn!(
246			"⚠ WATCHEXEC_LOG environment variable set or hardcoded, logging options have no effect"
247		);
248	}
249
250	debug!("expanding @argfile arguments if any");
251	let args = expand_args_up_to_doubledash().expect("while expanding @argfile");
252
253	debug!("parsing arguments");
254	let mut args = Args::parse_from(args);
255
256	let _log = if !prearg_logs {
257		logging::postargs(&args.logging).await?
258	} else {
259		None
260	};
261
262	args.output.normalise()?;
263	args.command.normalise().await?;
264	args.filtering.normalise(&args.command).await?;
265	args.events
266		.normalise(&args.command, &args.filtering, args.only_emit_events)?;
267
268	info!(?args, "got arguments");
269	Ok((args, Guards { _log }))
270}
271
272#[test]
273fn verify_cli() {
274	use clap::CommandFactory;
275	Args::command().debug_assert()
276}