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)]
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 #[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 #[arg(
109 long,
110 conflicts_with_all = ["program", "completions", "only_emit_events"],
111 display_order = 130,
112 )]
113 pub manual: bool,
114
115 #[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 #[arg(
138 long,
139 conflicts_with_all = ["program", "completions", "manual"],
140 display_order = 150,
141 )]
142 pub only_emit_events: bool,
143
144 #[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}