From e78a9ffa3d7d251be7a1410cef017a596995ef0e Mon Sep 17 00:00:00 2001 From: Patrick Gaskin Date: Thu, 16 Nov 2023 11:16:58 -0500 Subject: [PATCH] cmd/snappr: Refactor argument and input/output handling This will make it possible to test it later. --- cmd/snappr/main.go | 209 +++++++++++++++++++++++---------------------- 1 file changed, 108 insertions(+), 101 deletions(-) diff --git a/cmd/snappr/main.go b/cmd/snappr/main.go index 8de617b..9ca51ad 100644 --- a/cmd/snappr/main.go +++ b/cmd/snappr/main.go @@ -1,4 +1,4 @@ -// Command snappr +// Command snappr prunes time-based snapshots from stdin. package main import ( @@ -15,27 +15,19 @@ import ( "github.com/spf13/pflag" ) -var ( - Quiet = pflag.BoolP("quiet", "q", false, "do not show warnings about invalid or unmatched input lines") - Extract = pflag.StringP("extract", "e", "", "extract the timestamp from each input line using the provided regexp, which must contain up to one capture group") - Extended = pflag.BoolP("extended-regexp", "E", false, "use full regexp syntax rather than POSIX (see pkg.go.dev/regexp/syntax)") - Only = pflag.BoolP("only", "o", false, "only print the part of the line matching the regexp") - Parse = pflag.StringP("parse", "p", "", "parse the timestamp using the specified Go time format (see pkg.go.dev/time#pkg-constants and the examples below) rather than a unix timestamp") - ParseIn = pflag_TimezoneP("parse-timezone", "Z", nil, "use a specific timezone rather than whatever is set for --timezone if no timezone is parsed from the timestamp itself") - In = pflag_TimezoneP("timezone", "z", time.UTC, "convert all timestamps to this timezone while pruning snapshots (use \"local\" for the default system timezone)") - Invert = pflag.BoolP("invert", "v", false, "output the snapshots to keep instead of the ones to prune") - Why = pflag.BoolP("why", "w", false, "explain why each snapshot is being kept to stderr") - Summarize = pflag.BoolP("summarize", "s", false, "summarize retention policy results to stderr") - Help = pflag.BoolP("help", "h", false, "show this help text") -) +func main() { + if status := Main(os.Args, os.Stdin, os.Stdout, os.Stderr); status != 0 { + os.Exit(status) + } +} type timezoneFlag struct { loc *time.Location } -func pflag_TimezoneP(name, shorthand string, value *time.Location, usage string) **time.Location { +func pflag_TimezoneP(opt *pflag.FlagSet, name, shorthand string, value *time.Location, usage string) **time.Location { f := &timezoneFlag{value} - pflag.VarP(f, name, shorthand, usage) + opt.VarP(f, name, shorthand, usage) return &f.loc } @@ -68,12 +60,29 @@ func (t *timezoneFlag) Set(s string) error { return nil } -func main() { - pflag.Parse() +func Main(args []string, stdin io.Reader, stdout, stderr io.Writer) int { + opt := pflag.NewFlagSet(args[0], pflag.ContinueOnError) + var ( + Quiet = opt.BoolP("quiet", "q", false, "do not show warnings about invalid or unmatched input lines") + Extract = opt.StringP("extract", "e", "", "extract the timestamp from each input line using the provided regexp, which must contain up to one capture group") + Extended = opt.BoolP("extended-regexp", "E", false, "use full regexp syntax rather than POSIX (see pkg.go.dev/regexp/syntax)") + Only = opt.BoolP("only", "o", false, "only print the part of the line matching the regexp") + Parse = opt.StringP("parse", "p", "", "parse the timestamp using the specified Go time format (see pkg.go.dev/time#pkg-constants and the examples below) rather than a unix timestamp") + ParseIn = pflag_TimezoneP(opt, "parse-timezone", "Z", nil, "use a specific timezone rather than whatever is set for --timezone if no timezone is parsed from the timestamp itself") + In = pflag_TimezoneP(opt, "timezone", "z", time.UTC, "convert all timestamps to this timezone while pruning snapshots (use \"local\" for the default system timezone)") + Invert = opt.BoolP("invert", "v", false, "output the snapshots to keep instead of the ones to prune") + Why = opt.BoolP("why", "w", false, "explain why each snapshot is being kept to stderr") + Summarize = opt.BoolP("summarize", "s", false, "summarize retention policy results to stderr") + Help = opt.BoolP("help", "h", false, "show this help text") + ) + if err := opt.Parse(args[1:]); err != nil { + fmt.Fprintf(stderr, "snappr: fatal: %v\n", err) + return 2 + } - if pflag.NArg() < 1 || *Help { - fmt.Printf("usage: %s [options] policy...\n", os.Args[0]) - fmt.Printf("\noptions:\n%s", pflag.CommandLine.FlagUsages()) + if opt.NArg() < 1 || *Help { + fmt.Printf("usage: %s [options] policy...\n", args[0]) + fmt.Printf("\noptions:\n%s", opt.FlagUsages()) fmt.Printf("\ntime format examples:\n") fmt.Printf(" - Mon Jan 02 15:04:05 2006\n") fmt.Printf(" - 02 Jan 06 15:04 MST\n") @@ -99,26 +108,25 @@ func main() { fmt.Printf(" - if using --parse-in, beware of duplicate timestamps at DST transitions (if the offset isn't included whatever you use as the\n") fmt.Printf(" snapshot name, and your timezone has DST, you may end up with two snapshots for different times with the same name.\n") fmt.Printf(" - timezones will only affect the exact point at which calendar days/months/years are split\n") - if *Help { - os.Exit(0) - } else { - os.Exit(2) + if !*Help { + return 2 } + return 0 } if *In == nil { - fmt.Fprintf(os.Stderr, "snappr: fatal: timezone must not be empty\n") - os.Exit(2) + fmt.Fprintf(stderr, "snappr: fatal: timezone must not be empty\n") + return 2 } if *ParseIn == nil { *ParseIn = *In } - policy, err := snappr.ParsePolicy(pflag.Args()...) + policy, err := snappr.ParsePolicy(opt.Args()...) if err != nil { - fmt.Fprintf(os.Stderr, "snappr: fatal: invalid policy: %v\n", err) - os.Exit(2) + fmt.Fprintf(stderr, "snappr: fatal: invalid policy: %v\n", err) + return 2 } var extract *regexp.Regexp @@ -133,15 +141,73 @@ func main() { err = fmt.Errorf("must contain up to one capture group") } if err != nil { - fmt.Fprintf(os.Stderr, "snappr: fatal: --extract regexp is invalid: %v\n", err) - os.Exit(2) + fmt.Fprintf(stderr, "snappr: fatal: --extract regexp is invalid: %v\n", err) + return 2 } } - times, lines, err := scan(os.Stdin, extract, *ParseIn, *Parse, *Quiet, *Only) + times, lines, err := func() (times []time.Time, lines []string, err error) { + sc := bufio.NewScanner(stdin) + for sc.Scan() { + line := sc.Text() + if len(line) == 0 { + continue + } + + var bad bool + + var ts string + if extract == nil { + ts = strings.TrimSpace(line) + } else { + if m := extract.FindStringSubmatch(line); m == nil { + if !*Quiet { + fmt.Fprintf(stderr, "snappr: warning: failed to parse unix timestamp %q: %v\n", ts, err) + bad = true + } + } else { + if *Only { + line = m[0] + } + ts = m[len(m)-1] + } + } + + var t time.Time + if !bad { + if *Parse == "" { + if n, err := strconv.ParseInt(ts, 10, 64); err != nil { + if !*Quiet { + fmt.Fprintf(stderr, "snappr: warning: failed to parse unix timestamp %q: %v\n", ts, err) + } + bad = true + } else { + t = time.Unix(n, 0) + } + } else { + if v, err := time.ParseInLocation(*Parse, ts, *ParseIn); err != nil { + if !*Quiet { + fmt.Fprintf(stderr, "snappr: warning: failed to parse timestamp %q using layout %q: %v\n", ts, *Parse, err) + } + bad = true + } else { + t = v + } + } + } + + if bad { + times = append(times, time.Time{}) + } else { + times = append(times, t) + } + lines = append(lines, line) + } + return times, lines, sc.Err() + }() if err != nil { - fmt.Fprintf(os.Stderr, "snappr: fatal: failed to read stdin: %v\n", err) - os.Exit(2) + fmt.Fprintf(stderr, "snappr: fatal: failed to read stdin: %v\n", err) + return 1 } snapshots := make([]time.Time, 0, len(times)) @@ -169,7 +235,7 @@ func main() { continue } } - fmt.Println(lines[i]) + fmt.Fprintln(stdout, lines[i]) } var pruned int @@ -181,7 +247,7 @@ func main() { ps[i] = period.String() } if *Why { - fmt.Fprintf(os.Stderr, "snappr: why: keep [%*d/%*d] %s :: %s\n", ndig, at+1, ndig, len(keep), snapshots[at].Format("Mon 2006 Jan _2 15:04:05"), strings.Join(ps, ", ")) + fmt.Fprintf(stderr, "snappr: why: keep [%*d/%*d] %s :: %s\n", ndig, at+1, ndig, len(keep), snapshots[at].Format("Mon 2006 Jan _2 15:04:05"), strings.Join(ps, ", ")) } } else { pruned++ @@ -195,75 +261,16 @@ func main() { cdig := digits(cmax) need.Each(func(period snappr.Period, count int) { if count < 0 { - fmt.Fprintf(os.Stderr, "snappr: summary: (%s) %s\n", strings.Repeat("*", cdig), period) + fmt.Fprintf(stderr, "snappr: summary: (%s) %s\n", strings.Repeat("*", cdig), period) } else if count == 0 { - fmt.Fprintf(os.Stderr, "snappr: summary: (%*d) %s\n", cdig, policy.Get(period), period) + fmt.Fprintf(stderr, "snappr: summary: (%*d) %s\n", cdig, policy.Get(period), period) } else { - fmt.Fprintf(os.Stderr, "snappr: summary: (%*d) %s (missing %d)\n", cdig, policy.Get(period), period, count) + fmt.Fprintf(stderr, "snappr: summary: (%*d) %s (missing %d)\n", cdig, policy.Get(period), period, count) } }) - fmt.Fprintf(os.Stderr, "snappr: summary: pruning %d/%d snapshots\n", pruned, len(keep)) - } -} - -func scan(r io.Reader, extract *regexp.Regexp, tz *time.Location, layout string, quiet, only bool) (times []time.Time, lines []string, err error) { - sc := bufio.NewScanner(r) - for sc.Scan() { - line := sc.Text() - if len(line) == 0 { - continue - } - - var bad bool - - var ts string - if extract == nil { - ts = strings.TrimSpace(line) - } else { - if m := extract.FindStringSubmatch(line); m == nil { - if !quiet { - fmt.Fprintf(os.Stderr, "snappr: warning: failed to parse unix timestamp %q: %v\n", ts, err) - bad = true - } - } else { - if only { - line = m[0] - } - ts = m[len(m)-1] - } - } - - var t time.Time - if !bad { - if layout == "" { - if n, err := strconv.ParseInt(ts, 10, 64); err != nil { - if !quiet { - fmt.Fprintf(os.Stderr, "snappr: warning: failed to parse unix timestamp %q: %v\n", ts, err) - } - bad = true - } else { - t = time.Unix(n, 0) - } - } else { - if v, err := time.ParseInLocation(layout, ts, tz); err != nil { - if !quiet { - fmt.Fprintf(os.Stderr, "snappr: warning: failed to parse timestamp %q using layout %q: %v\n", ts, layout, err) - } - bad = true - } else { - t = v - } - } - } - - if bad { - times = append(times, time.Time{}) - } else { - times = append(times, t) - } - lines = append(lines, line) + fmt.Fprintf(stderr, "snappr: summary: pruning %d/%d snapshots\n", pruned, len(keep)) } - return times, lines, sc.Err() + return 0 } func digits(n int) int {