filewatcher 0.5.4 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/banner.txt +17 -0
- data/bin/filewatcher +61 -124
- data/lib/filewatcher.rb +69 -122
- data/lib/filewatcher/cycles.rb +47 -0
- data/lib/filewatcher/env.rb +29 -0
- data/lib/filewatcher/runner.rb +33 -0
- data/lib/filewatcher/version.rb +7 -0
- data/test/dumpers/env_dumper.rb +10 -0
- data/test/dumpers/watched_dumper.rb +5 -0
- data/test/filewatcher/test_env.rb +70 -0
- data/test/filewatcher/test_runner.rb +75 -0
- data/test/filewatcher/test_version.rb +13 -0
- data/test/helper.rb +134 -0
- data/test/test_filewatcher.rb +266 -142
- metadata +30 -22
- data/LICENSE +0 -20
- data/README.md +0 -269
- data/Rakefile +0 -19
- data/test/fixtures/file1.txt +0 -1
- data/test/fixtures/file2.txt +0 -1
- data/test/fixtures/file3.rb +0 -1
- data/test/fixtures/file4.rb +0 -1
- data/test/fixtures/subdir/file5.rb +0 -1
- data/test/fixtures/subdir/file6.rb +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 025e651355a5bbbae353527112232253ee4f682d
|
4
|
+
data.tar.gz: 95ca7990a7e002768ac0a7a814a4129841efe3d6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b8576d1ff169c063a664ae9c538c51ad079ccfae4a604e0c277b84df261dc438b25f912863e5bd9b2ae542bac48026ea8f5a9a0990ccb81265206751b6138a1a
|
7
|
+
data.tar.gz: 25b04df22331575a7ffbfce5c1acc5168c0ee58e9d15fa4d4f7f3dff309fe1382be5920c315015309217bbf108882d994777cda67376c511c1197d6840952e7e
|
data/bin/banner.txt
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
Filewatcher scans the filesystem and executes shell commands when files changes.
|
2
|
+
|
3
|
+
Usage:
|
4
|
+
filewatcher [--restart] '<filenames or patterns>' '<shell command>'
|
5
|
+
Where
|
6
|
+
filename: filename(s) to scan.
|
7
|
+
shell command: shell command to execute when file changes on disk.
|
8
|
+
|
9
|
+
Examples:
|
10
|
+
filewatcher "myfile" "echo 'myfile has changed'"
|
11
|
+
filewatcher '*.rb' 'ruby $FILENAME'
|
12
|
+
filewatcher '**/*.rb' 'ruby $FILENAME' # Watch subdirectories
|
13
|
+
|
14
|
+
Other available environment variables are BASENAME, ABSOLUTE_FILENAME,
|
15
|
+
RELATIVE_FILENAME, EVENT and DIRNAME.
|
16
|
+
|
17
|
+
Options:
|
data/bin/filewatcher
CHANGED
@@ -1,88 +1,75 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative '../lib/filewatcher'
|
5
|
+
require_relative '../lib/filewatcher/env'
|
6
|
+
require_relative '../lib/filewatcher/runner'
|
7
|
+
require_relative '../lib/filewatcher/version'
|
4
8
|
require 'trollop'
|
5
|
-
require 'pathname'
|
6
9
|
require 'thread'
|
7
10
|
|
8
|
-
options = Trollop
|
9
|
-
version "filewatcher, version #{
|
10
|
-
banner
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
opt :daemon, "Run in the background as system daemon.", :short => 'D', :type => :boolean, :default => false
|
32
|
-
opt :restart, "Restart process when filesystem is updated", :short => 'r', :type => :boolean, :default => false
|
33
|
-
opt :list, "Print name of files being watched"
|
34
|
-
opt :exec, "Execute file as a script when file is updated.", :short => 'e', :type => :boolean, :default => false
|
35
|
-
opt :include, "Include files", :type => :string, :default => "*"
|
36
|
-
opt :exclude, "Exclude file(s) matching", :type => :string, :default => ""
|
37
|
-
opt :interval, "Interval to scan filesystem.", :short => 'i', :type => :float, :default => 0.5
|
38
|
-
opt :spinner, "Show an ascii spinner", :short => 's', :type => :boolean, :default => false
|
11
|
+
options = Trollop.options do
|
12
|
+
version "filewatcher, version #{Filewatcher::VERSION} by Thomas Flemming 2016"
|
13
|
+
banner File.read File.join(__dir__, 'banner.txt')
|
14
|
+
|
15
|
+
opt :immediate, 'Immediately execute a command',
|
16
|
+
short: 'I', type: :boolean, default: false
|
17
|
+
opt :every, 'Run command for every updated file in one filesystem check',
|
18
|
+
short: 'E', type: :boolean, default: false
|
19
|
+
opt :daemon, 'Run in the background as system daemon',
|
20
|
+
short: 'D', type: :boolean, default: false
|
21
|
+
opt :restart, 'Restart process when filesystem is updated',
|
22
|
+
short: 'r', type: :boolean, default: false
|
23
|
+
opt :list, 'Print name of files being watched'
|
24
|
+
opt :exec, 'Execute file as a script when file is updated',
|
25
|
+
short: 'e', type: :boolean, default: false
|
26
|
+
opt :include, 'Include files',
|
27
|
+
type: :string, default: File.join('**', '*')
|
28
|
+
opt :exclude, 'Exclude file(s) matching',
|
29
|
+
type: :string, default: nil
|
30
|
+
opt :interval, 'Interval to scan filesystem',
|
31
|
+
short: 'i', type: :float, default: 0.5
|
32
|
+
opt :spinner, 'Show an ascii spinner',
|
33
|
+
short: 's', type: :boolean, default: false
|
39
34
|
end
|
40
35
|
|
41
|
-
Trollop
|
36
|
+
Trollop.die Trollop.educate if ARGV.empty?
|
42
37
|
|
43
|
-
files = []
|
44
|
-
ARGV[0...-1].each do |a|
|
45
|
-
files << a
|
46
|
-
end
|
38
|
+
files = ARGV[0..-2]
|
47
39
|
|
48
|
-
|
49
|
-
files << ARGV[0]
|
50
|
-
end
|
40
|
+
files << ARGV.first if files.empty?
|
51
41
|
|
52
42
|
def split_files_void_escaped_whitespace(files)
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
files = splitted_filenames.flatten.uniq
|
59
|
-
splitted_filenames = []
|
60
|
-
files.each do |name|
|
61
|
-
splitted_filenames << name.gsub('_ESCAPED_WHITESPACE_','\ ')
|
62
|
-
end
|
63
|
-
files = splitted_filenames
|
43
|
+
files
|
44
|
+
.map { |name| name.gsub(/\\\s/, '_ESCAPED_WHITESPACE_').split(/\s/) }
|
45
|
+
.flatten
|
46
|
+
.uniq
|
47
|
+
.map { |name| name.gsub('_ESCAPED_WHITESPACE_', '\ ') }
|
64
48
|
end
|
65
49
|
|
66
50
|
files = split_files_void_escaped_whitespace(files)
|
67
51
|
child_pid = nil
|
68
52
|
|
69
53
|
def restart(child_pid, env, cmd)
|
54
|
+
raise Errno::ESRCH unless child_pid
|
70
55
|
Process.kill(9, child_pid)
|
71
56
|
Process.wait(child_pid)
|
72
57
|
rescue Errno::ESRCH
|
73
|
-
# already killed
|
58
|
+
nil # already killed
|
74
59
|
ensure
|
75
|
-
|
60
|
+
Process.spawn(env, cmd)
|
76
61
|
end
|
77
62
|
|
78
|
-
if
|
79
|
-
options[:exclude] = split_files_void_escaped_whitespace(
|
63
|
+
if options[:exclude].to_s != ''
|
64
|
+
options[:exclude] = split_files_void_escaped_whitespace(
|
65
|
+
options[:exclude].split(' ')
|
66
|
+
)
|
80
67
|
end
|
81
68
|
|
82
69
|
begin
|
83
|
-
fw =
|
70
|
+
fw = Filewatcher.new(files, options)
|
84
71
|
|
85
|
-
if
|
72
|
+
if options[:list]
|
86
73
|
puts 'Watching:'
|
87
74
|
fw.last_found_filenames.each do |filename|
|
88
75
|
puts " #{filename}"
|
@@ -91,77 +78,27 @@ begin
|
|
91
78
|
|
92
79
|
Process.daemon(true, true) if options[:daemon]
|
93
80
|
|
94
|
-
fw.watch
|
95
|
-
cmd =
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
".js" => "node",
|
101
|
-
".rb" => "ruby",
|
102
|
-
".pl" => "perl",
|
103
|
-
".awk" => "awk",
|
104
|
-
".php" => "php",
|
105
|
-
".phtml" => "php",
|
106
|
-
".php4" => "php",
|
107
|
-
".php3" => "php",
|
108
|
-
".php5" => "php",
|
109
|
-
".phps" => "php"
|
110
|
-
}
|
111
|
-
runner = runners[extension]
|
112
|
-
if(runner)
|
113
|
-
cmd = "env #{runner.to_s} #{filename}"
|
81
|
+
fw.watch do |filename, event|
|
82
|
+
cmd =
|
83
|
+
if options[:exec] && File.exist?(filename)
|
84
|
+
Filewatcher::Runner.new(filename).command
|
85
|
+
elsif ARGV.length > 1
|
86
|
+
ARGV[-1]
|
114
87
|
end
|
115
|
-
elsif(ARGV.length > 1)
|
116
|
-
cmd = ARGV[-1]
|
117
|
-
end
|
118
88
|
|
119
|
-
|
120
|
-
path = Pathname.new(filename)
|
121
|
-
env = {
|
122
|
-
'FILENAME' => filename,
|
123
|
-
'BASENAME' => path.basename.to_s,
|
124
|
-
'FILEDIR' => File.join(Pathname.new('.').realpath.to_s, path.parent.to_s), # Deprecated
|
125
|
-
'FSEVENT' => event.to_s, # Deprecated,
|
126
|
-
'EVENT' => event.to_s,
|
127
|
-
'DIRNAME' => File.join(Pathname.new('.').realpath.to_s, path.parent.to_s),
|
128
|
-
'ABSOLUTE_FILENAME' => File.join(Pathname.new('.').realpath.to_s, path.to_s),
|
129
|
-
'RELATIVE_FILENAME' => File.join(Pathname.new('.').to_s, path.to_s)
|
130
|
-
}
|
131
|
-
|
132
|
-
if(event != :delete)
|
133
|
-
ENV['FILEPATH'] = path.realpath.to_s
|
134
|
-
end
|
135
|
-
|
136
|
-
if(options[:restart])
|
137
|
-
if child_pid.nil?
|
138
|
-
child_pid = Process.spawn(env, cmd)
|
139
|
-
else
|
140
|
-
child_pid = restart(child_pid, env, cmd)
|
141
|
-
end
|
142
|
-
else
|
143
|
-
begin
|
144
|
-
pid = Process.spawn(env, cmd)
|
145
|
-
Process.wait()
|
146
|
-
rescue SystemExit, Interrupt
|
147
|
-
exit(0)
|
148
|
-
end
|
149
|
-
end
|
89
|
+
next puts "file #{event}: #{filename}" unless cmd
|
150
90
|
|
91
|
+
env = Filewatcher::Env.new(filename, event).to_h
|
92
|
+
if options[:restart]
|
93
|
+
child_pid = restart(child_pid, env, cmd)
|
151
94
|
else
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
when :new
|
158
|
-
print "new file"
|
159
|
-
else
|
160
|
-
print event.to_s
|
95
|
+
begin
|
96
|
+
Process.spawn(env, cmd)
|
97
|
+
Process.wait
|
98
|
+
rescue SystemExit, Interrupt
|
99
|
+
exit(0)
|
161
100
|
end
|
162
|
-
puts ": " + filename
|
163
101
|
end
|
164
|
-
|
165
102
|
end
|
166
103
|
rescue SystemExit, Interrupt
|
167
104
|
fw.finalize
|
data/lib/filewatcher.rb
CHANGED
@@ -1,66 +1,40 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'filewatcher/cycles'
|
4
|
+
|
2
5
|
# Simple file watcher. Detect changes in files and directories.
|
3
6
|
#
|
4
7
|
# Issues: Currently doesn't monitor changes in directorynames
|
5
|
-
class
|
8
|
+
class Filewatcher
|
9
|
+
include Filewatcher::Cycles
|
6
10
|
|
7
|
-
|
8
|
-
|
9
|
-
def self.VERSION
|
10
|
-
return '0.5.4'
|
11
|
-
end
|
11
|
+
attr_writer :interval
|
12
12
|
|
13
13
|
def update_spinner(label)
|
14
|
-
return
|
15
|
-
@spinner ||= %w
|
14
|
+
return unless @show_spinner
|
15
|
+
@spinner ||= %w[\\ | / -]
|
16
16
|
print "#{' ' * 30}\r#{label} #{@spinner.rotate!.first}\r"
|
17
17
|
end
|
18
18
|
|
19
|
-
def initialize(unexpanded_filenames,
|
20
|
-
if(args.first)
|
21
|
-
options = args.first
|
22
|
-
else
|
23
|
-
options = {}
|
24
|
-
end
|
19
|
+
def initialize(unexpanded_filenames, options = {})
|
25
20
|
@unexpanded_filenames = unexpanded_filenames
|
26
21
|
@unexpanded_excluded_filenames = options[:exclude]
|
27
|
-
@filenames = nil
|
28
|
-
@stored_update = nil
|
29
22
|
@keep_watching = false
|
30
23
|
@pausing = false
|
31
|
-
@
|
32
|
-
@end_snapshot = nil
|
33
|
-
@dontwait = options[:dontwait]
|
24
|
+
@immediate = options[:immediate]
|
34
25
|
@show_spinner = options[:spinner]
|
35
|
-
@interval = options
|
26
|
+
@interval = options.fetch(:interval, 0.5)
|
27
|
+
@every = options[:every]
|
36
28
|
end
|
37
29
|
|
38
|
-
def watch(
|
39
|
-
trap(
|
40
|
-
@
|
41
|
-
if(@interval and @interval > 0)
|
42
|
-
@sleep = @interval
|
43
|
-
end
|
44
|
-
@stored_update = on_update
|
30
|
+
def watch(&on_update)
|
31
|
+
trap('SIGINT') { return }
|
32
|
+
@on_update = on_update
|
45
33
|
@keep_watching = true
|
46
|
-
if
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
@end_snapshot = mtime_snapshot if @pausing
|
51
|
-
while @keep_watching && @pausing
|
52
|
-
update_spinner('Pausing')
|
53
|
-
Kernel.sleep @sleep
|
54
|
-
end
|
55
|
-
while @keep_watching && !filesystem_updated? && !@pausing
|
56
|
-
update_spinner('Watching')
|
57
|
-
Kernel.sleep @sleep
|
58
|
-
end
|
59
|
-
# test and null @updated_file to prevent yielding the last
|
60
|
-
# file twice if @keep_watching has just been set to false
|
61
|
-
yield @updated_file, @event if @updated_file
|
62
|
-
@updated_file = nil
|
63
|
-
end
|
34
|
+
yield('', '') if @immediate
|
35
|
+
|
36
|
+
main_cycle
|
37
|
+
|
64
38
|
@end_snapshot = mtime_snapshot
|
65
39
|
finalize(&on_update)
|
66
40
|
end
|
@@ -68,18 +42,18 @@ class FileWatcher
|
|
68
42
|
def pause
|
69
43
|
@pausing = true
|
70
44
|
update_spinner('Initiating pause')
|
71
|
-
|
72
|
-
|
45
|
+
# Ensure we wait long enough to enter pause loop in #watch
|
46
|
+
sleep @interval
|
73
47
|
end
|
74
48
|
|
75
49
|
def resume
|
76
50
|
if !@keep_watching || !@pausing
|
77
51
|
raise "Can't resume unless #watch and #pause were first called"
|
78
52
|
end
|
79
|
-
@last_snapshot = mtime_snapshot
|
53
|
+
@last_snapshot = mtime_snapshot # resume with fresh snapshot
|
80
54
|
@pausing = false
|
81
55
|
update_spinner('Resuming')
|
82
|
-
|
56
|
+
sleep @interval # Wait long enough to exit pause loop in #watch
|
83
57
|
end
|
84
58
|
|
85
59
|
# Ends the watch, allowing any remaining changes to be finalized.
|
@@ -87,103 +61,76 @@ class FileWatcher
|
|
87
61
|
def stop
|
88
62
|
@keep_watching = false
|
89
63
|
update_spinner('Stopping')
|
90
|
-
|
64
|
+
nil
|
91
65
|
end
|
92
66
|
|
93
67
|
# Calls the update block repeatedly until all changes in the
|
94
68
|
# current snapshot are dealt with
|
95
69
|
def finalize(&on_update)
|
96
|
-
on_update = @
|
97
|
-
|
98
|
-
while filesystem_updated?(snapshot)
|
70
|
+
on_update = @on_update unless block_given?
|
71
|
+
while filesystem_updated?(@end_snapshot || mtime_snapshot)
|
99
72
|
update_spinner('Finalizing')
|
100
|
-
on_update
|
73
|
+
trigger_changes(on_update)
|
101
74
|
end
|
102
|
-
@end_snapshot =nil
|
103
|
-
|
75
|
+
@end_snapshot = nil
|
76
|
+
end
|
77
|
+
|
78
|
+
def last_found_filenames
|
79
|
+
last_snapshot.keys
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def last_snapshot
|
85
|
+
@last_snapshot ||= mtime_snapshot
|
104
86
|
end
|
105
87
|
|
106
88
|
# Takes a snapshot of the current status of watched files.
|
107
89
|
# (Allows avoidance of potential race condition during #finalize)
|
108
90
|
def mtime_snapshot
|
109
91
|
snapshot = {}
|
110
|
-
|
111
|
-
|
112
|
-
if(@unexpanded_excluded_filenames != nil and @unexpanded_excluded_filenames.size > 0)
|
113
|
-
# Remove files in the exclude filenames list
|
114
|
-
@filtered_filenames = []
|
115
|
-
@excluded_filenames = expand_directories(@unexpanded_excluded_filenames)
|
116
|
-
@filenames.each do |filename|
|
117
|
-
if(not(@excluded_filenames.include?(filename)))
|
118
|
-
@filtered_filenames << filename
|
119
|
-
end
|
120
|
-
end
|
121
|
-
@filenames = @filtered_filenames
|
122
|
-
end
|
92
|
+
filenames = expand_directories(@unexpanded_filenames)
|
123
93
|
|
124
|
-
|
125
|
-
|
94
|
+
# Remove files in the exclude filenames list
|
95
|
+
filenames -= expand_directories(@unexpanded_excluded_filenames)
|
96
|
+
|
97
|
+
filenames.each do |filename|
|
98
|
+
mtime = File.exist?(filename) ? File.mtime(filename) : Time.new(0)
|
126
99
|
snapshot[filename] = mtime
|
127
100
|
end
|
128
|
-
|
101
|
+
snapshot
|
129
102
|
end
|
130
103
|
|
131
|
-
def filesystem_updated?(
|
132
|
-
|
133
|
-
forward_changes = snapshot.to_a - @last_snapshot.to_a
|
134
|
-
|
135
|
-
forward_changes.each do |file,mtime|
|
136
|
-
@updated_file = file
|
137
|
-
unless @last_snapshot.fetch(@updated_file,false)
|
138
|
-
@last_snapshot[file] = mtime
|
139
|
-
@event = :new
|
140
|
-
return true
|
141
|
-
else
|
142
|
-
@last_snapshot[file] = mtime
|
143
|
-
@event = :changed
|
144
|
-
return true
|
145
|
-
end
|
146
|
-
end
|
104
|
+
def filesystem_updated?(snapshot = mtime_snapshot)
|
105
|
+
@changes = {}
|
147
106
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
@updated_file = file
|
153
|
-
@last_snapshot.delete(file)
|
154
|
-
@event = :delete
|
155
|
-
return true
|
107
|
+
# rubocop:disable Perfomance/HashEachMethods
|
108
|
+
## https://github.com/bbatsov/rubocop/issues/4732
|
109
|
+
(snapshot.to_a - last_snapshot.to_a).each do |file, _mtime|
|
110
|
+
@changes[file] = last_snapshot[file] ? :updated : :created
|
156
111
|
end
|
157
|
-
return false
|
158
|
-
end
|
159
|
-
|
160
|
-
def last_found_filenames
|
161
|
-
@last_snapshot.keys
|
162
|
-
end
|
163
112
|
|
164
|
-
|
165
|
-
|
166
|
-
patterns = [patterns]
|
113
|
+
(last_snapshot.keys - snapshot.keys).each do |file|
|
114
|
+
@changes[file] = :deleted
|
167
115
|
end
|
168
|
-
patterns.map { |it| Dir[fulldepth(expand_path(it))] }.flatten.uniq
|
169
|
-
end
|
170
|
-
|
171
|
-
private
|
172
116
|
|
173
|
-
|
174
|
-
|
175
|
-
"#{pattern}/**/*"
|
176
|
-
else
|
177
|
-
pattern
|
178
|
-
end
|
117
|
+
@last_snapshot = snapshot
|
118
|
+
@changes.any?
|
179
119
|
end
|
180
120
|
|
181
|
-
def
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
121
|
+
def expand_directories(patterns)
|
122
|
+
patterns = Array(patterns) unless patterns.is_a? Array
|
123
|
+
expanded_patterns = patterns.map do |pattern|
|
124
|
+
pattern = File.expand_path(pattern)
|
125
|
+
Dir[
|
126
|
+
File.directory?(pattern) ? File.join(pattern, '**', '*') : pattern
|
127
|
+
]
|
186
128
|
end
|
129
|
+
expanded_patterns.flatten!
|
130
|
+
expanded_patterns.uniq!
|
131
|
+
expanded_patterns
|
187
132
|
end
|
188
|
-
|
189
133
|
end
|
134
|
+
|
135
|
+
# Require at end of file to not overwrite `Filewatcher` class
|
136
|
+
require_relative 'filewatcher/version'
|