filewatcher 0.5.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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'
|