github.com/godaddy-x/freego@v1.0.156/job/parser.go (about)

     1  package job
     2  
     3  import (
     4  	"fmt"
     5  	"math"
     6  	"strconv"
     7  	"strings"
     8  	"time"
     9  )
    10  
    11  // Configuration options for creating a parser. Most options specify which
    12  // fields should be included, while others enable features. If a field is not
    13  // included the parser will assume a default value. These options do not change
    14  // the order fields are parse in.
    15  type ParseOption int
    16  
    17  const (
    18  	Second         ParseOption = 1 << iota // Seconds field, default 0
    19  	SecondOptional                         // Optional seconds field, default 0
    20  	Minute                                 // Minutes field, default 0
    21  	Hour                                   // Hours field, default 0
    22  	Dom                                    // Day of month field, default *
    23  	Month                                  // Month field, default *
    24  	Dow                                    // Day of week field, default *
    25  	DowOptional                            // Optional day of week field, default *
    26  	Descriptor                             // Allow descriptors such as @monthly, @weekly, etc.
    27  )
    28  
    29  var places = []ParseOption{
    30  	Second,
    31  	Minute,
    32  	Hour,
    33  	Dom,
    34  	Month,
    35  	Dow,
    36  }
    37  
    38  var defaults = []string{
    39  	"0",
    40  	"0",
    41  	"0",
    42  	"*",
    43  	"*",
    44  	"*",
    45  }
    46  
    47  // A custom Parser that can be configured.
    48  type Parser struct {
    49  	options ParseOption
    50  }
    51  
    52  // NewParser creates a Parser with custom options.
    53  //
    54  // It panics if more than one Optional is given, since it would be impossible to
    55  // correctly infer which optional is provided or missing in general.
    56  //
    57  // Examples
    58  //
    59  //  // Standard parser without descriptors
    60  //  specParser := NewParser(Minute | Hour | Dom | Month | Dow)
    61  //  sched, err := specParser.Parse("0 0 15 */3 *")
    62  //
    63  //  // Same as above, just excludes time fields
    64  //  subsParser := NewParser(Dom | Month | Dow)
    65  //  sched, err := specParser.Parse("15 */3 *")
    66  //
    67  //  // Same as above, just makes Dow optional
    68  //  subsParser := NewParser(Dom | Month | DowOptional)
    69  //  sched, err := specParser.Parse("15 */3")
    70  //
    71  func NewParser(options ParseOption) Parser {
    72  	optionals := 0
    73  	if options&DowOptional > 0 {
    74  		optionals++
    75  	}
    76  	if options&SecondOptional > 0 {
    77  		optionals++
    78  	}
    79  	if optionals > 1 {
    80  		panic("multiple optionals may not be configured")
    81  	}
    82  	return Parser{options}
    83  }
    84  
    85  // Parse returns a new crontab schedule representing the given spec.
    86  // It returns a descriptive error if the spec is not valid.
    87  // It accepts crontab specs and features configured by NewParser.
    88  func (p Parser) Parse(spec string) (Schedule, error) {
    89  	if len(spec) == 0 {
    90  		return nil, fmt.Errorf("empty spec string")
    91  	}
    92  
    93  	// Extract timezone if present
    94  	var loc = time.Local
    95  	if strings.HasPrefix(spec, "TZ=") || strings.HasPrefix(spec, "CRON_TZ=") {
    96  		var err error
    97  		i := strings.Index(spec, " ")
    98  		eq := strings.Index(spec, "=")
    99  		if loc, err = time.LoadLocation(spec[eq+1 : i]); err != nil {
   100  			return nil, fmt.Errorf("provided bad location %s: %v", spec[eq+1:i], err)
   101  		}
   102  		spec = strings.TrimSpace(spec[i:])
   103  	}
   104  
   105  	// Handle named schedules (descriptors), if configured
   106  	if strings.HasPrefix(spec, "@") {
   107  		if p.options&Descriptor == 0 {
   108  			return nil, fmt.Errorf("parser does not accept descriptors: %v", spec)
   109  		}
   110  		return parseDescriptor(spec, loc)
   111  	}
   112  
   113  	// Split on whitespace.
   114  	fields := strings.Fields(spec)
   115  
   116  	// Validate & fill in any omitted or optional fields
   117  	var err error
   118  	fields, err = normalizeFields(fields, p.options)
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  
   123  	field := func(field string, r bounds) uint64 {
   124  		if err != nil {
   125  			return 0
   126  		}
   127  		var bits uint64
   128  		bits, err = getField(field, r)
   129  		return bits
   130  	}
   131  
   132  	var (
   133  		second     = field(fields[0], seconds)
   134  		minute     = field(fields[1], minutes)
   135  		hour       = field(fields[2], hours)
   136  		dayofmonth = field(fields[3], dom)
   137  		month      = field(fields[4], months)
   138  		dayofweek  = field(fields[5], dow)
   139  	)
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  
   144  	return &SpecSchedule{
   145  		Second:   second,
   146  		Minute:   minute,
   147  		Hour:     hour,
   148  		Dom:      dayofmonth,
   149  		Month:    month,
   150  		Dow:      dayofweek,
   151  		Location: loc,
   152  	}, nil
   153  }
   154  
   155  // normalizeFields takes a subset set of the time fields and returns the full set
   156  // with defaults (zeroes) populated for unset fields.
   157  //
   158  // As part of performing this function, it also validates that the provided
   159  // fields are compatible with the configured options.
   160  func normalizeFields(fields []string, options ParseOption) ([]string, error) {
   161  	// Validate optionals & add their field to options
   162  	optionals := 0
   163  	if options&SecondOptional > 0 {
   164  		options |= Second
   165  		optionals++
   166  	}
   167  	if options&DowOptional > 0 {
   168  		options |= Dow
   169  		optionals++
   170  	}
   171  	if optionals > 1 {
   172  		return nil, fmt.Errorf("multiple optionals may not be configured")
   173  	}
   174  
   175  	// Figure out how many fields we need
   176  	max := 0
   177  	for _, place := range places {
   178  		if options&place > 0 {
   179  			max++
   180  		}
   181  	}
   182  	min := max - optionals
   183  
   184  	// Validate number of fields
   185  	if count := len(fields); count < min || count > max {
   186  		if min == max {
   187  			return nil, fmt.Errorf("expected exactly %d fields, found %d: %s", min, count, fields)
   188  		}
   189  		return nil, fmt.Errorf("expected %d to %d fields, found %d: %s", min, max, count, fields)
   190  	}
   191  
   192  	// Populate the optional field if not provided
   193  	if min < max && len(fields) == min {
   194  		switch {
   195  		case options&DowOptional > 0:
   196  			fields = append(fields, defaults[5]) // TODO: improve access to default
   197  		case options&SecondOptional > 0:
   198  			fields = append([]string{defaults[0]}, fields...)
   199  		default:
   200  			return nil, fmt.Errorf("unknown optional field")
   201  		}
   202  	}
   203  
   204  	// Populate all fields not part of options with their defaults
   205  	n := 0
   206  	expandedFields := make([]string, len(places))
   207  	copy(expandedFields, defaults)
   208  	for i, place := range places {
   209  		if options&place > 0 {
   210  			expandedFields[i] = fields[n]
   211  			n++
   212  		}
   213  	}
   214  	return expandedFields, nil
   215  }
   216  
   217  var standardParser = NewParser(
   218  	Minute | Hour | Dom | Month | Dow | Descriptor,
   219  )
   220  
   221  // ParseStandard returns a new crontab schedule representing the given
   222  // standardSpec (https://en.wikipedia.org/wiki/Cron). It requires 5 entries
   223  // representing: minute, hour, day of month, month and day of week, in that
   224  // order. It returns a descriptive error if the spec is not valid.
   225  //
   226  // It accepts
   227  //   - Standard crontab specs, e.g. "* * * * ?"
   228  //   - Descriptors, e.g. "@midnight", "@every 1h30m"
   229  func ParseStandard(standardSpec string) (Schedule, error) {
   230  	return standardParser.Parse(standardSpec)
   231  }
   232  
   233  // getField returns an Int with the bits set representing all of the times that
   234  // the field represents or error parsing field value.  A "field" is a comma-separated
   235  // list of "ranges".
   236  func getField(field string, r bounds) (uint64, error) {
   237  	var bits uint64
   238  	ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
   239  	for _, expr := range ranges {
   240  		bit, err := getRange(expr, r)
   241  		if err != nil {
   242  			return bits, err
   243  		}
   244  		bits |= bit
   245  	}
   246  	return bits, nil
   247  }
   248  
   249  // getRange returns the bits indicated by the given expression:
   250  //   number | number "-" number [ "/" number ]
   251  // or error parsing range.
   252  func getRange(expr string, r bounds) (uint64, error) {
   253  	var (
   254  		start, end, step uint
   255  		rangeAndStep     = strings.Split(expr, "/")
   256  		lowAndHigh       = strings.Split(rangeAndStep[0], "-")
   257  		singleDigit      = len(lowAndHigh) == 1
   258  		err              error
   259  	)
   260  
   261  	var extra uint64
   262  	if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
   263  		start = r.min
   264  		end = r.max
   265  		extra = starBit
   266  	} else {
   267  		start, err = parseIntOrName(lowAndHigh[0], r.names)
   268  		if err != nil {
   269  			return 0, err
   270  		}
   271  		switch len(lowAndHigh) {
   272  		case 1:
   273  			end = start
   274  		case 2:
   275  			end, err = parseIntOrName(lowAndHigh[1], r.names)
   276  			if err != nil {
   277  				return 0, err
   278  			}
   279  		default:
   280  			return 0, fmt.Errorf("too many hyphens: %s", expr)
   281  		}
   282  	}
   283  
   284  	switch len(rangeAndStep) {
   285  	case 1:
   286  		step = 1
   287  	case 2:
   288  		step, err = mustParseInt(rangeAndStep[1])
   289  		if err != nil {
   290  			return 0, err
   291  		}
   292  
   293  		// Special handling: "N/step" means "N-max/step".
   294  		if singleDigit {
   295  			end = r.max
   296  		}
   297  		if step > 1 {
   298  			extra = 0
   299  		}
   300  	default:
   301  		return 0, fmt.Errorf("too many slashes: %s", expr)
   302  	}
   303  
   304  	if start < r.min {
   305  		return 0, fmt.Errorf("beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
   306  	}
   307  	if end > r.max {
   308  		return 0, fmt.Errorf("end of range (%d) above maximum (%d): %s", end, r.max, expr)
   309  	}
   310  	if start > end {
   311  		return 0, fmt.Errorf("beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
   312  	}
   313  	if step == 0 {
   314  		return 0, fmt.Errorf("step of range should be a positive number: %s", expr)
   315  	}
   316  
   317  	return getBits(start, end, step) | extra, nil
   318  }
   319  
   320  // parseIntOrName returns the (possibly-named) integer contained in expr.
   321  func parseIntOrName(expr string, names map[string]uint) (uint, error) {
   322  	if names != nil {
   323  		if namedInt, ok := names[strings.ToLower(expr)]; ok {
   324  			return namedInt, nil
   325  		}
   326  	}
   327  	return mustParseInt(expr)
   328  }
   329  
   330  // mustParseInt parses the given expression as an int or returns an error.
   331  func mustParseInt(expr string) (uint, error) {
   332  	num, err := strconv.Atoi(expr)
   333  	if err != nil {
   334  		return 0, fmt.Errorf("failed to parse int from %s: %s", expr, err)
   335  	}
   336  	if num < 0 {
   337  		return 0, fmt.Errorf("negative number (%d) not allowed: %s", num, expr)
   338  	}
   339  
   340  	return uint(num), nil
   341  }
   342  
   343  // getBits sets all bits in the range [min, max], modulo the given step size.
   344  func getBits(min, max, step uint) uint64 {
   345  	var bits uint64
   346  
   347  	// If step is 1, use shifts.
   348  	if step == 1 {
   349  		return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
   350  	}
   351  
   352  	// Else, use a simple loop.
   353  	for i := min; i <= max; i += step {
   354  		bits |= 1 << i
   355  	}
   356  	return bits
   357  }
   358  
   359  // all returns all bits within the given bounds.  (plus the star bit)
   360  func all(r bounds) uint64 {
   361  	return getBits(r.min, r.max, 1) | starBit
   362  }
   363  
   364  // parseDescriptor returns a predefined schedule for the expression, or error if none matches.
   365  func parseDescriptor(descriptor string, loc *time.Location) (Schedule, error) {
   366  	switch descriptor {
   367  	case "@yearly", "@annually":
   368  		return &SpecSchedule{
   369  			Second:   1 << seconds.min,
   370  			Minute:   1 << minutes.min,
   371  			Hour:     1 << hours.min,
   372  			Dom:      1 << dom.min,
   373  			Month:    1 << months.min,
   374  			Dow:      all(dow),
   375  			Location: loc,
   376  		}, nil
   377  
   378  	case "@monthly":
   379  		return &SpecSchedule{
   380  			Second:   1 << seconds.min,
   381  			Minute:   1 << minutes.min,
   382  			Hour:     1 << hours.min,
   383  			Dom:      1 << dom.min,
   384  			Month:    all(months),
   385  			Dow:      all(dow),
   386  			Location: loc,
   387  		}, nil
   388  
   389  	case "@weekly":
   390  		return &SpecSchedule{
   391  			Second:   1 << seconds.min,
   392  			Minute:   1 << minutes.min,
   393  			Hour:     1 << hours.min,
   394  			Dom:      all(dom),
   395  			Month:    all(months),
   396  			Dow:      1 << dow.min,
   397  			Location: loc,
   398  		}, nil
   399  
   400  	case "@daily", "@midnight":
   401  		return &SpecSchedule{
   402  			Second:   1 << seconds.min,
   403  			Minute:   1 << minutes.min,
   404  			Hour:     1 << hours.min,
   405  			Dom:      all(dom),
   406  			Month:    all(months),
   407  			Dow:      all(dow),
   408  			Location: loc,
   409  		}, nil
   410  
   411  	case "@hourly":
   412  		return &SpecSchedule{
   413  			Second:   1 << seconds.min,
   414  			Minute:   1 << minutes.min,
   415  			Hour:     all(hours),
   416  			Dom:      all(dom),
   417  			Month:    all(months),
   418  			Dow:      all(dow),
   419  			Location: loc,
   420  		}, nil
   421  
   422  	}
   423  
   424  	const every = "@every "
   425  	if strings.HasPrefix(descriptor, every) {
   426  		duration, err := time.ParseDuration(descriptor[len(every):])
   427  		if err != nil {
   428  			return nil, fmt.Errorf("failed to parse duration %s: %s", descriptor, err)
   429  		}
   430  		return Every(duration), nil
   431  	}
   432  
   433  	return nil, fmt.Errorf("unrecognized descriptor: %s", descriptor)
   434  }