Skip to content

Commit fcc395c

Browse files
committed
Refactor parsing to return error instead of using panic/recover
Currently the parser code relies on recover function to catch errors and return them to the end user. The recommended go approach is to return errors from functions and check them. This commit refactors all log.Panicf calls to actually returning errors.
1 parent 23baae2 commit fcc395c

File tree

2 files changed

+237
-110
lines changed

2 files changed

+237
-110
lines changed

parser.go

+111-77
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package cron
22

33
import (
44
"fmt"
5-
"log"
65
"math"
76
"strconv"
87
"strings"
@@ -17,36 +16,46 @@ import (
1716
// It accepts
1817
// - Standard crontab specs, e.g. "* * * * ?"
1918
// - Descriptors, e.g. "@midnight", "@every 1h30m"
20-
func ParseStandard(standardSpec string) (_ Schedule, err error) {
21-
// Convert panics into errors
22-
defer func() {
23-
if recovered := recover(); recovered != nil {
24-
err = fmt.Errorf("%v", recovered)
25-
}
26-
}()
27-
19+
func ParseStandard(standardSpec string) (Schedule, error) {
2820
if standardSpec[0] == '@' {
29-
return parseDescriptor(standardSpec), nil
21+
return parseDescriptor(standardSpec)
3022
}
3123

3224
// Split on whitespace. We require exactly 5 fields.
3325
// (minute) (hour) (day of month) (month) (day of week)
3426
fields := strings.Fields(standardSpec)
3527
if len(fields) != 5 {
36-
log.Panicf("Expected exactly 5, found %d: %s", len(fields), standardSpec)
28+
return nil, fmt.Errorf("Expected exactly 5 fields, found %d: %s", len(fields), standardSpec)
3729
}
3830

39-
schedule := &SpecSchedule{
40-
Second: 1 << seconds.min,
41-
Minute: getField(fields[0], minutes),
42-
Hour: getField(fields[1], hours),
43-
Dom: getField(fields[2], dom),
44-
Month: getField(fields[3], months),
45-
Dow: getField(fields[4], dow),
31+
var err error
32+
field := func(field string, r bounds) uint64 {
33+
if err != nil {
34+
return uint64(0)
35+
}
36+
var bits uint64
37+
bits, err = getField(field, r)
38+
return bits
39+
}
40+
var (
41+
minute = field(fields[0], minutes)
42+
hour = field(fields[1], hours)
43+
dayofmonth = field(fields[2], dom)
44+
month = field(fields[3], months)
45+
dayofweek = field(fields[4], dow)
46+
)
47+
if err != nil {
48+
return nil, err
4649
}
4750

48-
return schedule, nil
49-
51+
return &SpecSchedule{
52+
Second: 1 << seconds.min,
53+
Minute: minute,
54+
Hour: hour,
55+
Dom: dayofmonth,
56+
Month: month,
57+
Dow: dayofweek,
58+
}, nil
5059
}
5160

5261
// Parse returns a new crontab schedule representing the given spec.
@@ -55,63 +64,81 @@ func ParseStandard(standardSpec string) (_ Schedule, err error) {
5564
// It accepts
5665
// - Full crontab specs, e.g. "* * * * * ?"
5766
// - Descriptors, e.g. "@midnight", "@every 1h30m"
58-
func Parse(spec string) (_ Schedule, err error) {
59-
// Convert panics into errors
60-
defer func() {
61-
if recovered := recover(); recovered != nil {
62-
err = fmt.Errorf("%v", recovered)
63-
}
64-
}()
65-
67+
func Parse(spec string) (Schedule, error) {
6668
if spec[0] == '@' {
67-
return parseDescriptor(spec), nil
69+
return parseDescriptor(spec)
6870
}
6971

7072
// Split on whitespace. We require 5 or 6 fields.
7173
// (second) (minute) (hour) (day of month) (month) (day of week, optional)
7274
fields := strings.Fields(spec)
7375
if len(fields) != 5 && len(fields) != 6 {
74-
log.Panicf("Expected 5 or 6 fields, found %d: %s", len(fields), spec)
76+
return nil, fmt.Errorf("Expected 5 or 6 fields, found %d: %s", len(fields), spec)
7577
}
7678

7779
// If a sixth field is not provided (DayOfWeek), then it is equivalent to star.
7880
if len(fields) == 5 {
7981
fields = append(fields, "*")
8082
}
8183

82-
schedule := &SpecSchedule{
83-
Second: getField(fields[0], seconds),
84-
Minute: getField(fields[1], minutes),
85-
Hour: getField(fields[2], hours),
86-
Dom: getField(fields[3], dom),
87-
Month: getField(fields[4], months),
88-
Dow: getField(fields[5], dow),
84+
var err error
85+
field := func(field string, r bounds) uint64 {
86+
if err != nil {
87+
return uint64(0)
88+
}
89+
var bits uint64
90+
bits, err = getField(field, r)
91+
return bits
92+
}
93+
var (
94+
second = field(fields[0], seconds)
95+
minute = field(fields[1], minutes)
96+
hour = field(fields[2], hours)
97+
dayofmonth = field(fields[3], dom)
98+
month = field(fields[4], months)
99+
dayofweek = field(fields[5], dow)
100+
)
101+
if err != nil {
102+
return nil, err
89103
}
90104

91-
return schedule, nil
105+
return &SpecSchedule{
106+
Second: second,
107+
Minute: minute,
108+
Hour: hour,
109+
Dom: dayofmonth,
110+
Month: month,
111+
Dow: dayofweek,
112+
}, nil
92113
}
93114

94115
// getField returns an Int with the bits set representing all of the times that
95-
// the field represents. A "field" is a comma-separated list of "ranges".
96-
func getField(field string, r bounds) uint64 {
97-
// list = range {"," range}
116+
// the field represents or error parsing field value. A "field" is a comma-separated
117+
// list of "ranges".
118+
func getField(field string, r bounds) (uint64, error) {
98119
var bits uint64
99120
ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
100121
for _, expr := range ranges {
101-
bits |= getRange(expr, r)
122+
bit, err := getRange(expr, r)
123+
if err != nil {
124+
return bits, err
125+
}
126+
bits |= bit
102127
}
103-
return bits
128+
return bits, nil
104129
}
105130

106131
// getRange returns the bits indicated by the given expression:
107132
// number | number "-" number [ "/" number ]
108-
func getRange(expr string, r bounds) uint64 {
109-
133+
// or error parsing range.
134+
func getRange(expr string, r bounds) (uint64, error) {
110135
var (
111136
start, end, step uint
112137
rangeAndStep = strings.Split(expr, "/")
113138
lowAndHigh = strings.Split(rangeAndStep[0], "-")
114139
singleDigit = len(lowAndHigh) == 1
140+
err error
141+
zero = uint64(0)
115142
)
116143

117144
var extra_star uint64
@@ -120,68 +147,77 @@ func getRange(expr string, r bounds) uint64 {
120147
end = r.max
121148
extra_star = starBit
122149
} else {
123-
start = parseIntOrName(lowAndHigh[0], r.names)
150+
start, err = parseIntOrName(lowAndHigh[0], r.names)
151+
if err != nil {
152+
return zero, err
153+
}
124154
switch len(lowAndHigh) {
125155
case 1:
126156
end = start
127157
case 2:
128-
end = parseIntOrName(lowAndHigh[1], r.names)
158+
end, err = parseIntOrName(lowAndHigh[1], r.names)
159+
if err != nil {
160+
return zero, err
161+
}
129162
default:
130-
log.Panicf("Too many hyphens: %s", expr)
163+
return zero, fmt.Errorf("Too many hyphens: %s", expr)
131164
}
132165
}
133166

134167
switch len(rangeAndStep) {
135168
case 1:
136169
step = 1
137170
case 2:
138-
step = mustParseInt(rangeAndStep[1])
171+
step, err = mustParseInt(rangeAndStep[1])
172+
if err != nil {
173+
return zero, err
174+
}
139175

140176
// Special handling: "N/step" means "N-max/step".
141177
if singleDigit {
142178
end = r.max
143179
}
144180
default:
145-
log.Panicf("Too many slashes: %s", expr)
181+
return zero, fmt.Errorf("Too many slashes: %s", expr)
146182
}
147183

148184
if start < r.min {
149-
log.Panicf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
185+
return zero, fmt.Errorf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
150186
}
151187
if end > r.max {
152-
log.Panicf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
188+
return zero, fmt.Errorf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
153189
}
154190
if start > end {
155-
log.Panicf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
191+
return zero, fmt.Errorf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
156192
}
157193
if step == 0 {
158-
log.Panicf("Step of range should be a positive number: %s", expr)
194+
return zero, fmt.Errorf("Step of range should be a positive number: %s", expr)
159195
}
160196

161-
return getBits(start, end, step) | extra_star
197+
return getBits(start, end, step) | extra_star, nil
162198
}
163199

164200
// parseIntOrName returns the (possibly-named) integer contained in expr.
165-
func parseIntOrName(expr string, names map[string]uint) uint {
201+
func parseIntOrName(expr string, names map[string]uint) (uint, error) {
166202
if names != nil {
167203
if namedInt, ok := names[strings.ToLower(expr)]; ok {
168-
return namedInt
204+
return namedInt, nil
169205
}
170206
}
171207
return mustParseInt(expr)
172208
}
173209

174-
// mustParseInt parses the given expression as an int or panics.
175-
func mustParseInt(expr string) uint {
210+
// mustParseInt parses the given expression as an int or returns an error.
211+
func mustParseInt(expr string) (uint, error) {
176212
num, err := strconv.Atoi(expr)
177213
if err != nil {
178-
log.Panicf("Failed to parse int from %s: %s", expr, err)
214+
return 0, fmt.Errorf("Failed to parse int from %s: %s", expr, err)
179215
}
180216
if num < 0 {
181-
log.Panicf("Negative number (%d) not allowed: %s", num, expr)
217+
return 0, fmt.Errorf("Negative number (%d) not allowed: %s", num, expr)
182218
}
183219

184-
return uint(num)
220+
return uint(num), nil
185221
}
186222

187223
// getBits sets all bits in the range [min, max], modulo the given step size.
@@ -205,10 +241,9 @@ func all(r bounds) uint64 {
205241
return getBits(r.min, r.max, 1) | starBit
206242
}
207243

208-
// parseDescriptor returns a pre-defined schedule for the expression, or panics
209-
// if none matches.
210-
func parseDescriptor(spec string) Schedule {
211-
switch spec {
244+
// parseDescriptor returns a predefined schedule for the expression, or error if none matches.
245+
func parseDescriptor(descriptor string) (Schedule, error) {
246+
switch descriptor {
212247
case "@yearly", "@annually":
213248
return &SpecSchedule{
214249
Second: 1 << seconds.min,
@@ -217,7 +252,7 @@ func parseDescriptor(spec string) Schedule {
217252
Dom: 1 << dom.min,
218253
Month: 1 << months.min,
219254
Dow: all(dow),
220-
}
255+
}, nil
221256

222257
case "@monthly":
223258
return &SpecSchedule{
@@ -227,7 +262,7 @@ func parseDescriptor(spec string) Schedule {
227262
Dom: 1 << dom.min,
228263
Month: all(months),
229264
Dow: all(dow),
230-
}
265+
}, nil
231266

232267
case "@weekly":
233268
return &SpecSchedule{
@@ -237,7 +272,7 @@ func parseDescriptor(spec string) Schedule {
237272
Dom: all(dom),
238273
Month: all(months),
239274
Dow: 1 << dow.min,
240-
}
275+
}, nil
241276

242277
case "@daily", "@midnight":
243278
return &SpecSchedule{
@@ -247,7 +282,7 @@ func parseDescriptor(spec string) Schedule {
247282
Dom: all(dom),
248283
Month: all(months),
249284
Dow: all(dow),
250-
}
285+
}, nil
251286

252287
case "@hourly":
253288
return &SpecSchedule{
@@ -257,18 +292,17 @@ func parseDescriptor(spec string) Schedule {
257292
Dom: all(dom),
258293
Month: all(months),
259294
Dow: all(dow),
260-
}
295+
}, nil
261296
}
262297

263298
const every = "@every "
264-
if strings.HasPrefix(spec, every) {
265-
duration, err := time.ParseDuration(spec[len(every):])
299+
if strings.HasPrefix(descriptor, every) {
300+
duration, err := time.ParseDuration(descriptor[len(every):])
266301
if err != nil {
267-
log.Panicf("Failed to parse duration %s: %s", spec, err)
302+
return nil, fmt.Errorf("Failed to parse duration %s: %s", descriptor, err)
268303
}
269-
return Every(duration)
304+
return Every(duration), nil
270305
}
271306

272-
log.Panicf("Unrecognized descriptor: %s", spec)
273-
return nil
307+
return nil, fmt.Errorf("Unrecognized descriptor: %s", descriptor)
274308
}

0 commit comments

Comments
 (0)