From e8fa029096328c1586f8e452f4e665da09be21ac Mon Sep 17 00:00:00 2001 From: winebarrel Date: Sun, 10 Nov 2024 13:57:26 +0900 Subject: [PATCH] Add cronskd command --- .gitignore | 2 + .goreleaser.yml | 33 +++++++++++++++- Makefile | 2 + README.md | 39 +++++++++++++++++++ cmd/cronskd/flags.go | 70 ++++++++++++++++++++++++++++++++++ cmd/cronskd/go.mod | 14 +++++++ cmd/cronskd/go.sum | 23 ++++++++++++ cmd/cronskd/main.go | 89 ++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 cmd/cronskd/flags.go create mode 100644 cmd/cronskd/go.mod create mode 100644 cmd/cronskd/go.sum create mode 100644 cmd/cronskd/main.go diff --git a/.gitignore b/.gitignore index 2e88b76..5583f5b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ /cronviz.exe /crongrep /crongrep.exe +/cronskd +/cronskd.exe /dist/ diff --git a/.goreleaser.yml b/.goreleaser.yml index ba67bed..0f2636a 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -31,6 +31,13 @@ builds: - -X main.version={{.Version}} env: - CGO_ENABLED=0 + - id: cronskd + binary: cronskd + dir: ./cmd/cronskd + ldflags: + - -X main.version={{.Version}} + env: + - CGO_ENABLED=0 checksum: name_template: "checksums.txt" archives: @@ -46,6 +53,9 @@ archives: - id: crongrep builds: [crongrep] name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + - id: cronskd + builds: [cronskd] + name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" brews: - name: cronplan ids: [cronplan] @@ -87,6 +97,16 @@ brews: license: MIT install: | bin.install 'crongrep' + - name: cronskd + ids: [cronskd] + repository: + owner: winebarrel + name: homebrew-cronplan + homepage: https://github.com/winebarrel/cronplan + description: cronskd is a tool to show a schedule of cron expressions. + license: MIT + install: | + bin.install 'cronskd' nfpms: - id: cronplan-nfpms builds: [cronplan] @@ -126,7 +146,18 @@ nfpms: file_name_template: "{{ .Binary }}_{{ .Version }}_{{ .Arch }}" homepage: https://github.com/winebarrel/cronplan maintainer: Genki Sugawara - description: crongrep is a tool to visualize cron schedule. + description: crongrep is a tool to grep with cron expression. + license: MIT + formats: + - deb + - rpm + bindir: /usr/bin + - id: cronskd-nfpms + builds: [cronskd] + file_name_template: "{{ .Binary }}_{{ .Version }}_{{ .Arch }}" + homepage: https://github.com/winebarrel/cronplan + maintainer: Genki Sugawara + description: cronskd is a tool to show a schedule of cron expressions. license: MIT formats: - deb diff --git a/Makefile b/Makefile index e3c13a9..9ffb6d4 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ build: cd ./cmd/cronmatch && go build -o ../../cronmatch cd ./cmd/cronviz && go build -o ../../cronviz cd ./cmd/crongrep && go build -o ../../crongrep + cd ./cmd/cronskd && go build -o ../../cronskd .PHONY: vet vet: @@ -26,3 +27,4 @@ clean: rm -f cronmatch cronmatch.exe rm -f cronviz cronviz.exe rm -f crongrep crongrep.exe + rm -f cronskd cronskd.exe diff --git a/README.md b/README.md index 4e54b07..7359683 100644 --- a/README.md +++ b/README.md @@ -247,6 +247,45 @@ Wed, 11 Oct 2023 12:10:00 Thu, 26 Oct 2023 12:10:00 ``` +# cronskd CLI + +CLI to show a schedule of cron expressions. + +## Installation + +``` +brew install winebarrel/cronplan/cronskd +``` + +## Usage + +``` +Usage: cronskd [OPTION] [FILE] + -e string + end date (default: end of day) + -s string + start date (default: beginning of day) + -version + print version and exit +``` + +``` +$ cat exprs.txt +0 10 * * ? * +15 12 * * ? * +0 18 ? * MON-FRI * +0 8 1 * ? * +5 8-10 ? * MON-FRI * + +$ cronskd -s '2024-11-11' exprs.txt +Mon, 11 Nov 2024 08:05:00 5 8-10 ? * MON-FRI * +Mon, 11 Nov 2024 09:05:00 5 8-10 ? * MON-FRI * +Mon, 11 Nov 2024 10:00:00 0 10 * * ? * +Mon, 11 Nov 2024 10:05:00 5 8-10 ? * MON-FRI * +Mon, 11 Nov 2024 12:15:00 15 12 * * ? * +Mon, 11 Nov 2024 18:00:00 0 18 ? * MON-FRI * +``` + ## Related Links * [Cron expressions reference - Amazon EventBridge](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-cron-expressions.html) diff --git a/cmd/cronskd/flags.go b/cmd/cronskd/flags.go new file mode 100644 index 0000000..46f76ae --- /dev/null +++ b/cmd/cronskd/flags.go @@ -0,0 +1,70 @@ +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "strings" +) + +var ( + version string +) + +type flags struct { + file string + start string + end string +} + +func init() { + cmdLine := flag.NewFlagSet(filepath.Base(os.Args[0]), flag.ExitOnError) + + cmdLine.Usage = func() { + fmt.Fprintf(cmdLine.Output(), "Usage: %s [OPTION] [FILE]\n", cmdLine.Name()) + cmdLine.PrintDefaults() + } + + flag.CommandLine = cmdLine +} + +func parseFlags() *flags { + flags := &flags{} + flag.StringVar(&flags.start, "s", "", "start date (default: beginning of day)") + flag.StringVar(&flags.end, "e", "", "end date (default: end of day)") + showVersion := flag.Bool("version", false, "print version and exit") + flag.Parse() + + if *showVersion { + printVersionAndExit() + } + + args := flag.Args() + + if len(args) > 1 { + printUsageAndExit() + } else if len(args) == 0 { + flags.file = "-" + } else { + flags.file = strings.TrimSpace(args[0]) + } + + return flags +} + +func printVersionAndExit() { + v := version + + if v == "" { + v = "" + } + + fmt.Fprintln(flag.CommandLine.Output(), v) + os.Exit(0) +} + +func printUsageAndExit() { + flag.CommandLine.Usage() + os.Exit(0) +} diff --git a/cmd/cronskd/go.mod b/cmd/cronskd/go.mod new file mode 100644 index 0000000..aa310a7 --- /dev/null +++ b/cmd/cronskd/go.mod @@ -0,0 +1,14 @@ +module github.com/winebarrel/cronplan/cmd/cronskd + +go 1.21 + +toolchain go1.23.3 + +replace github.com/winebarrel/cronplan => ../.. + +require ( + github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de + github.com/winebarrel/cronplan v0.0.0-00010101000000-000000000000 +) + +require github.com/alecthomas/participle/v2 v2.1.1 // indirect diff --git a/cmd/cronskd/go.sum b/cmd/cronskd/go.sum new file mode 100644 index 0000000..1241240 --- /dev/null +++ b/cmd/cronskd/go.sum @@ -0,0 +1,23 @@ +github.com/alecthomas/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVdDZXL0= +github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= +github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= +github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= +github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= +github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= +github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cmd/cronskd/main.go b/cmd/cronskd/main.go new file mode 100644 index 0000000..a7bbadd --- /dev/null +++ b/cmd/cronskd/main.go @@ -0,0 +1,89 @@ +package main + +import ( + "bufio" + "fmt" + "log" + "os" + "sort" + "strings" + "time" + + "github.com/araddon/dateparse" + "github.com/winebarrel/cronplan" +) + +func init() { + log.SetFlags(0) +} + +func main() { + flags := parseFlags() + + var scanner *bufio.Scanner + + if flags.file == "-" { + scanner = bufio.NewScanner(os.Stdin) + } else { + file, err := os.Open(flags.file) + + if err != nil { + log.Fatal(err) + } + + defer file.Close() + + scanner = bufio.NewScanner(file) + } + + type exprNext struct { + expr string + next time.Time + } + + schedule := []exprNext{} + + for scanner.Scan() { + expr := scanner.Text() + expr = strings.TrimSpace(expr) + expr = strings.TrimPrefix(expr, "cron(") + expr = strings.TrimSuffix(expr, ")") + + cron, err := cronplan.Parse(expr) + + if err != nil { + log.Fatal(err) + } + + var start, end time.Time + + if flags.start == "" { + now := time.Now() + start = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + } else { + start, err = dateparse.ParseAny(flags.start) + + if err != nil { + log.Fatal(err) + } + } + + if flags.end == "" { + end = time.Date(start.Year(), start.Month(), start.Day(), 23, 59, 50, 0, start.Location()) + } + + nexts := cron.Between(start, end) + + for _, n := range nexts { + schedule = append(schedule, exprNext{expr: expr, next: n}) + } + } + + sort.Slice(schedule, func(i, j int) bool { + return schedule[i].next.Before(schedule[j].next) + }) + + for _, ln := range schedule { + fmt.Printf("%s\t%s\n", ln.next.Format("Mon, 02 Jan 2006 15:04:05"), ln.expr) + } +}