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.
- data/lib/cronedit.rb +334 -0
- data/test/cronedit_test.rb +99 -0
- metadata +46 -0
data/lib/cronedit.rb
ADDED
@@ -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
|
+
|