cronedit 0.2.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.
Files changed (3) hide show
  1. data/lib/cronedit.rb +334 -0
  2. data/test/cronedit_test.rb +99 -0
  3. metadata +46 -0
@@ -0,0 +1,334 @@
1
+ # :title:CronEdit - Ruby editior library for cron and crontab - RDoc documentation
2
+ # =CronEdit - Ruby editor library for crontab.
3
+ #
4
+ # Allows to manipulate crontab from comfortably from ruby code.
5
+ # You can add/modify/remove (aka CRUD) named crontab entries individually with no effect on the rest of your crontab.
6
+ # You can define cron entry definitions as standart text definitions <tt>'10 * * * * echo 42'</tt> or using Hash notation <tt>{:minute=>10, :command=>'echo 42'}</tt> (see CronEntry ::DEFAULTS)
7
+ # Additionaly you can parse cron text definitions to Hash.
8
+ #
9
+ # ==Usage
10
+ #
11
+ # Class methods offer quick crontab operations. Three examples:
12
+ # CronEdit::Crontab.Add 'agent1', '5,35 0-23/2 * * * echo agent1'
13
+ # CronEdit::Crontab.Add 'agent2', {:minute=>5, :command=>'echo 42'}
14
+ # CronEdit::Crontab.Remove 'someId'
15
+ #
16
+ # or define a batch update and list the current content:
17
+ #
18
+ # cm = CronEdit:Crontab.new 'user'
19
+ # cm.add 'agent1', '5,35 0-23/2 * * * echo agent1'
20
+ # ...
21
+ # cm.add 'agent2', {:minute=>5, :command=>'echo 42'}
22
+ # cm.commit
23
+ # p cm.list
24
+ #
25
+ # see Crontab for all available methods
26
+ # ==Author
27
+ # Viktor Zigo, http://7inf.com, All rights reserved. You can redistribute it and/or modify it under the same terms as Ruby.
28
+ # (parts of the cron definition parsing code originaly by gotoken@notwork.org)
29
+ # ==History
30
+ # version: 0.2.0
31
+ #
32
+ # ==TODO
33
+ # add more commands (clean crontab); lift some methods to class; put tests to separate file; make rake, make gem; implement addAllfrom
34
+ # add Utils: getNext execution
35
+ module CronEdit
36
+ self::VERSION = '0.2.0' unless defined? self::VERSION
37
+
38
+ class Crontab
39
+ # Use crontab for user aUser
40
+ def initialize aUser = nil
41
+ @user = aUser
42
+ rollback()
43
+ end
44
+
45
+ class <<self
46
+ # Add a new crontab entry definition. See instance add method
47
+ def Add anId, aDef
48
+ cm = Crontab.new
49
+ entry = cm.add anId, aDef
50
+ cm.commit
51
+ entry
52
+ end
53
+
54
+ # Remove a crontab entry definition identified by anId from current crontab.
55
+ def Remove anId
56
+ cm = Crontab.new
57
+ cm.remove anId
58
+ cm.commit
59
+ end
60
+
61
+ # List current crontab.
62
+ def List
63
+ Crontab.new.list
64
+ end
65
+ end
66
+
67
+ # Add a new crontab entry definition. Becomes effective only after commit().
68
+ # * aDef is can be a standart text definition or a Hash definition (see CronEntry::DEFAULTS)
69
+ # * anId is an identification of the entry (for later modification or deletion)
70
+ # returns newly added CronEntry
71
+ def add anId, aDef
72
+ @adds[anId.to_s] = CronEntry.new( aDef )
73
+ end
74
+
75
+ # Bulk addition of crontab entry definitions from stream (String, File, IO)
76
+ def addAllFrom anIO
77
+ #TODO: load file, parse it
78
+ raise 'Not implemented yet'
79
+ end
80
+
81
+ # Remove a crontab entry definition identified by anId. Becomes effective only after commit().
82
+ def remove anId
83
+ @adds.delete anId.to_s
84
+ @removals[anId.to_s]=anId.to_s
85
+ end
86
+
87
+ # Merges the existing crontab with all modifications and installs the new crontab.
88
+ # returns the merged parsed crontab hash
89
+ def commit
90
+ # merge crontab
91
+ current = list()
92
+ current.delete_if {|id, entry| @removals.include? id}
93
+ merged = current.merge @adds
94
+
95
+ # install it
96
+ cmd = @user ? "crontab -u #{@user} -" : "crontab -"
97
+ IO.popen(cmd,'w') {|io|
98
+ merged.each {|id, entry|
99
+ io.puts "# #{id}"
100
+ io.puts entry
101
+ }
102
+ io.close
103
+ }
104
+ # No idea why but without this any wait crontab reads and writes appears not synchronizes
105
+ sleep 0.01
106
+ #clean changes :)
107
+ rollback()
108
+ merged
109
+ end
110
+
111
+ # Discards all modifications (since last commit, or creation)
112
+ def rollback
113
+ @adds = {}
114
+ @removals = {}
115
+ end
116
+
117
+ # Prints out the items to be added and removed
118
+ def review
119
+ puts "To be added: #{@adds.inspect}"
120
+ puts "To be removed: #{@removals.keys.inspect}"
121
+ end
122
+
123
+ # Read the current crontab and parse it
124
+ # returns a Hash (entry id or index)=>CronEntry
125
+ def list
126
+ cmd = @user ? "crontab -u #{@user} -l" : "crontab -l"
127
+ IO.popen(cmd) {|io|
128
+ return parseCrontab(io)
129
+ }
130
+ end
131
+
132
+ # Lists raw content from crontab
133
+ # returns array of text lines
134
+ def listRaw
135
+ cmd = @user ? "crontab -u #{@user} -l" : "crontab -l"
136
+ IO.popen(cmd) {|io|
137
+ entries = io.readlines
138
+ return (entries.first =~ /^no/).nil? ? entries : []
139
+ }
140
+ end
141
+
142
+ def parseCrontab anIO
143
+ entries = {}
144
+ idx = 0
145
+ id = nil
146
+ anIO.each_line { |l|
147
+ l.strip!
148
+ next if l.empty?
149
+ return {} unless (l =~ /^no/).nil?
150
+ if l=~/^#/
151
+ id = l[1, l.size-1].strip
152
+ else
153
+ key = id.nil? ? (idx+=1) : id
154
+ entries[key.to_s]=l
155
+ id = nil
156
+ end
157
+ }
158
+ entries
159
+ end
160
+ end
161
+
162
+
163
+ class CronEntry
164
+ DEFAULTS = {
165
+ :minute => '*',
166
+ :hour => '*',
167
+ :day => '*',
168
+ :month => '*',
169
+ :weekday => '*',
170
+ :command => ''
171
+ }
172
+
173
+ class FormatError < StandardError; end
174
+
175
+ # Hash def, or raw String def
176
+ def initialize aDef = {}
177
+ if aDef.kind_of? Hash
178
+ wrong = aDef.collect { |k,v| DEFAULTS.include?(k) ? nil : k}.compact
179
+ raise "Wrong definition, invalid constructs #{wrong}" unless wrong.empty?
180
+ @defHash = DEFAULTS.merge aDef
181
+ # TODO: validate values
182
+ @def = to_raw @defHash ;
183
+ else
184
+ @defHash = parseTextDef aDef
185
+ @def = aDef;
186
+ end
187
+ end
188
+
189
+ def to_s
190
+ @def.freeze
191
+ end
192
+
193
+ def to_hash
194
+ @defHash.freeze
195
+ end
196
+
197
+ def []aField
198
+ @defHash[aField]
199
+ end
200
+
201
+ def to_raw aHash = nil;
202
+ aHash ||= @defHash
203
+ "#{aHash[:minute]}\t#{aHash[:hour]}\t#{aHash[:day]}\t#{aHash[:month]}\t" +
204
+ "#{aHash[:weekday]}\t#{aHash[:command]}"
205
+ end
206
+
207
+ private
208
+
209
+ # Parses a raw text definition of crontab entry
210
+ # returns hash definition
211
+ # Original author of parsing: gotoken@notwork.org
212
+ def parseTextDef aLine
213
+ hashDef = parse_timedate aLine
214
+ hashDef[:command] = aLine.scan(/(?:\S+\s+){5}(.*)/).shift[-1]
215
+ ##TODO: raise( FormatError.new "Command cannot be empty") if aDef[:command].empty?
216
+ hashDef
217
+ end
218
+
219
+ # Original author of parsing: gotoken@notwork.org
220
+ def parse_timedate str, aDefHash = {}
221
+ minute, hour, day_of_month, month, day_of_week =
222
+ str.scan(/^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/).shift
223
+ day_of_week = day_of_week.downcase.gsub(/#{WDAY.join("|")}/){
224
+ WDAY.index($&)
225
+ }
226
+ aDefHash[:minute] = parse_field(minute, 0, 59)
227
+ aDefHash[:hour] = parse_field(hour, 0, 23)
228
+ aDefHash[:day] = parse_field(day_of_month, 1, 31)
229
+ aDefHash[:month] = parse_field(month, 1, 12)
230
+ aDefHash[:weekday] = parse_field(day_of_week, 0, 6)
231
+ aDefHash
232
+ end
233
+
234
+ # Original author of parsing: gotoken@notwork.org
235
+ def parse_field str, first, last
236
+ list = str.split(",")
237
+ list.map!{|r|
238
+ r, every = r.split("/")
239
+ every = every ? every.to_i : 1
240
+ f,l = r.split("-")
241
+ range = if f == "*"
242
+ first..last
243
+ elsif l.nil?
244
+ f.to_i .. f.to_i
245
+ elsif f.to_i < first
246
+ raise FormatError.new( "out of range (#{f} for #{first})")
247
+ elsif last < l.to_i
248
+ raise FormatError.new( "out of range (#{l} for #{last})")
249
+ else
250
+ f.to_i .. l.to_i
251
+ end
252
+ range.to_a.find_all{|i| (i - first) % every == 0}
253
+ }
254
+ list.flatten!
255
+ list.join ','
256
+ end
257
+
258
+ WDAY = %w(sun mon tue wed thu fri sut)
259
+ end
260
+
261
+ end
262
+
263
+ if __FILE__ == $0
264
+ include CronEdit
265
+
266
+
267
+
268
+
269
+ ents = [
270
+ CronEntry.new( "5,35 0-23/2 * * * echo 123" ),
271
+ CronEntry.new,
272
+ CronEntry.new( {:minute=>5, :command=>'echo 42'} ),
273
+ ]
274
+ ents.each { |e| puts e }
275
+
276
+ e= CronEntry.new( "5,35 0-23/2 * * * echo 123" )
277
+ puts "Minute: #{e[:minute].inspect}"
278
+ puts "Hour: #{e[:hour].inspect}"
279
+ puts "Day: #{e[:day].inspect}"
280
+ p e.to_s
281
+ p e.to_hash
282
+ p e.to_raw
283
+
284
+ begin
285
+ CronEntry.new( {:minuteZ=>5, :command=>'echo 42'} )
286
+ puts "Failure failed"
287
+ rescue
288
+ end
289
+
290
+ begin
291
+ CronEntry.new( "1-85 2 * * * echo 123" )
292
+ puts "Failure failed"
293
+ rescue CronEntry::FormatError
294
+ end
295
+
296
+ crontabTest=%Q{
297
+ 5,35 0-23/2 * * * echo 123
298
+ #agent1
299
+ 3 * * * * echo agent1
300
+
301
+
302
+ #agent2
303
+ 3 * * * * echo agent2
304
+ #ignored comment
305
+ #agent1
306
+ 3 * * * * echo agent3
307
+ }
308
+ puts "Crontab raw: #{Crontab.new.listRaw.inspect}"
309
+ puts "Crontab: #{Crontab.new.list.inspect}"
310
+ puts "Crontab test: #{Crontab.new.parseCrontab(crontabTest).inspect }"
311
+
312
+ #rollback test
313
+ cm = Crontab.new
314
+ cm. add 'agent1', '5,35 0-23/2 * * * "echo 123" '
315
+ cm.remove "agent2"
316
+ cm.review
317
+ cm.rollback
318
+
319
+ #commit test
320
+ cm = Crontab.new
321
+ cm. add "agent1", "5,35 0-23/2 * * * echo agent1"
322
+ cm. add "agent2", "0 2 * * * echo agent2"
323
+ cm.commit
324
+ current=cm.list
325
+ puts "New crontab 1: #{current.inspect}"
326
+
327
+ cm = Crontab.new
328
+ cm. add "agent1", '59 * * * * echo "modified agent1"'
329
+ cm.remove "agent2"
330
+ cm.commit
331
+ current=Crontab.List
332
+ puts "New crontab 2: #{current.inspect}"
333
+ raise "Assertion" unless current=={'agent1'=> '59 * * * * echo "modified agent1"'}
334
+ end
@@ -0,0 +1,99 @@
1
+ $:.unshift File.join(File.dirname(__FILE__),'..','lib')
2
+
3
+ require 'test/unit'
4
+ require 'cronedit.rb'
5
+ include CronEdit
6
+
7
+ class CronEdit_test < Test::Unit::TestCase
8
+ # def setup
9
+ # end
10
+ #
11
+ # def teardown
12
+ # end
13
+
14
+ def test_creation
15
+ e = CronEntry.new( "5,35 0-23/2 * * * echo 123" )
16
+ assert_equal( '5,35 0-23/2 * * * echo 123', e.to_s )
17
+
18
+ e = CronEntry.new
19
+ assert_equal( "*\t*\t*\t*\t*\t", e.to_s, 'default' )
20
+
21
+ e = CronEntry.new( {:minute=>5, :command=>'echo 42'} )
22
+ assert_equal( "5\t*\t*\t*\t*\techo 42", e.to_s )
23
+
24
+ end
25
+
26
+ def test_parsing
27
+ e = CronEntry.new( "5,35 0-23/2 * * * echo 123" )
28
+ assert_equal( "5,35", e[:minute])
29
+ assert_equal( "0,2,4,6,8,10,12,14,16,18,20,22", e[:hour])
30
+ assert_equal( "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", e[:day])
31
+ end
32
+ def test_wrongformat
33
+ assert_raise(CronEntry::FormatError){
34
+ CronEntry.new( "1-85 2 * * * echo 123" )
35
+ }
36
+ end
37
+
38
+ def test_wrongconfog
39
+ assert_raise(RuntimeError){
40
+ CronEntry.new( {:minuteZ=>5, :command=>'echo 42'} )
41
+ }
42
+ end
43
+
44
+ def test_zip
45
+ crontabTest=%Q{
46
+ 5,35 0-23/2 * * * echo 123
47
+ #agent1
48
+ 3 * * * * echo agent1
49
+
50
+
51
+ #agent2
52
+ 3 * * * * echo agent2
53
+ #ignored comment
54
+ #agent1
55
+ 3 * * * * echo agent3
56
+ }
57
+ expected = {"agent1"=>"3 * * * * echo agent3", "agent2"=>"3 * * * * echo agent2", "1"=>"5,35 0-23/2 * * * echo 123"}
58
+ assert_equal( expected, Crontab.new.parseCrontab(crontabTest), 'parsing of crontab file')
59
+ end
60
+
61
+ def test_emptycrontab
62
+ assert_equal( [], Crontab.new.listRaw )
63
+ assert_equal( {}, Crontab.new.list )
64
+ end
65
+
66
+ def test_rollback
67
+ #rollback test
68
+ assert_equal( {}, Crontab.new.list, 'precondition' )
69
+ cm = Crontab.new
70
+ cm. add 'agent1', '5,35 0-23/2 * * * "echo 123" '
71
+ cm.remove "agent2"
72
+ #cm.review
73
+ cm.rollback
74
+ assert_equal( {}, Crontab.new.list )
75
+ end
76
+
77
+ def test_commit
78
+ assert_equal( {}, Crontab.new.list, 'precondition' )
79
+ cm = Crontab.new
80
+ cm. add "agent1", "5,35 0-23/2 * * * echo agent1"
81
+ cm. add "agent2", "0 2 * * * echo agent2"
82
+ cm.commit
83
+ current=cm.list
84
+ expected = {"agent1"=>"5,35 0-23/2 * * * echo agent1", "agent2"=>"0 2 * * * echo agent2"}
85
+ assert_equal( expected, current, 'first commit' )
86
+
87
+ cm = Crontab.new
88
+ cm. add "agent1", '59 * * * * echo "modified agent1"'
89
+ cm.remove "agent2"
90
+ cm.commit
91
+ current = cm.list
92
+ expected = {"agent1"=>"59 * * * * echo \"modified agent1\""}
93
+ assert_equal( expected, current, 'second commit' )
94
+
95
+ Crontab.Remove "agent1"
96
+ assert_equal( {}, Crontab.List, 'precondition' )
97
+ end
98
+
99
+ end
metadata ADDED
@@ -0,0 +1,46 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.0
3
+ specification_version: 1
4
+ name: cronedit
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.2.0
7
+ date: 2007-12-09 00:00:00 +01:00
8
+ summary: CronEdit is a Ruby editor library for crontab.
9
+ require_paths:
10
+ - lib
11
+ email: viz@alephzarro.com
12
+ homepage: http://cronedit.rubyforge.org/
13
+ rubyforge_project:
14
+ description:
15
+ autorequire: lib
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Viktor Zigo
31
+ files:
32
+ - lib/cronedit.rb
33
+ test_files:
34
+ - test/cronedit_test.rb
35
+ rdoc_options: []
36
+
37
+ extra_rdoc_files: []
38
+
39
+ executables: []
40
+
41
+ extensions: []
42
+
43
+ requirements: []
44
+
45
+ dependencies: []
46
+