From 58b37940b80f15bb324688fb4d23365a8326bd1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Tue, 24 Aug 2021 22:45:31 +1200 Subject: [PATCH] Implement most existing options and mark the rest --- cli/src/args.rs | 245 +++++++++++++++++++++++----------------------- cli/src/config.rs | 126 ++++++++++++++++++++++-- cli/src/main.rs | 14 ++- 3 files changed, 254 insertions(+), 131 deletions(-) diff --git a/cli/src/args.rs b/cli/src/args.rs index 902e49b..5912f78 100644 --- a/cli/src/args.rs +++ b/cli/src/args.rs @@ -11,128 +11,129 @@ use color_eyre::eyre::{Context, Report, Result}; pub fn get_args() -> Result> { let app = App::new("watchexec") - .version(crate_version!()) - .about("Execute commands when watched files change") - .after_help("Use @argfile as first argument to load arguments from the file `argfile` (one argument per line) which will be inserted in place of the @argfile (further arguments on the CLI will override or add onto those in the file).") - .arg(Arg::with_name("command") - .help("Command to execute") - .multiple(true) - .required(true)) - .arg(Arg::with_name("extensions") - .help("Comma-separated list of file extensions to watch (e.g. js,css,html)") - .short("e") - .long("exts") - .takes_value(true)) - .arg(Arg::with_name("path") - .help("Watch a specific file or directory") - .short("w") - .long("watch") - .number_of_values(1) - .multiple(true) - .takes_value(true)) - .arg(Arg::with_name("clear") - .help("Clear screen before executing command") - .short("c") - .long("clear")) - .arg(Arg::with_name("on-busy-update") - .help("Select the behaviour to use when receiving events while the command is running. Current default is queue, will change to do-nothing in 2.0.") - .takes_value(true) - .possible_values(&["do-nothing", "queue", "restart", "signal"]) - .long("on-busy-update")) - .arg(Arg::with_name("restart") - .help("Restart the process if it's still running. Shorthand for --on-busy-update=restart") - .short("r") - .long("restart")) - .arg(Arg::with_name("signal") - .help("Send signal to process upon changes, e.g. SIGHUP") - .short("s") - .long("signal") - .takes_value(true) - .number_of_values(1) - .value_name("signal")) - .arg(Arg::with_name("kill") - .hidden(true) - .short("k") - .long("kill")) - .arg(Arg::with_name("debounce") - .help("Set the timeout between detected change and command execution, defaults to 100ms") - .takes_value(true) - .value_name("milliseconds") - .short("d") - .long("debounce")) - - .arg(Arg::with_name("verbose") - .help("Print debugging messages to stderr") - .short("v") - .long("verbose")) - .arg(Arg::with_name("changes") - .help("Only print path change information. Overridden by --verbose") - .long("changes-only")) - - .arg(Arg::with_name("filter") - .help("Ignore all modifications except those matching the pattern") - .short("f") - .long("filter") - .number_of_values(1) - .multiple(true) - .takes_value(true) - .value_name("pattern")) - .arg(Arg::with_name("ignore") - .help("Ignore modifications to paths matching the pattern") - .short("i") - .long("ignore") - .number_of_values(1) - .multiple(true) - .takes_value(true) - .value_name("pattern")) - .arg(Arg::with_name("no-vcs-ignore") - .help("Skip auto-loading of .gitignore files for filtering") - .long("no-vcs-ignore")) - .arg(Arg::with_name("no-ignore") - .help("Skip auto-loading of ignore files (.gitignore, .ignore, etc.) for filtering") - .long("no-ignore")) - .arg(Arg::with_name("no-default-ignore") - .help("Skip auto-ignoring of commonly ignored globs") - .long("no-default-ignore")) - .arg(Arg::with_name("postpone") - .help("Wait until first change to execute command") - .short("p") - .long("postpone")) - .arg(Arg::with_name("poll") - .help("Force polling mode (interval in milliseconds)") - .long("force-poll") - .value_name("interval")) - .arg(Arg::with_name("shell") - .help(if cfg!(windows) { - "Use a different shell, or `none`. Try --shell=powershell, which will become the default in 2.0." - } else { - "Use a different shell, or `none`. E.g. --shell=bash" - }) - .takes_value(true) - .long("shell")) - // -n short form will not be removed, and instead become a shorthand for --shell=none - .arg(Arg::with_name("no-shell") - .help("Do not wrap command in a shell. Deprecated: use --shell=none instead.") - .short("n") - .long("no-shell")) - .arg(Arg::with_name("no-meta") - .help("Ignore metadata changes") - .long("no-meta")) - .arg(Arg::with_name("no-environment") - .help("Do not set WATCHEXEC_*_PATH environment variables for the command") - .long("no-environment")) - .arg(Arg::with_name("no-process-group") - .help("Do not use a process group when running the command") - .long("no-process-group")) - .arg(Arg::with_name("once").short("1").hidden(true)) - .arg(Arg::with_name("watch-when-idle") - .help("Deprecated alias for --on-busy-update=do-nothing, which will become the default in 2.0.") - .short("W") - .long("watch-when-idle")) - .arg(Arg::with_name("notif") - .help("Send a desktop notification when watchexec notices a change (experimental, behaviour may change)") - .short("N") - .long("notify")); + .version(crate_version!()) + .about("Execute commands when watched files change") + .after_help("Use @argfile as first argument to load arguments from the file `argfile` (one argument per line) which will be inserted in place of the @argfile (further arguments on the CLI will override or add onto those in the file).") + .arg(Arg::with_name("command") + .help("Command to execute") + .multiple(true) + .required(true)) + .arg(Arg::with_name("extensions") // TODO + .help("Comma-separated list of file extensions to watch (e.g. js,css,html)") + .short("e") + .long("exts") + .takes_value(true)) + .arg(Arg::with_name("paths") + .help("Watch a specific file or directory") + .short("w") + .long("watch") + .number_of_values(1) + .multiple(true) + .takes_value(true)) + .arg(Arg::with_name("clear") + .help("Clear screen before executing command") + .short("c") + .long("clear")) + .arg(Arg::with_name("on-busy-update") + .help("Select the behaviour to use when receiving events while the command is running. Current default is queue, will change to do-nothing in 2.0.") + .takes_value(true) + .possible_values(&["do-nothing", "queue", "restart", "signal"]) + .long("on-busy-update")) + .arg(Arg::with_name("restart") + .help("Restart the process if it's still running. Shorthand for --on-busy-update=restart") + .short("r") + .long("restart")) + .arg(Arg::with_name("signal") + .help("Specify the signal to send when using --on-busy-update=signal") + .short("s") + .long("signal") + .takes_value(true) + .value_name("signal") + .default_value("SIGTERM") + .hidden(cfg!(windows))) + .arg(Arg::with_name("kill") + .hidden(true) + .short("k") + .long("kill")) + .arg(Arg::with_name("debounce") + .help("Set the timeout between detected change and command execution, defaults to 100ms") + .takes_value(true) + .value_name("milliseconds") + .short("d") + .long("debounce")) + .arg(Arg::with_name("verbose") + .help("Print debugging messages (-v, -vv, -vvv; use -vvv for bug reports)") + .multiple(true) + .short("v") + .long("verbose")) + .arg(Arg::with_name("print-events") + .help("Print events that trigger actions") + .long("print-events") + .alias("changes-only")) // --changes-only is deprecated (remove at v2) + .arg(Arg::with_name("filter") // TODO + .help("Ignore all modifications except those matching the pattern") + .short("f") + .long("filter") + .number_of_values(1) + .multiple(true) + .takes_value(true) + .value_name("pattern")) + .arg(Arg::with_name("ignore") // TODO + .help("Ignore modifications to paths matching the pattern") + .short("i") + .long("ignore") + .number_of_values(1) + .multiple(true) + .takes_value(true) + .value_name("pattern")) + .arg(Arg::with_name("no-vcs-ignore") // TODO + .help("Skip auto-loading of .gitignore files for filtering") + .long("no-vcs-ignore")) + .arg(Arg::with_name("no-ignore") // TODO + .help("Skip auto-loading of ignore files (.gitignore, .ignore, etc.) for filtering") + .long("no-ignore")) + .arg(Arg::with_name("no-default-ignore") // TODO + .help("Skip auto-ignoring of commonly ignored globs") + .long("no-default-ignore")) + .arg(Arg::with_name("postpone") + .help("Wait until first change to execute command") + .short("p") + .long("postpone")) + .arg(Arg::with_name("poll") + .help("Force polling mode (interval in milliseconds)") + .long("force-poll") + .value_name("interval")) + .arg(Arg::with_name("shell") + .help(if cfg!(windows) { + "Use a different shell, or `none`. Try --shell=powershell, which will become the default in 2.0." + } else { + "Use a different shell, or `none`. E.g. --shell=bash" + }) + .takes_value(true) + .long("shell")) + // -n short form will not be removed, and instead become a shorthand for --shell=none + .arg(Arg::with_name("no-shell") + .help("Do not wrap command in a shell. Deprecated: use --shell=none instead.") + .short("n") + .long("no-shell")) + .arg(Arg::with_name("no-meta") // TODO + .help("Ignore metadata changes") + .long("no-meta")) + .arg(Arg::with_name("no-environment") // TODO + .help("Do not set WATCHEXEC_*_PATH environment variables for the command") + .long("no-environment")) + .arg(Arg::with_name("no-process-group") // TODO + .help("Do not use a process group when running the command") + .long("no-process-group")) + .arg(Arg::with_name("once").short("1").hidden(true)) + .arg(Arg::with_name("watch-when-idle") + .help("Deprecated alias for --on-busy-update=do-nothing, which will become the default in 2.0.") + .short("W") + .long("watch-when-idle")) + .arg(Arg::with_name("notif") // TODO + .help("Send a desktop notification when watchexec notices a change (experimental, behaviour may change)") + .short("N") + .long("notify")); let mut raw_args: Vec = env::args_os().collect(); diff --git a/cli/src/config.rs b/cli/src/config.rs index 275260a..4c60bc3 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -1,25 +1,139 @@ +use std::{ + convert::Infallible, env::current_dir, io::stderr, path::Path, str::FromStr, time::Duration, +}; + use clap::ArgMatches; use color_eyre::eyre::{eyre, Result}; use watchexec::{ + action::{Action, Outcome, Signal}, command::Shell, config::{InitConfig, RuntimeConfig}, + event::Event, + fs::Watcher, + handler::PrintDisplay, }; pub fn new(args: &ArgMatches<'static>) -> Result<(InitConfig, RuntimeConfig)> { - Ok((init(&args)?, runtime(&args)?)) + Ok((init(args)?, runtime(args)?)) } -fn init(args: &ArgMatches<'static>) -> Result { - let mut config = InitConfig::builder(); +fn init(_args: &ArgMatches<'static>) -> Result { + let mut config = InitConfig::builder(); - Ok(config.build()?) + config.on_error(PrintDisplay(stderr())); + + Ok(config.build()?) } fn runtime(args: &ArgMatches<'static>) -> Result { - let mut config = RuntimeConfig::default(); + let mut config = RuntimeConfig::default(); + config.command( + args.values_of_lossy("command") + .ok_or_else(|| eyre!("(clap) Bug: command is not present"))? + .iter(), + ); - Ok(config) + config.pathset(match args.values_of_os("paths") { + Some(paths) => paths.map(|os| Path::new(os).to_owned()).collect(), + None => vec![current_dir()?], + }); + + config.action_throttle(Duration::from_millis( + args.value_of("debounce").unwrap_or("100").parse()?, + )); + + if let Some(interval) = args.value_of("poll") { + config.file_watcher(Watcher::Poll(Duration::from_millis(interval.parse()?))); + } + + config.command_shell(if args.is_present("no-shell") { + Shell::None + } else if let Some(s) = args.value_of("shell") { + if s.eq_ignore_ascii_case("powershell") { + Shell::Powershell + } else if s.eq_ignore_ascii_case("none") { + Shell::None + } else if s.eq_ignore_ascii_case("cmd") { + cmd_shell(s.into()) + } else { + Shell::Unix(s.into()) + } + } else { + default_shell() + }); + + let clear = args.is_present("clear"); + let mut on_busy = args + .value_of("on-busy-update") + .unwrap_or("queue") + .to_owned(); + + if args.is_present("restart") { + on_busy = "restart".into(); + } + + if args.is_present("watch-when-idle") { + on_busy = "do-nothing".into(); + } + + let mut signal = args + .value_of("signal") + .map(|s| Signal::from_str(s)) + .transpose()? + .unwrap_or(Signal::SIGTERM); + + if args.is_present("kill") { + signal = Signal::SIGKILL; + } + + let print_events = args.is_present("print-events"); + let once = args.is_present("once"); + + config.on_action(move |action: Action| { + let fut = async { Ok::<(), Infallible>(()) }; + + if print_events { + for event in &action.events { + for path in event.paths() { + eprintln!("[EVENT] Path: {}", path.display()); + } + + for signal in event.signals() { + eprintln!("[EVENT] Signal: {:?}", signal); + } + } + } + + if once && !action.events.iter().any(|e| e == &Event::default()) { + action.outcome(Outcome::Exit); + return fut; + } + + let when_running = match (clear, on_busy.as_str()) { + (_, "do-nothing") => Outcome::DoNothing, + (true, "restart") => { + Outcome::both(Outcome::Stop, Outcome::both(Outcome::Clear, Outcome::Start)) + } + (false, "restart") => Outcome::both(Outcome::Stop, Outcome::Start), + (_, "signal") => Outcome::Signal(signal), + // (true, "queue") => Outcome::wait(Outcome::both(Outcome::Clear, Outcome::Start)), + // (false, "queue") => Outcome::wait(Outcome::Start), + _ => Outcome::DoNothing, + }; + + let when_idle = if clear { + Outcome::both(Outcome::Clear, Outcome::Start) + } else { + Outcome::Start + }; + + action.outcome(Outcome::if_running(when_running, when_idle)); + + fut + }); + + Ok(config) } // until 2.0 diff --git a/cli/src/main.rs b/cli/src/main.rs index 2e6ff4c..e54e65b 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -2,7 +2,7 @@ use std::env::var; use color_eyre::eyre::Result; use tracing_subscriber::filter::LevelFilter; -use watchexec::Watchexec; +use watchexec::{event::Event, Watchexec}; mod args; mod config; @@ -19,16 +19,24 @@ async fn main() -> Result<()> { if args.is_present("verbose") { tracing_subscriber::fmt() - .with_max_level(LevelFilter::DEBUG) + .with_max_level(match args.occurrences_of("verbose") { + 0 => unreachable!(), + 1 => LevelFilter::INFO, + 2 => LevelFilter::DEBUG, + _ => LevelFilter::TRACE, + }) .try_init() .ok(); } let (init, runtime) = config::new(&args)?; - let config = runtime.clone(); let wx = Watchexec::new(init, runtime)?; + if !args.is_present("postpone") { + wx.send_event(Event::default()).await?; + } + wx.main().await??; Ok(())