2021-04-10 15:33:40 +02:00
use std ::{
2021-07-16 16:10:03 +02:00
env ,
ffi ::OsString ,
fs ::File ,
io ::{ BufRead , BufReader } ,
path ::{ Path , PathBuf , MAIN_SEPARATOR } ,
2021-04-10 19:13:44 +02:00
time ::Duration ,
2021-04-10 15:33:40 +02:00
} ;
2021-07-16 15:33:04 +02:00
use clap ::{ crate_version , value_t , values_t , App , Arg } ;
2021-07-16 16:10:03 +02:00
use color_eyre ::eyre ::{ Context , Report , Result } ;
2021-07-16 15:33:04 +02:00
use log ::LevelFilter ;
2021-07-30 18:24:05 +02:00
use watchexec ::{ config ::ConfigBuilder , run ::OnBusyUpdate , Shell } ;
use crate ::handler ::CliHandler ;
2021-04-10 15:33:40 +02:00
2021-07-30 18:24:05 +02:00
pub fn get_args ( ) -> Result < CliHandler > {
2021-04-10 15:33:40 +02:00
let app = App ::new ( " watchexec " )
. version ( crate_version! ( ) )
. about ( " Execute commands when watched files change " )
2021-07-16 16:10:03 +02:00
. 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). " )
2021-04-10 15:33:40 +02:00
. arg ( Arg ::with_name ( " command " )
. help ( " Command to execute " )
. multiple ( true )
. required ( true ) )
. arg ( Arg ::with_name ( " extensions " )
2021-04-10 19:06:32 +02:00
. help ( " Comma-separated list of file extensions to watch (e.g. js,css,html) " )
2021-04-10 15:33:40 +02:00
. 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 " ) )
2021-04-10 18:16:50 +02:00
. arg ( Arg ::with_name ( " on-busy-update " )
2021-04-10 19:06:32 +02:00
. 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 )
2021-04-10 18:16:50 +02:00
. possible_values ( & [ " do-nothing " , " queue " , " restart " , " signal " ] )
. long ( " on-busy-update " ) )
2021-04-10 15:33:40 +02:00
. arg ( Arg ::with_name ( " restart " )
2021-04-10 18:16:50 +02:00
. help ( " Restart the process if it's still running. Shorthand for --on-busy-update=restart " )
2021-04-10 15:33:40 +02:00
. 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 " )
2021-07-21 13:44:24 +02:00
. help ( " Set the timeout between detected change and command execution, defaults to 100ms " )
2021-04-10 15:33:40 +02:00
. 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 " ) )
2021-04-10 16:36:10 +02:00
. 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 "
} )
2021-04-10 19:29:55 +02:00
. takes_value ( true )
2021-04-10 16:36:10 +02:00
. long ( " shell " ) )
// -n short form will not be removed, and instead become a shorthand for --shell=none
2021-04-10 15:33:40 +02:00
. arg ( Arg ::with_name ( " no-shell " )
2021-04-10 16:36:38 +02:00
. help ( " Do not wrap command in a shell. Deprecated: use --shell=none instead. " )
2021-04-10 15:33:40 +02:00
. short ( " n " )
. long ( " no-shell " ) )
. arg ( Arg ::with_name ( " no-meta " )
. help ( " Ignore metadata changes " )
. long ( " no-meta " ) )
. arg ( Arg ::with_name ( " no-environment " )
2021-04-10 17:21:38 +02:00
. help ( " Do not set WATCHEXEC_*_PATH environment variables for the command " )
2021-04-10 15:33:40 +02:00
. long ( " no-environment " ) )
2021-07-21 14:56:35 +02:00
. arg ( Arg ::with_name ( " no-process-group " )
. help ( " Do not use a process group when running the command " )
. long ( " no-process-group " ) )
2021-04-10 15:33:40 +02:00
. arg ( Arg ::with_name ( " once " ) . short ( " 1 " ) . hidden ( true ) )
. arg ( Arg ::with_name ( " watch-when-idle " )
2021-04-10 18:16:50 +02:00
. help ( " Deprecated alias for --on-busy-update=do-nothing, which will become the default in 2.0. " )
2021-04-10 15:33:40 +02:00
. short ( " W " )
. long ( " watch-when-idle " ) ) ;
2021-07-16 16:10:03 +02:00
let mut raw_args : Vec < OsString > = env ::args_os ( ) . collect ( ) ;
if let Some ( first ) = raw_args . get ( 1 ) . and_then ( | s | s . to_str ( ) ) {
if let Some ( arg_path ) = first . strip_prefix ( '@' ) . map ( Path ::new ) {
let arg_file = BufReader ::new (
File ::open ( arg_path )
. wrap_err_with ( | | format! ( " Failed to open argument file {:?} " , arg_path ) ) ? ,
) ;
let mut more_args : Vec < OsString > = arg_file
. lines ( )
. map ( | l | l . map ( OsString ::from ) . map_err ( Report ::from ) )
. collect ::< Result < _ > > ( ) ? ;
more_args . insert ( 0 , raw_args . remove ( 0 ) ) ;
more_args . extend ( raw_args . into_iter ( ) . skip ( 1 ) ) ;
raw_args = more_args ;
}
}
let args = app . get_matches_from ( raw_args ) ;
2021-04-10 15:33:40 +02:00
let mut builder = ConfigBuilder ::default ( ) ;
2021-07-16 16:10:03 +02:00
let cmd : Vec < String > = values_t! ( args . values_of ( " command " ) , String ) ? ;
2021-04-10 15:33:40 +02:00
builder . cmd ( cmd ) ;
let paths : Vec < PathBuf > = values_t! ( args . values_of ( " path " ) , String )
. unwrap_or_else ( | _ | vec! [ " . " . into ( ) ] )
. iter ( )
. map ( | string_path | string_path . into ( ) )
. collect ( ) ;
builder . paths ( paths ) ;
// Treat --kill as --signal SIGKILL (for compatibility with deprecated syntax)
if args . is_present ( " kill " ) {
builder . signal ( " SIGKILL " ) ;
}
if let Some ( signal ) = args . value_of ( " signal " ) {
builder . signal ( signal ) ;
}
let mut filters = values_t! ( args . values_of ( " filter " ) , String ) . unwrap_or_else ( | _ | Vec ::new ( ) ) ;
if let Some ( extensions ) = args . values_of ( " extensions " ) {
2021-04-10 18:32:58 +02:00
for exts in extensions {
// TODO: refactor with flatten()
2021-04-10 15:33:40 +02:00
filters . extend ( exts . split ( ',' ) . filter_map ( | ext | {
if ext . is_empty ( ) {
None
} else {
Some ( format! ( " *. {} " , ext . replace ( " . " , " " ) ) )
}
} ) ) ;
}
}
builder . filters ( filters ) ;
let mut ignores = vec! [ ] ;
let default_ignores = vec! [
format! ( " ** {} .DS_Store " , MAIN_SEPARATOR ) ,
String ::from ( " *.py[co] " ) ,
String ::from ( " #*# " ) ,
String ::from ( " .#* " ) ,
String ::from ( " .*.kate-swp " ) ,
String ::from ( " .*.sw? " ) ,
String ::from ( " .*.sw?x " ) ,
format! ( " ** {} .git {} ** " , MAIN_SEPARATOR , MAIN_SEPARATOR ) ,
format! ( " ** {} .hg {} ** " , MAIN_SEPARATOR , MAIN_SEPARATOR ) ,
format! ( " ** {} .svn {} ** " , MAIN_SEPARATOR , MAIN_SEPARATOR ) ,
] ;
if args . occurrences_of ( " no-default-ignore " ) = = 0 {
ignores . extend ( default_ignores )
} ;
ignores . extend ( values_t! ( args . values_of ( " ignore " ) , String ) . unwrap_or_else ( | _ | Vec ::new ( ) ) ) ;
builder . ignores ( ignores ) ;
if args . occurrences_of ( " poll " ) > 0 {
2021-04-10 19:33:30 +02:00
builder . poll_interval ( Duration ::from_millis (
value_t! ( args . value_of ( " poll " ) , u64 ) . unwrap_or_else ( | e | e . exit ( ) ) ,
) ) ;
2021-04-10 15:33:40 +02:00
}
if args . occurrences_of ( " debounce " ) > 0 {
2021-04-10 19:33:30 +02:00
builder . debounce ( Duration ::from_millis (
value_t! ( args . value_of ( " debounce " ) , u64 ) . unwrap_or_else ( | e | e . exit ( ) ) ,
) ) ;
2021-04-10 15:33:40 +02:00
}
2021-04-10 18:16:50 +02:00
builder . on_busy_update ( if args . is_present ( " restart " ) {
OnBusyUpdate ::Restart
} else if args . is_present ( " watch-when-idle " ) {
OnBusyUpdate ::DoNothing
} else if let Some ( s ) = args . value_of ( " on-busy-update " ) {
match s . as_bytes ( ) {
b " do-nothing " = > OnBusyUpdate ::DoNothing ,
b " queue " = > OnBusyUpdate ::Queue ,
b " restart " = > OnBusyUpdate ::Restart ,
b " signal " = > OnBusyUpdate ::Signal ,
2021-04-10 18:32:58 +02:00
_ = > unreachable! ( " clap restricts on-busy-updates values " ) ,
2021-04-10 18:16:50 +02:00
}
} else {
// will become DoNothing in v2.0
OnBusyUpdate ::Queue
} ) ;
builder . shell ( if args . is_present ( " no-shell " ) {
2021-04-10 16:36:10 +02:00
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 ( )
2021-04-10 18:16:50 +02:00
} ) ;
2021-04-10 15:33:40 +02:00
builder . clear_screen ( args . is_present ( " clear " ) ) ;
builder . run_initially ( ! args . is_present ( " postpone " ) ) ;
builder . no_meta ( args . is_present ( " no-meta " ) ) ;
builder . no_environment ( args . is_present ( " no-environment " ) ) ;
builder . no_vcs_ignore ( args . is_present ( " no-vcs-ignore " ) ) ;
builder . no_ignore ( args . is_present ( " no-ignore " ) ) ;
builder . poll ( args . occurrences_of ( " poll " ) > 0 ) ;
2021-07-21 14:56:35 +02:00
builder . use_process_group ( ! args . is_present ( " no-process-group " ) ) ;
2021-04-10 15:33:40 +02:00
2021-07-16 15:33:04 +02:00
let mut config = builder . build ( ) ? ;
2021-04-10 15:33:40 +02:00
if args . is_present ( " once " ) {
config . once = true ;
}
let loglevel = if args . is_present ( " verbose " ) {
LevelFilter ::Debug
} else if args . is_present ( " changes " ) {
LevelFilter ::Info
} else {
LevelFilter ::Warn
} ;
2021-07-30 18:24:05 +02:00
CliHandler ::new ( config , loglevel , false )
2021-04-10 15:33:40 +02:00
}
2021-04-10 16:36:10 +02:00
// until 2.0
#[ cfg(windows) ]
fn default_shell ( ) -> Shell {
Shell ::Cmd
}
#[ cfg(not(windows)) ]
fn default_shell ( ) -> Shell {
Shell ::default ( )
}
// because Shell::Cmd is only on windows
#[ cfg(windows) ]
fn cmd_shell ( _ : String ) -> Shell {
Shell ::Cmd
}
#[ cfg(not(windows)) ]
fn cmd_shell ( s : String ) -> Shell {
Shell ::Unix ( s )
}