cron_parser.go (1584B)
1 package jci 2 3 import ( 4 "bufio" 5 "errors" 6 "fmt" 7 "io" 8 "os" 9 "path/filepath" 10 "strings" 11 ) 12 13 // LoadCronEntries opens .jci/crontab (if it exists) and parses all entries. 14 func LoadCronEntries(repoRoot string) ([]CronEntry, error) { 15 cronPath := filepath.Join(repoRoot, ".jci", "crontab") 16 f, err := os.Open(cronPath) 17 if err != nil { 18 if os.IsNotExist(err) { 19 return nil, nil 20 } 21 return nil, err 22 } 23 defer f.Close() 24 25 return parseCronEntries(f) 26 } 27 28 func parseCronEntries(r io.Reader) ([]CronEntry, error) { 29 scanner := bufio.NewScanner(r) 30 var entries []CronEntry 31 lineNum := 0 32 33 for scanner.Scan() { 34 lineNum++ 35 raw := strings.TrimSpace(scanner.Text()) 36 if raw == "" || strings.HasPrefix(raw, "#") { 37 continue 38 } 39 40 schedule, command, err := splitCronLine(raw) 41 if err != nil { 42 return nil, fmt.Errorf("line %d: %w", lineNum, err) 43 } 44 45 entries = append(entries, CronEntry{ 46 Line: lineNum, 47 Schedule: schedule, 48 Command: command, 49 Raw: raw, 50 }) 51 } 52 53 if err := scanner.Err(); err != nil { 54 return nil, err 55 } 56 57 return entries, nil 58 } 59 60 func splitCronLine(raw string) (string, string, error) { 61 if strings.HasPrefix(raw, "@") { 62 parts := strings.Fields(raw) 63 if len(parts) < 2 { 64 return "", "", errors.New("missing command for cron entry") 65 } 66 return parts[0], strings.Join(parts[1:], " "), nil 67 } 68 69 parts := strings.Fields(raw) 70 if len(parts) < 6 { 71 return "", "", errors.New("cron entry must have 5 time fields and a command") 72 } 73 74 schedule := strings.Join(parts[:5], " ") 75 command := strings.Join(parts[5:], " ") 76 return schedule, command, nil 77 }