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 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'