feat: make it possible to watch non-recursively (#827)

Fixes #227
Fixes #174

docs(cli): be more precise in print-events advice to use `-v`
docs(cli): improve jaq error help
feat(cli): add `-W` for non-recursive watches
feat(cli): use non-blocking logging
feat(globset): hide `fmt::Debug` spew from ignore crate
feat(ignore-files): hide `fmt::Debug` spew from ignore crate
feat(lib): make it possible to watch non-recursively
fix(lib): inserting `WatchedPath`s directly should be possible
refactor(lib): move `WatchedPath` out of `fs` mod
This commit is contained in:
Félix Saparelli 2024-04-28 18:33:07 +12:00 committed by GitHub
parent ee3795d776
commit 6c23afe839
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 538 additions and 318 deletions

13
Cargo.lock generated
View File

@ -3685,6 +3685,18 @@ dependencies = [
"tracing-core", "tracing-core",
] ]
[[package]]
name = "tracing-appender"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf"
dependencies = [
"crossbeam-channel",
"thiserror",
"time",
"tracing-subscriber",
]
[[package]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.27" version = "0.1.27"
@ -4060,6 +4072,7 @@ dependencies = [
"termcolor", "termcolor",
"tokio", "tokio",
"tracing", "tracing",
"tracing-appender",
"tracing-subscriber", "tracing-subscriber",
"tracing-test", "tracing-test",
"uuid", "uuid",

View File

@ -19,7 +19,7 @@ _watchexec() {
case "${cmd}" in case "${cmd}" in
watchexec) watchexec)
opts="-w -c -o -r -s -d -p -n -E -1 -N -q -e -f -j -i -v -h -V --watch --clear --on-busy-update --restart --signal --stop-signal --stop-timeout --map-signal --debounce --stdin-quit --no-vcs-ignore --no-project-ignore --no-global-ignore --no-default-ignore --no-discover-ignore --ignore-nothing --postpone --delay-run --poll --shell --no-environment --emit-events-to --only-emit-events --env --no-process-group --wrap-process --notify --color --timings --quiet --bell --project-origin --workdir --exts --filter --filter-file --filter-prog --ignore --ignore-file --fs-events --no-meta --print-events --verbose --log-file --manual --completions --help --version [COMMAND]..." opts="-w -W -c -o -r -s -d -p -n -E -1 -N -q -e -f -j -i -v -h -V --watch --watch-non-recursive --clear --on-busy-update --restart --signal --stop-signal --stop-timeout --map-signal --debounce --stdin-quit --no-vcs-ignore --no-project-ignore --no-global-ignore --no-default-ignore --no-discover-ignore --ignore-nothing --postpone --delay-run --poll --shell --no-environment --emit-events-to --only-emit-events --env --no-process-group --wrap-process --notify --color --timings --quiet --bell --project-origin --workdir --exts --filter --filter-file --filter-prog --ignore --ignore-file --fs-events --no-meta --print-events --manual --completions --verbose --log-file --help --version [COMMAND]..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -33,6 +33,14 @@ _watchexec() {
COMPREPLY=($(compgen -f "${cur}")) COMPREPLY=($(compgen -f "${cur}"))
return 0 return 0
;; ;;
--watch-non-recursive)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-W)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--clear) --clear)
COMPREPLY=($(compgen -W "clear reset" -- "${cur}")) COMPREPLY=($(compgen -W "clear reset" -- "${cur}"))
return 0 return 0
@ -189,14 +197,14 @@ _watchexec() {
COMPREPLY=($(compgen -W "access create remove rename modify metadata" -- "${cur}")) COMPREPLY=($(compgen -W "access create remove rename modify metadata" -- "${cur}"))
return 0 return 0
;; ;;
--log-file)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--completions) --completions)
COMPREPLY=($(compgen -W "bash elvish fish nu powershell zsh" -- "${cur}")) COMPREPLY=($(compgen -W "bash elvish fish nu powershell zsh" -- "${cur}"))
return 0 return 0
;; ;;
--log-file)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;

View File

