From baf46e74438fb4c68743c9384874146ae520eb1d Mon Sep 17 00:00:00 2001 From: Alex Chaffee Date: Wed, 24 Jan 2018 13:18:14 -0500 Subject: [PATCH] add support for .rerun config file --- Gemfile | 1 + README.md | 24 +++++++- bin/rerun | 12 +++- lib/rerun/options.rb | 115 ++++++++++++++++++------------------ lib/rerun/runner.rb | 9 ++- spec/options_spec.rb | 135 ++++++++++++++++++++++++++----------------- 6 files changed, 180 insertions(+), 116 deletions(-) diff --git a/Gemfile b/Gemfile index 761993f..d41c5d1 100644 --- a/Gemfile +++ b/Gemfile @@ -15,6 +15,7 @@ end group :test do gem 'rspec', ">=3.0" gem 'wrong', ">=0.6.2" + gem 'files' end gem 'wdm', '>= 0.1.0' if Gem.win_platform? diff --git a/README.md b/README.md index 63da4ba..aac99b3 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,8 @@ Procfile processes locally and restart them all when necessary. # Options: +These options can be specified on the command line and/or inside a `.rerun` config file (see below). + `--dir` directory (or directories) to watch (default = "."). Separate multiple paths with ',' and/or use multiple `-d` options. `--pattern` glob to match inside directory. This uses the Ruby Dir glob style -- see for details. @@ -167,6 +169,23 @@ This may be useful for forcing the respective process to terminate as quickly as Also `--version` and `--help`, naturally. +## Config file + +If the current directory contains a file named `.rerun`, it will be parsed with the same rules as command-line arguments. Newlines are the same as any other whitespace, so you can stack options vertically, like this: + +``` +--quiet +--pattern **/*.{rb,js,scss,sass,html,md} +``` + +Options specified on the command line will override those in the config file. You can negate boolean options with `--no-`, so for example, with the above config file, to re-enable logging, you could say: + +```sh +rerun --no-quiet rackup +``` + +If you're not sure what options are being overwritten, use `--verbose` and rerun will show you the final result of the parsing. + # Notifications If you have `growlnotify` available on the `PATH`, it sends notifications to @@ -250,11 +269,11 @@ rerun -p "**/*.rb" rake test # To Do: ## Must have for v1.0 -* ".rerun" file to specify options per project or in $HOME. * Make sure to pass through quoted options correctly to target process [bug] * Optionally do "bundle install" before and "bundle exec" during launch ## Nice to have +* ".rerun" file in $HOME * If the last element of the command is a `.ru` file and there's no other command then use `rackup` * Figure out an algorithm so "-x" is not needed (if possible) -- maybe by accepting a "--port" option or reading `config.ru` * Specify (or deduce) port to listen for to determine success of a web server launch @@ -368,6 +387,9 @@ Based upon and/or inspired by: # Version History +* + * `.rerun` config file + * v0.12.0 23 January 2018 * smarter `--signal` option, allowing you to specify a series of signals to try in order; also `--wait` to change how long between tries * `--force-polling` option (thanks ajduncan) diff --git a/bin/rerun b/bin/rerun index f936bd4..bd59059 100755 --- a/bin/rerun +++ b/bin/rerun @@ -7,6 +7,12 @@ $LOAD_PATH.unshift libdir unless $LOAD_PATH.include?(libdir) require 'rerun' require 'optparse' -options = Rerun::Options.parse -exit if options.nil? -runner = Rerun::Runner.keep_running(options[:cmd], options) +options = Rerun::Options.parse config_file: ".rerun" + +if options and options[:verbose] + puts "\nrerun options:\n\t#{options}" +end + +exit if options.nil? or options[:cmd].nil? or options[:cmd].empty? + +Rerun::Runner.keep_running(options[:cmd], options) diff --git a/lib/rerun/options.rb b/lib/rerun/options.rb index d284a52..13d5150 100644 --- a/lib/rerun/options.rb +++ b/lib/rerun/options.rb @@ -17,126 +17,129 @@ class Options DEFAULT_DIRS = ["."] DEFAULTS = { - :pattern => DEFAULT_PATTERN, - :signal => (windows? ? "TERM,KILL" : "TERM,INT,KILL"), - :wait => 2, - :notify => true, - :quiet => false, - :verbose => false, - :name => Pathname.getwd.basename.to_s.capitalize, - :ignore => [], - :dir => DEFAULT_DIRS, - :force_polling => false, + :pattern => DEFAULT_PATTERN, + :signal => (windows? ? "TERM,KILL" : "TERM,INT,KILL"), + :wait => 2, + :notify => true, + :quiet => false, + :verbose => false, + :background => false, + :name => Pathname.getwd.basename.to_s.capitalize, + :ignore => [], + :dir => DEFAULT_DIRS, + :force_polling => false, } - def self.parse args = ARGV + def self.parse args: ARGV, config_file: nil default_options = DEFAULTS.dup options = { - ignore: [] + ignore: [] } - opts = OptionParser.new("", 24, ' ') do |opts| - opts.banner = "Usage: rerun [options] [--] cmd" + if config_file && File.exist?(config_file) + require 'shellwords' + config_args = File.read(config_file).shellsplit + args = config_args + args + end + + option_parser = OptionParser.new("", 24, ' ') do |o| + o.banner = "Usage: rerun [options] [--] cmd" - opts.separator "" - opts.separator "Launches an app, and restarts it when the filesystem changes." - opts.separator "See http://github.com/alexch/rerun for more info." - opts.separator "Version: #{$spec.version}" - opts.separator "" - opts.separator "Options:" + o.separator "" + o.separator "Launches an app, and restarts it when the filesystem changes." + o.separator "See http://github.com/alexch/rerun for more info." + o.separator "Version: #{$spec.version}" + o.separator "" + o.separator "Options:" - opts.on("-d dir", "--dir dir", "directory to watch, default = \"#{DEFAULT_DIRS}\". Specify multiple paths with ',' or separate '-d dir' option pairs.") do |dir| + o.on("-d dir", "--dir dir", "directory to watch, default = \"#{DEFAULT_DIRS}\". Specify multiple paths with ',' or separate '-d dir' option pairs.") do |dir| elements = dir.split(",") options[:dir] = (options[:dir] || []) + elements end # todo: rename to "--watch" - opts.on("-p pattern", "--pattern pattern", "file glob to watch, default = \"#{DEFAULTS[:pattern]}\"") do |pattern| + o.on("-p pattern", "--pattern pattern", "file glob to watch, default = \"#{DEFAULTS[:pattern]}\"") do |pattern| options[:pattern] = pattern end - opts.on("-i pattern", "--ignore pattern", "file glob to ignore (can be set many times). To ignore a directory, you must append '/*' e.g. --ignore 'coverage/*'") do |pattern| + o.on("-i pattern", "--ignore pattern", "file glob to ignore (can be set many times). To ignore a directory, you must append '/*' e.g. --ignore 'coverage/*'") do |pattern| options[:ignore] += [pattern] end - opts.on("-s signal", "--signal signal", "terminate process using this signal. To try several signals in series, use a comma-delimited list. Default: \"#{DEFAULTS[:signal]}\"") do |signal| + o.on("-s signal", "--signal signal", "terminate process using this signal. To try several signals in series, use a comma-delimited list. Default: \"#{DEFAULTS[:signal]}\"") do |signal| options[:signal] = signal end - opts.on("-w sec", "--wait sec", "after asking the process to terminate, wait this long (in seconds) before either aborting, or trying the next signal in series. Default: #{DEFAULTS[:wait]} sec") + o.on("-w sec", "--wait sec", "after asking the process to terminate, wait this long (in seconds) before either aborting, or trying the next signal in series. Default: #{DEFAULTS[:wait]} sec") - opts.on("-r", "--restart", "expect process to restart itself, so just send a signal and continue watching. Uses the HUP signal unless overridden using --signal") do |signal| + o.on("-r", "--restart", "expect process to restart itself, so just send a signal and continue watching. Uses the HUP signal unless overridden using --signal") do |signal| options[:restart] = true default_options[:signal] = "HUP" end - opts.on("-x", "--exit", "expect the program to exit. With this option, rerun checks the return value; without it, rerun checks that the process is running.") do |dir| - options[:exit] = true + o.on("-x", "--exit", "expect the program to exit. With this option, rerun checks the return value; without it, rerun checks that the process is running.") do |value| + options[:exit] = value end - opts.on("-c", "--clear", "clear screen before each run") do - options[:clear] = true + o.on("-c", "--clear", "clear screen before each run") do |value| + options[:clear] = value end - opts.on("-b", "--background", "disable on-the-fly commands, allowing the process to be backgrounded") do - options[:background] = true + o.on("-b", "--background", "disable on-the-fly commands, allowing the process to be backgrounded") do |value| + options[:background] = value end - opts.on("-n name", "--name name", "name of app used in logs and notifications, default = \"#{DEFAULTS[:name]}\"") do |name| + o.on("-n name", "--name name", "name of app used in logs and notifications, default = \"#{DEFAULTS[:name]}\"") do |name| options[:name] = name end - opts.on("--force-polling", "use polling instead of a native filesystem scan (useful for Vagrant)") do - options[:force_polling] = true + o.on("--[no-]force-polling", "use polling instead of a native filesystem scan (useful for Vagrant)") do |value| + options[:force_polling] = value end - opts.on("--no-growl", "don't use growl [OBSOLETE]") do + o.on("--no-growl", "don't use growl [OBSOLETE]") do options[:growl] = false $stderr.puts "--no-growl is obsolete; use --no-notify instead" return end - opts.on("--[no-]notify [notifier]", "send messages through a desktop notification application. Supports growl (requires growlnotify), osx (requires terminal-notifier gem), and notify-send on GNU/Linux (notify-send must be installed)") do |notifier| + o.on("--[no-]notify [notifier]", "send messages through a desktop notification application. Supports growl (requires growlnotify), osx (requires terminal-notifier gem), and notify-send on GNU/Linux (notify-send must be installed)") do |notifier| notifier = true if notifier.nil? options[:notify] = notifier end - opts.on("-q", "--quiet", "don't output any logs") do - options[:quiet] = true + o.on("-q", "--[no-]quiet", "don't output any logs") do |value| + options[:quiet] = value end - opts.on("--verbose", "log extra stuff like PIDs (unless you also specified `--quiet`") do - options[:verbose] = true + o.on("--[no-]verbose", "log extra stuff like PIDs (unless you also specified `--quiet`") do |value| + options[:verbose] = value end - opts.on_tail("-h", "--help", "--usage", "show this message") do - puts opts + o.on_tail("-h", "--help", "--usage", "show this message and immediately exit") do + puts o return end - opts.on_tail("--version", "show version") do + o.on_tail("--version", "show version and immediately exit") do puts $spec.version return end - opts.on_tail "" - opts.on_tail "On top of --pattern and --ignore, we ignore any changes to files and dirs starting with a dot." + o.on_tail "" + o.on_tail "On top of --pattern and --ignore, we ignore any changes to files and dirs starting with a dot." end - if args.empty? - puts opts - nil - else - opts.parse! args - default_options[:cmd] = args.join(" ") + option_parser.parse! args + options = default_options.merge(options) + options[:cmd] = args.join(" ").strip # todo: better arg word handling - options = default_options.merge(options) + puts option_parser if args.empty? - options - end + options end - end + end diff --git a/lib/rerun/runner.rb b/lib/rerun/runner.rb index f357c00..cc00190 100644 --- a/lib/rerun/runner.rb +++ b/lib/rerun/runner.rb @@ -333,6 +333,10 @@ def say msg puts "#{Time.now.strftime("%T")} [rerun] #{msg}" unless quiet? end + def stty(args) + system "stty #{args}" + end + # non-blocking stdin reader. # returns a 1-char string if a key was pressed; otherwise nil # @@ -346,7 +350,7 @@ def key_pressed # looks like "raw" flips off the OPOST bit 0x00000001 /* enable following output processing */ # which disables #define ONLCR 0x00000002 /* map NL to CR-NL (ala CRMOD) */ # so this sets it back on again since all we care about is raw input, not raw output - system("stty raw opost") + stty "raw opost" c = nil if $stdin.ready? @@ -354,9 +358,10 @@ def key_pressed end c.chr if c ensure - system "stty -raw" # turn raw input off + stty "-raw" # turn raw input off end + # note: according to 'man tty' the proper way restore the settings is # tty_state=`stty -g` # ensure diff --git a/spec/options_spec.rb b/spec/options_spec.rb index 63d7a92..01fe6b0 100644 --- a/spec/options_spec.rb +++ b/spec/options_spec.rb @@ -6,99 +6,100 @@ module Rerun describe Options do it "has good defaults" do - defaults = Options.parse ["foo"] - assert { defaults[:cmd] = "foo" } + defaults = Options.parse args: ["foo"] + assert {defaults[:cmd] = "foo"} - assert { defaults[:dir] == ["."] } - assert { defaults[:pattern] == Options::DEFAULT_PATTERN } - assert { defaults[:signal].include?('KILL') } - assert { defaults[:wait] == 2 } - assert { defaults[:notify] == true } - assert { defaults[:quiet] == false } - assert { defaults[:verbose] == false } - assert { defaults[:name] == 'Rerun' } - assert { defaults[:force_polling] == false } + assert {defaults[:dir] == ["."]} + assert {defaults[:pattern] == Options::DEFAULT_PATTERN} + assert {defaults[:signal].include?('KILL')} + assert {defaults[:wait] == 2} + assert {defaults[:notify] == true} + assert {defaults[:quiet] == false} + assert {defaults[:verbose] == false} + assert {defaults[:name] == 'Rerun'} + assert {defaults[:force_polling] == false} - assert { defaults[:clear].nil? } - assert { defaults[:exit].nil? } - assert { defaults[:background].nil? } + assert {defaults[:clear].nil?} + assert {defaults[:exit].nil?} + + assert {defaults[:background] == false} end ["--help", "-h", "--usage", "--version"].each do |arg| describe "when passed #{arg}" do it "returns nil" do capturing do - Options.parse([arg]).should be_nil + Options.parse(args: [arg]).should be_nil end end end end it "accepts --quiet" do - options = Options.parse ["--quiet", "foo"] - assert { options[:quiet] == true } + options = Options.parse args: ["--quiet", "foo"] + assert {options[:quiet] == true} end it "accepts --verbose" do - options = Options.parse ["--verbose", "foo"] - assert { options[:verbose] == true } + options = Options.parse args: ["--verbose", "foo"] + assert {options[:verbose] == true} end it "splits directories" do - options = Options.parse ["--dir", "a,b", "foo"] - assert { options[:dir] == ["a", "b"] } + options = Options.parse args: ["--dir", "a,b", "foo"] + assert {options[:dir] == ["a", "b"]} end it "adds directories specified individually with --dir" do - options = Options.parse ["--dir", "a", "--dir", "b"] - assert { options[:dir] == ["a", "b"] } + options = Options.parse args: ["--dir", "a", "--dir", "b"] + assert {options[:dir] == ["a", "b"]} end it "adds directories specified individually with -d" do - options = Options.parse ["-d", "a", "-d", "b"] - assert { options[:dir] == ["a", "b"] } + options = Options.parse args: ["-d", "a", "-d", "b"] + assert {options[:dir] == ["a", "b"]} end it "adds directories specified individually using mixed -d and --dir" do - options = Options.parse ["-d", "a", "--dir", "b"] - assert { options[:dir] == ["a", "b"] } + options = Options.parse args: ["-d", "a", "--dir", "b"] + assert {options[:dir] == ["a", "b"]} end it "adds individual directories and splits comma-separated ones" do - options = Options.parse ["--dir", "a", "--dir", "b", "--dir", "foo,other"] - assert { options[:dir] == ["a", "b", "foo", "other"] } + options = Options.parse args: ["--dir", "a", "--dir", "b", "--dir", "foo,other"] + assert {options[:dir] == ["a", "b", "foo", "other"]} end it "accepts --name for a custom application name" do - options = Options.parse ["--name", "scheduler"] - assert { options[:name] == "scheduler" } + options = Options.parse args: ["--name", "scheduler"] + assert {options[:name] == "scheduler"} end it "accepts --force-polling to force listener polling" do - options = Options.parse ["--force-polling"] - assert { options[:force_polling] == true } + options = Options.parse args: ["--force-polling"] + assert {options[:force_polling] == true} end it "accepts --ignore" do - options = Options.parse ["--ignore", "log/*"] - assert { options[:ignore] == ["log/*"] } + options = Options.parse args: ["--ignore", "log/*"] + assert {options[:ignore] == ["log/*"]} end it "accepts --ignore multiple times" do - options = Options.parse ["--ignore", "log/*", "--ignore", "*.tmp"] - assert { options[:ignore] == ["log/*", "*.tmp"] } + options = Options.parse args: ["--ignore", "log/*", "--ignore", "*.tmp"] + assert {options[:ignore] == ["log/*", "*.tmp"]} end it "accepts --restart which allows the process to restart itself, defaulting to HUP" do - options = Options.parse ["--restart"] - assert { options[:restart] } - assert { options[:signal] == "HUP" } + options = Options.parse args: ["--restart"] + assert {options[:restart]} + assert {options[:signal] == "HUP"} end it "allows user to override HUP signal when --restart is specified" do - options = Options.parse %w[--restart --signal INT] - assert { options[:restart] } - assert { options[:signal] == "INT" } + options = Options.parse args: %w[--restart --signal INT] + assert {options[:restart]} + assert {options[:signal] == "INT"} end # notifications @@ -106,33 +107,59 @@ module Rerun it "rejects --no-growl" do options = nil err = capturing(:stderr) do - options = Options.parse %w[--no-growl echo foo] + options = Options.parse args: %w[--no-growl echo foo] end - assert { options == nil } - assert { err.include? "use --no-notify" } + assert {options.nil?} + assert {err.include? "use --no-notify"} end it "defaults to --notify true (meaning 'use what works')" do - options = Options.parse %w[echo foo] - assert { options[:notify] == true } + options = Options.parse args: %w[echo foo] + assert {options[:notify] == true} end it "accepts bare --notify" do - options = Options.parse %w[--notify -- echo foo] - assert { options[:notify] == true } + options = Options.parse args: %w[--notify -- echo foo] + assert {options[:notify] == true} end %w[growl osx].each do |notifier| it "accepts --notify #{notifier}" do - options = Options.parse ["--notify", notifier, "echo foo"] - assert { options[:notify] == notifier } + options = Options.parse args: ["--notify", notifier, "echo foo"] + assert {options[:notify] == notifier} end end it "accepts --no-notify" do - options = Options.parse %w[--no-notify echo foo] - assert { options[:notify] == false } + options = Options.parse args: %w[--no-notify echo foo] + assert {options[:notify] == false} + end + + describe 'reading from a config file' do + require 'files' + include ::Files + + let!(:config_file) { + file 'test-rerun-config', <<-TEXT +--quiet +--pattern **/*.foo + TEXT + } + + it 'uses the config file\'s values over the defaults' do + o = Options.parse(args: [], config_file: config_file) + assert { o[:quiet] } + assert { o[:pattern] == '**/*.foo' } + end + + it 'uses the command-line args over the config file\'s' do + o = Options.parse(args: %w{--no-quiet --pattern **/*.bar --verbose}, + config_file: config_file) + assert { o[:verbose] == true } + assert { not o[:quiet] } + assert { o[:pattern] == '**/*.bar' } + end end end