From cf5632e5d627265d429a3cb843aa970e8a53a641 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 30 Apr 2026 17:27:10 -0600 Subject: [PATCH] feat(ipcohort-contains): add --format pretty|tsv|csv|json Auto-detects format when --format is empty: pretty when stdout is a TTY, tsv when piped/redirected. JSON emits an array of {ip, found} objects; CSV adds an 'ip,status' header row. --- net/ipcohort/cmd/ipcohort-contains/main.go | 99 ++++++++++++++++++++-- net/ipcohort/go.mod | 4 + net/ipcohort/go.sum | 4 + net/ipcohort/ipcohort_test.go | 2 - 4 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 net/ipcohort/go.sum diff --git a/net/ipcohort/cmd/ipcohort-contains/main.go b/net/ipcohort/cmd/ipcohort-contains/main.go index 62a50b0..94863fb 100644 --- a/net/ipcohort/cmd/ipcohort-contains/main.go +++ b/net/ipcohort/cmd/ipcohort-contains/main.go @@ -12,6 +12,8 @@ package main import ( "bufio" + "encoding/csv" + "encoding/json" "errors" "flag" "fmt" @@ -20,6 +22,8 @@ import ( "strings" "time" + "golang.org/x/term" + "github.com/therootcompany/golib/net/ipcohort" ) @@ -50,13 +54,87 @@ func commafy(n int) string { } type Config struct { - IP string + IP string + Format string +} + +const ( + formatPretty = "pretty" + formatTSV = "tsv" + formatCSV = "csv" + formatJSON = "json" +) + +func parseFormat(s string) (string, error) { + switch s { + case "": + if term.IsTerminal(int(os.Stdout.Fd())) { + return formatPretty, nil + } + return formatTSV, nil + case formatPretty, formatTSV, formatCSV, formatJSON: + return s, nil + default: + return "", fmt.Errorf("unknown format %q (want pretty, tsv, csv, json)", s) + } +} + +type result struct { + IP string `json:"ip"` + Found bool `json:"found"` +} + +func writeResults(format string, results []result) error { + switch format { + case formatPretty: + w := 0 + for _, r := range results { + if len(r.IP) > w { + w = len(r.IP) + } + } + for _, r := range results { + status := "NOT FOUND" + if r.Found { + status = "FOUND" + } + fmt.Printf("%-*s %s\n", w, r.IP, status) + } + case formatTSV: + for _, r := range results { + status := "NOT FOUND" + if r.Found { + status = "FOUND" + } + fmt.Printf("%s\t%s\n", r.IP, status) + } + case formatCSV: + cw := csv.NewWriter(os.Stdout) + _ = cw.Write([]string{"ip", "status"}) + for _, r := range results { + status := "NOT FOUND" + if r.Found { + status = "FOUND" + } + if err := cw.Write([]string{r.IP, status}); err != nil { + return err + } + } + cw.Flush() + return cw.Error() + case formatJSON: + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(results) + } + return nil } func main() { cfg := Config{} fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) fs.StringVar(&cfg.IP, "ip", "", "IP address to check (alternative to -- separator)") + fs.StringVar(&cfg.Format, "format", "", "output format: pretty, tsv, csv, json (default: auto)") fs.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: %s [flags] ... -- ...\n", os.Args[0]) fmt.Fprintf(os.Stderr, " %s --ip ...\n", os.Args[0]) @@ -145,15 +223,26 @@ func main() { } fmt.Fprintln(os.Stderr) + + format, err := parseFormat(cfg.Format) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(2) + } + + results := make([]result, 0, len(ips)) allFound := true for _, ip := range ips { - if cohort.Contains(ip) { - fmt.Printf("%s\tFOUND\n", ip) - } else { - fmt.Printf("%s\tNOT FOUND\n", ip) + found := cohort.Contains(ip) + results = append(results, result{IP: ip, Found: found}) + if !found { allFound = false } } + if err := writeResults(format, results); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(2) + } if !allFound { os.Exit(1) diff --git a/net/ipcohort/go.mod b/net/ipcohort/go.mod index 65d7ae7..25d6bb7 100644 --- a/net/ipcohort/go.mod +++ b/net/ipcohort/go.mod @@ -1,3 +1,7 @@ module github.com/therootcompany/golib/net/ipcohort go 1.26.0 + +require golang.org/x/term v0.42.0 + +require golang.org/x/sys v0.43.0 // indirect diff --git a/net/ipcohort/go.sum b/net/ipcohort/go.sum new file mode 100644 index 0000000..11efb67 --- /dev/null +++ b/net/ipcohort/go.sum @@ -0,0 +1,4 @@ +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= diff --git a/net/ipcohort/ipcohort_test.go b/net/ipcohort/ipcohort_test.go index c5e9d53..041e4d5 100644 --- a/net/ipcohort/ipcohort_test.go +++ b/net/ipcohort/ipcohort_test.go @@ -103,7 +103,6 @@ func TestContains_FailClosed(t *testing.T) { } } - func TestContains_Empty(t *testing.T) { c, err := ipcohort.Parse(nil) if err != nil { @@ -163,4 +162,3 @@ func TestLoadFiles_Merge(t *testing.T) { t.Error("missing merged CIDR member") } } -