package cronimport ()// Configuration options for creating a parser. Most options specify which// fields should be included, while others enable features. If a field is not// included the parser will assume a default value. These options do not change// the order fields are parse in.typeParseOptionintconst (SecondParseOption = 1 << iota// Seconds field, default 0SecondOptional// Optional seconds field, default 0Minute// Minutes field, default 0Hour// Hours field, default 0Dom// Day of month field, default *Month// Month field, default *Dow// Day of week field, default *DowOptional// Optional day of week field, default *Descriptor// Allow descriptors such as @monthly, @weekly, etc.)var places = []ParseOption{Second,Minute,Hour,Dom,Month,Dow,}var defaults = []string{"0","0","0","*","*","*",}// A custom Parser that can be configured.typeParserstruct { options ParseOption}// NewParser creates a Parser with custom options.//// It panics if more than one Optional is given, since it would be impossible to// correctly infer which optional is provided or missing in general.//// Examples//// // Standard parser without descriptors// specParser := NewParser(Minute | Hour | Dom | Month | Dow)// sched, err := specParser.Parse("0 0 15 */3 *")//// // Same as above, just excludes time fields// subsParser := NewParser(Dom | Month | Dow)// sched, err := specParser.Parse("15 */3 *")//// // Same as above, just makes Dow optional// subsParser := NewParser(Dom | Month | DowOptional)// sched, err := specParser.Parse("15 */3")//func ( ParseOption) Parser { := 0if &DowOptional > 0 { ++ }if &SecondOptional > 0 { ++ }if > 1 {panic("multiple optionals may not be configured") }returnParser{}}// Parse returns a new crontab schedule representing the given spec.// It returns a descriptive error if the spec is not valid.// It accepts crontab specs and features configured by NewParser.func ( Parser) ( string) (Schedule, error) {iflen() == 0 {returnnil, fmt.Errorf("empty spec string") }// Extract timezone if presentvar = time.Localifstrings.HasPrefix(, "TZ=") || strings.HasPrefix(, "CRON_TZ=") {varerror := strings.Index(, " ") := strings.Index(, "=")if , = time.LoadLocation([+1 : ]); != nil {returnnil, fmt.Errorf("provided bad location %s: %v", [+1:], ) } = strings.TrimSpace([:]) }// Handle named schedules (descriptors), if configuredifstrings.HasPrefix(, "@") {if .options&Descriptor == 0 {returnnil, fmt.Errorf("parser does not accept descriptors: %v", ) }returnparseDescriptor(, ) }// Split on whitespace. := strings.Fields()// Validate & fill in any omitted or optional fieldsvarerror , = normalizeFields(, .options)if != nil {returnnil, } := func( string, bounds) uint64 {if != nil {return0 }varuint64 , = getField(, )return }var ( = ([0], seconds) = ([1], minutes) = ([2], hours) = ([3], dom) = ([4], months) = ([5], dow) )if != nil {returnnil, }return &SpecSchedule{Second: ,Minute: ,Hour: ,Dom: ,Month: ,Dow: ,Location: , }, nil}// normalizeFields takes a subset set of the time fields and returns the full set// with defaults (zeroes) populated for unset fields.//// As part of performing this function, it also validates that the provided// fields are compatible with the configured options.func normalizeFields( []string, ParseOption) ([]string, error) {// Validate optionals & add their field to options := 0if &SecondOptional > 0 { |= Second ++ }if &DowOptional > 0 { |= Dow ++ }if > 1 {returnnil, fmt.Errorf("multiple optionals may not be configured") }// Figure out how many fields we need := 0for , := rangeplaces {if & > 0 { ++ } } := - // Validate number of fieldsif := len(); < || > {if == {returnnil, fmt.Errorf("expected exactly %d fields, found %d: %s", , , ) }returnnil, fmt.Errorf("expected %d to %d fields, found %d: %s", , , , ) }// Populate the optional field if not providedif < && len() == {switch {case &DowOptional > 0: = append(, defaults[5]) // TODO: improve access to defaultcase &SecondOptional > 0: = append([]string{defaults[0]}, ...)default:returnnil, fmt.Errorf("unknown optional field") } }// Populate all fields not part of options with their defaults := 0 := make([]string, len(places))copy(, defaults)for , := rangeplaces {if & > 0 { [] = [] ++ } }return , nil}var standardParser = NewParser(Minute | Hour | Dom | Month | Dow | Descriptor,)// ParseStandard returns a new crontab schedule representing the given// standardSpec (https://en.wikipedia.org/wiki/Cron). It requires 5 entries// representing: minute, hour, day of month, month and day of week, in that// order. It returns a descriptive error if the spec is not valid.//// It accepts// - Standard crontab specs, e.g. "* * * * ?"// - Descriptors, e.g. "@midnight", "@every 1h30m"func ( string) (Schedule, error) {returnstandardParser.Parse()}// getField returns an Int with the bits set representing all of the times that// the field represents or error parsing field value. A "field" is a comma-separated// list of "ranges".func getField( string, bounds) (uint64, error) {varuint64 := strings.FieldsFunc(, func( rune) bool { return == ',' })for , := range { , := getRange(, )if != nil {return , } |= }return , nil}// getRange returns the bits indicated by the given expression:// number | number "-" number [ "/" number ]// or error parsing range.func getRange( string, bounds) (uint64, error) {var ( , , uint = strings.Split(, "/") = strings.Split([0], "-") = len() == 1error )varuint64if [0] == "*" || [0] == "?" { = .min = .max = starBit } else { , = parseIntOrName([0], .names)if != nil {return0, }switchlen() {case1: = case2: , = parseIntOrName([1], .names)if != nil {return0, }default:return0, fmt.Errorf("too many hyphens: %s", ) } }switchlen() {case1: = 1case2: , = mustParseInt([1])if != nil {return0, }// Special handling: "N/step" means "N-max/step".if { = .max }if > 1 { = 0 }default:return0, fmt.Errorf("too many slashes: %s", ) }if < .min {return0, fmt.Errorf("beginning of range (%d) below minimum (%d): %s", , .min, ) }if > .max {return0, fmt.Errorf("end of range (%d) above maximum (%d): %s", , .max, ) }if > {return0, fmt.Errorf("beginning of range (%d) beyond end of range (%d): %s", , , ) }if == 0 {return0, fmt.Errorf("step of range should be a positive number: %s", ) }returngetBits(, , ) | , nil}// parseIntOrName returns the (possibly-named) integer contained in expr.func parseIntOrName( string, map[string]uint) (uint, error) {if != nil {if , := [strings.ToLower()]; {return , nil } }returnmustParseInt()}// mustParseInt parses the given expression as an int or returns an error.func mustParseInt( string) (uint, error) { , := strconv.Atoi()if != nil {return0, fmt.Errorf("failed to parse int from %s: %s", , ) }if < 0 {return0, fmt.Errorf("negative number (%d) not allowed: %s", , ) }returnuint(), nil}// getBits sets all bits in the range [min, max], modulo the given step size.func getBits(, , uint) uint64 {varuint64// If step is 1, use shifts.if == 1 {return ^(math.MaxUint64 << ( + 1)) & (math.MaxUint64 << ) }// Else, use a simple loop.for := ; <= ; += { |= 1 << }return}// all returns all bits within the given bounds. (plus the star bit)func all( bounds) uint64 {returngetBits(.min, .max, 1) | starBit}// parseDescriptor returns a predefined schedule for the expression, or error if none matches.func parseDescriptor( string, *time.Location) (Schedule, error) {switch {case"@yearly", "@annually":return &SpecSchedule{Second: 1 << seconds.min,Minute: 1 << minutes.min,Hour: 1 << hours.min,Dom: 1 << dom.min,Month: 1 << months.min,Dow: all(dow),Location: , }, nilcase"@monthly":return &SpecSchedule{Second: 1 << seconds.min,Minute: 1 << minutes.min,Hour: 1 << hours.min,Dom: 1 << dom.min,Month: all(months),Dow: all(dow),Location: , }, nilcase"@weekly":return &SpecSchedule{Second: 1 << seconds.min,Minute: 1 << minutes.min,Hour: 1 << hours.min,Dom: all(dom),Month: all(months),Dow: 1 << dow.min,Location: , }, nilcase"@daily", "@midnight":return &SpecSchedule{Second: 1 << seconds.min,Minute: 1 << minutes.min,Hour: 1 << hours.min,Dom: all(dom),Month: all(months),Dow: all(dow),Location: , }, nilcase"@hourly":return &SpecSchedule{Second: 1 << seconds.min,Minute: 1 << minutes.min,Hour: all(hours),Dom: all(dom),Month: all(months),Dow: all(dow),Location: , }, nil }const = "@every "ifstrings.HasPrefix(, ) { , := time.ParseDuration([len():])if != nil {returnnil, fmt.Errorf("failed to parse duration %s: %s", , ) }returnEvery(), nil }returnnil, fmt.Errorf("unrecognized descriptor: %s", )}
The pages are generated with Goldsv0.6.7. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @Go100and1 (reachable from the left QR code) to get the latest news of Golds.