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",
]
[[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]]
name = "tracing-attributes"
version = "0.1.27"
@ -4060,6 +4072,7 @@ dependencies = [
"termcolor",
"tokio",
"tracing",
"tracing-appender",
"tracing-subscriber",
"tracing-test",
"uuid",

View File

@ -19,7 +19,7 @@ _watchexec() {
case "${cmd}" in
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
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
@ -33,6 +33,14 @@ _watchexec() {
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--watch-non-recursive)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-W)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--clear)
COMPREPLY=($(compgen -W "clear reset" -- "${cur}"))
return 0
@ -189,14 +197,14 @@ _watchexec() {
COMPREPLY=($(compgen -W "access create remove rename modify metadata" -- "${cur}"))
return 0
;;
--log-file)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--completions)
COMPREPLY=($(compgen -W "bash elvish fish nu powershell zsh" -- "${cur}"))
return 0
;;
--log-file)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;

View File

@ -20,6 +20,8 @@ set edit:completion:arg-completer[watchexec] = {|@words|
&'watchexec'= {
cand -w '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 --clear 'Clear screen before running command'
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-file 'Files to load ignores from'
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 --log-file 'Write diagnostic logs to a file'
cand -r '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'
@ -78,9 +80,9 @@ set edit:completion:arg-completer[watchexec] = {|@words|
cand --bell 'Ring the terminal bell on command completion'
cand --no-meta 'Don''t emit fs events for metadata changes'
cand --print-events 'Print events that trigger actions'
cand --manual 'Show the manual page'
cand -v '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 --help 'Print help (see more with ''--help'')'
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-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 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
@ -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 -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 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 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 -l stdin-quit -d 'Exit when stdin closes'
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 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 -s v -l verbose -d 'Set diagnostic log level'
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 V -l version -d 'Print version'

View File

@ -32,6 +32,7 @@ module completions {
export extern watchexec [
...command: string # Command to run on changes
--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
--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
@ -75,10 +76,10 @@ module completions {
--fs-events: string@"nu-complete watchexec filter_fs_events" # Filesystem events to filter to
--no-meta # Don't emit fs events for metadata changes
--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
--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')
--version(-V) # Print version
]

View File

@ -23,6 +23,8 @@ Register-ArgumentCompleter -Native -CommandName 'watchexec' -ScriptBlock {
'watchexec' {
[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('-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('--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')
@ -55,8 +57,8 @@ Register-ArgumentCompleter -Native -CommandName 'watchexec' -ScriptBlock {
[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('--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('--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('--restart', 'restart', [CompletionResultType]::ParameterName, 'Restart the process if it''s still running')
[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('--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('--manual', 'manual', [CompletionResultType]::ParameterName, 'Show the manual page')
[CompletionResult]::new('-v', 'v', [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('--help', 'help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
[CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version')

View File

@ -17,6 +17,8 @@ _watchexec() {
_arguments "${_arguments_options[@]}" \
'*-w+[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]' \
'--clear=[Clear screen before running command]' \
'-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-file=[Files to load ignores from]:PATH:_files' \
'*--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)' \
'--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)--restart[Restart the process if it'\''s still running]' \
'--stdin-quit[Exit when stdin closes]' \
@ -75,9 +77,9 @@ _watchexec() {
'--bell[Ring the terminal bell on command completion]' \
'(--fs-events)--no-meta[Don'\''t emit fs events for metadata changes]' \
'--print-events[Print events that trigger actions]' \
'(--completions)--manual[Show the manual page]' \
'*-v[Set diagnostic log level]' \
'*--verbose[Set diagnostic log level]' \
'(--completions)--manual[Show the manual page]' \
'-h[Print help (see more with '\''--help'\'')]' \
'--help[Print help (see more with '\''--help'\'')]' \
'-V[Print version]' \

View File

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

View File

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

View File

@ -1,21 +1,28 @@
use std::{
collections::BTreeSet,
ffi::{OsStr, OsString},
path::PathBuf,
fs::canonicalize,
mem::take,
path::{Path, PathBuf},
str::FromStr,
time::Duration,
};
use clap::{
builder::TypedValueParser, error::ErrorKind, Arg, ArgAction, Command, CommandFactory, Parser,
ValueEnum, ValueHint,
builder::TypedValueParser, error::ErrorKind, Arg, Command, CommandFactory, Parser, ValueEnum,
ValueHint,
};
use miette::{IntoDiagnostic, Result};
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 crate::filterer::parse::parse_filter_program;
mod logging;
const OPTSET_FILTERING: &str = "Filtering";
const OPTSET_COMMAND: &str = "Command";
const OPTSET_DEBUGGING: &str = "Debugging";
@ -128,7 +135,25 @@ pub struct Args {
value_hint = ValueHint::AnyPath,
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
///
@ -791,6 +816,7 @@ pub struct Args {
///
/// 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.
/// 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:
///
@ -921,54 +947,13 @@ pub struct Args {
/// 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 '-v' when you need more diagnostic information.
/// Use '-vvv' instead when you need more diagnostic information.
#[arg(
long,
help_heading = OPTSET_DEBUGGING,
)]
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
///
/// 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"],
)]
pub completions: Option<ShellCompletion>,
#[command(flatten)]
pub logging: logging::LoggingArgs,
}
#[derive(Clone, Copy, Debug, Default, ValueEnum)]
@ -1159,11 +1147,10 @@ fn expand_args_up_to_doubledash() -> Result<Vec<OsString>, std::io::Error> {
}
#[inline]
pub async fn get_args() -> Result<Args> {
use tracing::{debug, trace, warn};
if std::env::var("RUST_LOG").is_ok() {
warn!("⚠ RUST_LOG environment variable set, logging options have no effect");
pub async fn get_args() -> Result<(Args, Option<WorkerGuard>)> {
let prearg_logs = logging::preargs();
if prearg_logs {
warn!("⚠ RUST_LOG environment variable set or hardcoded, logging options have no effect");
}
debug!("expanding @argfile arguments if any");
@ -1172,6 +1159,12 @@ pub async fn get_args() -> Result<Args> {
debug!("parsing arguments");
let mut args = Args::parse_from(args);
let log_guard = if !prearg_logs {
logging::postargs(&args.logging).await?
} else {
None
};
// https://no-color.org/
if args.color == ColourMode::Auto && std::env::var("NO_COLOR").is_ok() {
args.color = ColourMode::Never;
@ -1192,10 +1185,12 @@ pub async fn get_args() -> Result<Args> {
}
if args.no_environment {
warn!("--no-environment is deprecated");
args.emit_events_to = EmitEvents::None;
}
if args.no_process_group {
warn!("--no-process-group is deprecated");
args.wrap_process = WrapMode::None;
}
@ -1221,6 +1216,63 @@ pub async fn get_args() -> Result<Args> {
.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() {
if let Some(progpath) = prog.strip_prefix('@') {
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()
.enumerate()
.map(parse_filter_program)
.collect::<Result<_, _>>()?;
debug!(?args, "got arguments");
Ok(args)
debug_assert!(args.workdir.is_some());
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::{
borrow::Cow,
collections::HashMap,
env::{current_dir, var},
env::var,
ffi::{OsStr, OsString},
fs::File,
io::{IsTerminal, Write},
path::Path,
process::Stdio,
sync::{
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));
});
config.pathset(if args.paths.is_empty() {
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.pathset(args.paths.clone());
config.throttle(args.debounce.0);
config.keyboard_events(args.stdin_quit);

View File

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

View File

@ -16,7 +16,6 @@ use watchexec_filterer_globset::GlobsetFilterer;
use crate::args::{Args, FsEvent};
mod dirs;
pub(crate) mod parse;
mod proglib;
mod progs;
@ -71,13 +70,14 @@ impl Filterer for WatchexecFilterer {
impl WatchexecFilterer {
/// Create a new filterer from the given arguments
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 {
Vec::new()
} else {
let vcs_types = dirs::vcs_types(&project_origin).await;
dirs::ignores(args, &vcs_types, &project_origin).await?
let vcs_types = crate::dirs::vcs_types(&project_origin).await;
crate::dirs::ignores(args, &vcs_types).await?
};
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())
.collect::<Vec<_>>()
.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))

View File

@ -1,7 +1,7 @@
#![deny(rust_2018_idioms)]
#![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 clap::CommandFactory;
@ -9,8 +9,8 @@ use clap_complete::{Generator, Shell};
use clap_mangen::Man;
use is_terminal::IsTerminal;
use miette::{IntoDiagnostic, Result};
use tokio::{fs::metadata, io::AsyncWriteExt, process::Command};
use tracing::{debug, info, warn};
use tokio::{io::AsyncWriteExt, process::Command};
use tracing::{debug, info};
use watchexec::Watchexec;
use watchexec_events::{Event, Priority};
@ -18,86 +18,11 @@ use crate::filterer::WatchexecFilterer;
pub mod args;
mod config;
mod dirs;
mod emits;
mod filterer;
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<()> {
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<()> {
let args = init().await?;
debug!(?args, "arguments");
let (args, _log_guard) = args::get_args().await?;
if args.manual {
run_manpage(args).await

View File

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

View File

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

View File

@ -10,6 +10,7 @@
use std::{
ffi::OsString,
fmt,
path::{Path, PathBuf},
};
@ -21,7 +22,7 @@ use watchexec_events::{Event, FileType, Priority};
use watchexec_filterer_ignore::IgnoreFilterer;
/// A simple filterer in the style of the watchexec v1.17 filter.
#[derive(Debug)]
#[cfg_attr(feature = "full_debug", derive(Debug))]
pub struct GlobsetFilterer {
#[cfg_attr(not(unix), allow(dead_code))]
origin: PathBuf,
@ -31,6 +32,19 @@ pub struct GlobsetFilterer {
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 {
/// Create a new `GlobsetFilterer` from a project origin, allowed extensions, and lists of globs.
///

View File

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

View File

@ -40,3 +40,9 @@ path = "../project-origins"
[dev-dependencies]
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 futures::stream::{FuturesUnordered, StreamExt};
@ -11,12 +12,23 @@ use tracing::{trace, trace_span};
use crate::{simplify_path, Error, IgnoreFile};
#[derive(Clone, Debug)]
#[derive(Clone)]
#[cfg_attr(feature = "full_debug", derive(Debug))]
struct Ignore {
gitignore: Gitignore,
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.
///
/// 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)
- 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)
- 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`].
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 tracing::{debug, trace};
@ -195,9 +195,9 @@ impl Config {
pub fn pathset<I, P>(&self, pathset: I) -> &Self
where
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");
self.pathset.replace(pathset);
self.signal_change()

View File

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

View File

@ -4,7 +4,6 @@ use std::{
collections::{HashMap, HashSet},
fs::metadata,
mem::take,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
@ -20,6 +19,9 @@ use crate::{
Config,
};
// re-export for compatibility, until next major version
pub use crate::WatchedPath;
/// What kind of filesystem watcher to use.
///
/// 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.
///
/// 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:
let config_pathset = config.pathset.get();
tracing::info!(?config_pathset, "obtaining pathset");
let (to_watch, to_drop) = if pathset.is_empty() {
// if the current pathset is empty, we can take a shortcut
(config_pathset, Vec::new())
@ -222,7 +189,7 @@ pub async fn worker(
for path in to_drop {
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");
for e in notify_multi_path_errors(watcher_type, path, err, true) {
errors.send(e).await?;
@ -234,13 +201,18 @@ pub async fn worker(
for path in to_watch {
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");
for e in notify_multi_path_errors(watcher_type, path, err, false) {
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 {
pathset.insert(path);
}
@ -250,13 +222,13 @@ pub async fn worker(
fn notify_multi_path_errors(
kind: Watcher,
path: WatchedPath,
watched_path: WatchedPath,
mut err: notify::Error,
rm: bool,
) -> Vec<RuntimeError> {
let mut paths = take(&mut err.paths);
if paths.is_empty() {
paths.push(path.into());
paths.push(watched_path.into());
}
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
watchexec \- Execute commands when watched files change
.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
Execute commands when watched files change.
.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.
.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
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.
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:
@ -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.
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
\fB\-v\fR, \fB\-\-verbose\fR
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.
.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
Print help (see a summary with \*(Aq\-h\*(Aq)
.TP

View File

@ -4,7 +4,8 @@ watchexec - Execute commands when watched files change
# SYNOPSIS
**watchexec** \[**-w**\|**\--watch**\] \[**-c**\|**\--clear**\]
**watchexec** \[**-w**\|**\--watch**\]
\[**-W**\|**\--watch-non-recursive**\] \[**-c**\|**\--clear**\]
\[**-o**\|**\--on-busy-update**\] \[**-r**\|**\--restart**\]
\[**-s**\|**\--signal**\] \[**\--stop-signal**\] \[**\--stop-timeout**\]
\[**\--map-signal**\] \[**-d**\|**\--debounce**\] \[**\--stdin-quit**\]
@ -20,10 +21,10 @@ watchexec - Execute commands when watched files change
\[**\--workdir**\] \[**-e**\|**\--exts**\] \[**-f**\|**\--filter**\]
\[**\--filter-file**\] \[**-j**\|**\--filter-prog**\]
\[**-i**\|**\--ignore**\] \[**\--ignore-file**\] \[**\--fs-events**\]
\[**\--no-meta**\] \[**\--print-events**\]
\[**-v**\|**\--verbose**\]\... \[**\--log-file**\] \[**\--manual**\]
\[**\--completions**\] \[**-h**\|**\--help**\]
\[**-V**\|**\--version**\] \[*COMMAND*\]
\[**\--no-meta**\] \[**\--print-events**\] \[**\--manual**\]
\[**\--completions**\] \[**-v**\|**\--verbose**\]\...
\[**\--log-file**\] \[**-h**\|**\--help**\] \[**-V**\|**\--version**\]
\[*COMMAND*\]
# 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
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*
: Clear screen before running command
@ -630,7 +640,8 @@ 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
\--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
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
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**
@ -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
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**
: Print help (see a summary with -h)