package crontab import ( "bufio" "fmt" "io" "regexp" "strings" "time" "github.com/aptible/supercronic/cronexpr" "github.com/sirupsen/logrus" ) var ( jobLineSeparator = regexp.MustCompile(`\S+`) envLineMatcher = regexp.MustCompile(`^([^\s=]+)\s*=\s*(.*)$`) parameterCounts = []int{ 7, // POSIX + seconds + years 6, // POSIX + years 5, // POSIX 1, // shorthand (e.g. @hourly) } ) func parseJobLine(line string) (*CrontabLine, error) { indices := jobLineSeparator.FindAllStringIndex(line, -1) for _, count := range parameterCounts { if len(indices) <= count { continue } scheduleEnds := indices[count-1][1] commandStarts := indices[count][0] toParse := line[:scheduleEnds] // TODO: Should receive a logger? logrus.Debugf("try parse (%d fields): '%s'", count, toParse) expr, err := cronexpr.ParseStrict(toParse) if err != nil { logrus.Debugf("failed to parse (%d fields): '%s': failed: %v", count, toParse, err) continue } return &CrontabLine{ Expression: expr, Schedule: line[:scheduleEnds], Command: line[commandStarts:], }, nil } return nil, fmt.Errorf("bad crontab line: '%s' (use -debug for details)", line) } func ParseCrontab(reader io.Reader) (*Crontab, error) { scanner := bufio.NewScanner(reader) position := 0 jobs := make([]*Job, 0) environ := make(map[string]string) shell := "/bin/sh" tz := time.Local for scanner.Scan() { line := strings.TrimLeft(scanner.Text(), " \t") if line == "" { continue } if line[0] == '#' { continue } r := envLineMatcher.FindAllStringSubmatch(line, -1) if len(r) == 1 && len(r[0]) == 3 { envKey := r[0][1] envVal := r[0][2] // Remove quotes (this emulates what Vixie cron does) if envVal[0] == '"' || envVal[0] == '\'' { if len(envVal) > 1 && envVal[0] == envVal[len(envVal)-1] { envVal = envVal[1 : len(envVal)-1] } } if envKey == "SHELL" { logrus.Infof("processes will be spawned using shell: %s", envVal) shell = envVal } if envKey == "USER" { logrus.Warnf("processes will NOT be spawned as USER=%s", envVal) } if envKey == "CRON_TZ" { var err error tz, err = time.LoadLocation(envVal) if err != nil { return nil, err } logrus.Infof("processes will be spawned using TZ: %v", tz) } environ[envKey] = envVal continue } jobLine, err := parseJobLine(line) if err != nil { return nil, err } jobs = append(jobs, &Job{CrontabLine: *jobLine, Position: position}) position++ } if err := scanner.Err(); err != nil { return nil, err } return &Crontab{ Jobs: jobs, Context: &Context{ Shell: shell, Environ: environ, Timezone: tz, }, }, nil }