crontab 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ = What is this?
2
+
3
+ A crontab(5) parser.
4
+
5
+ == Usage
6
+
7
+ crontab = Crontab.parse(src)
8
+
9
+ from_date = Date.parse('2009-10-10')
10
+ to_date = Date.parse('2009-10-20')
11
+
12
+ crontab.entries.each do |e|
13
+ puts e.command
14
+ e.schedule.from(from_date).until(to_date) do |timing|
15
+ puts timing
16
+ end
17
+ end
@@ -0,0 +1,23 @@
1
+ require 'rake/gempackagetask'
2
+ require 'rake/rdoctask'
3
+ require 'spec/rake/spectask'
4
+
5
+ task :default => :spec
6
+
7
+ spec = Gem::Specification.load('crontab.gemspec')
8
+
9
+ Rake::GemPackageTask.new(spec) do |pkg|
10
+ pkg.need_zip = true
11
+ pkg.need_tar_bz2 = true
12
+ end
13
+
14
+ Rake::RDocTask.new(:rdoc) do |rdoc|
15
+ rdoc.main = 'README.rdoc'
16
+ rdoc.rdoc_files.include('README.rdoc', 'lib/**/*.rb')
17
+ rdoc.options << '--all'
18
+ end
19
+
20
+ Spec::Rake::SpecTask.new do |t|
21
+ t.warning = true
22
+ t.rcov = false
23
+ end
@@ -0,0 +1,22 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'crontab'
3
+ s.version = '0.0.2'
4
+ s.author = 'OZAWA Sakuro'
5
+ s.email = 'github@2238club.org'
6
+ s.homepage = 'http://github.com/sakuro/crontab'
7
+ s.platform = Gem::Platform::RUBY
8
+ s.summary = 'A crontab(5) entry parser'
9
+ s.files = [
10
+ 'lib/crontab.rb',
11
+ 'lib/crontab/entry.rb',
12
+ 'lib/crontab/schedule.rb',
13
+ 'spec/crontab_spec.rb',
14
+ 'spec/crontab/entry_spec.rb',
15
+ 'spec/crontab/schedule_spec.rb',
16
+ 'crontab.gemspec',
17
+ 'Rakefile',
18
+ 'README.rdoc'
19
+ ]
20
+ s.has_rdoc = true
21
+ s.rubyforge_project = 'n/a'
22
+ end
@@ -0,0 +1,34 @@
1
+ require 'crontab/schedule'
2
+ require 'crontab/entry'
3
+
4
+ # A class which represents crontab(5) content.
5
+ class Crontab
6
+ class << self
7
+ # Parses a crontab(5) text.
8
+ #
9
+ # * <tt>src</tt> string in crontab(5) format
10
+ def parse(src)
11
+ entries = []
12
+ env = {}
13
+ src.lines.each do |line|
14
+ line.strip!
15
+ case line
16
+ when /\A[\d*]/
17
+ entries << Crontab::Entry.parse(line)
18
+ when /\A([A-Z][A-Z0-9_]*)\s*=\s*/, /\A"([A-Z][A-Z0-9_]*)"\s*=\s*/, /\A'([A-Z][A-Z0-9_]*)'\s*=\s*/
19
+ name = $1
20
+ value = $'
21
+ value = $1 if /\A'(.*)'\z/ =~ value or /\A"(.*)"\z/ =~ value
22
+ env[name] = value
23
+ end
24
+ end
25
+ new(entries, env)
26
+ end
27
+ end
28
+
29
+ def initialize(entries, env)
30
+ @entries = entries.dup.freeze
31
+ @env = env.dup.freeze
32
+ end
33
+ attr_reader :entries, :env
34
+ end
@@ -0,0 +1,49 @@
1
+ class Crontab
2
+ # A class which represents a job line in crontab(5).
3
+ class Entry
4
+ # Creates a crontab(5) entry.
5
+ #
6
+ # * <tt>schedule</tt> A Crontab::Schedule instance.
7
+ # * <tt>command</tt>
8
+ # * <tt>uid</tt>
9
+ def initialize(schedule, command, uid=nil)
10
+ raise ArgumentError, 'invalid schedule' unless schedule.is_a? Schedule
11
+ raise ArgumentError, 'invalid command' unless command.is_a? String
12
+
13
+ @schedule = schedule.freeze
14
+ @command = command.freeze
15
+ @uid =
16
+ case uid
17
+ when String
18
+ Etc.getpwnam(uid).uid
19
+ when Integer
20
+ uid
21
+ when nil
22
+ Process.uid
23
+ else
24
+ raise ArgumentError, 'invalid uid'
25
+ end
26
+ end
27
+
28
+ attr_reader :schedule, :command, :uid
29
+ class << self
30
+ # Parses a string line in crontab(5) job format.
31
+ #
32
+ # * <tt>options[:system]</tt> when true system wide crontab is assumed
33
+ # and <tt>@uid</tt> is extracted from <i>line</i>.
34
+ def parse(line, options={})
35
+ options = { :system => false }.merge(options)
36
+ line = line.strip
37
+ number_of_fields = 1
38
+ number_of_fields += line.start_with?('@') ? 1 : 5
39
+ number_of_fields += 1 if options[:system]
40
+ words = line.split(/\s+/, number_of_fields)
41
+ command = words.pop
42
+ uid = options[:system] ? words.pop : Process.uid
43
+ spec = words.join(' ')
44
+ schedule = Crontab::Schedule.new(spec)
45
+ new(schedule, command, uid)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,244 @@
1
+ require 'date'
2
+
3
+ class Crontab
4
+
5
+ # A class which represents schedules in crontab(5).
6
+ class Schedule
7
+
8
+ MONTH_NAMES = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
9
+ DAY_OF_WEEK_NAMES = %w(Sun Mon Tue Wed Thu Fri Sat)
10
+ DAYS_IN_MONTH = [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
11
+
12
+ # Creates a cron job schedule.
13
+ #
14
+ # * <tt>spec</tt> schedule specifier defined in crontab(5).
15
+ # Both numeric and named spec are supported.
16
+ # * <tt>start</tt> the time *or* date this schedule begins.
17
+ #
18
+ # Supported named specs are:
19
+ # * <tt>@yearly</tt>, <tt>@annually</tt>
20
+ # * <tt>@monthly</tt>
21
+ # * <tt>@weekly</tt>
22
+ # * <tt>@daily</tt>, <tt>@midnight</tt>
23
+ # * <tt>@hourly</tt>
24
+ def initialize(spec, start=Time.now)
25
+ raise ArgumentError, 'empty spec' if spec == '' or spec.nil?
26
+ @start = ensure_time(start)
27
+ spec = spec.strip
28
+ if spec.start_with?('@')
29
+ parse_symbolic_spec(spec)
30
+ else
31
+ parse_spec(spec)
32
+ end
33
+ end
34
+
35
+ def ==(other)
36
+ self.minutes == other.minutes &&
37
+ self.hours == other.hours &&
38
+ self.day_of_months == other.day_of_months &&
39
+ self.months == other.months &&
40
+ self.day_of_months_given? == other.day_of_months_given? &&
41
+ self.day_of_weeks == other.day_of_weeks &&
42
+ self.day_of_weeks_given? == other.day_of_weeks_given? &&
43
+ self.start == other.start
44
+ end
45
+
46
+ def hash
47
+ [ minutes, hours, day_of_months, months, day_of_weeks, day_of_months_given?, day_of_weeks_given?, start ].map(&:hash).inject(:^)
48
+ end
49
+
50
+ attr_reader :minutes, :hours, :day_of_months, :months, :day_of_weeks, :start
51
+
52
+ # Changes the start timing of this schedule to <i>time_or_date</i>.
53
+ def start=(time_or_date)
54
+ @start = ensure_time(time_or_date)
55
+ end
56
+
57
+ # Creates new schedule which starts at the given <i>time_or_date</i>.
58
+ def from(time_or_date)
59
+ self.dup.tap {|new_schedule| new_schedule.start = ensure_time(time_or_date) }
60
+ end
61
+
62
+ def day_of_months_given?
63
+ !!@day_of_months_given # ensures boolean
64
+ end
65
+ protected :day_of_months_given?
66
+
67
+ def day_of_weeks_given?
68
+ !!@day_of_weeks_given # ensures boolean
69
+ end
70
+ protected :day_of_weeks_given?
71
+
72
+ # Iterates over timings specified in this schedule, starting at <tt>@start</tt>.
73
+ def each
74
+ return to_enum unless block_given?
75
+ year = @start.year
76
+ seeking = Hash.new {|h,k| h[k] = true }
77
+ loop do
78
+ @months.each do |month|
79
+ next if seeking[:month] and month < @start.month and @start.month <= @months.max
80
+ seeking[:month] = false
81
+ days = matching_days(year, month)
82
+ days.each do |day_of_month|
83
+ next if seeking[:day_of_month] and day_of_month < @start.day and @start.day <= days.max
84
+ seeking[:day_of_month] = false
85
+ @hours.each do |hour|
86
+ next if seeking[:hour] and hour < @start.hour and @start.hour <= @hours.max
87
+ seeking[:hour] = false
88
+ @minutes.each do |minute|
89
+ begin
90
+ t = Time.local(year, month, day_of_month, hour, minute)
91
+ rescue ArgumentError
92
+ raise StopIteration
93
+ end
94
+ yield(t) if @start <= t
95
+ end
96
+ end
97
+ end
98
+ end
99
+ year += 1
100
+ end
101
+ end
102
+
103
+ include Enumerable
104
+
105
+ # Iterates over timings from <tt>@start</tt> until given <i>time_or_date</i>.
106
+ def until(time_or_date)
107
+ time = ensure_time(time_or_date)
108
+ if block_given?
109
+ each do |t|
110
+ break if time < t
111
+ yield(t)
112
+ end
113
+ else
114
+ inject([]) do |timings, t|
115
+ break timings if time < t
116
+ timings << t
117
+ end
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ def parse_spec(spec)
124
+ args = spec.split(/\s+/)
125
+ raise ArgumentError, 'wrong number of spec fields: %d' % args.size unless args.size == 5
126
+ @minutes = parse_spec_field(args[0], 0..59, :accept_name => false)
127
+ @hours = parse_spec_field(args[1], 0..23, :accept_name => false)
128
+ @day_of_months = parse_spec_field(args[2], 1..31, :accept_name => false)
129
+ @day_of_months_given = args[2] != '*'
130
+ @months = parse_spec_field(args[3], 1..12, :names => MONTH_NAMES, :base => 1, :accept_name => true)
131
+ @day_of_weeks = parse_spec_field(args[4], 0..6, :names => DAY_OF_WEEK_NAMES, :base => 0, :accept_name => true, :allow_end => true)
132
+ @day_of_weeks_given = args[4] != '*'
133
+ end
134
+
135
+ def parse_symbolic_spec(spec)
136
+ case spec
137
+ when '@yearly', '@annually'
138
+ parse_spec('0 0 1 1 *')
139
+ when '@monthly'
140
+ parse_spec('0 0 1 * *')
141
+ when '@weekly'
142
+ parse_spec('0 0 * * 0')
143
+ when '@daily', '@midnight'
144
+ parse_spec('0 0 * * *')
145
+ when '@hourly'
146
+ parse_spec('0 * * * *')
147
+ when '@reboot'
148
+ raise NotImplementedError, '@reboot is not supported'
149
+ else
150
+ raise ArgumentError, 'unknown crontab spec: %s' % spec
151
+ end
152
+ end
153
+
154
+ def parse_spec_field(spec, range, options={})
155
+ options = { :accept_name => true, :allow_end => false }.merge(options)
156
+ case spec
157
+ when '*'
158
+ range.to_a
159
+ when /\A\d+\z/
160
+ [ parse_number(spec, range, options) ]
161
+ when /\A[a-z]{3}\z/i
162
+ [ parse_name(spec, range, options) ]
163
+ when /\A(\d+)-(\d+)\z/i
164
+ parse_range($1, $2, range, options.merge(:accept_name => false))
165
+ when /,/
166
+ parse_list(spec.split(/,/), range, options.merge(:accept_name => false))
167
+ when /\A(\*|\d+-\d+)\/(\d+)\z/
168
+ parse_step($1, $2.to_i, range, options.merge(:accept_name => false))
169
+ else
170
+ raise ArgumentError, 'wrong spec format: %s' % spec
171
+ end.tap do |result|
172
+ if options[:allow_end] && result.include?(range.first) && result.include?(range.last.succ)
173
+ result.delete(range.last.succ)
174
+ end
175
+ end.sort.uniq.freeze
176
+ end
177
+
178
+ def parse_number(spec, range, options)
179
+ v = spec.to_i
180
+ return v if range.include?(v) or options[:allow_end] && range.last.succ == v
181
+ raise ArgumentError, 'argument out of range: %s' % spec
182
+ end
183
+
184
+ def parse_name(spec, range, options)
185
+ raise ArgumentError, 'names not allowed in this field: %s' % spec unless options[:names] and options[:base]
186
+ raise ArgumentError, 'names not allowed in this context: %s' % spec unless options[:accept_name]
187
+ v = options[:names].index {|name| name.downcase == spec.downcase }
188
+ base = options[:base]
189
+ return v + base if v && range.include?(v + base)
190
+ raise ArgumentError, 'argument out of range: %s' % spec
191
+ end
192
+
193
+ def parse_range(from, to, range, options)
194
+ from = parse_number(from, range, options[:allow_end])
195
+ to = parse_number(to, range, options[:allow_end])
196
+ raise ArgumentError, 'start is after or equal to end: %s' % spec unless from < to
197
+ (from..to).to_a
198
+ end
199
+
200
+ def parse_list(specs, range, options)
201
+ specs.map{|spec| parse_spec_field(spec, range, options) }.flatten
202
+ end
203
+
204
+ def parse_step(first, step, range, options)
205
+ v = parse_spec_field(first, range, options)
206
+ v.first.step(v.size == 1 ? range.last : v.last, step).to_a
207
+ end
208
+
209
+ def matching_days(year, month)
210
+ days = number_of_days(year, month)
211
+ (1..days).select do |day|
212
+ wday = day_of_week(year, month, day)
213
+ if day_of_months_given?
214
+ if day_of_weeks_given?
215
+ @day_of_months.include?(day) or @day_of_weeks.include?(wday)
216
+ else
217
+ @day_of_months.include?(day)
218
+ end
219
+ else
220
+ if day_of_weeks_given?
221
+ @day_of_weeks.include?(wday)
222
+ else
223
+ true
224
+ end
225
+ end
226
+ end
227
+ end
228
+
229
+ def ensure_time(time_or_date)
230
+ time_or_date.is_a?(Date) ? Time.local(time_or_date.year, time_or_date.month, time_or_date.day) : time_or_date
231
+ end
232
+
233
+ def number_of_days(year, month)
234
+ days = DAYS_IN_MONTH[month]
235
+ days += 1 if month == 2 && (year % 4 == 0 && year % 100 != 0 || year % 400 == 0)
236
+ days
237
+ end
238
+
239
+ def day_of_week(year, month, day)
240
+ Date.new(year, month, day).wday
241
+ end
242
+ end
243
+
244
+ end
@@ -0,0 +1,128 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+ require 'crontab/entry'
3
+ require 'crontab/schedule'
4
+
5
+ describe Crontab::Entry do
6
+ describe 'when instantiating' do
7
+ before :each do
8
+ @schedule = Crontab::Schedule.new('0 0 * * *') # @daily
9
+ @command = 'echo hello'
10
+ end
11
+
12
+ it 'should accpet Crontab::Schedule and command String' do
13
+ entry = Crontab::Entry.new(@schedule, @command)
14
+ entry.uid.should == Process.uid
15
+ end
16
+
17
+ it 'should accpet Crontab::Schedule, command String and user String' do
18
+ uid = Process.uid
19
+ user = Etc.getpwuid(uid)
20
+ entry = Crontab::Entry.new(@schedule, @command, user.name)
21
+ entry.uid.should == uid
22
+ end
23
+
24
+ it 'should accpet Crontab::Schedule, command String and user ID' do
25
+ uid = Process.uid
26
+ entry = Crontab::Entry.new(@schedule, @command, uid)
27
+ entry.uid.should == uid
28
+ end
29
+
30
+ it 'should reject invalid arguments' do
31
+ lambda { Crontab::Entry.new(@schedule, nil) }.should raise_error(ArgumentError)
32
+ lambda { Crontab::Entry.new(@schedule, Object.new) }.should raise_error(ArgumentError)
33
+ lambda { Crontab::Entry.new(@schedule) }.should raise_error(ArgumentError)
34
+ lambda { Crontab::Entry.new(nil, @command) }.should raise_error(ArgumentError)
35
+ lambda { Crontab::Entry.new(Object.new, @command) }.should raise_error(ArgumentError)
36
+ lambda { Crontab::Entry.new(@command) }.should raise_error(ArgumentError)
37
+ lambda { Crontab::Entry.new }.should raise_error(ArgumentError)
38
+ lambda { Crontab::Entry.new(nil) }.should raise_error(ArgumentError)
39
+ end
40
+
41
+ describe 'by parsing' do
42
+ it 'should parse crontab entry line' do
43
+ entry = Crontab::Entry.parse('0 0 * * * echo hello')
44
+ entry.schedule.from(@schedule.start).should == @schedule and
45
+ entry.command.should == @command and
46
+ entry.uid.should == Process.uid
47
+
48
+ entry = Crontab::Entry.parse('0 0 * * * echo hello', :system => false)
49
+ entry.schedule.from(@schedule.start).should == @schedule and
50
+ entry.command.should == @command and
51
+ entry.uid.should == Process.uid
52
+ end
53
+
54
+ it 'should parse crontab entry line with symbolic schedule' do
55
+ entry = Crontab::Entry.parse('@daily echo hello')
56
+ entry.schedule.from(@schedule.start).should == @schedule and
57
+ entry.command.should == @command and
58
+ entry.uid.should == Process.uid
59
+
60
+ entry = Crontab::Entry.parse('0 0 * * * echo hello', :system => false)
61
+ entry.schedule.from(@schedule.start).should == @schedule and
62
+ entry.command.should == @command and
63
+ entry.uid.should == Process.uid
64
+ end
65
+
66
+ it 'should parse system crontab entry line' do
67
+ entry = Crontab::Entry.parse('0 0 * * * root echo hello', :system => true)
68
+ entry.schedule.from(@schedule.start).should == @schedule and
69
+ entry.command.should == @command and
70
+ entry.uid.should == Etc.getpwnam('root').uid
71
+ end
72
+
73
+ it 'should parse system crontab entry line with symbolic schedule' do
74
+ entry = Crontab::Entry.parse('@daily root echo hello', :system => true)
75
+ entry.schedule.from(@schedule.start).should == @schedule and
76
+ entry.command.should == @command and
77
+ entry.uid.should == Etc.getpwnam('root').uid
78
+ end
79
+
80
+ it 'should ignore leading whitespaces' do
81
+ entry = Crontab::Entry.parse(' @daily echo hello')
82
+ entry.schedule.from(@schedule.start).should == @schedule and
83
+ entry.command.should == @command
84
+
85
+ entry = Crontab::Entry.parse(' 0 0 * * * echo hello')
86
+ entry.schedule.from(@schedule.start).should == @schedule and
87
+ entry.command.should == @command
88
+ end
89
+ end
90
+ end
91
+
92
+ describe 'when accessing' do
93
+ before :each do
94
+ @schedule = Crontab::Schedule.new('@hourly')
95
+ @other_schedule = Crontab::Schedule.new('@monthly')
96
+ @command = 'ehco hello'
97
+ @other_command = 'echo bonjour'
98
+ @entry = Crontab::Entry.new(@schedule, @command)
99
+ end
100
+
101
+ it 'should be read-accessible to schedule' do
102
+ @entry.schedule.should == @schedule and
103
+ lambda { @entry.schedule = @other_schedule }.should raise_error(NoMethodError)
104
+ @entry.schedule.should == @schedule
105
+ end
106
+
107
+ it 'should freeze schedule' do
108
+ @entry.schedule.should be_frozen
109
+ end
110
+
111
+ it 'should be read-accessible to command' do
112
+ @entry.command.should == @command and
113
+ lambda { @entry.command = @other_command }.should raise_error(NoMethodError)
114
+ @entry.command.should == @command
115
+ end
116
+
117
+ it 'should freeze command' do
118
+ @entry.command.should be_frozen
119
+ end
120
+
121
+ it 'should be read-accessible to uid' do
122
+ @entry.uid.should == Process.uid and
123
+ Process.uid.should_not == 0 and
124
+ lambda { @entry.uid = 0 }.should raise_error(NoMethodError)
125
+ @entry.uid.should == Process.uid
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,356 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+ require 'crontab/schedule'
3
+
4
+ describe Crontab::Schedule do
5
+ describe 'when parsing spec' do
6
+ it 'should accept spec with asterisks' do
7
+ schedule = Crontab::Schedule.new('* * * * *')
8
+ schedule.minutes.should == (0..59).to_a and
9
+ schedule.hours.should == (0..23).to_a and
10
+ schedule.day_of_months.should == (1..31).to_a and
11
+ schedule.months.should == (1..12).to_a and
12
+ schedule.day_of_weeks.should == (0..6).to_a
13
+ end
14
+
15
+ it 'should accept spec with simple numbers' do
16
+ schedule = Crontab::Schedule.new('0 0 1 1 0')
17
+ schedule.minutes.should == [0] and
18
+ schedule.hours.should == [0] and
19
+ schedule.day_of_months.should == [1] and
20
+ schedule.months.should == [1] and
21
+ schedule.day_of_weeks.should == [0]
22
+ end
23
+
24
+ it 'should accept spec with names(Jan, Sun etc.)' do
25
+ schedule = Crontab::Schedule.new('0 0 1 Jan sUn')
26
+ schedule.minutes.should == [0] and
27
+ schedule.hours.should == [0] and
28
+ schedule.day_of_months.should == [1] and
29
+ schedule.months.should == [1] and
30
+ schedule.day_of_weeks.should == [0]
31
+ end
32
+
33
+ it 'should accept spec with ranges(N-N)' do
34
+ schedule = Crontab::Schedule.new('0-1 0-1 1-2 1-2 0-1')
35
+ schedule.minutes.should == [0, 1] and
36
+ schedule.hours.should == [0, 1] and
37
+ schedule.day_of_months.should == [1, 2] and
38
+ schedule.months.should == [1, 2] and
39
+ schedule.day_of_weeks.should == [0, 1]
40
+ end
41
+
42
+ it 'should accept spec with lists(comma separaged ranges/numbers)' do
43
+ schedule = Crontab::Schedule.new('0,10,20 0,10,20 1,11,21 1,3,5 0,4,5,6')
44
+ schedule.minutes.should == [0, 10, 20] and
45
+ schedule.hours.should == [0, 10, 20] and
46
+ schedule.day_of_months.should == [1, 11, 21] and
47
+ schedule.months.should == [1, 3, 5] and
48
+ schedule.day_of_weeks.should == [0, 4, 5, 6]
49
+
50
+ schedule = Crontab::Schedule.new('0,1-5,10 0,1-5,10 1,2-5,10 1,2-4,10 0,1-3,7')
51
+ schedule.minutes.should == [0, 1, 2, 3, 4, 5, 10] and
52
+ schedule.hours.should == [0, 1, 2, 3, 4, 5, 10] and
53
+ schedule.day_of_months.should == [1, 2, 3, 4, 5, 10] and
54
+ schedule.months.should == [1, 2, 3, 4, 10] and
55
+ schedule.day_of_weeks.should == [0, 1, 2, 3]
56
+ end
57
+
58
+ it 'should accept spec with steps(range/STEP or */STEP)' do
59
+ schedule = Crontab::Schedule.new('0-30/10 1-20/5 5-30/7 4-10/2 1-5/2')
60
+ schedule.minutes.should == [0, 10, 20, 30] and
61
+ schedule.hours.should == [1, 6, 11, 16 ] and
62
+ schedule.day_of_months.should == [5, 12, 19, 26 ] and
63
+ schedule.months.should == [4, 6, 8, 10] and
64
+ schedule.day_of_weeks.should == [1, 3, 5]
65
+
66
+ schedule = Crontab::Schedule.new('*/25 */5 */10 */4 */2')
67
+ schedule.minutes.should == [0, 25, 50] and
68
+ schedule.hours.should == [0, 5, 10, 15, 20] and
69
+ schedule.day_of_months.should == [1, 11, 21, 31] and
70
+ schedule.months.should == [1, 5, 9] and
71
+ schedule.day_of_weeks.should == [0, 2, 4, 6]
72
+ end
73
+
74
+ it 'should reject spec with incorrect number of fields' do
75
+ lambda { Crontab::Schedule.new }.should raise_error(ArgumentError)
76
+ lambda { Crontab::Schedule.new(nil) }.should raise_error(ArgumentError)
77
+ lambda { Crontab::Schedule.new('') }.should raise_error(ArgumentError)
78
+ lambda { Crontab::Schedule.new('x') }.should raise_error(ArgumentError)
79
+ lambda { Crontab::Schedule.new('* * * *') }.should raise_error(ArgumentError)
80
+ lambda { Crontab::Schedule.new('* * * * * *') }.should raise_error(ArgumentError)
81
+ end
82
+
83
+ it 'should reject spec with incorrect values' do
84
+ lambda { Crontab::Schedule.new('x 0 1 0 0') }.should raise_error(ArgumentError)
85
+ lambda { Crontab::Schedule.new('-1 0 1 1 0') }.should raise_error(ArgumentError)
86
+ lambda { Crontab::Schedule.new('60 0 1 1 0') }.should raise_error(ArgumentError)
87
+ lambda { Crontab::Schedule.new('0 24 1 1 0') }.should raise_error(ArgumentError)
88
+ lambda { Crontab::Schedule.new('0 0 32 1 0') }.should raise_error(ArgumentError)
89
+ lambda { Crontab::Schedule.new('0 0 1 13 0') }.should raise_error(ArgumentError)
90
+ lambda { Crontab::Schedule.new('0 0 1 1 8') }.should raise_error(ArgumentError)
91
+ lambda { Crontab::Schedule.new('0 0 1 Mon 8') }.should raise_error(ArgumentError)
92
+ lambda { Crontab::Schedule.new('Jan Sun 1 1 8') }.should raise_error(ArgumentError)
93
+ end
94
+
95
+ it 'should reject spec with names in lists and ranges' do
96
+ lambda { Crontab::Schedule.new('0 0 1 Jan-Feb *') }.should raise_error(ArgumentError)
97
+ lambda { Crontab::Schedule.new('0 0 1 Jan,Feb *') }.should raise_error(ArgumentError)
98
+ lambda { Crontab::Schedule.new('0 0 1 * Sun-Fri') }.should raise_error(ArgumentError)
99
+ lambda { Crontab::Schedule.new('0 0 1 * Sun,Fri') }.should raise_error(ArgumentError)
100
+ end
101
+
102
+ it 'should reject spec with simple numbers with step' do
103
+ lambda { Crontab::Schedule.new('0/2 0 1 1 0') }.should raise_error(ArgumentError)
104
+ lambda { Crontab::Schedule.new('0 0/2 1 1 0') }.should raise_error(ArgumentError)
105
+ lambda { Crontab::Schedule.new('0 0 1/2 1 0') }.should raise_error(ArgumentError)
106
+ lambda { Crontab::Schedule.new('0 0 1 1/2 0') }.should raise_error(ArgumentError)
107
+ lambda { Crontab::Schedule.new('0 0 1 1 0/2') }.should raise_error(ArgumentError)
108
+ end
109
+
110
+ it 'should accept symblic specs' do
111
+ start = Time.now
112
+ Crontab::Schedule.new('@yearly', start).should == Crontab::Schedule.new('0 0 1 1 *', start) and
113
+ Crontab::Schedule.new('@annually', start).should == Crontab::Schedule.new('0 0 1 1 *', start) and
114
+ Crontab::Schedule.new('@monthly', start).should == Crontab::Schedule.new('0 0 1 * *', start) and
115
+ Crontab::Schedule.new('@weekly', start).should == Crontab::Schedule.new('0 0 * * 0', start) and
116
+ Crontab::Schedule.new('@daily', start).should == Crontab::Schedule.new('0 0 * * *', start) and
117
+ Crontab::Schedule.new('@midnight', start).should == Crontab::Schedule.new('0 0 * * *', start) and
118
+ Crontab::Schedule.new('@hourly', start).should == Crontab::Schedule.new('0 * * * *', start)
119
+ end
120
+
121
+ it 'should reject @reboot' do
122
+ lambda { Crontab::Schedule.new('@reboot') }.should raise_error(NotImplementedError)
123
+ end
124
+
125
+ it 'should reject unknown symbolic spec' do
126
+ lambda { Crontab::Schedule.new('@unknown') }.should raise_error(ArgumentError)
127
+ end
128
+
129
+ it 'should ignore leading/trailing whitespaces' do
130
+ lambda {
131
+ Crontab::Schedule.new(' @daily')
132
+ Crontab::Schedule.new('@daily ')
133
+ Crontab::Schedule.new(' * * * * *')
134
+ Crontab::Schedule.new('* * * * * ')
135
+ }.should_not raise_error
136
+ end
137
+ end
138
+
139
+ describe 'when accessing' do
140
+ it 'should accept Time as start parameter' do
141
+ start = Time.local(2009, 3, 25, 10, 20, 30)
142
+ lambda {
143
+ schedule = Crontab::Schedule.new('* * * * *', start)
144
+ schedule.start.should == start
145
+ }.should_not raise_error
146
+ end
147
+
148
+ it 'should accept Date as start parameter' do
149
+ start = Date.new(2009, 3, 25)
150
+ lambda {
151
+ schedule = Crontab::Schedule.new('* * * * *', start)
152
+ schedule.start.should == Time.local(start.year, start.month, start.day)
153
+ }.should_not raise_error
154
+ end
155
+
156
+ it 'should set start to given Time' do
157
+ initial_start = Time.local(2009, 3, 25, 10, 20, 30)
158
+ schedule = Crontab::Schedule.new('* * * * *', initial_start)
159
+ schedule.start.should == initial_start and
160
+
161
+ new_start = Time.local(2009, 4, 25, 11, 21, 31)
162
+ schedule.start = new_start
163
+ schedule.start.should == new_start
164
+ end
165
+
166
+ it 'should set start to given Date' do
167
+ initial_start = Time.local(2009, 3, 25, 10, 20, 30)
168
+ schedule = Crontab::Schedule.new('* * * * *', initial_start)
169
+ schedule.start.should == initial_start and
170
+
171
+ new_start = Date.new(2009, 4, 25)
172
+ new_start_as_time = Time.local(new_start.year, new_start.month, new_start.day)
173
+ schedule.start = new_start
174
+ schedule.start.should == new_start_as_time
175
+ end
176
+ end
177
+
178
+ describe 'when duplicating' do
179
+ it 'should return a new schedule starting at given Time' do
180
+ initial_start = Time.local(2009, 3, 25, 10, 20, 30)
181
+ schedule = Crontab::Schedule.new('* * * * *', initial_start)
182
+ new_start = Time.local(2009, 4, 25, 11, 21, 31)
183
+ new_schedule = schedule.from(new_start)
184
+
185
+ new_schedule.object_id.should_not == schedule.object_id and
186
+ new_schedule.hash.should_not == schedule.hash and
187
+ new_schedule.should_not == schedule and
188
+
189
+ new_schedule.start.should == new_start and
190
+ new_schedule.minutes.should == schedule.minutes and
191
+ new_schedule.hours.should == schedule.hours and
192
+ new_schedule.day_of_months.should == schedule.day_of_months and
193
+ new_schedule.months.should == schedule.months and
194
+ new_schedule.day_of_weeks.should == schedule.day_of_weeks and
195
+ new_schedule.send(:day_of_months_given?).should == schedule.send(:day_of_months_given?) and
196
+ new_schedule.send(:day_of_weeks_given?).should == schedule.send(:day_of_weeks_given?)
197
+ end
198
+
199
+ it 'should return a new schedule starting at given Date' do
200
+ initial_start = Time.local(2009, 3, 25, 10, 20, 30)
201
+ schedule = Crontab::Schedule.new('* * * * *', initial_start)
202
+ new_start = Date.new(2009, 4, 25)
203
+ new_start_as_time = Time.local(new_start.year, new_start.month, new_start.day)
204
+ new_schedule = schedule.from(new_start)
205
+
206
+ new_schedule.object_id.should_not == schedule.object_id and
207
+ new_schedule.hash.should_not == schedule.hash and
208
+ new_schedule.should_not == schedule and
209
+
210
+ new_schedule.start.should == new_start_as_time and
211
+ new_schedule.minutes.should == schedule.minutes and
212
+ new_schedule.hours.should == schedule.hours and
213
+ new_schedule.day_of_months.should == schedule.day_of_months and
214
+ new_schedule.months.should == schedule.months and
215
+ new_schedule.day_of_weeks.should == schedule.day_of_weeks and
216
+ new_schedule.send(:day_of_months_given?).should == schedule.send(:day_of_months_given?) and
217
+ new_schedule.send(:day_of_weeks_given?).should == schedule.send(:day_of_weeks_given?)
218
+ end
219
+ end
220
+
221
+ describe 'when comparing' do
222
+ it 'should equal to other if and only if both are created from equivalent spec and start time' do
223
+ start = Time.now
224
+ Crontab::Schedule.new('0 0 1 1 0', start).should == Crontab::Schedule.new('0 0 1 1 0', start) and
225
+ Crontab::Schedule.new('0 0 1 1 0', start).should == Crontab::Schedule.new('0 0 1 Jan 0', start) and
226
+ Crontab::Schedule.new('0 0 1 1 0', start).should == Crontab::Schedule.new('0 0 1 1 Sun', start) and
227
+
228
+ Crontab::Schedule.new('0 0 1 1 0', start).should_not == Crontab::Schedule.new('1 0 1 1 0', start) and
229
+ Crontab::Schedule.new('0 0 1 1 0', start).should_not == Crontab::Schedule.new('0 1 1 1 0', start) and
230
+ Crontab::Schedule.new('0 0 1 1 0', start).should_not == Crontab::Schedule.new('0 0 2 1 0', start) and
231
+ Crontab::Schedule.new('0 0 1 1 0', start).should_not == Crontab::Schedule.new('0 0 1 2 0', start) and
232
+ Crontab::Schedule.new('0 0 1 1 0', start).should_not == Crontab::Schedule.new('0 0 1 1 1', start) and
233
+
234
+ Crontab::Schedule.new('0 0 1-5 1 0', start).should == Crontab::Schedule.new('0 0 1,2,3,4,5 1 0', start) and
235
+ Crontab::Schedule.new('0 0 1 1 0-4', start).should == Crontab::Schedule.new('0 0 1 1 0,1,2,3,4', start)
236
+ end
237
+ end
238
+
239
+ describe 'when enumerating' do
240
+ it 'should return given number of timings' do
241
+ schedule = Crontab::Schedule.new('* * * * *', Time.local(2009, 3, 8, 4, 30, 15))
242
+ expected = [
243
+ Time.local(2009, 3, 8, 4, 31),
244
+ Time.local(2009, 3, 8, 4, 32),
245
+ Time.local(2009, 3, 8, 4, 33),
246
+ Time.local(2009, 3, 8, 4, 34),
247
+ Time.local(2009, 3, 8, 4, 35),
248
+ ]
249
+ schedule.first(expected.size).should == expected and
250
+
251
+ schedule = Crontab::Schedule.new('5 10 3-5,10 * *', Time.local(2009, 3, 8, 10, 30, 15))
252
+ expected = [
253
+ Time.local(2009, 3, 10, 10, 5),
254
+ Time.local(2009, 4, 3, 10, 5),
255
+ Time.local(2009, 4, 4, 10, 5),
256
+ Time.local(2009, 4, 5, 10, 5),
257
+ Time.local(2009, 4, 10, 10, 5),
258
+ Time.local(2009, 5, 3, 10, 5),
259
+ Time.local(2009, 5, 4, 10, 5),
260
+ Time.local(2009, 5, 5, 10, 5),
261
+ Time.local(2009, 5, 10, 10, 5),
262
+ Time.local(2009, 6, 3, 10, 5),
263
+ ]
264
+ schedule.first(expected.size).should == expected
265
+ end
266
+
267
+ it 'should return timings matching day of month or day of week' do
268
+ schedule = Crontab::Schedule.new('0 0 1,10,11 3 0,1', Time.local(2009, 3, 8, 10, 30, 15))
269
+ expected = [
270
+ Time.local(2009, 3, 9, 0, 0),
271
+ Time.local(2009, 3, 10, 0, 0),
272
+ Time.local(2009, 3, 11, 0, 0),
273
+ Time.local(2009, 3, 15, 0, 0),
274
+ Time.local(2009, 3, 16, 0, 0),
275
+ Time.local(2009, 3, 22, 0, 0),
276
+ Time.local(2009, 3, 23, 0, 0),
277
+ Time.local(2009, 3, 29, 0, 0),
278
+ Time.local(2009, 3, 30, 0, 0),
279
+ ]
280
+ schedule.first(expected.size).should == expected and
281
+
282
+ schedule = Crontab::Schedule.new('0 0 1,10,11 3 *', Time.local(2009, 3, 8, 10, 30, 15))
283
+ expected = [
284
+ Time.local(2009, 3, 10, 0, 0),
285
+ Time.local(2009, 3, 11, 0, 0),
286
+ ]
287
+ schedule.first(expected.size).should == expected and
288
+
289
+ schedule = Crontab::Schedule.new('0 0 * 3 0,1', Time.local(2009, 3, 8, 10, 30, 15))
290
+ expected = [
291
+ Time.local(2009, 3, 9, 0, 0),
292
+ Time.local(2009, 3, 15, 0, 0),
293
+ Time.local(2009, 3, 16, 0, 0),
294
+ Time.local(2009, 3, 22, 0, 0),
295
+ Time.local(2009, 3, 23, 0, 0),
296
+ Time.local(2009, 3, 29, 0, 0),
297
+ Time.local(2009, 3, 30, 0, 0),
298
+ ]
299
+ schedule.first(expected.size).should == expected
300
+ end
301
+
302
+ it 'should return timings until given time' do
303
+ schedule = Crontab::Schedule.new('* * * * *', Time.local(2009, 3, 8, 4, 30, 15))
304
+ expected = [
305
+ Time.local(2009, 3, 8, 4, 31),
306
+ Time.local(2009, 3, 8, 4, 32),
307
+ Time.local(2009, 3, 8, 4, 33),
308
+ Time.local(2009, 3, 8, 4, 34),
309
+ Time.local(2009, 3, 8, 4, 35),
310
+ Time.local(2009, 3, 8, 4, 36),
311
+ Time.local(2009, 3, 8, 4, 37),
312
+ Time.local(2009, 3, 8, 4, 38),
313
+ Time.local(2009, 3, 8, 4, 39),
314
+ Time.local(2009, 3, 8, 4, 40),
315
+ ]
316
+ schedule.until(Time.local(2009, 3, 8, 4, 40, 10)).should == expected and
317
+
318
+ result = []
319
+ schedule.until(Time.local(2009, 3, 8, 4, 40, 10)) do |t|
320
+ result << t
321
+ end
322
+ result.should == expected and
323
+
324
+ schedule = Crontab::Schedule.new('*/20 0 21 * *', Time.local(2009, 3, 8, 4, 30, 15))
325
+ expected = [
326
+ Time.local(2009, 3, 21, 0, 0),
327
+ Time.local(2009, 3, 21, 0, 20),
328
+ Time.local(2009, 3, 21, 0, 40),
329
+ Time.local(2009, 4, 21, 0, 0),
330
+ Time.local(2009, 4, 21, 0, 20),
331
+ Time.local(2009, 4, 21, 0, 40),
332
+ Time.local(2009, 5, 21, 0, 00),
333
+ Time.local(2009, 5, 21, 0, 20),
334
+ Time.local(2009, 5, 21, 0, 40),
335
+ ]
336
+ schedule.until(Time.local(2009, 6, 1)).should == expected
337
+ end
338
+
339
+ it 'should exclude invalid day of month' do
340
+ schedule = Crontab::Schedule.new('5 0 27,29,31 * *', Time.local(2009, 1, 1, 10, 30, 15))
341
+ expected = [
342
+ Time.local(2009, 1, 27, 0, 5),
343
+ Time.local(2009, 1, 29, 0, 5),
344
+ Time.local(2009, 1, 31, 0, 5),
345
+ Time.local(2009, 2, 27, 0, 5),
346
+ # Time.local(2009, 2, 29, 0, 5), # should exclude this!
347
+ # Time.local(2009, 2, 31, 0, 5), # should exclude this!
348
+ Time.local(2009, 3, 27, 0, 5),
349
+ Time.local(2009, 3, 29, 0, 5),
350
+ Time.local(2009, 3, 31, 0, 5),
351
+ ]
352
+
353
+ schedule.first(expected.size).should == expected
354
+ end
355
+ end
356
+ end
@@ -0,0 +1,37 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+ require 'crontab'
3
+
4
+ describe Crontab do
5
+ describe 'parsing' do
6
+ it 'should recognize entries' do
7
+ crontab = Crontab.parse(File.read('spec/data/crontab.txt'))
8
+ crontab.entries.size.should == 5 and
9
+ crontab.entries.should be_frozen
10
+ crontab.entries[0].tap do |e|
11
+ e.schedule.minutes.should == [8] and
12
+ e.schedule.hours.should == [3] and
13
+ e.schedule.day_of_months.should == (1..31).to_a and
14
+ e.schedule.months.should == (1..12).to_a and
15
+ e.schedule.day_of_weeks.should == [6]
16
+ end
17
+ end
18
+
19
+ it 'should recognize variables assignments' do
20
+ crontab = Crontab.parse(File.read('spec/data/crontab.txt'))
21
+ crontab.env.should == {
22
+ 'SHELL' => '/bin/zsh',
23
+ 'MAIL_TO' => 'nobody@example.com',
24
+ 'FOO' => 'foo',
25
+ 'BAR' => 'bar',
26
+ 'BAZ' => 'baz'
27
+ }
28
+ end
29
+ end
30
+ end
31
+ # 8 3 * * 6 run-parts $HOME/.cron.d/weekly
32
+ # 50 2 * * * run-parts $HOME/.cron.d/daily
33
+ # 3 * * * * run-parts $HOME/.cron.d/hourly
34
+ #
35
+ # */5 * * * * curl -sI http://example.com >/dev/null
36
+ #
37
+ # * * * * * awk 'BEGIN{ srand(); exit(rand() * 24 * 60.0 < 12.0 ? 0 : 1); }' && fortune
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: crontab
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 2
10
+ version: 0.0.2
11
+ platform: ruby
12
+ authors:
13
+ - OZAWA Sakuro
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-02-04 00:00:00 +09:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description:
23
+ email: github@2238club.org
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files: []
29
+
30
+ files:
31
+ - lib/crontab.rb
32
+ - lib/crontab/entry.rb
33
+ - lib/crontab/schedule.rb
34
+ - spec/crontab_spec.rb
35
+ - spec/crontab/entry_spec.rb
36
+ - spec/crontab/schedule_spec.rb
37
+ - crontab.gemspec
38
+ - Rakefile
39
+ - README.rdoc
40
+ has_rdoc: true
41
+ homepage: http://github.com/sakuro/crontab
42
+ licenses: []
43
+
44
+ post_install_message:
45
+ rdoc_options: []
46
+
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ hash: 3
55
+ segments:
56
+ - 0
57
+ version: "0"
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ hash: 3
64
+ segments:
65
+ - 0
66
+ version: "0"
67
+ requirements: []
68
+
69
+ rubyforge_project: n/a
70
+ rubygems_version: 1.3.7
71
+ signing_key:
72
+ specification_version: 3
73
+ summary: A crontab(5) entry parser
74
+ test_files: []
75
+