@ -20,6 +20,8 @@ set edit:completion:arg-completer[watchexec] = {|@words|
&'watchexec'= { &'watchexec'= {
cand -w 'Watch a specific file or directory' cand -w 'Watch a specific file or directory'
cand --watch 'Watch a specific file or directory' cand --watch 'Watch a specific file or directory'
cand -W 'Watch a specific directory, non-recursively'
cand --watch-non-recursive 'Watch a specific directory, non-recursively'
cand -c 'Clear screen before running command' cand -c 'Clear screen before running command'
cand --clear 'Clear screen before running command' cand --clear 'Clear screen before running command'
cand -o 'What to do when receiving events while the command is running' cand -o 'What to do when receiving events while the command is running'
@ -52,8 +54,8 @@ set edit:completion:arg-completer[watchexec] = {|@words|
cand --ignore 'Filename patterns to filter out' cand --ignore 'Filename patterns to filter out'
cand --ignore-file 'Files to load ignores from' cand --ignore-file 'Files to load ignores from'
cand --fs-events 'Filesystem events to filter to' cand --fs-events 'Filesystem events to filter to'
cand --log-file 'Write diagnostic logs to a file'
cand --completions 'Generate a shell completions script' cand --completions 'Generate a shell completions script'
cand --log-file 'Write diagnostic logs to a file'
cand -r 'Restart the process if it''s still running' cand -r 'Restart the process if it''s still running'
cand --restart 'Restart the process if it''s still running' cand --restart 'Restart the process if it''s still running'
cand --stdin-quit 'Exit when stdin closes' cand --stdin-quit 'Exit when stdin closes'
@ -78,9 +80,9 @@ set edit:completion:arg-completer[watchexec] = {|@words|
cand --bell 'Ring the terminal bell on command completion' cand --bell 'Ring the terminal bell on command completion'
cand --no-meta 'Don''t emit fs events for metadata changes' cand --no-meta 'Don''t emit fs events for metadata changes'
cand --print-events 'Print events that trigger actions' cand --print-events 'Print events that trigger actions'
cand --manual 'Show the manual page'
cand -v 'Set diagnostic log level' cand -v 'Set diagnostic log level'
cand --verbose 'Set diagnostic log level' cand --verbose 'Set diagnostic log level'
cand --manual 'Show the manual page'
cand -h 'Print help (see more with ''--help'')' cand -h 'Print help (see more with ''--help'')'
cand --help 'Print help (see more with ''--help'')' cand --help 'Print help (see more with ''--help'')'
cand -V 'Print version' cand -V 'Print version'

View File

@ -1,4 +1,5 @@
complete -c watchexec -s w -l watch -d 'Watch a specific file or directory' -r -F complete -c watchexec -s w -l watch -d 'Watch a specific file or directory' -r -F
complete -c watchexec -s W -l watch-non-recursive -d 'Watch a specific directory, non-recursively' -r -F
complete -c watchexec -s c -l clear -d 'Clear screen before running command' -r -f -a "{clear '',reset ''}" complete -c watchexec -s c -l clear -d 'Clear screen before running command' -r -f -a "{clear '',reset ''}"
complete -c watchexec -s o -l on-busy-update -d 'What to do when receiving events while the command is running' -r -f -a "{queue '',do-nothing '',restart '',signal ''}" complete -c watchexec -s o -l on-busy-update -d 'What to do when receiving events while the command is running' -r -f -a "{queue '',do-nothing '',restart '',signal ''}"
complete -c watchexec -s s -l signal -d 'Send a signal to the process when it\'s still running' -r complete -c watchexec -s s -l signal -d 'Send a signal to the process when it\'s still running' -r
@ -22,8 +23,8 @@ complete -c watchexec -s j -l filter-prog -d '[experimental] Filter programs' -r
complete -c watchexec -s i -l ignore -d 'Filename patterns to filter out' -r complete -c watchexec -s i -l ignore -d 'Filename patterns to filter out' -r
complete -c watchexec -l ignore-file -d 'Files to load ignores from' -r -F complete -c watchexec -l ignore-file -d 'Files to load ignores from' -r -F
complete -c watchexec -l fs-events -d 'Filesystem events to filter to' -r -f -a "{access '',create '',remove '',rename '',modify '',metadata ''}" complete -c watchexec -l fs-events -d 'Filesystem events to filter to' -r -f -a "{access '',create '',remove '',rename '',modify '',metadata ''}"
complete -c watchexec -l log-file -d 'Write diagnostic logs to a file' -r -F
complete -c watchexec -l completions -d 'Generate a shell completions script' -r -f -a "{bash '',elvish '',fish '',nu '',powershell '',zsh ''}" complete -c watchexec -l completions -d 'Generate a shell completions script' -r -f -a "{bash '',elvish '',fish '',nu '',powershell '',zsh ''}"
complete -c watchexec -l log-file -d 'Write diagnostic logs to a file' -r -F
complete -c watchexec -s r -l restart -d 'Restart the process if it\'s still running' complete -c watchexec -s r -l restart -d 'Restart the process if it\'s still running'
complete -c watchexec -l stdin-quit -d 'Exit when stdin closes' complete -c watchexec -l stdin-quit -d 'Exit when stdin closes'
complete -c watchexec -l no-vcs-ignore -d 'Don\'t load gitignores' complete -c watchexec -l no-vcs-ignore -d 'Don\'t load gitignores'
@ -44,7 +45,7 @@ complete -c watchexec -s q -l quiet -d 'Don\'t print starting and stopping messa
complete -c watchexec -l bell -d 'Ring the terminal bell on command completion' complete -c watchexec -l bell -d 'Ring the terminal bell on command completion'
complete -c watchexec -l no-meta -d 'Don\'t emit fs events for metadata changes' complete -c watchexec -l no-meta -d 'Don\'t emit fs events for metadata changes'
complete -c watchexec -l print-events -d 'Print events that trigger actions' complete -c watchexec -l print-events -d 'Print events that trigger actions'
complete -c watchexec -s v -l verbose -d 'Set diagnostic log level'
complete -c watchexec -l manual -d 'Show the manual page' complete -c watchexec -l manual -d 'Show the manual page'
complete -c watchexec -s v -l verbose -d 'Set diagnostic log level'
complete -c watchexec -s h -l help -d 'Print help (see more with \'--help\')' complete -c watchexec -s h -l help -d 'Print help (see more with \'--help\')'
complete -c watchexec -s V -l version -d 'Print version' complete -c watchexec -s V -l version -d 'Print version'

View File

@ -32,6 +32,7 @@ module completions {
export extern watchexec [ export extern watchexec [
...command: string # Command to run on changes ...command: string # Command to run on changes
--watch(-w): string # Watch a specific file or directory --watch(-w): string # Watch a specific file or directory
--watch-non-recursive(-W): string # Watch a specific directory, non-recursively
--clear(-c): string@"nu-complete watchexec screen_clear" # Clear screen before running command --clear(-c): string@"nu-complete watchexec screen_clear" # Clear screen before running command
--on-busy-update(-o): string@"nu-complete watchexec on_busy_update" # What to do when receiving events while the command is running --on-busy-update(-o): string@"nu-complete watchexec on_busy_update" # What to do when receiving events while the command is running
--restart(-r) # Restart the process if it's still running --restart(-r) # Restart the process if it's still running
@ -75,10 +76,10 @@ module completions {
--fs-events: string@"nu-complete watchexec filter_fs_events" # Filesystem events to filter to --fs-events: string@"nu-complete watchexec filter_fs_events" # Filesystem events to filter to
--no-meta # Don't emit fs events for metadata changes --no-meta # Don't emit fs events for metadata changes
--print-events # Print events that trigger actions --print-events # Print events that trigger actions
--verbose(-v) # Set diagnostic log level
--log-file: string # Write diagnostic logs to a file
--manual # Show the manual page --manual # Show the manual page
--completions: string@"nu-complete watchexec completions" # Generate a shell completions script --completions: string@"nu-complete watchexec completions" # Generate a shell completions script
--verbose(-v) # Set diagnostic log level
--log-file: string # Write diagnostic logs to a file
--help(-h) # Print help (see more with '--help') --help(-h) # Print help (see more with '--help')
--version(-V) # Print version --version(-V) # Print version
] ]

View File

@ -23,6 +23,8 @@ Register-ArgumentCompleter -Native -CommandName 'watchexec' -ScriptBlock {
'watchexec' { 'watchexec' {
[CompletionResult]::new('-w', 'w', [CompletionResultType]::ParameterName, 'Watch a specific file or directory') [CompletionResult]::new('-w', 'w', [CompletionResultType]::ParameterName, 'Watch a specific file or directory')
[CompletionResult]::new('--watch', 'watch', [CompletionResultType]::ParameterName, 'Watch a specific file or directory') [CompletionResult]::new('--watch', 'watch', [CompletionResultType]::ParameterName, 'Watch a specific file or directory')
[CompletionResult]::new('-W', 'W ', [CompletionResultType]::ParameterName, 'Watch a specific directory, non-recursively')
[CompletionResult]::new('--watch-non-recursive', 'watch-non-recursive', [CompletionResultType]::ParameterName, 'Watch a specific directory, non-recursively')
[CompletionResult]::new('-c', 'c', [CompletionResultType]::ParameterName, 'Clear screen before running command') [CompletionResult]::new('-c', 'c', [CompletionResultType]::ParameterName, 'Clear screen before running command')
[CompletionResult]::new('--clear', 'clear', [CompletionResultType]::ParameterName, 'Clear screen before running command') [CompletionResult]::new('--clear', 'clear', [CompletionResultType]::ParameterName, 'Clear screen before running command')
[CompletionResult]::new('-o', 'o', [CompletionResultType]::ParameterName, 'What to do when receiving events while the command is running') [CompletionResult]::new('-o', 'o', [CompletionResultType]::ParameterName, 'What to do when receiving events while the command is running')
@ -55,8 +57,8 @@ Register-ArgumentCompleter -Native -CommandName 'watchexec' -ScriptBlock {
[CompletionResult]::new('--ignore', 'ignore', [CompletionResultType]::ParameterName, 'Filename patterns to filter out') [CompletionResult]::new('--ignore', 'ignore', [CompletionResultType]::ParameterName, 'Filename patterns to filter out')
[CompletionResult]::new('--ignore-file', 'ignore-file', [CompletionResultType]::ParameterName, 'Files to load ignores from') [CompletionResult]::new('--ignore-file', 'ignore-file', [CompletionResultType]::ParameterName, 'Files to load ignores from')
[CompletionResult]::new('--fs-events', 'fs-events', [CompletionResultType]::ParameterName, 'Filesystem events to filter to') [CompletionResult]::new('--fs-events', 'fs-events', [CompletionResultType]::ParameterName, 'Filesystem events to filter to')
[CompletionResult]::new('--log-file', 'log-file', [CompletionResultType]::ParameterName, 'Write diagnostic logs to a file')
[CompletionResult]::new('--completions', 'completions', [CompletionResultType]::ParameterName, 'Generate a shell completions script') [CompletionResult]::new('--completions', 'completions', [CompletionResultType]::ParameterName, 'Generate a shell completions script')
[CompletionResult]::new('--log-file', 'log-file', [CompletionResultType]::ParameterName, 'Write diagnostic logs to a file')
[CompletionResult]::new('-r', 'r', [CompletionResultType]::ParameterName, 'Restart the process if it''s still running') [CompletionResult]::new('-r', 'r', [CompletionResultType]::ParameterName, 'Restart the process if it''s still running')
[CompletionResult]::new('--restart', 'restart', [CompletionResultType]::ParameterName, 'Restart the process if it''s still running') [CompletionResult]::new('--restart', 'restart', [CompletionResultType]::ParameterName, 'Restart the process if it''s still running')
[CompletionResult]::new('--stdin-quit', 'stdin-quit', [CompletionResultType]::ParameterName, 'Exit when stdin closes') [CompletionResult]::new('--stdin-quit', 'stdin-quit', [CompletionResultType]::ParameterName, 'Exit when stdin closes')
@ -81,9 +83,9 @@ Register-ArgumentCompleter -Native -CommandName 'watchexec' -ScriptBlock {
[CompletionResult]::new('--bell', 'bell', [CompletionResultType]::ParameterName, 'Ring the terminal bell on command completion') [CompletionResult]::new('--bell', 'bell', [CompletionResultType]::ParameterName, 'Ring the terminal bell on command completion')
[CompletionResult]::new('--no-meta', 'no-meta', [CompletionResultType]::ParameterName, 'Don''t emit fs events for metadata changes') [CompletionResult]::new('--no-meta', 'no-meta', [CompletionResultType]::ParameterName, 'Don''t emit fs events for metadata changes')
[CompletionResult]::new('--print-events', 'print-events', [CompletionResultType]::ParameterName, 'Print events that trigger actions') [CompletionResult]::new('--print-events', 'print-events', [CompletionResultType]::ParameterName, 'Print events that trigger actions')
[CompletionResult]::new('--manual', 'manual', [CompletionResultType]::ParameterName, 'Show the manual page')
[CompletionResult]::new('-v', 'v', [CompletionResultType]::ParameterName, 'Set diagnostic log level') [CompletionResult]::new('-v', 'v', [CompletionResultType]::ParameterName, 'Set diagnostic log level')
[CompletionResult]::new('--verbose', 'verbose', [CompletionResultType]::ParameterName, 'Set diagnostic log level') [CompletionResult]::new('--verbose', 'verbose', [CompletionResultType]::ParameterName, 'Set diagnostic log level')
[CompletionResult]::new('--manual', 'manual', [CompletionResultType]::ParameterName, 'Show the manual page')
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
[CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version') [CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version')

View File

@ -17,6 +17,8 @@ _watchexec() {
_arguments "${_arguments_options[@]}" \ _arguments "${_arguments_options[@]}" \
'*-w+[Watch a specific file or directory]:PATH:_files' \ '*-w+[Watch a specific file or directory]:PATH:_files' \
'*--watch=[Watch a specific file or directory]:PATH:_files' \ '*--watch=[Watch a specific file or directory]:PATH:_files' \
'*-W+[Watch a specific directory, non-recursively]:PATH:_files' \
'*--watch-non-recursive=[Watch a specific directory, non-recursively]:PATH:_files' \
'-c+[Clear screen before running command]' \ '-c+[Clear screen before running command]' \
'--clear=[Clear screen before running command]' \ '--clear=[Clear screen before running command]' \
'-o+[What to do when receiving events while the command is running]:MODE:(queue do-nothing restart signal)' \ '-o+[What to do when receiving events while the command is running]:MODE:(queue do-nothing restart signal)' \
@ -49,8 +51,8 @@ _watchexec() {
'*--ignore=[Filename patterns to filter out]:PATTERN: ' \ '*--ignore=[Filename patterns to filter out]:PATTERN: ' \
'*--ignore-file=[Files to load ignores from]:PATH:_files' \ '*--ignore-file=[Files to load ignores from]:PATH:_files' \
'*--fs-events=[Filesystem events to filter to]:EVENTS:(access create remove rename modify metadata)' \ '*--fs-events=[Filesystem events to filter to]:EVENTS:(access create remove rename modify metadata)' \
'--log-file=[Write diagnostic logs to a file]' \
'(--manual)--completions=[Generate a shell completions script]:COMPLETIONS:(bash elvish fish nu powershell zsh)' \ '(--manual)--completions=[Generate a shell completions script]:COMPLETIONS:(bash elvish fish nu powershell zsh)' \
'--log-file=[Write diagnostic logs to a file]' \
'(-o --on-busy-update)-r[Restart the process if it'\''s still running]' \ '(-o --on-busy-update)-r[Restart the process if it'\''s still running]' \
'(-o --on-busy-update)--restart[Restart the process if it'\''s still running]' \ '(-o --on-busy-update)--restart[Restart the process if it'\''s still running]' \
'--stdin-quit[Exit when stdin closes]' \ '--stdin-quit[Exit when stdin closes]' \
@ -75,9 +77,9 @@ _watchexec() {
'--bell[Ring the terminal bell on command completion]' \ '--bell[Ring the terminal bell on command completion]' \
'(--fs-events)--no-meta[Don'\''t emit fs events for metadata changes]' \ '(--fs-events)--no-meta[Don'\''t emit fs events for metadata changes]' \
'--print-events[Print events that trigger actions]' \ '--print-events[Print events that trigger actions]' \
'(--completions)--manual[Show the manual page]' \
'*-v[Set diagnostic log level]' \ '*-v[Set diagnostic log level]' \
'*--verbose[Set diagnostic log level]' \ '*--verbose[Set diagnostic log level]' \
'(--completions)--manual[Show the manual page]' \
'-h[Print help (see more with '\''--help'\'')]' \ '-h[Print help (see more with '\''--help'\'')]' \
'--help[Print help (see more with '\''--help'\'')]' \ '--help[Print help (see more with '\''--help'\'')]' \
'-V[Print version]' \ '-V[Print version]' \

View File

@ -44,6 +44,7 @@ serde_json = "1.0.107"
tempfile = "3.8.1" tempfile = "3.8.1"
termcolor = "1.4.0" termcolor = "1.4.0"
tracing = "0.1.40" tracing = "0.1.40"
tracing-appender = "0.2.3"
which = "6.0.1" which = "6.0.1"
[dependencies.blake3] [dependencies.blake3]

View File

@ -2,6 +2,8 @@ pre-release-commit-message = "release: cli v{{version}}"
tag-prefix = "" tag-prefix = ""
tag-message = "watchexec {{version}}" tag-message = "watchexec {{version}}"
pre-release-hook = ["sh", "-c", "cd ../.. && bin/completions && bin/manpage"]
[[pre-release-replacements]] [[pre-release-replacements]]
file = "watchexec.exe.manifest" file = "watchexec.exe.manifest"
search = "^ version=\"[\\d.]+[.]0\"" search = "^ version=\"[\\d.]+[.]0\""

View File

@ -1,21 +1,28 @@
use std::{ use std::{
collections::BTreeSet,
ffi::{OsStr, OsString}, ffi::{OsStr, OsString},
path::PathBuf, fs::canonicalize,
mem::take,
path::{Path, PathBuf},
str::FromStr, str::FromStr,
time::Duration, time::Duration,
}; };
use clap::{ use clap::{
builder::TypedValueParser, error::ErrorKind, Arg, ArgAction, Command, CommandFactory, Parser, builder::TypedValueParser, error::ErrorKind, Arg, Command, CommandFactory, Parser, ValueEnum,
ValueEnum, ValueHint, ValueHint,
}; };
use miette::{IntoDiagnostic, Result}; use miette::{IntoDiagnostic, Result};
use tokio::{fs::File, io::AsyncReadExt}; use tokio::{fs::File, io::AsyncReadExt};
use watchexec::paths::PATH_SEPARATOR; use tracing::{debug, info, trace, warn};
use tracing_appender::non_blocking::WorkerGuard;
use watchexec::{paths::PATH_SEPARATOR, sources::fs::WatchedPath};
use watchexec_signals::Signal; use watchexec_signals::Signal;
use crate::filterer::parse::parse_filter_program; use crate::filterer::parse::parse_filter_program;
mod logging;
const OPTSET_FILTERING: &str = "Filtering"; const OPTSET_FILTERING: &str = "Filtering";
const OPTSET_COMMAND: &str = "Command"; const OPTSET_COMMAND: &str = "Command";
const OPTSET_DEBUGGING: &str = "Debugging"; const OPTSET_DEBUGGING: &str = "Debugging";
@ -128,7 +135,25 @@ pub struct Args {
value_hint = ValueHint::AnyPath, value_hint = ValueHint::AnyPath,
value_name = "PATH", value_name = "PATH",
)] )]
pub paths: Vec<PathBuf>, pub recursive_paths: Vec<PathBuf>,
/// Watch a specific directory, non-recursively
///
/// Unlike '-w', folders watched with this option are not recursed into.
///
/// This option can be specified multiple times to watch multiple directories non-recursively.
#[arg(
short = 'W',
long = "watch-non-recursive",
help_heading = OPTSET_FILTERING,
value_hint = ValueHint::AnyPath,
value_name = "PATH",
)]
pub non_recursive_paths: Vec<PathBuf>,
#[doc(hidden)]
#[arg(skip)]
pub paths: Vec<WatchedPath>,
/// Clear screen before running command /// Clear screen before running command
/// ///
@ -791,6 +816,7 @@ pub struct Args {
/// ///
/// Provide your own custom filter programs in jaq (similar to jq) syntax. Programs are given /// Provide your own custom filter programs in jaq (similar to jq) syntax. Programs are given
/// an event in the same format as described in '--emit-events-to' and must return a boolean. /// an event in the same format as described in '--emit-events-to' and must return a boolean.
/// Invalid programs will make watchexec fail to start; use '-v' to see program runtime errors.
/// ///
/// In addition to the jaq stdlib, watchexec adds some custom filter definitions: /// In addition to the jaq stdlib, watchexec adds some custom filter definitions:
/// ///
@ -921,54 +947,13 @@ pub struct Args {
/// This prints the events that triggered the action when handling it (after debouncing), in a /// This prints the events that triggered the action when handling it (after debouncing), in a
/// human readable form. This is useful for debugging filters. /// human readable form. This is useful for debugging filters.
/// ///
/// Use '-v' when you need more diagnostic information. /// Use '-vvv' instead when you need more diagnostic information.
#[arg( #[arg(
long, long,
help_heading = OPTSET_DEBUGGING, help_heading = OPTSET_DEBUGGING,
)] )]
pub print_events: bool, pub print_events: bool,
/// Set diagnostic log level
///
/// This enables diagnostic logging, which is useful for investigating bugs or gaining more
/// insight into faulty filters or "missing" events. Use multiple times to increase verbosity.
///
/// Goes up to '-vvvv'. When submitting bug reports, default to a '-vvv' log level.
///
/// You may want to use with '--log-file' to avoid polluting your terminal.
///
/// Setting $RUST_LOG also works, and takes precedence, but is not recommended. However, using
/// $RUST_LOG is the only way to get logs from before these options are parsed.
#[arg(
long,
short,
help_heading = OPTSET_DEBUGGING,
action = ArgAction::Count,
num_args = 0,
)]
pub verbose: Option<u8>,
/// Write diagnostic logs to a file
///
/// This writes diagnostic logs to a file, instead of the terminal, in JSON format. If a log
/// level was not already specified, this will set it to '-vvv'.
///
/// If a path is not provided, the default is the working directory. Note that with
/// '--ignore-nothing', the write events to the log will likely get picked up by Watchexec,
/// causing a loop; prefer setting a path outside of the watched directory.
///
/// If the path provided is a directory, a file will be created in that directory. The file name
/// will be the current date and time, in the format 'watchexec.YYYY-MM-DDTHH-MM-SSZ.log'.
#[arg(
long,
help_heading = OPTSET_DEBUGGING,
num_args = 0..=1,
default_missing_value = ".",
value_hint = ValueHint::AnyPath,
value_name = "PATH",
)]
pub log_file: Option<PathBuf>,
/// Show the manual page /// Show the manual page
/// ///
/// This shows the manual page for Watchexec, if the output is a terminal and the 'man' program /// This shows the manual page for Watchexec, if the output is a terminal and the 'man' program
@ -993,6 +978,9 @@ pub struct Args {
conflicts_with_all = ["command", "manual"], conflicts_with_all = ["command", "manual"],
)] )]
pub completions: Option<ShellCompletion>, pub completions: Option<ShellCompletion>,
#[command(flatten)]
pub logging: logging::LoggingArgs,
} }
#[derive(Clone, Copy, Debug, Default, ValueEnum)] #[derive(Clone, Copy, Debug, Default, ValueEnum)]
@ -1159,11 +1147,10 @@ fn expand_args_up_to_doubledash() -> Result<Vec<OsString>, std::io::Error> {
} }
#[inline] #[inline]
pub async fn get_args() -> Result<Args> { pub async fn get_args() -> Result<(Args, Option<WorkerGuard>)> {
use tracing::{debug, trace, warn}; let prearg_logs = logging::preargs();
if prearg_logs {
if std::env::var("RUST_LOG").is_ok() { warn!("⚠ RUST_LOG environment variable set or hardcoded, logging options have no effect");
warn!("⚠ RUST_LOG environment variable set, logging options have no effect");
} }
debug!("expanding @argfile arguments if any"); debug!("expanding @argfile arguments if any");
@ -1172,6 +1159,12 @@ pub async fn get_args() -> Result<Args> {
debug!("parsing arguments"); debug!("parsing arguments");
let mut args = Args::parse_from(args); let mut args = Args::parse_from(args);
let log_guard = if !prearg_logs {
logging::postargs(&args.logging).await?
} else {
None
};
// https://no-color.org/ // https://no-color.org/
if args.color == ColourMode::Auto && std::env::var("NO_COLOR").is_ok() { if args.color == ColourMode::Auto && std::env::var("NO_COLOR").is_ok() {
args.color = ColourMode::Never; args.color = ColourMode::Never;
@ -1192,10 +1185,12 @@ pub async fn get_args() -> Result<Args> {
} }
if args.no_environment { if args.no_environment {
warn!("--no-environment is deprecated");
args.emit_events_to = EmitEvents::None; args.emit_events_to = EmitEvents::None;
} }
if args.no_process_group { if args.no_process_group {
warn!("--no-process-group is deprecated");
args.wrap_process = WrapMode::None; args.wrap_process = WrapMode::None;
} }
@ -1221,6 +1216,63 @@ pub async fn get_args() -> Result<Args> {
.exit(); .exit();
} }
let workdir = if let Some(w) = take(&mut args.workdir) {
w
} else {
let curdir = std::env::current_dir().into_diagnostic()?;
canonicalize(curdir).into_diagnostic()?
};
info!(path=?workdir, "effective working directory");
args.workdir = Some(workdir.clone());
let project_origin = if let Some(p) = take(&mut args.project_origin) {
p
} else {
crate::dirs::project_origin(&args).await?
};
info!(path=?project_origin, "effective project origin");
args.project_origin = Some(project_origin.clone());
args.paths = take(&mut args.recursive_paths)
.into_iter()
.map(|path| {
{
if path.is_absolute() {
Ok(path)
} else {
canonicalize(project_origin.join(path)).into_diagnostic()
}
}
.map(WatchedPath::non_recursive)
})
.chain(take(&mut args.non_recursive_paths).into_iter().map(|path| {
{
if path.is_absolute() {
Ok(path)
} else {
canonicalize(project_origin.join(path)).into_diagnostic()
}
}
.map(WatchedPath::non_recursive)
}))
.collect::<Result<BTreeSet<_>>>()?
.into_iter()
.collect();
if args.paths.len() == 1
&& args
.paths
.first()
.map_or(false, |p| p.as_ref() == Path::new("/dev/null"))
{
info!("only path is /dev/null, not watching anything");
args.paths = Vec::new();
} else if args.paths.is_empty() {
info!("no paths, using current directory");
args.paths.push(args.workdir.clone().unwrap().into());
}
info!(paths=?args.paths, "effective watched paths");
for (n, prog) in args.filter_programs.iter_mut().enumerate() { for (n, prog) in args.filter_programs.iter_mut().enumerate() {
if let Some(progpath) = prog.strip_prefix('@') { if let Some(progpath) = prog.strip_prefix('@') {
trace!(?n, path=?progpath, "reading filter program from file"); trace!(?n, path=?progpath, "reading filter program from file");
@ -1233,12 +1285,14 @@ pub async fn get_args() -> Result<Args> {
} }
} }
args.filter_programs_parsed = std::mem::take(&mut args.filter_programs) args.filter_programs_parsed = take(&mut args.filter_programs)
.into_iter() .into_iter()
.enumerate() .enumerate()
.map(parse_filter_program) .map(parse_filter_program)
.collect::<Result<_, _>>()?; .collect::<Result<_, _>>()?;
debug!(?args, "got arguments"); debug_assert!(args.workdir.is_some());
Ok(args) debug_assert!(args.project_origin.is_some());
info!(?args, "got arguments");
Ok((args, log_guard))
} }

View File

@ -0,0 +1,132 @@
use std::{env::var, io::stderr, path::PathBuf};
use clap::{ArgAction, Parser, ValueHint};
use miette::{bail, Result};
use tokio::fs::metadata;
use tracing::{info, warn};
use tracing_appender::{non_blocking, non_blocking::WorkerGuard, rolling};
#[derive(Debug, Clone, Parser)]
pub struct LoggingArgs {
/// Set diagnostic log level
///
/// This enables diagnostic logging, which is useful for investigating bugs or gaining more
/// insight into faulty filters or "missing" events. Use multiple times to increase verbosity.
///
/// Goes up to '-vvvv'. When submitting bug reports, default to a '-vvv' log level.
///
/// You may want to use with '--log-file' to avoid polluting your terminal.
///
/// Setting $RUST_LOG also works, and takes precedence, but is not recommended. However, using
/// $RUST_LOG is the only way to get logs from before these options are parsed.
#[arg(
long,
short,
help_heading = super::OPTSET_DEBUGGING,
action = ArgAction::Count,
default_value = "0",
num_args = 0,
)]
pub verbose: u8,
/// Write diagnostic logs to a file
///
/// This writes diagnostic logs to a file, instead of the terminal, in JSON format. If a log
/// level was not already specified, this will set it to '-vvv'.
///
/// If a path is not provided, the default is the working directory. Note that with
/// '--ignore-nothing', the write events to the log will likely get picked up by Watchexec,
/// causing a loop; prefer setting a path outside of the watched directory.
///
/// If the path provided is a directory, a file will be created in that directory. The file name
/// will be the current date and time, in the format 'watchexec.YYYY-MM-DDTHH-MM-SSZ.log'.
#[arg(
long,
help_heading = super::OPTSET_DEBUGGING,
num_args = 0..=1,
default_missing_value = ".",
value_hint = ValueHint::AnyPath,
value_name = "PATH",
)]
pub log_file: Option<PathBuf>,
}
pub fn preargs() -> bool {
let mut log_on = false;
#[cfg(feature = "dev-console")]
match console_subscriber::try_init() {
Ok(_) => {
warn!("dev-console enabled");
log_on = true;
}
Err(e) => {
eprintln!("Failed to initialise tokio console, falling back to normal logging\n{e}")
}
}
if !log_on && var("RUST_LOG").is_ok() {
match tracing_subscriber::fmt::try_init() {
Ok(()) => {
warn!(RUST_LOG=%var("RUST_LOG").unwrap(), "logging configured from RUST_LOG");
log_on = true;
}
Err(e) => eprintln!("Failed to initialise logging with RUST_LOG, falling back\n{e}"),
}
}
log_on
}
pub async fn postargs(args: &LoggingArgs) -> Result<Option<WorkerGuard>> {
if args.verbose == 0 {
return Ok(None);
}
let (log_writer, guard) = if let Some(file) = &args.log_file {
let is_dir = metadata(&file).await.map_or(false, |info| info.is_dir());
let (dir, filename) = if is_dir {
(
file.to_owned(),
PathBuf::from(format!(
"watchexec.{}.log",
chrono::Utc::now().format("%Y-%m-%dT%H-%M-%SZ")
)),
)
} else if let (Some(parent), Some(file_name)) = (file.parent(), file.file_name()) {
(parent.into(), PathBuf::from(file_name))
} else {
bail!("Failed to determine log file name");
};
non_blocking(rolling::never(dir, filename))
} else {
non_blocking(stderr())
};
let mut builder = tracing_subscriber::fmt().with_env_filter(match args.verbose {
0 => unreachable!("checked by if earlier"),
1 => "warn",
2 => "info",
3 => "debug",
_ => "trace",
});
if args.verbose > 2 {
use tracing_subscriber::fmt::format::FmtSpan;
builder = builder.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE);
}
match if args.log_file.is_some() {
builder.json().with_writer(log_writer).try_init()
} else if args.verbose > 3 {
builder.pretty().with_writer(log_writer).try_init()
} else {
builder.with_writer(log_writer).try_init()
} {
Ok(()) => info!("logging initialised"),
Err(e) => eprintln!("Failed to initialise logging, continuing with none\n{e}"),
}
Ok(Some(guard))
}

