-
Notifications
You must be signed in to change notification settings - Fork 481
/
guard.rb
542 lines (483 loc) · 17.3 KB
/
guard.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
require 'rbconfig'
require 'thread'
require 'listen'
# Guard is the main module for all Guard related modules and classes.
# Also Guard plugins should use this namespace.
#
module Guard
require 'guard/dsl'
require 'guard/guardfile'
require 'guard/group'
require 'guard/interactor'
require 'guard/notifier'
require 'guard/runner'
require 'guard/ui'
require 'guard/watcher'
# The Guardfile template for `guard init`
GUARDFILE_TEMPLATE = File.expand_path('../guard/templates/Guardfile', __FILE__)
# The location of user defined templates
HOME_TEMPLATES = File.expand_path('~/.guard/templates')
WINDOWS = RbConfig::CONFIG['host_os'] =~ %r!(msdos|mswin|djgpp|mingw)!
DEV_NULL = WINDOWS ? 'NUL' : '/dev/null'
class << self
attr_accessor :options, :interactor, :runner, :listener, :lock, :scope, :running
# Initialize the Guard singleton:
#
# - Initialize the internal Guard state.
# - Create the interactor when necessary for user interaction.
# - Select and initialize the file change listener.
#
# @option options [Boolean] clear if auto clear the UI should be done
# @option options [Boolean] notify if system notifications should be shown
# @option options [Boolean] debug if debug output should be shown
# @option options [Array<String>] group the list of groups to start
# @option options [Array<String>] watchdir the directories to watch
# @option options [String] guardfile the path to the Guardfile
# @deprecated @option options [Boolean] watch_all_modifications watches all file modifications if true
# @deprecated @option options [Boolean] no_vendor ignore vendored dependencies
#
def setup(options = {})
@running = true
@lock = Mutex.new
@options = options.dup
@runner = ::Guard::Runner.new
@scope = { :plugins => [], :groups => [] }
@watchdirs = [Dir.pwd]
if options[:watchdir]
# Ensure we have an array
@watchdirs = Array(options[:watchdir]).map { |dir| File.expand_path dir }
end
::Guard::UI.clear(:force => true)
setup_debug
deprecated_options_warning
setup_groups
setup_guards
setup_listener
setup_signal_traps
setup_from_guardfile
setup_scopes
runner.deprecation_warning if options[:show_deprecations]
setup_notifier
setup_interactor
self
end
def setup_debug
if options[:debug]
Thread.abort_on_exception = true
::Guard::UI.options[:level] = :debug
debug_command_execution
end
end
# Initialize the groups array with the `:default` group.
#
# @see Guard.groups
#
def setup_groups
@groups = [Group.new(:default)]
end
# Initialize the guards array to an empty array.
#
# @see Guard.guards
#
def setup_guards
@guards = []
end
# Initializes the listener and registers a callback for changes.
#
def setup_listener
listener_callback = lambda do |modified, added, removed|
# Convert to relative paths (respective to the watchdir it came from)
@watchdirs.each do |watchdir|
[modified, added, removed].each do |paths|
paths.map! do |path|
if path.start_with? watchdir
path.sub "#{watchdir}#{File::SEPARATOR}", ''
else
path
end
end
end
end
::Guard::Dsl.reevaluate_guardfile if ::Guard::Watcher.match_guardfile?(modified)
::Guard.within_preserved_state do
runner.run_on_changes(modified, added, removed)
end
end
listener_options = {}
%w[latency force_polling].each do |option|
listener_options[option.to_sym] = options[option] if options.key?(option)
end
listen_args = @watchdirs + [listener_options]
@listener = Listen.to(*listen_args).change(&listener_callback)
end
# Sets up traps to catch signals used to control Guard.
#
# Currently two signals are caught:
# - `USR1` which pauses listening to changes.
# - `USR2` which resumes listening to changes.
# - 'INT' which is delegated to Pry if active, otherwise stops Guard.
#
def setup_signal_traps
unless defined?(JRUBY_VERSION)
if Signal.list.keys.include?('USR1')
Signal.trap('USR1') { ::Guard.pause unless listener.paused? }
end
if Signal.list.keys.include?('USR2')
Signal.trap('USR2') { ::Guard.pause if listener.paused? }
end
if Signal.list.keys.include?('INT')
Signal.trap('INT') do
if interactor
interactor.thread.raise(Interrupt)
else
::Guard.stop
end
end
end
end
end
def setup_from_guardfile
::Guard::Dsl.evaluate_guardfile(options)
::Guard::UI.error 'No guards found in Guardfile, please add at least one.' if @guards.empty?
end
def setup_scopes
scope[:groups] = options[:group].map { |g| ::Guard.groups(g) } if options[:group]
scope[:plugins] = options[:plugin].map { |p| ::Guard.guards(p) } if options[:plugin]
end
# Enables or disables the notifier based on user's configurations.
#
def setup_notifier
options[:notify] && ENV['GUARD_NOTIFY'] != 'false' ? ::Guard::Notifier.turn_on : ::Guard::Notifier.turn_off
end
# Initializes the interactor unless the user has specified not to.
#
def setup_interactor
unless options[:no_interactions] || !::Guard::Interactor.enabled
@interactor = ::Guard::Interactor.new
end
end
# Start Guard by evaluating the `Guardfile`, initializing declared Guard plugins
# and starting the available file change listener.
# Main method for Guard that is called from the CLI when Guard starts.
#
# - Setup Guard internals
# - Evaluate the `Guardfile`
# - Configure Notifiers
# - Initialize the declared Guard plugins
# - Start the available file change listener
#
# @option options [Boolean] clear if auto clear the UI should be done
# @option options [Boolean] notify if system notifications should be shown
# @option options [Boolean] debug if debug output should be shown
# @option options [Array<String>] group the list of groups to start
# @option options [String] watchdir the director to watch
# @option options [String] guardfile the path to the Guardfile
#
def start(options = {})
setup(options)
within_preserved_state do
::Guard::UI.debug 'Guard starts all plugins'
runner.run(:start)
::Guard::UI.info "Guard is now watching at '#{ @watchdirs.join "', '" }'"
listener.start
end
end
# Stop Guard listening to file changes
#
def stop
within_preserved_state(false) do
::Guard::UI.debug 'Guard stops all plugins'
runner.run(:stop)
::Guard::Notifier.turn_off
::Guard::UI.info 'Bye bye...', :reset => true
listener.stop
@running = false
end
end
# Reload Guardfile and all Guard plugins currently enabled.
# If no scope is given, then the Guardfile will be re-evaluated,
# which results in a stop/start, which makes the reload obsolete.
#
# @param [Hash] scopes hash with a Guard plugin or a group scope
#
def reload(scopes = {})
scopes = convert_scopes(scopes)
within_preserved_state do
::Guard::UI.clear(:force => true)
::Guard::UI.action_with_scopes('Reload', scopes)
if scopes.empty?
::Guard::Dsl.reevaluate_guardfile
else
runner.run(:reload, scopes)
end
end
end
# Trigger `run_all` on all Guard plugins currently enabled.
#
# @param [Hash] scopes hash with a Guard plugin or a group scope
#
def run_all(scopes = {})
scopes = convert_scopes(scopes)
within_preserved_state do
::Guard::UI.clear(:force => true)
::Guard::UI.action_with_scopes('Run', scopes)
runner.run(:run_all, scopes)
end
end
# Pause Guard listening to file changes.
#
def pause
if listener.paused?
::Guard::UI.info 'Un-paused files modification listening', :reset => true
listener.unpause
else
::Guard::UI.info 'Paused files modification listening', :reset => true
listener.pause
end
end
# Smart accessor for retrieving a specific Guard plugin or several Guard plugins at once.
#
# @see Guard.groups
#
# @example Filter Guard plugins by String or Symbol
# Guard.guards('rspec')
# Guard.guards(:rspec)
#
# @example Filter Guard plugins by Regexp
# Guard.guards(/rsp.+/)
#
# @example Filter Guard plugins by Hash
# Guard.guards({ :name => 'rspec', :group => 'backend' })
#
# @param [String, Symbol, Regexp, Hash] filter the filter to apply to the Guard plugins
# @return [Array<Guard>] the filtered Guard plugins
#
def guards(filter = nil)
@guards ||= []
case filter
when String, Symbol
@guards.find { |guard| guard.class.to_s.downcase.sub('guard::', '') == filter.to_s.downcase.gsub('-', '') }
when Regexp
@guards.find_all { |guard| guard.class.to_s.downcase.sub('guard::', '') =~ filter }
when Hash
filter.inject(@guards) do |matches, (k, v)|
if k.to_sym == :name
matches.find_all { |guard| guard.class.to_s.downcase.sub('guard::', '') == v.to_s.downcase.gsub('-', '') }
else
matches.find_all { |guard| guard.send(k).to_sym == v.to_sym }
end
end
else
@guards
end
end
# Smart accessor for retrieving a specific plugin group or several plugin groups at once.
#
# @see Guard.guards
#
# @example Filter groups by String or Symbol
# Guard.groups('backend')
# Guard.groups(:backend)
#
# @example Filter groups by Regexp
# Guard.groups(/(back|front)end/)
#
# @param [String, Symbol, Regexp] filter the filter to apply to the Groups
# @return [Array<Group>] the filtered groups
#
def groups(filter = nil)
case filter
when String, Symbol
@groups.find { |group| group.name == filter.to_sym }
when Regexp
@groups.find_all { |group| group.name.to_s =~ filter }
else
@groups
end
end
# Add a Guard plugin to use.
#
# @param [String] name the Guard name
# @param [Array<Watcher>] watchers the list of declared watchers
# @param [Array<Hash>] callbacks the list of callbacks
# @param [Hash] options the plugin options (see the given Guard documentation)
# @return [Guard::Guard] the added Guard plugin
#
def add_guard(name, watchers = [], callbacks = [], options = {})
if name.to_sym == :ego
::Guard::UI.deprecation('Guard::Ego is now part of Guard. You can remove it from your Guardfile.')
else
guard_class = get_guard_class(name)
callbacks.each { |callback| Hook.add_callback(callback[:listener], guard_class, callback[:events]) }
guard = guard_class.new(watchers, options)
@guards << guard
guard
end
end
# Add a Guard plugin group.
#
# @param [String] name the group name
# @option options [Boolean] halt_on_fail if a task execution
# should be halted for all Guard plugins in this group if one Guard throws `:task_has_failed`
# @return [Guard::Group] the group added (or retrieved from the `@groups` variable if already present)
#
def add_group(name, options = {})
group = groups(name)
if group.nil?
group = ::Guard::Group.new(name, options)
@groups << group
end
group
end
# Runs a block where the interactor is
# blocked and execution is synchronized
# to avoid state inconsistency.
#
# @param [Boolean] restart_interactor whether to restart the interactor or not
# @yield the block to run
#
def within_preserved_state(restart_interactor = true)
lock.synchronize do
begin
interactor.stop if interactor
@result = yield
rescue Interrupt
# Bring back Pry when the block is halted with Ctrl-C
end
interactor.start if interactor && restart_interactor
end
@result
end
# Tries to load the Guard plugin main class. This transforms the supplied Guard plugin
# name into a class name:
#
# * `guardname` will become `Guard::Guardname`
# * `dashed-guard-name` will become `Guard::DashedGuardName`
# * `underscore_guard_name` will become `Guard::UnderscoreGuardName`
#
# When no class is found with the strict case sensitive rules, another
# try is made to locate the class without matching case:
#
# * `rspec` will find a class `Guard::RSpec`
#
# @param [String] name the name of the Guard
# @param [Boolean] fail_gracefully whether error messages should not be printed
# @return [Class, nil] the loaded class
#
def get_guard_class(name, fail_gracefully=false)
name = name.to_s
try_require = false
const_name = name.gsub(/\/(.?)/) { "::#{ $1.upcase }" }.gsub(/(?:^|[_-])(.)/) { $1.upcase }
begin
require "guard/#{ name.downcase }" if try_require
self.const_get(self.constants.find { |c| c.to_s == const_name } || self.constants.find { |c| c.to_s.downcase == const_name.downcase })
rescue TypeError
if try_require
::Guard::UI.error "Could not find class Guard::#{ const_name.capitalize }"
else
try_require = true
retry
end
rescue LoadError => loadError
unless fail_gracefully
::Guard::UI.error "Could not load 'guard/#{ name.downcase }' or find class Guard::#{ const_name.capitalize }"
::Guard::UI.error loadError.to_s
end
end
end
# Locate a path to a Guard plugin gem.
#
# @param [String] name the name of the Guard plugin without the prefix `guard-`
# @return [String] the full path to the Guard gem
#
def locate_guard(name)
if Gem::Version.create(Gem::VERSION) >= Gem::Version.create('1.8.0')
Gem::Specification.find_by_name("guard-#{ name }").full_gem_path
else
Gem.source_index.find_name("guard-#{ name }").last.full_gem_path
end
rescue
::Guard::UI.error "Could not find 'guard-#{ name }' gem path."
end
# Returns a list of Guard plugin Gem names installed locally.
#
# @return [Array<String>] a list of Guard plugin gem names
#
def guard_gem_names
if Gem::Version.create(Gem::VERSION) >= Gem::Version.create('1.8.0')
Gem::Specification.find_all.select do |x|
if x.name =~ /^guard-/
true
elsif x.name != 'guard'
guard_plugin_path = File.join(x.full_gem_path, "lib/guard/#{ x.name }.rb")
File.exists?( guard_plugin_path )
end
end
else
Gem.source_index.find_name(/^guard-/)
end.map { |x| x.name.sub(/^guard-/, '') }
end
# Adds a command logger in debug mode. This wraps common command
# execution functions and logs the executed command before execution.
#
def debug_command_execution
Kernel.send(:alias_method, :original_system, :system)
Kernel.send(:define_method, :system) do |command, *args|
::Guard::UI.debug "Command execution: #{ command } #{ args.join(' ') }"
original_system command, *args
end
Kernel.send(:alias_method, :original_backtick, :'`')
Kernel.send(:define_method, :'`') do |command|
::Guard::UI.debug "Command execution: #{ command }"
original_backtick command
end
end
# Deprecation message for the `watch_all_modifications` start option
WATCH_ALL_MODIFICATIONS_DEPRECATION = <<-EOS.gsub(/^\s*/, '')
Starting with Guard v1.1 the 'watch_all_modifications' option is removed and is now always on.
EOS
# Deprecation message for the `no_vendor` start option
NO_VENDOR_DEPRECATION = <<-EOS.gsub(/^\s*/, '')
Starting with Guard v1.1 the 'no_vendor' option is removed because the monitoring
gems are now part of a new gem called Listen. (https://github.com/guard/listen)
You can specify a custom version of any monitoring gem directly in your Gemfile
if you want to overwrite Listen's default monitoring gems.
EOS
# Displays a warning for each deprecated options used.
#
def deprecated_options_warning
::Guard::UI.deprecation(WATCH_ALL_MODIFICATIONS_DEPRECATION) if options[:watch_all_modifications]
::Guard::UI.deprecation(NO_VENDOR_DEPRECATION) if options[:no_vendor]
end
# Convert the old scope format to the new scope format.
#
# @example Convert old scopes
# convert_scopes({ :guard => :rspec, :group => :backend })
# => { :plugins => [:rspec], :groups => [:backend] }
#
def convert_scopes(scopes)
if plugin = scopes.delete(:guard)
scopes[:plugins] = [plugin]
end
if group = scopes.delete(:group)
scopes[:groups] = [group]
end
scopes
end
# Determine if Guard needs to quit. This
# checks for Ctrl-D pressed.
#
# @return [Boolean] whether to quit or not
#
def quit?
STDIN.read_nonblock(1)
false
rescue Errno::EINTR
false
rescue Errno::EAGAIN
false
rescue EOFError
true
end
end
end