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