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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1530083e28860bdfe9ffd4cc1c754b6026e25814
4
- data.tar.gz: d9fad06f13778bb85fbc6206e9975885cb83a874
3
+ metadata.gz: 025e651355a5bbbae353527112232253ee4f682d
4
+ data.tar.gz: 95ca7990a7e002768ac0a7a814a4129841efe3d6
5
5
  SHA512:
6
- metadata.gz: 3f3044ce20143591b8c257ab09eab7c0a14b1058784020abea5653fb114ef61fa4f98e6813f61a1d7c825a54a3787b9d20416cd1086a9179524068550fbfcefa
7
- data.tar.gz: c1409c73a378c5c0debc73725904a5f8260de6e828e5e93863f8f8018eeafbb907e3aa2d735fdc423818c80aec8a8207bd76857f088106d74162d9f877bb86eb
6
+ metadata.gz: b8576d1ff169c063a664ae9c538c51ad079ccfae4a604e0c277b84df261dc438b25f912863e5bd9b2ae542bac48026ea8f5a9a0990ccb81265206751b6138a1a
7
+ data.tar.gz: 25b04df22331575a7ffbfce5c1acc5168c0ee58e9d15fa4d4f7f3dff309fe1382be5920c315015309217bbf108882d994777cda67376c511c1197d6840952e7e
@@ -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:
@@ -1,88 +1,75 @@
1
1
  #!/usr/bin/env ruby
2
- require 'rubygems'
3
- require 'filewatcher'
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::options do
9
- version "filewatcher, version #{FileWatcher.VERSION} by Thomas Flemming 2016"
10
- banner <<-EOS
11
- Filewatcher scans the filesystem and executes shell commands when files changes.
12
-
13
- Usage:
14
- filewatcher [--restart] '<filenames or patterns>' '<shell command>'
15
- Where
16
- filename: filename(s) to scan.
17
- shell command: shell command to execute when file changes on disk.
18
-
19
- Examples:
20
- filewatcher "myfile" "echo 'myfile has changed'"
21
- filewatcher '*.rb' 'ruby $FILENAME'
22
- filewatcher '**/*.rb' 'ruby $FILENAME' # Watch subdirectories
23
-
24
- Other available environment variables are BASENAME, ABSOLUTE_FILENAME,
25
- RELATIVE_FILENAME, EVENT and DIRNAME.
26
-
27
- Options:
28
- EOS
29
-
30
- opt :dontwait, "Do not wait for filesystem updates before running", :short => 'd', :type => :boolean, :default => false
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::die Trollop::educate if(ARGV.size == 0)
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
- if(ARGV.length == 1)
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
- splitted_filenames = []
54
- files.each do |name|
55
- name = name.gsub(/\\\s/,'_ESCAPED_WHITESPACE_')
56
- splitted_filenames << name.split(/\s/)
57
- end
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
- return Process.spawn(env, cmd)
60
+ Process.spawn(env, cmd)
76
61
  end
77
62
 
78
- if(options[:exclude] != "")
79
- options[:exclude] = split_files_void_escaped_whitespace(options[:exclude].split(" "))
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 = FileWatcher.new(files, options)
70
+ fw = Filewatcher.new(files, options)
84
71
 
85
- if(options[:list])
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(options[:interval]) do |filename, event|
95
- cmd = nil
96
- if(options[:exec] and File.exist?(filename))
97
- extension = filename[/(\.[^\.]*)$/,0]
98
- runners = {
99
- ".py" => "python",
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
- if(cmd)
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
- case(event)
153
- when :changed
154
- print "file updated"
155
- when :delete
156
- print "file deleted"
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
@@ -1,66 +1,40 @@
1
- # coding: utf-8
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 FileWatcher
8
+ class Filewatcher
9
+ include Filewatcher::Cycles
6
10
 
7
- attr_accessor :filenames
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 nil unless @show_spinner
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, *args)
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
- @last_snapshot = mtime_snapshot
32
- @end_snapshot = nil
33
- @dontwait = options[:dontwait]
24
+ @immediate = options[:immediate]
34
25
  @show_spinner = options[:spinner]
35
- @interval = options[:interval]
26
+ @interval = options.fetch(:interval, 0.5)
27
+ @every = options[:every]
36
28
  end
37
29
 
38
- def watch(sleep=0.5, &on_update)
39
- trap("SIGINT") {return }
40
- @sleep = sleep
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(@dontwait)
47
- yield '',''
48
- end
49
- while @keep_watching
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
- Kernel.sleep @sleep # Ensure we wait long enough to enter pause loop
72
- # in #watch
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 # resume with fresh snapshot
53
+ @last_snapshot = mtime_snapshot # resume with fresh snapshot
80
54
  @pausing = false
81
55
  update_spinner('Resuming')
82
- Kernel.sleep @sleep # Wait long enough to exit pause loop in #watch
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
- return nil
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 = @stored_update if !block_given?
97
- snapshot = @end_snapshot ? @end_snapshot : mtime_snapshot
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.call(@updated_file, @event)
73
+ trigger_changes(on_update)
101
74
  end
102
- @end_snapshot =nil
103
- return nil
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
- @filenames = expand_directories(@unexpanded_filenames)
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
- @filenames.each do |filename|
125
- mtime = File.exist?(filename) ? File.stat(filename).mtime : Time.new(0)
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
- return snapshot
101
+ snapshot
129
102
  end
130
103
 
131
- def filesystem_updated?(snapshot_to_use = nil)
132
- snapshot = snapshot_to_use ? snapshot_to_use : mtime_snapshot
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
- backward_changes = @last_snapshot.to_a - snapshot.to_a
149
- forward_names = forward_changes.map{|change| change.first}
150
- backward_changes.reject!{|f,m| forward_names.include?(f)}
151
- backward_changes.each do |file,mtime|
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
- def expand_directories(patterns)
165
- if(!patterns.kind_of?Array)
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
- def fulldepth(pattern)
174
- if File.directory? pattern
175
- "#{pattern}/**/*"
176
- else
177
- pattern
178
- end
117
+ @last_snapshot = snapshot
118
+ @changes.any?
179
119
  end
180
120
 
181
- def expand_path(pattern)
182
- if pattern.start_with?('~')
183
- File.expand_path(pattern)
184
- else
185
- pattern
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'