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#[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() -> 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}