Skip to content

Commit

Permalink
cmd/snappr: Refactor argument and input/output handling
Browse files Browse the repository at this point in the history
This will make it possible to test it later.
  • Loading branch information
pgaskin committed Nov 16, 2023
1 parent c50451a commit e78a9ff
Showing 1 changed file with 108 additions and 101 deletions.
209 changes: 108 additions & 101 deletions cmd/snappr/main.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Command snappr
// Command snappr prunes time-based snapshots from stdin.
package main

import (
Expand All @@ -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
}

Expand Down Expand Up @@ -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")
Expand All @@ -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
Expand All @@ -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))
Expand Down Expand Up @@ -169,7 +235,7 @@ func main() {
continue
}
}
fmt.Println(lines[i])
fmt.Fprintln(stdout, lines[i])
}

var pruned int
Expand All @@ -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++
Expand All @@ -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 {
Expand Down

0 comments on commit e78a9ff

Please sign in to comment.