From 2c1dd94a93796b7b348e538c7e8a3016ee7d6b34 Mon Sep 17 00:00:00 2001 From: Alex Harford Date: Fri, 25 Aug 2023 10:45:16 -0700 Subject: [PATCH] bind: Add JSON formatting option to DescribeFlags This is useful for exporting or recording config settings. --- bind/flag.go | 47 ++++++++- bind/flag_test.go | 156 +++++++++++++++++++++++++++++ cmd/forwarder/httpbin/httpbin.go | 5 +- cmd/forwarder/pac/server/server.go | 5 +- cmd/forwarder/run/run.go | 5 +- 5 files changed, 210 insertions(+), 8 deletions(-) create mode 100644 bind/flag_test.go diff --git a/bind/flag.go b/bind/flag.go index ee75f81c2..d6938458b 100644 --- a/bind/flag.go +++ b/bind/flag.go @@ -7,10 +7,12 @@ package bind import ( + "encoding/json" "fmt" "net/netip" "net/url" "os" + "sort" "strings" "github.com/mmatczuk/anyflag" @@ -23,6 +25,13 @@ import ( "github.com/spf13/pflag" ) +type Format int + +const ( + Plain Format = 0 + JSON Format = 1 +) + func ConfigFile(fs *pflag.FlagSet, configFile *string) { fs.StringVarP(configFile, "config-file", "c", *configFile, ""+ @@ -268,13 +277,41 @@ func MarkFlagFilename(cmd *cobra.Command, names ...string) { } } -func DescribeFlags(fs *pflag.FlagSet) string { - var b strings.Builder +func DescribeFlags(fs *pflag.FlagSet, showHidden bool, format Format) (string, error) { + args := make(map[string]any, 0) + keys := make([]string, 0) + fs.VisitAll(func(flag *pflag.Flag) { - if flag.Hidden || flag.Name == "help" { + if flag.Name == "help" { + return + } + + if flag.Hidden && !showHidden { return } - b.WriteString(fmt.Sprintf("%s=%s\n", flag.Name, strings.Trim(flag.Value.String(), "[]"))) + + if flag.Value.Type() == "bool" { + args[flag.Name] = flag.Value + } else { + args[flag.Name] = strings.Trim(flag.Value.String(), "[]") + } + + keys = append(keys, flag.Name) }) - return b.String() + + sort.Strings(keys) + + switch format { + case Plain: + var b strings.Builder + for _, name := range keys { + b.WriteString(fmt.Sprintf("%s=%s\n", name, args[name])) + } + return b.String(), nil + case JSON: + encoded, err := json.Marshal(args) + return string(encoded), err + default: + return "", fmt.Errorf("Unknown format requested") + } } diff --git a/bind/flag_test.go b/bind/flag_test.go new file mode 100644 index 000000000..50f7e8466 --- /dev/null +++ b/bind/flag_test.go @@ -0,0 +1,156 @@ +// Copyright 2023 Sauce Labs Inc. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package bind + +import ( + "testing" + + "github.com/spf13/pflag" +) + +func TestDescribeFlagsAsPlain(t *testing.T) { + tests := map[string]struct { + input map[string]interface{} + expected string + isErr bool + isHidden bool + showHidden bool + }{ + "keys are sorted": { + input: map[string]interface{}{"foo": false, "bar": true}, + expected: "bar=true\nfoo=false\n", + isErr: false, + }, + "bool is correctly formatted": { + input: map[string]interface{}{"key": false}, + expected: "key=false\n", + isErr: false, + }, + "string is correctly formatted": { + input: map[string]interface{}{"key": "val"}, + expected: "key=val\n", + isErr: false, + }, + "help is not shown": { + input: map[string]interface{}{"key": false, "help": true}, + expected: "key=false\n", + isErr: false, + }, + "hidden is shown": { + input: map[string]interface{}{"key": false}, + expected: "key=false\n", + isErr: false, + isHidden: true, + showHidden: true, + }, + "hidden is not shown": { + input: map[string]interface{}{"key": false}, + expected: ``, + isErr: false, + isHidden: true, + showHidden: false, + }, + } + + for name, tc := range tests { + fs := pflag.NewFlagSet("flags", pflag.ContinueOnError) + + for k, v := range tc.input { + switch val := v.(type) { + case bool: + fs.Bool(k, val, "") + case string: + fs.String(k, val, "") + } + + if tc.isHidden { + err := fs.MarkHidden(k) + if err != nil { + t.Errorf("%s: test setup failed: %s", name, err) + } + } + } + result, err := DescribeFlags(fs, tc.showHidden, Plain) + + if (err != nil) != tc.isErr { + t.Errorf("%s: expected error: %v, got %s", name, tc.isErr, err) + } + + if result != tc.expected { + t.Errorf("%s: expected %s, got %s", name, tc.expected, result) + } + } +} + +func TestDescribeFlagsAsJSON(t *testing.T) { + tests := map[string]struct { + input map[string]interface{} + expected string + isErr bool + isHidden bool + showHidden bool + }{ + "bool is not quoted": { + input: map[string]interface{}{"key": false}, + expected: `{"key":false}`, + isErr: false, + }, + "help is not shown": { + input: map[string]interface{}{"key": false, "help": true}, + expected: `{"key":false}`, + isErr: false, + }, + "hidden is shown": { + input: map[string]interface{}{"key": false}, + expected: `{"key":false}`, + isErr: false, + isHidden: true, + showHidden: true, + }, + "hidden is not shown": { + input: map[string]interface{}{"key": false}, + expected: `{}`, + isErr: false, + isHidden: true, + showHidden: false, + }, + "string is quoted": { + input: map[string]interface{}{"key": "val"}, + expected: `{"key":"val"}`, + isErr: false, + }, + } + + for name, tc := range tests { + fs := pflag.NewFlagSet("flags", pflag.ContinueOnError) + + for k, v := range tc.input { + switch val := v.(type) { + case bool: + fs.Bool(k, val, "") + case string: + fs.String(k, val, "") + } + + if tc.isHidden { + err := fs.MarkHidden(k) + if err != nil { + t.Errorf("%s: test setup failed: %s", name, err) + } + } + } + result, err := DescribeFlags(fs, tc.showHidden, JSON) + + if (err != nil) != tc.isErr { + t.Errorf("%s: expected error: %v, got %s", name, tc.isErr, err) + } + + if result != tc.expected { + t.Errorf("%s: expected %s, got %s", name, tc.expected, result) + } + } +} diff --git a/cmd/forwarder/httpbin/httpbin.go b/cmd/forwarder/httpbin/httpbin.go index 7df5ab3e2..549dc5a58 100644 --- a/cmd/forwarder/httpbin/httpbin.go +++ b/cmd/forwarder/httpbin/httpbin.go @@ -18,7 +18,10 @@ type command struct { } func (c *command) RunE(cmd *cobra.Command, args []string) error { - config := bind.DescribeFlags(cmd.Flags()) + config, err := bind.DescribeFlags(cmd.Flags(), false, bind.Plain) + if err != nil { + return err + } if f := c.logConfig.File; f != nil { defer f.Close() diff --git a/cmd/forwarder/pac/server/server.go b/cmd/forwarder/pac/server/server.go index 2fca36109..818e7bd6d 100644 --- a/cmd/forwarder/pac/server/server.go +++ b/cmd/forwarder/pac/server/server.go @@ -28,7 +28,10 @@ type command struct { } func (c *command) RunE(cmd *cobra.Command, args []string) error { - config := bind.DescribeFlags(cmd.Flags()) + config, err := bind.DescribeFlags(cmd.Flags(), false, bind.Plain) + if err != nil { + return err + } if f := c.logConfig.File; f != nil { defer f.Close() diff --git a/cmd/forwarder/run/run.go b/cmd/forwarder/run/run.go index 51dabef2c..805181520 100644 --- a/cmd/forwarder/run/run.go +++ b/cmd/forwarder/run/run.go @@ -41,7 +41,10 @@ type command struct { } func (c *command) RunE(cmd *cobra.Command, args []string) error { - config := bind.DescribeFlags(cmd.Flags()) + config, err := bind.DescribeFlags(cmd.Flags(), false, bind.JSON) + if err != nil { + return err + } if f := c.logConfig.File; f != nil { defer f.Close()