View File

@ -1,11 +1,10 @@
use std::{ use std::{
borrow::Cow, borrow::Cow,
collections::HashMap, collections::HashMap,
env::{current_dir, var}, env::var,
ffi::{OsStr, OsString}, ffi::{OsStr, OsString},
fs::File, fs::File,
io::{IsTerminal, Write}, io::{IsTerminal, Write},
path::Path,
process::Stdio, process::Stdio,
sync::{ sync::{
atomic::{AtomicBool, AtomicU8, Ordering}, atomic::{AtomicBool, AtomicU8, Ordering},
@ -68,19 +67,7 @@ pub fn make_config(args: &Args, state: &State) -> Result<Config> {
eprintln!("[[Error (not fatal)]]\n{}", Report::new(err.error)); eprintln!("[[Error (not fatal)]]\n{}", Report::new(err.error));
}); });
config.pathset(if args.paths.is_empty() { config.pathset(args.paths.clone());
vec![current_dir().into_diagnostic()?]
} else if args.paths.len() == 1
&& args
.paths
.first()
.map_or(false, |p| p == Path::new("/dev/null"))
{
// special case: /dev/null means "don't start the fs event source"
Vec::new()
} else {
args.paths.clone()
});
config.throttle(args.debounce.0); config.throttle(args.debounce.0);
config.keyboard_events(args.stdin_quit); config.keyboard_events(args.stdin_quit);

View File

@ -1,7 +1,5 @@
use std::{ use std::{
borrow::Cow,
collections::HashSet, collections::HashSet,
env,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
@ -14,16 +12,7 @@ use watchexec::paths::common_prefix;
use crate::args::Args; use crate::args::Args;
type ProjectOriginPath = PathBuf; pub async fn project_origin(args: &Args) -> Result<PathBuf> {
type WorkDirPath = PathBuf;
/// Extract relevant directories (in particular the project origin and work directory)
/// given the command line arguments that were provided
pub async fn dirs(args: &Args) -> Result<(ProjectOriginPath, WorkDirPath)> {
let curdir = env::current_dir().into_diagnostic()?;
let curdir = canonicalize(curdir).await.into_diagnostic()?;
debug!(?curdir, "current directory");
let project_origin = if let Some(origin) = &args.project_origin { let project_origin = if let Some(origin) = &args.project_origin {
debug!(?origin, "project origin override"); debug!(?origin, "project origin override");
canonicalize(origin).await.into_diagnostic()? canonicalize(origin).await.into_diagnostic()?
@ -34,27 +23,19 @@ pub async fn dirs(args: &Args) -> Result<(ProjectOriginPath, WorkDirPath)> {
}; };
debug!(?homedir, "home directory"); debug!(?homedir, "home directory");
let mut paths = HashSet::new(); let homedir_requested = homedir.as_ref().map_or(false, |home| {
for path in &args.paths { args.paths
paths.insert(canonicalize(path).await.into_diagnostic()?); .binary_search_by_key(home, |w| PathBuf::from(w.clone()))
} .is_ok()
});
let homedir_requested = homedir.as_ref().map_or(false, |home| paths.contains(home));
debug!( debug!(
?homedir_requested, ?homedir_requested,
"resolved whether the homedir is explicitly requested" "resolved whether the homedir is explicitly requested"
); );
if paths.is_empty() {
debug!("no paths, using current directory");
paths.insert(curdir.clone());
}
debug!(?paths, "resolved all watched paths");
let mut origins = HashSet::new(); let mut origins = HashSet::new();
for path in paths { for path in &args.paths {
origins.extend(project_origins::origins(&path).await); origins.extend(project_origins::origins(path).await);
} }
match (homedir, homedir_requested) { match (homedir, homedir_requested) {
@ -67,7 +48,7 @@ pub async fn dirs(args: &Args) -> Result<(ProjectOriginPath, WorkDirPath)> {
if origins.is_empty() { if origins.is_empty() {
debug!("no origins, using current directory"); debug!("no origins, using current directory");
origins.insert(curdir.clone()); origins.insert(args.workdir.clone().unwrap());
} }
debug!(?origins, "resolved all project origins"); debug!(?origins, "resolved all project origins");
@ -80,12 +61,9 @@ pub async fn dirs(args: &Args) -> Result<(ProjectOriginPath, WorkDirPath)> {
.await .await
.into_diagnostic()? .into_diagnostic()?
}; };
info!(?project_origin, "resolved common/project origin"); debug!(?project_origin, "resolved common/project origin");
let workdir = curdir; Ok(project_origin)
info!(?workdir, "resolved working directory");
Ok((project_origin, workdir))
} }
pub async fn vcs_types(origin: &Path) -> Vec<ProjectType> { pub async fn vcs_types(origin: &Path) -> Vec<ProjectType> {
@ -94,41 +72,34 @@ pub async fn vcs_types(origin: &Path) -> Vec<ProjectType> {
.into_iter() .into_iter()
.filter(|pt| pt.is_vcs()) .filter(|pt| pt.is_vcs())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
info!(?vcs_types, "resolved vcs types"); info!(?vcs_types, "effective vcs types");
vcs_types vcs_types
} }
pub async fn ignores( pub async fn ignores(args: &Args, vcs_types: &[ProjectType]) -> Result<Vec<IgnoreFile>> {
args: &Args, let origin = args.project_origin.clone().unwrap();
vcs_types: &[ProjectType],
origin: &Path,
) -> Result<Vec<IgnoreFile>> {
fn higher_make_absolute_if_needed<'a>(
origin: &'a Path,
) -> impl 'a + Fn(&'a PathBuf) -> Cow<'a, Path> {
|path| {
if path.is_absolute() {
Cow::Borrowed(path)
} else {
Cow::Owned(origin.join(path))
}
}
}
let mut skip_git_global_excludes = false; let mut skip_git_global_excludes = false;
let mut ignores = if args.no_project_ignore { let mut ignores = if args.no_project_ignore {
Vec::new() Vec::new()
} else { } else {
let make_absolute_if_needed = higher_make_absolute_if_needed(origin); let ignore_files = args.ignore_files.iter().map(|path| {
let include_paths = args.paths.iter().map(&make_absolute_if_needed); if path.is_absolute() {
let ignore_files = args.ignore_files.iter().map(&make_absolute_if_needed); path.into()
} else {
origin.join(path)
}
});
let (mut ignores, errors) = ignore_files::from_origin( let (mut ignores, errors) = ignore_files::from_origin(
IgnoreFilesFromOriginArgs::new_unchecked(origin, include_paths, ignore_files) IgnoreFilesFromOriginArgs::new_unchecked(
.canonicalise() &origin,
.await args.paths.iter().map(PathBuf::from),
.into_diagnostic()?, ignore_files,
)
.canonicalise()
.await
.into_diagnostic()?,
) )
.await; .await;
@ -221,7 +192,7 @@ pub async fn ignores(
.filter(|ig| { .filter(|ig| {
!ig.applies_in !ig.applies_in
.as_ref() .as_ref()
.map_or(false, |p| p.starts_with(origin)) .map_or(false, |p| p.starts_with(&origin))
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
debug!( debug!(

View File

@ -16,7 +16,6 @@ use watchexec_filterer_globset::GlobsetFilterer;
use crate::args::{Args, FsEvent}; use crate::args::{Args, FsEvent};
mod dirs;
pub(crate) mod parse; pub(crate) mod parse;
mod proglib; mod proglib;
mod progs; mod progs;
@ -71,13 +70,14 @@ impl Filterer for WatchexecFilterer {
impl WatchexecFilterer { impl WatchexecFilterer {
/// Create a new filterer from the given arguments /// Create a new filterer from the given arguments
pub async fn new(args: &Args) -> Result<Arc<Self>> { pub async fn new(args: &Args) -> Result<Arc<Self>> {
let (project_origin, workdir) = dirs::dirs(args).await?; let project_origin = args.project_origin.clone().unwrap();
let workdir = args.workdir.clone().unwrap();
let ignore_files = if args.no_discover_ignore { let ignore_files = if args.no_discover_ignore {
Vec::new() Vec::new()
} else { } else {
let vcs_types = dirs::vcs_types(&project_origin).await; let vcs_types = crate::dirs::vcs_types(&project_origin).await;
dirs::ignores(args, &vcs_types, &project_origin).await? crate::dirs::ignores(args, &vcs_types).await?
}; };
let mut ignores = Vec::new(); let mut ignores = Vec::new();

View File

@ -10,7 +10,7 @@ pub fn parse_filter_program((n, prog): (usize, String)) -> Result<jaq_syn::Main>
.map(|err| err.to_string()) .map(|err| err.to_string())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\n"); .join("\n");
return Err(miette!("failed to load filter program #{}: {:?}", n, errs)); return Err(miette!("{}", errs).wrap_err(format!("failed to load filter program #{}", n)));
} }
main.ok_or_else(|| miette!("failed to load filter program #{} (no reason given)", n)) main.ok_or_else(|| miette!("failed to load filter program #{} (no reason given)", n))

View File

@ -1,7 +1,7 @@
#![deny(rust_2018_idioms)] #![deny(rust_2018_idioms)]
#![allow(clippy::missing_const_for_fn, clippy::future_not_send)] #![allow(clippy::missing_const_for_fn, clippy::future_not_send)]
use std::{env::var, fs::File, io::Write, process::Stdio, sync::Mutex}; use std::{io::Write, process::Stdio};
use args::{Args, ShellCompletion}; use args::{Args, ShellCompletion};
use clap::CommandFactory; use clap::CommandFactory;
@ -9,8 +9,8 @@ use clap_complete::{Generator, Shell};
use clap_mangen::Man; use clap_mangen::Man;
use is_terminal::IsTerminal; use is_terminal::IsTerminal;
use miette::{IntoDiagnostic, Result}; use miette::{IntoDiagnostic, Result};
use tokio::{fs::metadata, io::AsyncWriteExt, process::Command}; use tokio::{io::AsyncWriteExt, process::Command};
use tracing::{debug, info, warn}; use tracing::{debug, info};
use watchexec::Watchexec; use watchexec::Watchexec;
use watchexec_events::{Event, Priority}; use watchexec_events::{Event, Priority};
@ -18,86 +18,11 @@ use crate::filterer::WatchexecFilterer;
pub mod args; pub mod args;
mod config; mod config;
mod dirs;
mod emits; mod emits;
mod filterer; mod filterer;
mod state; mod state;
async fn init() -> Result<Args> {
let mut log_on = false;
#[cfg(feature = "dev-console")]
match console_subscriber::try_init() {
Ok(_) => {
warn!("dev-console enabled");
log_on = true;
}
Err(e) => {
eprintln!("Failed to initialise tokio console, falling back to normal logging\n{e}")
}
}
if !log_on && var("RUST_LOG").is_ok() {
match tracing_subscriber::fmt::try_init() {
Ok(()) => {
warn!(RUST_LOG=%var("RUST_LOG").unwrap(), "logging configured from RUST_LOG");
log_on = true;
}
Err(e) => eprintln!("Failed to initialise logging with RUST_LOG, falling back\n{e}"),
}
}
let args = args::get_args().await?;
let verbosity = args.verbose.unwrap_or(0);
if log_on {
warn!("ignoring logging options from args");
} else if verbosity > 0 {
let log_file = if let Some(file) = &args.log_file {
let is_dir = metadata(&file).await.map_or(false, |info| info.is_dir());
let path = if is_dir {
let filename = format!(
"watchexec.{}.log",
chrono::Utc::now().format("%Y-%m-%dT%H-%M-%SZ")
);
file.join(filename)
} else {
file.to_owned()
};
// TODO: use tracing-appender instead
Some(File::create(path).into_diagnostic()?)
} else {
None
};
let mut builder = tracing_subscriber::fmt().with_env_filter(match verbosity {
0 => unreachable!("checked by if earlier"),
1 => "warn",
2 => "info",
3 => "debug",
_ => "trace",
});
if verbosity > 2 {
use tracing_subscriber::fmt::format::FmtSpan;
builder = builder.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE);
}
match if let Some(writer) = log_file {
builder.json().with_writer(Mutex::new(writer)).try_init()
} else if verbosity > 3 {
builder.pretty().try_init()
} else {
builder.try_init()
} {
Ok(()) => info!("logging initialised"),
Err(e) => eprintln!("Failed to initialise logging, continuing with none\n{e}"),
}
}
Ok(args)
}
async fn run_watchexec(args: Args) -> Result<()> { async fn run_watchexec(args: Args) -> Result<()> {
info!(version=%env!("CARGO_PKG_VERSION"), "constructing Watchexec from CLI"); info!(version=%env!("CARGO_PKG_VERSION"), "constructing Watchexec from CLI");
@ -191,8 +116,7 @@ async fn run_completions(shell: ShellCompletion) -> Result<()> {
} }
pub async fn run() -> Result<()> { pub async fn run() -> Result<()> {
let args = init().await?; let (args, _log_guard) = args::get_args().await?;
debug!(?args, "arguments");
if args.manual { if args.manual {
run_manpage(args).await run_manpage(args).await

View File

@ -2,6 +2,8 @@
## Next (YYYY-MM-DD) ## Next (YYYY-MM-DD)
- Hide fmt::Debug spew from ignore crate, use `full_debug` feature to restore.
## v4.0.0 (2024-04-20) ## v4.0.0 (2024-04-20)
- Deps: watchexec 4 - Deps: watchexec 4

View File

@ -47,3 +47,9 @@ features = [
"rt-multi-thread", "rt-multi-thread",
"macros", "macros",
] ]
[features]
default = []
## Don't hide ignore::gitignore::Gitignore Debug impl
full_debug = []

View File

@ -10,6 +10,7 @@
use std::{ use std::{
ffi::OsString, ffi::OsString,
fmt,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
@ -21,7 +22,7 @@ use watchexec_events::{Event, FileType, Priority};
use watchexec_filterer_ignore::IgnoreFilterer; use watchexec_filterer_ignore::IgnoreFilterer;
/// A simple filterer in the style of the watchexec v1.17 filter. /// A simple filterer in the style of the watchexec v1.17 filter.
#[derive(Debug)] #[cfg_attr(feature = "full_debug", derive(Debug))]
pub struct GlobsetFilterer { pub struct GlobsetFilterer {
#[cfg_attr(not(unix), allow(dead_code))] #[cfg_attr(not(unix), allow(dead_code))]
origin: PathBuf, origin: PathBuf,
@ -31,6 +32,19 @@ pub struct GlobsetFilterer {
extensions: Vec<OsString>, extensions: Vec<OsString>,
} }
#[cfg(not(feature = "full_debug"))]
impl fmt::Debug for GlobsetFilterer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GlobsetFilterer")
.field("origin", &self.origin)
.field("filters", &"ignore::gitignore::Gitignore{...}")
.field("ignores", &"ignore::gitignore::Gitignore{...}")
.field("ignore_files", &self.ignore_files)
.field("extensions", &self.extensions)
.finish()
}
}
impl GlobsetFilterer { impl GlobsetFilterer {
/// Create a new `GlobsetFilterer` from a project origin, allowed extensions, and lists of globs. /// Create a new `GlobsetFilterer` from a project origin, allowed extensions, and lists of globs.
/// ///

View File

@ -2,6 +2,8 @@
## Next (YYYY-MM-DD) ## Next (YYYY-MM-DD)
- Hide fmt::Debug spew from ignore crate, use `full_debug` feature to restore.
## v3.0.0 (2024-04-20) ## v3.0.0 (2024-04-20)
- Deps: gix-config 0.36 - Deps: gix-config 0.36

View File

@ -40,3 +40,9 @@ path = "../project-origins"
[dev-dependencies] [dev-dependencies]
tracing-subscriber = "0.3.6" tracing-subscriber = "0.3.6"
[features]
default = []
## Don't hide ignore::gitignore::Gitignore Debug impl
full_debug = []

View File

@ -1,3 +1,4 @@
use std::fmt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use futures::stream::{FuturesUnordered, StreamExt}; use futures::stream::{FuturesUnordered, StreamExt};
@ -11,12 +12,23 @@ use tracing::{trace, trace_span};
use crate::{simplify_path, Error, IgnoreFile}; use crate::{simplify_path, Error, IgnoreFile};
#[derive(Clone, Debug)] #[derive(Clone)]
#[cfg_attr(feature = "full_debug", derive(Debug))]
struct Ignore { struct Ignore {
gitignore: Gitignore, gitignore: Gitignore,
builder: Option<GitignoreBuilder>, builder: Option<GitignoreBuilder>,
} }
#[cfg(not(feature = "full_debug"))]
impl fmt::Debug for Ignore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Ignore")
.field("gitignore", &"ignore::gitignore::Gitignore{...}")
.field("builder", &"ignore::gitignore::GitignoreBuilder{...}")
.finish()
}
}
/// A mutable filter dedicated to ignore files and trees of ignore files. /// A mutable filter dedicated to ignore files and trees of ignore files.
/// ///
/// This reads and compiles ignore files, and should be used for handling ignore files. It's created /// This reads and compiles ignore files, and should be used for handling ignore files. It's created

View File

@ -2,6 +2,10 @@
## Next (YYYY-MM-DD) ## Next (YYYY-MM-DD)
- Feature: non-recursive watches with `WatchedPath::non_recursive()`
- Fix: `config.pathset()` now preserves `WatchedPath` attributes
- Refactor: move `WatchedPath` to the root of the crate (old path remains as re-export for now)
## v4.0.0 (2024-04-20) ## v4.0.0 (2024-04-20)
- Deps: replace command-group with process-wrap (in supervisor, but has flow-on effects) - Deps: replace command-group with process-wrap (in supervisor, but has flow-on effects)

View File

@ -1,6 +1,6 @@
//! Configuration and builders for [`crate::Watchexec`]. //! Configuration and builders for [`crate::Watchexec`].
use std::{future::Future, path::Path, pin::pin, sync::Arc, time::Duration}; use std::{future::Future, pin::pin, sync::Arc, time::Duration};
use tokio::sync::Notify; use tokio::sync::Notify;
use tracing::{debug, trace}; use tracing::{debug, trace};
@ -195,9 +195,9 @@ impl Config {
pub fn pathset<I, P>(&self, pathset: I) -> &Self pub fn pathset<I, P>(&self, pathset: I) -> &Self
where where
I: IntoIterator<Item = P>, I: IntoIterator<Item = P>,
P: AsRef<Path>, P: Into<WatchedPath>,
{ {
let pathset = pathset.into_iter().map(|p| p.as_ref().into()).collect(); let pathset = pathset.into_iter().map(|p| p.into()).collect();
debug!(?pathset, "Config: pathset"); debug!(?pathset, "Config: pathset");
self.pathset.replace(pathset); self.pathset.replace(pathset);
self.signal_change() self.signal_change()

View File

@ -68,12 +68,14 @@ pub mod config;
mod id; mod id;
mod late_join_set; mod late_join_set;
mod watched_path;
mod watchexec; mod watchexec;
#[doc(inline)] #[doc(inline)]
pub use crate::{ pub use crate::{
id::Id, id::Id,
watchexec::{ErrorHook, Watchexec}, watchexec::{ErrorHook, Watchexec},
watched_path::WatchedPath,
}; };
#[doc(no_inline)] #[doc(no_inline)]

View File

@ -4,7 +4,6 @@ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
fs::metadata, fs::metadata,
mem::take, mem::take,
path::{Path, PathBuf},
sync::Arc, sync::Arc,
time::Duration, time::Duration,
}; };
@ -20,6 +19,9 @@ use crate::{
Config, Config,
}; };
// re-export for compatibility, until next major version
pub use crate::WatchedPath;
/// What kind of filesystem watcher to use. /// What kind of filesystem watcher to use.
/// ///
/// For now only native and poll watchers are supported. In the future there may be additional /// For now only native and poll watchers are supported. In the future there may be additional
@ -72,42 +74,6 @@ impl Watcher {
} }
} }
/// A path to watch.
///
/// This is currently only a wrapper around a [`PathBuf`], but may be augmented in the future.
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct WatchedPath(PathBuf);
impl From<PathBuf> for WatchedPath {
fn from(path: PathBuf) -> Self {
Self(path)
}
}
impl From<&str> for WatchedPath {
fn from(path: &str) -> Self {
Self(path.into())
}
}
impl From<&Path> for WatchedPath {
fn from(path: &Path) -> Self {
Self(path.into())
}
}
impl From<WatchedPath> for PathBuf {
fn from(path: WatchedPath) -> Self {
path.0
}
}
impl AsRef<Path> for WatchedPath {
fn as_ref(&self) -> &Path {
self.0.as_ref()
}
}
/// Launch the filesystem event worker. /// Launch the filesystem event worker.
/// ///
/// While you can run several, you should only have one. /// While you can run several, you should only have one.
@ -190,6 +156,7 @@ pub async fn worker(
// now let's calculate which paths we should add to the watch, and which we should drop: // now let's calculate which paths we should add to the watch, and which we should drop:
let config_pathset = config.pathset.get(); let config_pathset = config.pathset.get();
tracing::info!(?config_pathset, "obtaining pathset");
let (to_watch, to_drop) = if pathset.is_empty() { let (to_watch, to_drop) = if pathset.is_empty() {
// if the current pathset is empty, we can take a shortcut // if the current pathset is empty, we can take a shortcut
(config_pathset, Vec::new()) (config_pathset, Vec::new())
@ -222,7 +189,7 @@ pub async fn worker(
for path in to_drop { for path in to_drop {
trace!(?path, "removing path from the watcher"); trace!(?path, "removing path from the watcher");
if let Err(err) = watcher.unwatch(path.as_ref()) { if let Err(err) = watcher.unwatch(path.path.as_ref()) {
error!(?err, "notify unwatch() error"); error!(?err, "notify unwatch() error");
for e in notify_multi_path_errors(watcher_type, path, err, true) { for e in notify_multi_path_errors(watcher_type, path, err, true) {
errors.send(e).await?; errors.send(e).await?;
@ -234,13 +201,18 @@ pub async fn worker(
for path in to_watch { for path in to_watch {
trace!(?path, "adding path to the watcher"); trace!(?path, "adding path to the watcher");
if let Err(err) = watcher.watch(path.as_ref(), notify::RecursiveMode::Recursive) { if let Err(err) = watcher.watch(
path.path.as_ref(),
if path.recursive {
notify::RecursiveMode::Recursive
} else {
notify::RecursiveMode::NonRecursive
},
) {
error!(?err, "notify watch() error"); error!(?err, "notify watch() error");
for e in notify_multi_path_errors(watcher_type, path, err, false) { for e in notify_multi_path_errors(watcher_type, path, err, false) {
errors.send(e).await?; errors.send(e).await?;
} }
// TODO: unwatch and re-watch manually while ignoring all the erroring paths
// See https://github.com/watchexec/watchexec/issues/218
} else { } else {
pathset.insert(path); pathset.insert(path);
} }
@ -250,13 +222,13 @@ pub async fn worker(
fn notify_multi_path_errors( fn notify_multi_path_errors(
kind: Watcher, kind: Watcher,
path: WatchedPath, watched_path: WatchedPath,
mut err: notify::Error, mut err: notify::Error,
rm: bool, rm: bool,
) -> Vec<RuntimeError> { ) -> Vec<RuntimeError> {
let mut paths = take(&mut err.paths); let mut paths = take(&mut err.paths);
if paths.is_empty() { if paths.is_empty() {
paths.push(path.into()); paths.push(watched_path.into());
} }
let generic = err.to_string(); let generic = err.to_string();

View File

@ -0,0 +1,82 @@
use std::path::{Path, PathBuf};
/// A path to watch.
///
/// Can be a recursive or non-recursive watch.
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct WatchedPath {
pub(crate) path: PathBuf,
pub(crate) recursive: bool,
}
impl From<PathBuf> for WatchedPath {
fn from(path: PathBuf) -> Self {
Self {
path,
recursive: true,
}
}
}
impl From<&str> for WatchedPath {
fn from(path: &str) -> Self {
Self {
path: path.into(),
recursive: true,
}
}
}
impl From<String> for WatchedPath {
fn from(path: String) -> Self {
Self {
path: path.into(),
recursive: true,
}
}
}
impl From<&Path> for WatchedPath {
fn from(path: &Path) -> Self {
Self {
path: path.into(),
recursive: true,
}
}
}
impl From<WatchedPath> for PathBuf {
fn from(path: WatchedPath) -> Self {
path.path
}
}
impl From<&WatchedPath> for PathBuf {
fn from(path: &WatchedPath) -> Self {
path.path.clone()
}
}
impl AsRef<Path> for WatchedPath {
fn as_ref(&self) -> &Path {
self.path.as_ref()
}
}
impl WatchedPath {
/// Create a new watched path, recursively descending into subdirectories.
pub fn recursive(path: impl Into<PathBuf>) -> Self {
Self {
path: path.into(),
recursive: true,
}
}
/// Create a new watched path, not descending into subdirectories.
pub fn non_recursive(path: impl Into<PathBuf>) -> Self {
Self {
path: path.into(),
recursive: false,
}
}
}

View File

@ -4,7 +4,7 @@
.SH NAME .SH NAME
watchexec \- Execute commands when watched files change watchexec \- Execute commands when watched files change
.SH SYNOPSIS .SH SYNOPSIS
\fBwatchexec\fR [\fB\-w\fR|\fB\-\-watch\fR] [\fB\-c\fR|\fB\-\-clear\fR] [\fB\-o\fR|\fB\-\-on\-busy\-update\fR] [\fB\-r\fR|\fB\-\-restart\fR] [\fB\-s\fR|\fB\-\-signal\fR] [\fB\-\-stop\-signal\fR] [\fB\-\-stop\-timeout\fR] [\fB\-\-map\-signal\fR] [\fB\-d\fR|\fB\-\-debounce\fR] [\fB\-\-stdin\-quit\fR] [\fB\-\-no\-vcs\-ignore\fR] [\fB\-\-no\-project\-ignore\fR] [\fB\-\-no\-global\-ignore\fR] [\fB\-\-no\-default\-ignore\fR] [\fB\-\-no\-discover\-ignore\fR] [\fB\-\-ignore\-nothing\fR] [\fB\-p\fR|\fB\-\-postpone\fR] [\fB\-\-delay\-run\fR] [\fB\-\-poll\fR] [\fB\-\-shell\fR] [\fB\-n \fR] [\fB\-\-emit\-events\-to\fR] [\fB\-\-only\-emit\-events\fR] [\fB\-E\fR|\fB\-\-env\fR] [\fB\-\-no\-process\-group\fR] [\fB\-\-wrap\-process\fR] [\fB\-N\fR|\fB\-\-notify\fR] [\fB\-\-color\fR] [\fB\-\-timings\fR] [\fB\-q\fR|\fB\-\-quiet\fR] [\fB\-\-bell\fR] [\fB\-\-project\-origin\fR] [\fB\-\-workdir\fR] [\fB\-e\fR|\fB\-\-exts\fR] [\fB\-f\fR|\fB\-\-filter\fR] [\fB\-\-filter\-file\fR] [\fB\-j\fR|\fB\-\-filter\-prog\fR] [\fB\-i\fR|\fB\-\-ignore\fR] [\fB\-\-ignore\-file\fR] [\fB\-\-fs\-events\fR] [\fB\-\-no\-meta\fR] [\fB\-\-print\-events\fR] [\fB\-v\fR|\fB\-\-verbose\fR]... [\fB\-\-log\-file\fR] [\fB\-\-manual\fR] [\fB\-\-completions\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fICOMMAND\fR] \fBwatchexec\fR [\fB\-w\fR|\fB\-\-watch\fR] [\fB\-W\fR|\fB\-\-watch\-non\-recursive\fR] [\fB\-c\fR|\fB\-\-clear\fR] [\fB\-o\fR|\fB\-\-on\-busy\-update\fR] [\fB\-r\fR|\fB\-\-restart\fR] [\fB\-s\fR|\fB\-\-signal\fR] [\fB\-\-stop\-signal\fR] [\fB\-\-stop\-timeout\fR] [\fB\-\-map\-signal\fR] [\fB\-d\fR|\fB\-\-debounce\fR] [\fB\-\-stdin\-quit\fR] [\fB\-\-no\-vcs\-ignore\fR] [\fB\-\-no\-project\-ignore\fR] [\fB\-\-no\-global\-ignore\fR] [\fB\-\-no\-default\-ignore\fR] [\fB\-\-no\-discover\-ignore\fR] [\fB\-\-ignore\-nothing\fR] [\fB\-p\fR|\fB\-\-postpone\fR] [\fB\-\-delay\-run\fR] [\fB\-\-poll\fR] [\fB\-\-shell\fR] [\fB\-n \fR] [\fB\-\-emit\-events\-to\fR] [\fB\-\-only\-emit\-events\fR] [\fB\-E\fR|\fB\-\-env\fR] [\fB\-\-no\-process\-group\fR] [\fB\-\-wrap\-process\fR] [\fB\-N\fR|\fB\-\-notify\fR] [\fB\-\-color\fR] [\fB\-\-timings\fR] [\fB\-q\fR|\fB\-\-quiet\fR] [\fB\-\-bell\fR] [\fB\-\-project\-origin\fR] [\fB\-\-workdir\fR] [\fB\-e\fR|\fB\-\-exts\fR] [\fB\-f\fR|\fB\-\-filter\fR] [\fB\-\-filter\-file\fR] [\fB\-j\fR|\fB\-\-filter\-prog\fR] [\fB\-i\fR|\fB\-\-ignore\fR] [\fB\-\-ignore\-file\fR] [\fB\-\-fs\-events\fR] [\fB\-\-no\-meta\fR] [\fB\-\-print\-events\fR] [\fB\-\-manual\fR] [\fB\-\-completions\fR] [\fB\-v\fR|\fB\-\-verbose\fR]... [\fB\-\-log\-file\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fICOMMAND\fR]
.SH DESCRIPTION .SH DESCRIPTION
Execute commands when watched files change. Execute commands when watched files change.
.PP .PP
@ -48,6 +48,13 @@ This option can be specified multiple times to watch multiple files or directori
The special value \*(Aq/dev/null\*(Aq, provided as the only path watched, will cause Watchexec to not watch any paths. Other event sources (like signals or key events) may still be used. The special value \*(Aq/dev/null\*(Aq, provided as the only path watched, will cause Watchexec to not watch any paths. Other event sources (like signals or key events) may still be used.
.TP .TP
\fB\-W\fR, \fB\-\-watch\-non\-recursive\fR=\fIPATH\fR
Watch a specific directory, non\-recursively
Unlike \*(Aq\-w\*(Aq, folders watched with this option are not recursed into.
This option can be specified multiple times to watch multiple directories non\-recursively.
.TP
\fB\-c\fR, \fB\-\-clear\fR=\fIMODE\fR \fB\-c\fR, \fB\-\-clear\fR=\fIMODE\fR
Clear screen before running command Clear screen before running command
@ -439,7 +446,7 @@ This can also be used via the $WATCHEXEC_FILTER_FILES environment variable.
/!\\ This option is EXPERIMENTAL and may change and/or vanish without notice. /!\\ This option is EXPERIMENTAL and may change and/or vanish without notice.
Provide your own custom filter programs in jaq (similar to jq) syntax. Programs are given an event in the same format as described in \*(Aq\-\-emit\-events\-to\*(Aq and must return a boolean. Provide your own custom filter programs in jaq (similar to jq) syntax. Programs are given an event in the same format as described in \*(Aq\-\-emit\-events\-to\*(Aq and must return a boolean. Invalid programs will make watchexec fail to start; use \*(Aq\-v\*(Aq to see program runtime errors.
In addition to the jaq stdlib, watchexec adds some custom filter definitions: In addition to the jaq stdlib, watchexec adds some custom filter definitions:
@ -510,7 +517,19 @@ Print events that trigger actions
This prints the events that triggered the action when handling it (after debouncing), in a human readable form. This is useful for debugging filters. This prints the events that triggered the action when handling it (after debouncing), in a human readable form. This is useful for debugging filters.
Use \*(Aq\-v\*(Aq when you need more diagnostic information. Use \*(Aq\-vvv\*(Aq instead when you need more diagnostic information.
.TP
\fB\-\-manual\fR
Show the manual page
This shows the manual page for Watchexec, if the output is a terminal and the \*(Aqman\*(Aq program is available. If not, the manual page is printed to stdout in ROFF format (suitable for writing to a watchexec.1 file).
.TP
\fB\-\-completions\fR=\fICOMPLETIONS\fR
Generate a shell completions script
Provides a completions script or configuration for the given shell. If Watchexec is not distributed with pre\-generated completions, you can use this to generate them yourself.
Supported shells: bash, elvish, fish, nu, powershell, zsh.
.TP .TP
\fB\-v\fR, \fB\-\-verbose\fR \fB\-v\fR, \fB\-\-verbose\fR
Set diagnostic log level Set diagnostic log level
@ -532,18 +551,6 @@ If a path is not provided, the default is the working directory. Note that with
If the path provided is a directory, a file will be created in that directory. The file name will be the current date and time, in the format \*(Aqwatchexec.YYYY\-MM\-DDTHH\-MM\-SSZ.log\*(Aq. If the path provided is a directory, a file will be created in that directory. The file name will be the current date and time, in the format \*(Aqwatchexec.YYYY\-MM\-DDTHH\-MM\-SSZ.log\*(Aq.
.TP .TP
\fB\-\-manual\fR
Show the manual page
This shows the manual page for Watchexec, if the output is a terminal and the \*(Aqman\*(Aq program is available. If not, the manual page is printed to stdout in ROFF format (suitable for writing to a watchexec.1 file).
.TP
\fB\-\-completions\fR=\fICOMPLETIONS\fR
Generate a shell completions script
Provides a completions script or configuration for the given shell. If Watchexec is not distributed with pre\-generated completions, you can use this to generate them yourself.
Supported shells: bash, elvish, fish, nu, powershell, zsh.
.TP
\fB\-h\fR, \fB\-\-help\fR \fB\-h\fR, \fB\-\-help\fR
Print help (see a summary with \*(Aq\-h\*(Aq) Print help (see a summary with \*(Aq\-h\*(Aq)
.TP .TP

View File

@ -4,7 +4,8 @@ watchexec - Execute commands when watched files change
# SYNOPSIS # SYNOPSIS
**watchexec** \[**-w**\|**\--watch**\] \[**-c**\|**\--clear**\] **watchexec** \[**-w**\|**\--watch**\]
\[**-W**\|**\--watch-non-recursive**\] \[**-c**\|**\--clear**\]
\[**-o**\|**\--on-busy-update**\] \[**-r**\|**\--restart**\] \[**-o**\|**\--on-busy-update**\] \[**-r**\|**\--restart**\]
\[**-s**\|**\--signal**\] \[**\--stop-signal**\] \[**\--stop-timeout**\] \[**-s**\|**\--signal**\] \[**\--stop-signal**\] \[**\--stop-timeout**\]
\[**\--map-signal**\] \[**-d**\|**\--debounce**\] \[**\--stdin-quit**\] \[**\--map-signal**\] \[**-d**\|**\--debounce**\] \[**\--stdin-quit**\]
@ -20,10 +21,10 @@ watchexec - Execute commands when watched files change
\[**\--workdir**\] \[**-e**\|**\--exts**\] \[**-f**\|**\--filter**\] \[**\--workdir**\] \[**-e**\|**\--exts**\] \[**-f**\|**\--filter**\]
\[**\--filter-file**\] \[**-j**\|**\--filter-prog**\] \[**\--filter-file**\] \[**-j**\|**\--filter-prog**\]
\[**-i**\|**\--ignore**\] \[**\--ignore-file**\] \[**\--fs-events**\] \[**-i**\|**\--ignore**\] \[**\--ignore-file**\] \[**\--fs-events**\]
\[**\--no-meta**\] \[**\--print-events**\] \[**\--no-meta**\] \[**\--print-events**\] \[**\--manual**\]
\[**-v**\|**\--verbose**\]\... \[**\--log-file**\] \[**\--manual**\] \[**\--completions**\] \[**-v**\|**\--verbose**\]\...
\[**\--completions**\] \[**-h**\|**\--help**\] \[**\--log-file**\] \[**-h**\|**\--help**\] \[**-V**\|**\--version**\]
\[**-V**\|**\--version**\] \[*COMMAND*\] \[*COMMAND*\]
# DESCRIPTION # DESCRIPTION
@ -82,6 +83,15 @@ The special value /dev/null, provided as the only path watched, will
cause Watchexec to not watch any paths. Other event sources (like cause Watchexec to not watch any paths. Other event sources (like
signals or key events) may still be used. signals or key events) may still be used.
**-W**, **\--watch-non-recursive**=*PATH*
: Watch a specific directory, non-recursively
Unlike -w, folders watched with this option are not recursed into.
This option can be specified multiple times to watch multiple
directories non-recursively.
**-c**, **\--clear**=*MODE* **-c**, **\--clear**=*MODE*
: Clear screen before running command : Clear screen before running command
@ -630,7 +640,8 @@ notice.
Provide your own custom filter programs in jaq (similar to jq) syntax. Provide your own custom filter programs in jaq (similar to jq) syntax.
Programs are given an event in the same format as described in Programs are given an event in the same format as described in
\--emit-events-to and must return a boolean. \--emit-events-to and must return a boolean. Invalid programs will make
watchexec fail to start; use -v to see program runtime errors.
In addition to the jaq stdlib, watchexec adds some custom filter In addition to the jaq stdlib, watchexec adds some custom filter
definitions: definitions:
@ -746,7 +757,25 @@ This prints the events that triggered the action when handling it (after
debouncing), in a human readable form. This is useful for debugging debouncing), in a human readable form. This is useful for debugging
filters. filters.
Use -v when you need more diagnostic information. Use -vvv instead when you need more diagnostic information.
**\--manual**
: Show the manual page
This shows the manual page for Watchexec, if the output is a terminal
and the man program is available. If not, the manual page is printed to
stdout in ROFF format (suitable for writing to a watchexec.1 file).
**\--completions**=*COMPLETIONS*
: Generate a shell completions script
Provides a completions script or configuration for the given shell. If
Watchexec is not distributed with pre-generated completions, you can use
this to generate them yourself.
Supported shells: bash, elvish, fish, nu, powershell, zsh.
**-v**, **\--verbose** **-v**, **\--verbose**
@ -782,24 +811,6 @@ If the path provided is a directory, a file will be created in that
directory. The file name will be the current date and time, in the directory. The file name will be the current date and time, in the
format watchexec.YYYY-MM-DDTHH-MM-SSZ.log. format watchexec.YYYY-MM-DDTHH-MM-SSZ.log.
**\--manual**
: Show the manual page
This shows the manual page for Watchexec, if the output is a terminal
and the man program is available. If not, the manual page is printed to
stdout in ROFF format (suitable for writing to a watchexec.1 file).
**\--completions**=*COMPLETIONS*
: Generate a shell completions script
Provides a completions script or configuration for the given shell. If
Watchexec is not distributed with pre-generated completions, you can use
this to generate them yourself.
Supported shells: bash, elvish, fish, nu, powershell, zsh.
**-h**, **\--help** **-h**, **\--help**
: Print help (see a summary with -h) : Print help (see a summary with -h)