diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs index 581139b..4ef6546 100644 --- a/crates/cli/src/args.rs +++ b/crates/cli/src/args.rs @@ -1,6 +1,9 @@ use std::{ + collections::BTreeSet, ffi::{OsStr, OsString}, - path::PathBuf, + fs::canonicalize, + mem::take, + path::{Path, PathBuf}, str::FromStr, time::Duration, }; @@ -11,7 +14,7 @@ use clap::{ }; use miette::{IntoDiagnostic, Result}; use tokio::{fs::File, io::AsyncReadExt}; -use watchexec::paths::PATH_SEPARATOR; +use watchexec::{paths::PATH_SEPARATOR, sources::fs::WatchedPath}; use watchexec_signals::Signal; use crate::filterer::parse::parse_filter_program; @@ -128,7 +131,25 @@ pub struct Args { value_hint = ValueHint::AnyPath, value_name = "PATH", )] - pub paths: Vec, + pub recursive_paths: Vec, + + /// 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, + + #[doc(hidden)] + #[arg(skip)] + pub paths: Vec, /// Clear screen before running command /// @@ -1221,6 +1242,61 @@ pub async fn get_args() -> Result { .exit(); } + args.workdir = Some(if let Some(w) = take(&mut args.workdir) { + w + } else { + let curdir = std::env::current_dir().into_diagnostic()?; + canonicalize(curdir).into_diagnostic()? + }); + debug!(workdir=?args.workdir, "current directory"); + + let project_origin = if let Some(p) = take(&mut args.project_origin) { + p + } else { + crate::dirs::project_origin(&args).await? + }; + + 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::>>()? + .into_iter() + .collect(); + + if args.paths.len() == 1 + && args + .paths + .first() + .map_or(false, |p| p.as_ref() == Path::new("/dev/null")) + { + debug!("only path is /dev/null, not watching anything"); + args.paths = Vec::new(); + } else if args.paths.is_empty() { + debug!("no paths, using current directory"); + args.paths.push(args.workdir.clone().unwrap().into()); + } + + debug!(paths=?args.paths, "resolved all 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,7 +1309,7 @@ pub async fn get_args() -> Result { } } - 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) diff --git a/crates/cli/src/config.rs b/crates/cli/src/config.rs index 41a4620..55867a8 100644 --- a/crates/cli/src/config.rs +++ b/crates/cli/src/config.rs @@ -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 { 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); diff --git a/crates/cli/src/filterer/dirs.rs b/crates/cli/src/dirs.rs similarity index 73% rename from crates/cli/src/filterer/dirs.rs rename to crates/cli/src/dirs.rs index 82a8b78..d63e0f6 100644 --- a/crates/cli/src/filterer/dirs.rs +++ b/crates/cli/src/dirs.rs @@ -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 { 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"); @@ -82,10 +63,7 @@ pub async fn dirs(args: &Args) -> Result<(ProjectOriginPath, WorkDirPath)> { }; info!(?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 { @@ -98,37 +76,30 @@ pub async fn vcs_types(origin: &Path) -> Vec { vcs_types } -pub async fn ignores( - args: &Args, - vcs_types: &[ProjectType], - origin: &Path, -) -> Result> { - 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> { + 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::>(); debug!( diff --git a/crates/cli/src/filterer.rs b/crates/cli/src/filterer.rs index 6d9ec5c..4461560 100644 --- a/crates/cli/src/filterer.rs +++ b/crates/cli/src/filterer.rs @@ -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> { - 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(); diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index bd50cea..ac4f45c 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -18,6 +18,7 @@ use crate::filterer::WatchexecFilterer; pub mod args; mod config; +mod dirs; mod emits; mod filterer; mod state;