parser.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. package cron
  2. import (
  3. "fmt"
  4. "log"
  5. "math"
  6. "strconv"
  7. "strings"
  8. "time"
  9. )
  10. // Parse returns a new crontab schedule representing the given spec.
  11. // It returns a descriptive error if the spec is not valid.
  12. //
  13. // It accepts
  14. // - Full crontab specs, e.g. "* * * * * ?"
  15. // - Descriptors, e.g. "@midnight", "@every 1h30m"
  16. func Parse(spec string) (_ Schedule, err error) {
  17. // Convert panics into errors
  18. defer func() {
  19. if recovered := recover(); recovered != nil {
  20. err = fmt.Errorf("%v", recovered)
  21. }
  22. }()
  23. // Extract timezone if present
  24. var loc = time.Local
  25. if strings.HasPrefix(spec, "TZ=") {
  26. i := strings.Index(spec, " ")
  27. if loc, err = time.LoadLocation(spec[3:i]); err != nil {
  28. log.Panicf("Provided bad location %s: %v", spec[3:i], err)
  29. }
  30. spec = strings.TrimSpace(spec[i:])
  31. }
  32. // Handle named schedules (descriptors)
  33. if strings.HasPrefix(spec, "@") {
  34. return parseDescriptor(spec, loc), nil
  35. }
  36. // Split on whitespace. We require 5 or 6 fields.
  37. // (second, optional) (minute) (hour) (day of month) (month) (day of week)
  38. fields := strings.Fields(spec)
  39. if len(fields) != 5 && len(fields) != 6 {
  40. log.Panicf("Expected 5 or 6 fields, found %d: %s", len(fields), spec)
  41. }
  42. // Add 0 for second field if necessary.
  43. if len(fields) == 5 {
  44. fields = append([]string{"0"}, fields...)
  45. }
  46. schedule := &SpecSchedule{
  47. Second: getField(fields[0], seconds),
  48. Minute: getField(fields[1], minutes),
  49. Hour: getField(fields[2], hours),
  50. Dom: getField(fields[3], dom),
  51. Month: getField(fields[4], months),
  52. Dow: getField(fields[5], dow),
  53. Location: loc,
  54. }
  55. return schedule, nil
  56. }
  57. // getField returns an Int with the bits set representing all of the times that
  58. // the field represents. A "field" is a comma-separated list of "ranges".
  59. func getField(field string, r bounds) uint64 {
  60. // list = range {"," range}
  61. var bits uint64
  62. ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
  63. for _, expr := range ranges {
  64. bits |= getRange(expr, r)
  65. }
  66. return bits
  67. }
  68. // getRange returns the bits indicated by the given expression:
  69. // number | number "-" number [ "/" number ]
  70. func getRange(expr string, r bounds) uint64 {
  71. var (
  72. start, end, step uint
  73. rangeAndStep = strings.Split(expr, "/")
  74. lowAndHigh = strings.Split(rangeAndStep[0], "-")
  75. singleDigit = len(lowAndHigh) == 1
  76. extraStar uint64
  77. )
  78. if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
  79. start = r.min
  80. end = r.max
  81. extraStar = starBit
  82. } else {
  83. start = parseIntOrName(lowAndHigh[0], r.names)
  84. switch len(lowAndHigh) {
  85. case 1:
  86. end = start
  87. case 2:
  88. end = parseIntOrName(lowAndHigh[1], r.names)
  89. default:
  90. log.Panicf("Too many hyphens: %s", expr)
  91. }
  92. }
  93. switch len(rangeAndStep) {
  94. case 1:
  95. step = 1
  96. case 2:
  97. step = mustParseInt(rangeAndStep[1])
  98. // Special handling: "N/step" means "N-max/step".
  99. if singleDigit {
  100. end = r.max
  101. }
  102. default:
  103. log.Panicf("Too many slashes: %s", expr)
  104. }
  105. if start < r.min {
  106. log.Panicf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
  107. }
  108. if end > r.max {
  109. log.Panicf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
  110. }
  111. if start > end {
  112. log.Panicf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
  113. }
  114. return getBits(start, end, step) | extraStar
  115. }
  116. // parseIntOrName returns the (possibly-named) integer contained in expr.
  117. func parseIntOrName(expr string, names map[string]uint) uint {
  118. if names != nil {
  119. if namedInt, ok := names[strings.ToLower(expr)]; ok {
  120. return namedInt
  121. }
  122. }
  123. return mustParseInt(expr)
  124. }
  125. // mustParseInt parses the given expression as an int or panics.
  126. func mustParseInt(expr string) uint {
  127. num, err := strconv.Atoi(expr)
  128. if err != nil {
  129. log.Panicf("Failed to parse int from %s: %s", expr, err)
  130. }
  131. if num < 0 {
  132. log.Panicf("Negative number (%d) not allowed: %s", num, expr)
  133. }
  134. return uint(num)
  135. }
  136. // getBits sets all bits in the range [min, max], modulo the given step size.
  137. func getBits(min, max, step uint) uint64 {
  138. var bits uint64
  139. // If step is 1, use shifts.
  140. if step == 1 {
  141. return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
  142. }
  143. // Else, use a simple loop.
  144. for i := min; i <= max; i += step {
  145. bits |= 1 << i
  146. }
  147. return bits
  148. }
  149. // all returns all bits within the given bounds. (plus the star bit)
  150. func all(r bounds) uint64 {
  151. return getBits(r.min, r.max, 1) | starBit
  152. }
  153. // parseDescriptor returns a pre-defined schedule for the expression, or panics
  154. // if none matches.
  155. func parseDescriptor(spec string, loc *time.Location) Schedule {
  156. switch spec {
  157. case "@yearly", "@annually":
  158. return &SpecSchedule{
  159. Second: 1 << seconds.min,
  160. Minute: 1 << minutes.min,
  161. Hour: 1 << hours.min,
  162. Dom: 1 << dom.min,
  163. Month: 1 << months.min,
  164. Dow: all(dow),
  165. Location: loc,
  166. }
  167. case "@monthly":
  168. return &SpecSchedule{
  169. Second: 1 << seconds.min,
  170. Minute: 1 << minutes.min,
  171. Hour: 1 << hours.min,
  172. Dom: 1 << dom.min,
  173. Month: all(months),
  174. Dow: all(dow),
  175. Location: loc,
  176. }
  177. case "@weekly":
  178. return &SpecSchedule{
  179. Second: 1 << seconds.min,
  180. Minute: 1 << minutes.min,
  181. Hour: 1 << hours.min,
  182. Dom: all(dom),
  183. Month: all(months),
  184. Dow: 1 << dow.min,
  185. Location: loc,
  186. }
  187. case "@daily", "@midnight":
  188. return &SpecSchedule{
  189. Second: 1 << seconds.min,
  190. Minute: 1 << minutes.min,
  191. Hour: 1 << hours.min,
  192. Dom: all(dom),
  193. Month: all(months),
  194. Dow: all(dow),
  195. Location: loc,
  196. }
  197. case "@hourly":
  198. return &SpecSchedule{
  199. Second: 1 << seconds.min,
  200. Minute: 1 << minutes.min,
  201. Hour: all(hours),
  202. Dom: all(dom),
  203. Month: all(months),
  204. Dow: all(dow),
  205. Location: loc,
  206. }
  207. }
  208. const every = "@every "
  209. if strings.HasPrefix(spec, every) {
  210. duration, err := time.ParseDuration(spec[len(every):])
  211. if err != nil {
  212. log.Panicf("Failed to parse duration %s: %s", spec, err)
  213. }
  214. return Every(duration)
  215. }
  216. log.Panicf("Unrecognized descriptor: %s", spec)
  217. return nil
  218. }