schedule_job 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0aa14b51b198289543636f7048935e3599a08d5eb2920a2bb7e67b80d259f19d
4
+ data.tar.gz: 8841e4a6ec99c02c3485927d4ba6e4335a0347b52bba332fc131fff727225e4f
5
+ SHA512:
6
+ metadata.gz: 0f7832e1cc148800389da6b5f6ff3c053878cc930496f578e37af46c665f5dcb708db558b8220568c35c6ba5363e3a16351ee3453d65503cc494b1ed3b19c61d
7
+ data.tar.gz: c7d18b320759e7dccac481d1b4b9af06e91c09b0954d80e53650d7112ff53e202b4ed4c02ebb23039d4a634b53de9800aad9f0d1517fd60786d833d353af38e6
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in schedule.gemspec
4
+ gemspec
5
+
6
+ gem "citrus"
7
+ # gem "whenever"
8
+ gem "cronex"
9
+ gem "parse-cron"
10
+ gem "activesupport"
11
+ # gem "treetop"
12
+ # gem "polyglot"
data/Gemfile.lock ADDED
@@ -0,0 +1,58 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ schedule_job (1.0.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ activesupport (6.1.4.1)
10
+ concurrent-ruby (~> 1.0, >= 1.0.2)
11
+ i18n (>= 1.6, < 2)
12
+ minitest (>= 5.1)
13
+ tzinfo (~> 2.0)
14
+ zeitwerk (~> 2.3)
15
+ citrus (3.0.2)
16
+ concurrent-ruby (1.1.9)
17
+ cronex (0.11.1)
18
+ tzinfo
19
+ unicode
20
+ diff-lcs (1.4.4)
21
+ i18n (1.8.10)
22
+ concurrent-ruby (~> 1.0)
23
+ minitest (5.14.4)
24
+ parse-cron (0.1.4)
25
+ rake (13.0.6)
26
+ rspec (3.10.0)
27
+ rspec-core (~> 3.10.0)
28
+ rspec-expectations (~> 3.10.0)
29
+ rspec-mocks (~> 3.10.0)
30
+ rspec-core (3.10.1)
31
+ rspec-support (~> 3.10.0)
32
+ rspec-expectations (3.10.1)
33
+ diff-lcs (>= 1.2.0, < 2.0)
34
+ rspec-support (~> 3.10.0)
35
+ rspec-mocks (3.10.2)
36
+ diff-lcs (>= 1.2.0, < 2.0)
37
+ rspec-support (~> 3.10.0)
38
+ rspec-support (3.10.2)
39
+ tzinfo (2.0.4)
40
+ concurrent-ruby (~> 1.0)
41
+ unicode (0.4.4.4)
42
+ zeitwerk (2.5.1)
43
+
44
+ PLATFORMS
45
+ x86_64-darwin-18
46
+
47
+ DEPENDENCIES
48
+ activesupport
49
+ bundler
50
+ citrus
51
+ cronex
52
+ parse-cron
53
+ rake
54
+ rspec
55
+ schedule_job!
56
+
57
+ BUNDLED WITH
58
+ 2.2.16
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 David Ellis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # Schedule
2
+
3
+ schedule is a frontend around crontab to add/remove cron jobs from user cron tables.
4
+
5
+ ## Installation
6
+
7
+ ```
8
+ gem install schedule_job
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ## License
14
+
15
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/schedule ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "schedule"
4
+
5
+ def main
6
+ Schedule::Cli.run(ARGV)
7
+ end
8
+
9
+ main
@@ -0,0 +1,118 @@
1
+ require "optparse"
2
+
3
+ # schedule --every 10m "my_command --foo --bar"
4
+
5
+ module Schedule
6
+ class Cli
7
+ def self.run(argv = ARGV)
8
+ cli = Cli.new
9
+ args = cli.get_args(argv)
10
+ cli.execute(args)
11
+ end
12
+
13
+ def get_args(argv = ARGV)
14
+ options = {}
15
+
16
+ OptionParser.new do |opts|
17
+ opts.banner = "Usage: schedule [options] command"
18
+
19
+ opts.on("-d", "--dryrun", "Report what would happen but do not install the scheduled job.") do |dryrun|
20
+ options[:dryrun] = dryrun
21
+ end
22
+
23
+ opts.on("-l", "--list", "List installed cron jobs.") do |list|
24
+ options[:list_jobs] = list
25
+ end
26
+
27
+ opts.on("-r", "--rm JOB_ID", "Remove the specified job id as indicated by --list") do |job_id_to_remove|
28
+ options[:remove_job] = job_id_to_remove
29
+ end
30
+
31
+ opts.on("-u", "--user USER", "User that the crontab belongs to.") do |user|
32
+ options[:crontab_user] = user
33
+ end
34
+
35
+ opts.on("-e", "--every DURATION", "Run command every DURATION units of time.
36
+ For example:
37
+ --every 10m # meaning, every 10 minutes
38
+ --every 5h # meaning, every 5 hours
39
+ --every 2d # meaning, every 2 days
40
+ --every 3M # meaning, every 3 months
41
+ ") do |every|
42
+ options[:every] = every
43
+ end
44
+
45
+ opts.on("-c", "--cron CRON", "Run command on the given cron schedule.
46
+ For example:
47
+ --cron \"*/5 15 * * 1-5\" # meaning, Every 5 minutes, at 3:00 PM, Monday through Friday
48
+ --cron \"0 0/30 8-9 5,20 * ?\" # meaning, Every 30 minutes, between 8:00 AM and 9:59 AM, on day 5 and 20 of the month
49
+ ") do |cron_schedule|
50
+ options[:cron] = cron_schedule
51
+ end
52
+ end.parse!
53
+
54
+ options[:command] = ARGV.join(" ").strip
55
+ options
56
+ end
57
+
58
+ def execute(args)
59
+ case
60
+ when args[:list_jobs] # read jobs
61
+ list_jobs(args)
62
+ when args[:remove_job] # remove job
63
+ remove_job(args)
64
+ when args[:cron] || args[:every] # write jobs
65
+ install_job(args)
66
+ end
67
+ end
68
+
69
+ def list_jobs(args)
70
+ user = args[:crontab_user]
71
+
72
+ table = Cron::Table.new(user)
73
+
74
+ puts table.to_s unless table.empty?
75
+ end
76
+
77
+ def install_job(args)
78
+ dry_run = args[:dryrun]
79
+ command = args[:command]
80
+ user = args[:crontab_user]
81
+
82
+ if command.empty?
83
+ puts "A cron job command hasn't been specified. Please specify which command you would like to run."
84
+ exit(1)
85
+ end
86
+
87
+ job = case
88
+ when args[:cron]
89
+ cron_schedule = args[:cron]
90
+ Cron::Job.new(cron_schedule, command)
91
+ when args[:every]
92
+ duration = args[:every]
93
+ match = duration.match(/(\d+)(m|h|d|M)/)
94
+ if match
95
+ qty = match[1].to_i
96
+ unit_of_time = match[2].to_s
97
+ Cron::Job.every(qty, unit_of_time, command)
98
+ else
99
+ puts "'#{duration}' is an invalid duration. The duration must be specified as: integer followed immediately by m (minutes), h (hours), d (days), M (months) (e.g. 10m)"
100
+ exit(1)
101
+ end
102
+ end
103
+
104
+ table = Cron::Table.new(user)
105
+ table.add(job, dry_run)
106
+ end
107
+
108
+ def remove_job(args)
109
+ dry_run = args[:dryrun]
110
+ user = args[:crontab_user]
111
+ job_id = args[:remove_job].to_i # this is a 1-based index that should match the indices from the --list command
112
+
113
+ table = Cron::Table.new(user)
114
+
115
+ table.remove(job_id, dry_run)
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,167 @@
1
+ grammar ScheduleCronParser
2
+ rule user_crontab
3
+ sep?
4
+ (environment sep)?
5
+ jobspecs?
6
+ sep?
7
+ end
8
+
9
+ rule environment
10
+ directive (sep directive)*
11
+ end
12
+
13
+ rule directive
14
+ var space "=" space expr
15
+ end
16
+
17
+ rule var
18
+ alpha alphanumeric?
19
+ end
20
+
21
+ rule expr
22
+ command
23
+ end
24
+
25
+ rule jobspecs
26
+ jobspec (sep jobspec)*
27
+ end
28
+
29
+ rule jobspec
30
+ schedule_spec space command ws*
31
+ end
32
+
33
+ rule schedule_spec
34
+ standard | special
35
+ end
36
+
37
+ rule standard
38
+ minute space hour space dayofmonth space month space dayofweek
39
+ end
40
+
41
+ rule special
42
+ "@" ("yearly" | "annually" | "monthly" | "weekly" | "daily" | "hourly" | "reboot")
43
+ end
44
+
45
+ rule minute
46
+ step
47
+ end
48
+
49
+ rule hour
50
+ step
51
+ end
52
+
53
+ rule dayofmonthtypes
54
+ step
55
+ end
56
+
57
+ rule monthtypes
58
+ step | altmonths
59
+ end
60
+
61
+ rule dayofweektypes
62
+ step | altdays
63
+ end
64
+
65
+ rule dayofmonth
66
+ dayofmonthtypes ("," dayofmonthtypes)*
67
+ end
68
+
69
+ rule month
70
+ monthtypes ("," monthtypes)*
71
+ end
72
+
73
+ rule dayofweek
74
+ dayofweektypes ("," dayofweektypes)*
75
+ end
76
+
77
+ rule command
78
+ quoted_string
79
+ | (!("\n" | comment) .)+
80
+ end
81
+
82
+ rule step
83
+ common ("/" int)?
84
+ end
85
+
86
+ rule common
87
+ range | int | any
88
+ end
89
+
90
+ rule range
91
+ int "-" int
92
+ end
93
+
94
+ rule altdays
95
+ days ("," days)*
96
+ end
97
+
98
+ rule altmonths
99
+ months ("," months)*
100
+ end
101
+
102
+ rule days
103
+ "MON" | "TUE" | "WED" | "THU" | "FRI" | "SAT" | "SUN"
104
+ end
105
+
106
+ rule months
107
+ "JAN" | "FEB" | "MAR" | "APR" | "MAY" | "JUN" | "JUL" | "AUG" | "SEP" | "OCT" | "NOV" | "DEC"
108
+ end
109
+
110
+ rule any
111
+ "*"
112
+ end
113
+
114
+ rule int
115
+ [0-9]+
116
+ end
117
+
118
+ rule alpha
119
+ [a-zA-Z]+
120
+ end
121
+
122
+ rule alphanumeric
123
+ [a-zA-Z0-9]+
124
+ end
125
+
126
+ rule sep
127
+ (ws* nl)+
128
+ end
129
+
130
+ rule ws
131
+ space | comment
132
+ end
133
+
134
+ rule space
135
+ [ \t]+
136
+ end
137
+
138
+ rule nl
139
+ [\n]+
140
+ end
141
+
142
+ rule comment
143
+ "#" (!"\n" .)*
144
+ end
145
+
146
+ rule quoted_string
147
+ "\"" ( (!("\"" | "\\") .) | escape)* "\""
148
+ | "'" ( (!("'" | "\\") .) | escape)* "'"
149
+ end
150
+
151
+ rule escape
152
+ "\\" escape_sequence
153
+ end
154
+
155
+ rule escape_sequence
156
+ "'"
157
+ | "\""
158
+ | "\\"
159
+ | "b"
160
+ | "f"
161
+ | "n"
162
+ | "r"
163
+ | "t"
164
+ | "v"
165
+ end
166
+
167
+ end
@@ -0,0 +1,233 @@
1
+ require "active_support/core_ext/numeric/time"
2
+ require "active_support/core_ext/date_time"
3
+ require "cronex"
4
+ require "open3"
5
+ require "parse-cron"
6
+ require "shellwords"
7
+ require "stringio"
8
+
9
+ require_relative "./cron_parser"
10
+
11
+ module Schedule
12
+ module Cron
13
+
14
+ class EnvironmentVar
15
+ def initialize(name, value)
16
+ @name = name
17
+ @value = value
18
+ end
19
+
20
+ def to_s
21
+ "#{@name} = #{@value}"
22
+ end
23
+ end
24
+
25
+ class Table
26
+ def initialize(user = nil)
27
+ @user = user
28
+ load(user)
29
+ end
30
+
31
+ def empty?
32
+ @environment_vars.empty? && @jobs.empty?
33
+ end
34
+
35
+ def load(user = @user)
36
+ @raw_crontab = read_crontab(user)
37
+ @environment_vars, @jobs = parse_crontab(@raw_crontab)
38
+ end
39
+
40
+ def read_crontab(user = @user)
41
+ command = ["crontab", "-l"]
42
+ command << "-u #{user}" if user
43
+ command = command.join(" ")
44
+
45
+ stdout, stderr, exit_status = Open3.capture3(command)
46
+ crontab_output = stdout
47
+ error_output = stderr
48
+
49
+ no_crontab = !exit_status.success? && error_output =~ /no crontab/
50
+
51
+ raise("Unable to read crontab: #{error_output}") if !exit_status.success? && !no_crontab
52
+
53
+ # puts "read_crontab()"
54
+ # puts "stdout:"
55
+ # puts stdout
56
+ # puts "stderr:"
57
+ # puts stderr
58
+
59
+ crontab_output
60
+ end
61
+
62
+ # returns lines that represent valid cron job specification lines
63
+ def parse_crontab(user_crontab)
64
+ parser = TableParser.new(user_crontab)
65
+
66
+ # user_crontab.each_line do |line|
67
+ # puts line
68
+ # puts "valid? #{LineParser.valid?(line)}"
69
+ # puts LineParser.parse(line)
70
+ # end
71
+
72
+ return [], [] unless parser.valid?
73
+
74
+ # puts "valid!"
75
+
76
+ environment_vars = parser.environment_vars&.map do |env_directive|
77
+ name = env_directive[:var].to_s
78
+ expr = env_directive[:expr].to_s
79
+ EnvironmentVar.new(name, expr)
80
+ end || []
81
+
82
+ jobs = parser.job_specs&.map do |jobspec|
83
+ schedule_spec = jobspec.capture(:schedule_spec).to_s
84
+ command = jobspec.capture(:command).to_s
85
+ line_number = parser.get_line_number(jobspec)
86
+ Job.new(schedule_spec, command, line_number, jobspec.offset)
87
+ end || []
88
+
89
+ [environment_vars, jobs]
90
+ end
91
+
92
+ def add(job, dry_run = false)
93
+ if @dry_run
94
+ puts "Would schedule: #{job.to_s}"
95
+ else
96
+ puts "Scheduling: #{job.to_s}"
97
+ install_cron_job(job)
98
+ end
99
+ puts "Next three runs:"
100
+ cron_parser = CronParser.new(job.schedule_spec)
101
+ first_run_time = cron_parser.next(Time.now)
102
+ second_run_time = cron_parser.next(first_run_time)
103
+ third_run_time = cron_parser.next(second_run_time)
104
+ puts "1. #{first_run_time}"
105
+ puts "2. #{second_run_time}"
106
+ puts "3. #{third_run_time}"
107
+ end
108
+
109
+ def install_cron_job(job)
110
+ job_spec = job.specification
111
+ # puts "Installing new cron job: #{job_spec}"
112
+
113
+ new_crontab = [read_crontab(@user).strip, job_spec.strip].reject(&:empty?).join("\n")
114
+ write_crontab(new_crontab)
115
+ end
116
+
117
+ # because we print the table out with 1-based job IDs, job_id is a 1-based index into @jobs
118
+ def remove(job_id, dry_run = false)
119
+ job_index = job_id - 1
120
+
121
+ if job_index >= @jobs.size
122
+ puts "The specified job ID does not exist."
123
+ exit(1)
124
+ end
125
+
126
+ job = @jobs[job_index]
127
+
128
+ if dry_run
129
+ puts "Would remove: #{job}"
130
+ else
131
+ puts "Removing: #{job}"
132
+ new_crontab = @raw_crontab.lines.reject.with_index{|line, line_index| line_index == job.line_number - 1 }.join
133
+ write_crontab(new_crontab)
134
+ end
135
+ end
136
+
137
+ def write_crontab(new_crontab, user = @user)
138
+ command = ["crontab"]
139
+ command << "-u #{user}" if user
140
+ command << "-"
141
+ command = command.join(" ")
142
+
143
+ # command = "(crontab -l ; echo \"#{job_spec}\") | crontab -" # add new job to bottom of crontab, per https://stackoverflow.com/questions/610839/how-can-i-programmatically-create-a-new-cron-job
144
+ # puts "writing new crontab:"
145
+ # puts new_crontab
146
+ # puts "*" * 80
147
+
148
+ # puts "write_crontab:"
149
+ # puts "command: #{command}"
150
+ # puts "new crontab:"
151
+ # puts new_crontab
152
+
153
+ stdout, stderr, exit_status = Open3.capture3(command, stdin_data: new_crontab)
154
+ crontab_output = stdout
155
+ error_output = stderr
156
+
157
+ load(user)
158
+
159
+ raise("Unable to write to crontab: #{error_output}") unless exit_status.success?
160
+ end
161
+
162
+ def to_s
163
+ s = StringIO.new
164
+ if !@environment_vars.empty?
165
+ s.puts "Environment"
166
+ s.puts @environment_vars.join("\n")
167
+ end
168
+ if !@jobs.empty?
169
+ s.puts "Jobs"
170
+ @jobs.each_with_index do |job, i|
171
+ s.puts "#{i + 1}. #{job}"
172
+ end
173
+ end
174
+ s.string
175
+ end
176
+ end
177
+
178
+
179
+ class Job
180
+ def self.every(duration_quantity, duration_unit_of_time, command)
181
+ now = DateTime.now
182
+ next_moment = now + 1.minute
183
+ minute = next_moment.minute
184
+ hour = next_moment.hour
185
+ day = next_moment.day
186
+ schedule_spec = case duration_unit_of_time
187
+ when "m"
188
+ "*/#{duration_quantity} * * * *"
189
+ when "h"
190
+ "#{minute} */#{duration_quantity} * * *"
191
+ when "d"
192
+ "#{minute} #{hour} */#{duration_quantity} * *"
193
+ when "M"
194
+ "#{minute} #{hour} #{day} */#{duration_quantity} *"
195
+ end
196
+ self.new(schedule_spec, command)
197
+ end
198
+
199
+ attr_reader :schedule_spec
200
+ attr_reader :line_number
201
+
202
+ # schedule_spec is a cron specification string: minutes hours day-of-month month day-of-week
203
+ # see https://pkg.go.dev/github.com/robfig/cron?utm_source=godoc#hdr-CRON_Expression_Format
204
+ # see https://github.com/mileusna/crontab
205
+ # see https://github.com/josiahcarlson/parse-crontab
206
+
207
+ # line_numbrer is the 1-based index into the crontab file that this job was found at
208
+ # pos_offset is the 0-based index into the crontab file that this job was found at
209
+ def initialize(schedule_spec, command, line_number = nil, pos_offset = nil)
210
+ @schedule_spec = schedule_spec
211
+ @command = command
212
+ @pos_offset = pos_offset
213
+ @line_number = line_number
214
+ end
215
+
216
+ def specification
217
+ "#{@schedule_spec} #{escaped_command}"
218
+ end
219
+
220
+ def escaped_command
221
+ Shellwords.escape(@command).gsub("%", "\%") # % is a special character in cron job specifications; see https://serverfault.com/questions/274475/escaping-double-quotes-and-percent-signs-in-cron
222
+ end
223
+
224
+ def to_s
225
+ schedule = Cronex::ExpressionDescriptor.new(@schedule_spec).description
226
+ # str = "#{@command} #{schedule} (line #{@line_number}, pos #{@pos_offset})"
227
+ str = "#{@command} #{schedule}"
228
+ end
229
+
230
+ end
231
+
232
+ end
233
+ end
@@ -0,0 +1,167 @@
1
+ grammar Crontab
2
+ rule user_crontab
3
+ sep?
4
+ environment_spec:(environment sep)?
5
+ jobspecs?
6
+ sep?
7
+ end
8
+
9
+ rule environment
10
+ head:directive tail:(sep directive)*
11
+ end
12
+
13
+ rule directive
14
+ var space "=" space expr
15
+ end
16
+
17
+ rule var
18
+ alpha alphanumeric?
19
+ end
20
+
21
+ rule expr
22
+ command
23
+ end
24
+
25
+ rule jobspecs
26
+ head:jobspec tail:(sep jobspec)*
27
+ end
28
+
29
+ rule jobspec
30
+ schedule_spec space command ws*
31
+ end
32
+
33
+ rule schedule_spec
34
+ standard / special
35
+ end
36
+
37
+ rule standard
38
+ minute space hour space dayofmonth space month space dayofweek
39
+ end
40
+
41
+ rule special
42
+ "@" ("yearly" / "annually" / "monthly" / "weekly" / "daily" / "hourly" / "reboot")
43
+ end
44
+
45
+ rule minute
46
+ step
47
+ end
48
+
49
+ rule hour
50
+ step
51
+ end
52
+
53
+ rule dayofmonthtypes
54
+ step
55
+ end
56
+
57
+ rule monthtypes
58
+ step / altmonths
59
+ end
60
+
61
+ rule dayofweektypes
62
+ step / altdays
63
+ end
64
+
65
+ rule dayofmonth
66
+ dayofmonthtypes ("," dayofmonthtypes)*
67
+ end
68
+
69
+ rule month
70
+ monthtypes ("," monthtypes)*
71
+ end
72
+
73
+ rule dayofweek
74
+ dayofweektypes ("," dayofweektypes)*
75
+ end
76
+
77
+ rule command
78
+ quoted_string
79
+ / (!("\n" / comment) .)+
80
+ end
81
+
82
+ rule step
83
+ common ("/" int)?
84
+ end
85
+
86
+ rule common
87
+ range / int / any
88
+ end
89
+
90
+ rule range
91
+ int "-" int
92
+ end
93
+
94
+ rule altdays
95
+ days ("," days)*
96
+ end
97
+
98
+ rule altmonths
99
+ months ("," months)*
100
+ end
101
+
102
+ rule days
103
+ "MON" / "TUE" / "WED" / "THU" / "FRI" / "SAT" / "SUN"
104
+ end
105
+
106
+ rule months
107
+ "JAN" / "FEB" / "MAR" / "APR" / "MAY" / "JUN" / "JUL" / "AUG" / "SEP" / "OCT" / "NOV" / "DEC"
108
+ end
109
+
110
+ rule any
111
+ "*"
112
+ end
113
+
114
+ rule int
115
+ [0-9]+
116
+ end
117
+
118
+ rule alpha
119
+ [a-zA-Z]+
120
+ end
121
+
122
+ rule alphanumeric
123
+ [a-zA-Z0-9]+
124
+ end
125
+
126
+ rule sep
127
+ (ws* nl)+
128
+ end
129
+
130
+ rule ws
131
+ space / comment
132
+ end
133
+
134
+ rule space
135
+ [ \t]+
136
+ end
137
+
138
+ rule nl
139
+ [\n]+
140
+ end
141
+
142
+ rule comment
143
+ "#" (!"\n" .)*
144
+ end
145
+
146
+ rule quoted_string
147
+ "\"" ( (!("\"" / "\\") .) / escape)* "\""
148
+ / "'" ( (!("'" / "\\") .) / escape)* "'"
149
+ end
150
+
151
+ rule escape
152
+ "\\" escape_sequence
153
+ end
154
+
155
+ rule escape_sequence
156
+ "'"
157
+ / "\""
158
+ / "\\"
159
+ / "b"
160
+ / "f"
161
+ / "n"
162
+ / "r"
163
+ / "t"
164
+ / "v"
165
+ end
166
+
167
+ end
@@ -0,0 +1,85 @@
1
+ require "citrus"
2
+
3
+ module Citrus
4
+ class Input < StringScanner
5
+ def line_index(pos = pos())
6
+ p = n = 0
7
+ string.each_line do |line|
8
+ next_p = p + line.length
9
+ return n if p <= pos && pos < next_p
10
+ p = next_p
11
+ n += 1
12
+ end
13
+ 0
14
+ end
15
+ end
16
+ end
17
+
18
+ Citrus.require("schedule/cron")
19
+
20
+ # require "polyglot"
21
+ # require "treetop"
22
+ # require "schedule/cron.treetop"
23
+
24
+ module Schedule
25
+ module Cron
26
+ Grammar = ScheduleCronParser
27
+
28
+ class TableParser
29
+ def self.valid?(user_crontab_string)
30
+ !!parse(user_crontab_string)
31
+ rescue Citrus::ParseError => e
32
+ false
33
+ end
34
+
35
+ def self.parse(user_crontab_string)
36
+ Grammar.parse(user_crontab_string, root: :user_crontab)
37
+ # ::CrontabParser.new.parse(user_crontab_string, root: :user_crontab)
38
+ end
39
+
40
+ def initialize(user_crontab_string)
41
+ @user_crontab = TableParser.parse(user_crontab_string) rescue nil
42
+ end
43
+
44
+ def valid?
45
+ !!@user_crontab
46
+ end
47
+
48
+ def get_line_index(match)
49
+ @user_crontab.input.line_index(match.offset)
50
+ end
51
+
52
+ def get_line_number(match)
53
+ @user_crontab.input.line_number(match.offset)
54
+ end
55
+
56
+ def environment_vars
57
+ @user_crontab&.capture(:environment)&.captures(:directive)
58
+ # puts @user_crontab.methods.sort.inspect
59
+ # puts @user_crontab.inspect
60
+ # puts "*" * 80
61
+ # puts @user_crontab.environment_spec.inspect
62
+ # exit()
63
+ # @user_crontab&.environment&.directive
64
+ end
65
+
66
+ def job_specs
67
+ @user_crontab&.capture(:jobspecs)&.captures(:jobspec)
68
+ # @user_crontab&.jobspecs&.jobspec
69
+ end
70
+ end
71
+
72
+ class LineParser
73
+ def self.valid?(cron_line)
74
+ !!parse(cron_line)
75
+ rescue Citrus::ParseError => e
76
+ false
77
+ end
78
+
79
+ def self.parse(cron_line)
80
+ Grammar.parse(cron_line, root: :jobspec, consume: false)
81
+ # ::CrontabParser.new.parse(cron_line, root: :jobspec, consume_all_input: false)
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require "schedule/cli"
5
+ require "schedule/cron"
6
+
7
+ module ScheduleJob
8
+ VERSION = "1.0.0"
9
+ end
@@ -0,0 +1,23 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "schedule_job"
3
+ spec.version = "1.0.0"
4
+ spec.authors = ["David Ellis"]
5
+ spec.email = ["david@conquerthelawn.com"]
6
+
7
+ spec.summary = "job scheduler"
8
+ spec.description = "job scheduler"
9
+ spec.homepage = "https://github.com/davidkellis/scheduler"
10
+ spec.license = "MIT"
11
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
12
+
13
+ spec.files = Dir["**/**"].
14
+ grep_v(/.gem$/).
15
+ grep_v(%r{\A(?:test|spec|features)/})
16
+ spec.bindir = "bin"
17
+ spec.executables = spec.files.grep(%r{\Abin/}) { |f| File.basename(f) }
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "bundler"
21
+ spec.add_development_dependency "rake"
22
+ spec.add_development_dependency "rspec"
23
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: schedule_job
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - David Ellis
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-11-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: job scheduler
56
+ email:
57
+ - david@conquerthelawn.com
58
+ executables:
59
+ - schedule
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - Gemfile
64
+ - Gemfile.lock
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - bin/schedule
69
+ - lib/schedule/cli.rb
70
+ - lib/schedule/cron.citrus
71
+ - lib/schedule/cron.rb
72
+ - lib/schedule/cron.treetop
73
+ - lib/schedule/cron_parser.rb
74
+ - lib/schedule_job.rb
75
+ - schedule_job.gemspec
76
+ homepage: https://github.com/davidkellis/scheduler
77
+ licenses:
78
+ - MIT
79
+ metadata: {}
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 2.7.0
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubygems_version: 3.2.3
96
+ signing_key:
97
+ specification_version: 4
98
+ summary: job scheduler
99
+ test_files: []