1use itertools::Either;
2use notify_debouncer_full::{
3 DebouncedEvent, Debouncer, FileIdMap, new_debouncer,
4 notify::{
5 self, EventKind, RecommendedWatcher, RecursiveMode, Watcher,
6 event::{DataChange, ModifyKind, RenameMode},
7 },
8};
9use nu_engine::{ClosureEval, command_prelude::*};
10use nu_protocol::{
11 Signals,
12 engine::Closure,
13 report_shell_error, report_shell_warning,
14 shell_error::{generic::GenericError, io::IoError},
15};
16use std::{
17 path::{Path, PathBuf},
18 sync::mpsc::{Receiver, RecvTimeoutError, channel},
19 time::Duration,
20};
21
22const CHECK_CTRL_C_FREQUENCY: Duration = Duration::from_millis(100);
24const DEFAULT_WATCH_DEBOUNCE_DURATION: Duration = Duration::from_millis(100);
25
26enum WatchGlob {
28 Legacy(nu_glob::Pattern),
29 DcGlob(nu_glob::dc_glob::DcPattern),
30}
31
32impl WatchGlob {
33 fn matches_path(&self, path: &Path) -> bool {
34 match self {
35 WatchGlob::Legacy(p) => p.matches_path(path),
36 WatchGlob::DcGlob(p) => p.matches_path(path),
37 }
38 }
39}
40
41#[derive(Clone)]
42pub struct Watch;
43
44impl Command for Watch {
45 fn name(&self) -> &str {
46 "watch"
47 }
48
49 fn description(&self) -> &str {
50 "Watch for file changes and execute Nu code when they happen."
51 }
52
53 fn extra_description(&self) -> &str {
54 "When run without a closure, `watch` returns a stream of events instead."
55 }
56
57 fn search_terms(&self) -> Vec<&str> {
58 vec!["watcher", "reload", "filesystem"]
59 }
60
61 fn signature(&self) -> nu_protocol::Signature {
62 Signature::build("watch")
63 .input_output_types(vec![
64 (Type::Nothing, Type::Nothing),
65 (
66 Type::Nothing,
67 Type::Table(
68 vec![
69 ("operation".into(), Type::String),
70 ("path".into(), Type::one_of([Type::String, Type::Nothing])),
71 (
72 "new_path".into(),
73 Type::one_of([Type::String, Type::Nothing]),
74 ),
75 ]
76 .into(),
77 ),
78 ),
79 ])
80 .required(
81 "path",
82 SyntaxShape::Filepath,
83 "The path to watch. Can be a file or directory.",
84 )
85 .optional(
86 "closure",
87 SyntaxShape::Closure(Some(vec![
88 SyntaxShape::String,
89 SyntaxShape::String,
90 SyntaxShape::String,
91 ])),
92 "Some Nu code to run whenever a file changes. \
93 The closure will be passed `operation`, `path`, \
94 and `new_path` (for renames only) arguments in that order (deprecated).",
95 )
96 .named(
97 "debounce",
98 SyntaxShape::Duration,
99 "Debounce changes for this duration (default: 100ms). \
100 Adjust if you find that single writes are reported as multiple events.",
101 Some('d'),
102 )
103 .named(
104 "glob",
105 SyntaxShape::String,
107 "Only report changes for files that match this glob pattern (default: all files)",
108 Some('g'),
109 )
110 .named(
111 "recursive",
112 SyntaxShape::Boolean,
113 "Watch all directories under `<path>` recursively. \
114 Will be ignored if `<path>` is a file (default: true).",
115 Some('r'),
116 )
117 .switch(
118 "quiet",
119 "Hide the initial status message (default: false).",
120 Some('q'),
121 )
122 .switch(
123 "verbose",
124 "Operate in verbose mode (default: false).",
125 Some('v'),
126 )
127 .category(Category::FileSystem)
128 }
129
130 fn run(
131 &self,
132 engine_state: &EngineState,
133 stack: &mut Stack,
134 call: &Call,
135 _input: PipelineData,
136 ) -> Result<PipelineData, ShellError> {
137 let head = call.head;
138 let path = {
139 let cwd = engine_state.cwd_as_string(Some(stack))?;
140 let path_arg: Spanned<String> = call.req(engine_state, stack, 0)?;
141 let path_no_whitespace = path_arg
142 .item
143 .trim_end_matches(|x| matches!(x, '\x09'..='\x0d'));
144
145 nu_path::absolute_with(path_no_whitespace, cwd).map_err(|err| {
146 ShellError::Io(IoError::new(
147 err,
148 path_arg.span,
149 PathBuf::from(path_no_whitespace),
150 ))
151 })?
152 };
153 let closure: Option<Spanned<Closure>> = call.opt(engine_state, stack, 1)?;
154 let verbose = call.has_flag(engine_state, stack, "verbose")?;
155 let quiet = call.has_flag(engine_state, stack, "quiet")?;
156 let debounce_duration: Duration = call
157 .get_flag(engine_state, stack, "debounce")?
158 .unwrap_or(DEFAULT_WATCH_DEBOUNCE_DURATION);
159
160 let glob_pattern = call
161 .get_flag::<Spanned<String>>(engine_state, stack, "glob")?
162 .map(|glob| {
163 let absolute_path = path.join(glob.item);
164 if verbose {
165 eprintln!("Absolute glob path: {absolute_path:?}");
166 }
167 let path_str = absolute_path.to_string_lossy();
168 if nu_experimental::DC_GLOB.get() {
169 nu_glob::dc_glob::DcPattern::new(path_str.as_ref())
170 .map(WatchGlob::DcGlob)
171 .map_err(|_| ShellError::TypeMismatch {
172 err_message: "Glob pattern is invalid".to_string(),
173 span: glob.span,
174 })
175 } else {
176 nu_glob::Pattern::new(path_str.as_ref())
177 .map(WatchGlob::Legacy)
178 .map_err(|_| ShellError::TypeMismatch {
179 err_message: "Glob pattern is invalid".to_string(),
180 span: glob.span,
181 })
182 }
183 })
184 .transpose()?;
185
186 let recursive_mode = {
187 let recursive_flag = call
188 .get_flag::<bool>(engine_state, stack, "recursive")?
189 .unwrap_or(true);
190 match recursive_flag {
191 true => RecursiveMode::Recursive,
192 false => RecursiveMode::NonRecursive,
193 }
194 };
195
196 let iter = {
197 let (tx, rx) = channel();
198
199 let mut debouncer = new_debouncer(debounce_duration, None, tx)
200 .and_then(|mut debouncer| {
201 debouncer.watcher().watch(&path, recursive_mode)?;
202 Ok(debouncer)
203 })
204 .map_err(|err| {
205 ShellError::Generic(GenericError::new(
206 "Failed to create watcher",
207 err.to_string(),
208 call.head,
209 ))
210 })?;
211
212 debouncer.cache().add_root(&path, recursive_mode);
214
215 WatchIterator::new(debouncer, rx, engine_state.signals().clone())
216 };
217
218 if let Some(closure) = closure {
219 report_shell_warning(
220 Some(stack),
221 engine_state,
222 &ShellWarning::Deprecated {
223 dep_type: "Argument".into(),
224 label: "remove this".into(),
225 span: closure.span,
226 help: "Since 0.113.0, running a closure with `watch` is deprecated. \
227 Instead, use `watch` by piping its output to `each` \
228 or as the source of a `for` loop."
229 .to_string()
230 .into(),
231 report_mode: nu_protocol::ReportMode::FirstUse,
232 },
233 );
234
235 #[allow(deprecated)]
236 run_closure(
237 engine_state,
238 stack,
239 head,
240 quiet,
241 verbose,
242 &path,
243 glob_pattern,
244 iter,
245 closure.item,
246 )
247 } else {
248 let out = iter
249 .flat_map(|e| match e {
250 Ok(events) => Either::Right(events.into_iter().map(Ok)),
251 Err(err) => Either::Left(std::iter::once(Err(err))),
252 })
253 .filter_map(move |e| match e {
254 Ok(ev) if glob_filter(glob_pattern.as_ref(), &ev) => Some(ev.into_value(head)),
255 Ok(_) => None,
256 Err(err) => Some(Value::error(err, head)),
257 })
258 .into_pipeline_data(head, engine_state.signals().clone());
259 Ok(out)
260 }
261 }
262
263 fn examples(&self) -> Vec<Example<'_>> {
264 vec![
265 Example {
266 description: "Run `cargo test` whenever a Rust file changes.",
267 example: "for _ in (watch . --glob=**/*.rs) { cargo test }",
268 result: None,
269 },
270 Example {
271 description: "Watch all changes in the current directory.",
272 example: "watch . | each { print }",
273 result: None,
274 },
275 Example {
276 description: "Filter, limit and modify `watch`'s output by using it as part of a pipeline.",
277 example: r#"watch /foo/bar
278 | where operation == Create
279 | first 5
280 | each {|e| $"New file!: ($e.path)" }
281 | to text
282 | save --append changes_in_bar.log"#,
283 result: None,
284 },
285 Example {
286 description: "Print file changes with a debounce time of 5 minutes.",
287 example: r#"watch /foo/bar --debounce 5min | each {|e| $"Registered ($e.operation) on ($e.path)" | print }"#,
288 result: None,
289 },
290 Example {
291 description: "Note: if you are looking to run a command every N units of time, this can be accomplished with a loop and sleep.",
292 example: "loop { command; sleep duration }",
293 result: None,
294 },
295 Example {
296 description: "Run `cargo test` whenever a Rust file changes (with the deprecated closure argument).",
297 example: "watch . --glob=**/*.rs {|| cargo test }",
298 result: None,
299 },
300 ]
301 }
302}
303
304fn glob_filter(glob: Option<&WatchGlob>, ev: &WatchEvent) -> bool {
305 let Some(glob) = glob else { return true };
306 let Some(path) = ev.path.as_deref().or(ev.new_path.as_deref()) else {
307 return false;
308 };
309
310 glob.matches_path(path)
311}
312
313#[allow(clippy::too_many_arguments)]
314#[inline]
315#[deprecated(since = "0.113.0")]
316fn run_closure(
317 engine_state: &EngineState,
318 stack: &mut Stack,
319 head: Span,
320 quiet: bool,
321 verbose: bool,
322 path: &Path,
323 glob_pattern: Option<WatchGlob>,
324 iter: WatchIterator,
325 closure: Closure,
326) -> Result<PipelineData, ShellError> {
327 if !quiet {
328 eprintln!("Now watching files at {path:?}. Press ctrl+c to abort.");
329 }
330
331 let mut closure = ClosureEval::new(engine_state, stack, closure);
332 for events in iter {
333 for event in events? {
334 let matches_glob = glob_filter(glob_pattern.as_ref(), &event);
335
336 if verbose && glob_pattern.is_some() {
337 eprintln!("Matches glob: {matches_glob}");
338 }
339
340 if matches_glob {
341 let result = closure
342 .add_arg(event.operation.into_value(head))?
343 .add_arg(event.path.into_value(head))?
344 .add_arg(event.new_path.into_value(head))?
345 .run_with_input(PipelineData::empty());
346
347 match result {
348 Ok(val) => val.print_table(engine_state, stack, false, false)?,
349 Err(err) => report_shell_error(Some(stack), engine_state, &err),
350 };
351 }
352 }
353 }
354
355 Ok(PipelineData::empty())
356}
357
358#[derive(IntoValue)]
359struct WatchEvent {
360 operation: WatchEventKind,
361 path: Option<PathBuf>,
362 new_path: Option<PathBuf>,
363}
364
365#[derive(IntoValue)]
366#[nu_value(rename_all = "UpperCamelCase")]
367enum WatchEventKind {
368 Create,
369 Write,
370 Rename,
371 Remove,
372}
373
374impl TryFrom<EventKind> for WatchEventKind {
375 type Error = ();
376
377 fn try_from(value: EventKind) -> Result<Self, Self::Error> {
378 Ok(match value {
379 EventKind::Create(_) => Self::Create,
380 EventKind::Remove(_) => Self::Remove,
381 EventKind::Modify(
382 ModifyKind::Data(DataChange::Content | DataChange::Any) | ModifyKind::Any,
383 ) => Self::Write,
384 EventKind::Modify(ModifyKind::Name(
385 RenameMode::Both | RenameMode::From | RenameMode::To,
386 )) => Self::Rename,
387 _ => return Err(()),
388 })
389 }
390}
391
392impl TryFrom<DebouncedEvent> for WatchEvent {
393 type Error = ();
394
395 fn try_from(ev: DebouncedEvent) -> Result<Self, Self::Error> {
396 let DebouncedEvent {
398 event: notify::Event {
399 kind, mut paths, ..
400 },
401 ..
402 } = ev;
403
404 let (path, new_path) = match paths.as_mut_slice() {
405 [path] => (std::mem::take(path), None),
406 [path, new_path] => (std::mem::take(path), Some(std::mem::take(new_path))),
407 _ => return Err(()),
408 };
409
410 if let EventKind::Modify(ModifyKind::Name(RenameMode::To)) = kind {
411 Ok(WatchEvent {
412 operation: WatchEventKind::Rename,
413 path: None,
414 new_path: Some(path),
415 })
416 } else {
417 Ok(WatchEvent {
418 operation: kind.try_into()?,
419 path: Some(path),
420 new_path,
421 })
422 }
423 }
424}
425
426struct WatchIterator {
427 _debouncer: Debouncer<RecommendedWatcher, FileIdMap>,
429 rx: Option<Receiver<Result<Vec<DebouncedEvent>, Vec<notify::Error>>>>,
430 signals: Signals,
431}
432
433impl WatchIterator {
434 fn new(
435 debouncer: Debouncer<RecommendedWatcher, FileIdMap>,
436 rx: Receiver<Result<Vec<DebouncedEvent>, Vec<notify::Error>>>,
437 signals: Signals,
438 ) -> Self {
439 Self {
440 _debouncer: debouncer,
441 rx: Some(rx),
442 signals,
443 }
444 }
445}
446
447impl Iterator for WatchIterator {
448 type Item = Result<Vec<WatchEvent>, ShellError>;
449
450 fn next(&mut self) -> Option<Self::Item> {
451 let rx = self.rx.as_ref()?;
452 while !self.signals.interrupted() {
453 let x = match rx.recv_timeout(CHECK_CTRL_C_FREQUENCY) {
454 Ok(x) => x,
455 Err(RecvTimeoutError::Timeout) => continue,
456 Err(RecvTimeoutError::Disconnected) => {
457 self.rx = None;
458 return Some(Err(ShellError::Generic(GenericError::new_internal(
459 "Disconnected",
460 "Unexpected disconnect from file watcher",
461 ))));
462 }
463 };
464
465 let Ok(events) = x else {
466 self.rx = None;
467 return Some(Err(ShellError::Generic(GenericError::new_internal(
468 "Receiving events failed",
469 "Unexpected errors when receiving events",
470 ))));
471 };
472
473 let watch_events = events
474 .into_iter()
475 .filter_map(|ev| WatchEvent::try_from(ev).ok())
476 .collect::<Vec<_>>();
477
478 return Some(Ok(watch_events));
479 }
480 self.rx = None;
481 None
482 }
483}