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#[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 #[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 #[arg(
115 long,
116 conflicts_with_all = ["program", "completions", "only_emit_events"],
117 display_order = 130,
118 )]
119 pub manual: bool,
120
121 #[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 #[arg(
144 long,
145 conflicts_with_all = ["program", "completions", "manual"],
146 display_order = 150,
147 )]
148 pub only_emit_events: bool,
149
150 #[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(¬_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}