From b2ab3a02d664bab9345dedcfb0113ff0b68daf5a Mon Sep 17 00:00:00 2001 From: broothie Date: Tue, 25 Feb 2025 14:29:50 -0800 Subject: [PATCH 1/2] add env var defaults --- command_test.go | 4 ++-- flag.go | 21 +++++++++++---------- flag_options.go | 20 ++++++++++++++++++++ help.go | 7 ++++++- help.tmpl | 4 ++-- help_test.go | 4 ++-- options_test.go | 4 ++-- 7 files changed, 45 insertions(+), 19 deletions(-) diff --git a/command_test.go b/command_test.go index 2167be9..dbe5708 100644 --- a/command_test.go +++ b/command_test.go @@ -26,9 +26,9 @@ func ExampleNewCommand() { // server v0.1.0: An http server. // // Usage: - // server [flags] [sub-commands] + // server [flags] [sub-command] // - // Sub-commands: + // Sub-command: // proxy: Proxy requests to another server. // // Flags: diff --git a/flag.go b/flag.go index 8b77348..ebd76f4 100644 --- a/flag.go +++ b/flag.go @@ -11,16 +11,17 @@ import ( const helpFlagName = "help" type Flag struct { - name string - description string - aliases []string - shorts []rune - isHelp bool - isVersion bool - isHidden bool - isInherited bool - parser argParser - defaultValue any + name string + description string + aliases []string + shorts []rune + isHelp bool + isVersion bool + isHidden bool + isInherited bool + parser argParser + defaultEnvName string + defaultValue any value any } diff --git a/flag_options.go b/flag_options.go index 8ac478c..300c4fd 100644 --- a/flag_options.go +++ b/flag_options.go @@ -1,6 +1,9 @@ package cli import ( + "os" + + "github.com/bobg/errors" "github.com/broothie/option" ) @@ -46,6 +49,7 @@ func SetFlagDefault[T Parseable](defaultValue T) option.Func[*Flag] { flag.parser = argParser flag.defaultValue = defaultValue + flag.defaultEnvName = "" return flag, nil } } @@ -55,6 +59,22 @@ func SetFlagDefaultAndParser[T any](defaultValue T, argParser ArgParser[T]) opti return func(flag *Flag) (*Flag, error) { flag.parser = argParser flag.defaultValue = defaultValue + flag.defaultEnvName = "" + return flag, nil + } +} + +// SetFlagDefaultEnvAndParser sets the default value to that of the corresponding environment variable, and parser of the flag. +func SetFlagDefaultEnvAndParser[T any](name string, argParser ArgParser[T]) option.Func[*Flag] { + return func(flag *Flag) (*Flag, error) { + defaultValue, err := argParser(os.Getenv(name)) + if err != nil { + return nil, errors.Wrapf(err, "parsing value of $%s", name) + } + + flag.parser = argParser + flag.defaultValue = defaultValue + flag.defaultEnvName = name return flag, nil } } diff --git a/help.go b/help.go index ec4d214..a21840b 100644 --- a/help.go +++ b/help.go @@ -104,12 +104,17 @@ func (h helpContext) FlagTable() (string, error) { shorts = fmt.Sprintf("-%s", string(flag.shorts)) } + defaultValueHelp := fmt.Sprintf("%q", fmt.Sprint(flag.defaultValue)) + if flag.defaultEnvName != "" { + defaultValueHelp = fmt.Sprintf("$%s", flag.defaultEnvName) + } + return []string{ "", strings.Join(longs, " "), shorts, flag.description, - fmt.Sprintf("(type: %T, default: %q)", flag.parser.Type(), fmt.Sprint(flag.defaultValue)), + fmt.Sprintf("(type: %T, default: %s)", flag.parser.Type(), defaultValueHelp), }, true })) } diff --git a/help.tmpl b/help.tmpl index 03f69df..e21299a 100644 --- a/help.tmpl +++ b/help.tmpl @@ -1,10 +1,10 @@ {{.RootName}}{{ if .Version }} {{.Version}}{{ end }}: {{.RootDescription}} Usage: - {{.QualifiedName}} {{- if .Flags }} [flags]{{ end -}} {{- if .SubCommands }} [sub-commands]{{ end }}{{ if .ArgumentList }} {{.ArgumentList}}{{ end }} + {{.QualifiedName}} {{- if .Flags }} [flags]{{ end -}} {{- if .SubCommands }} [sub-command]{{ end }}{{ if .ArgumentList }} {{.ArgumentList}}{{ end }} {{ if .SubCommands -}} -Sub-commands: +Sub-command: {{.SubCommandsTable}} {{ end -}} diff --git a/help_test.go b/help_test.go index 7b1b3db..5469cb9 100644 --- a/help_test.go +++ b/help_test.go @@ -84,9 +84,9 @@ func TestCommand_renderHelp(t *testing.T) { test v1.2.3-rc10: test command Usage: - test [flags] [sub-commands] + test [flags] [sub-command] - Sub-commands: + Sub-command: some-command: some command Flags: diff --git a/options_test.go b/options_test.go index c94cd1d..f5cb930 100644 --- a/options_test.go +++ b/options_test.go @@ -126,9 +126,9 @@ func ExampleAddSubCmd() { // server: An http server. // // Usage: - // server [sub-commands] + // server [sub-command] // - // Sub-commands: + // Sub-command: // start: Start the server } From c2d6910eabbeff03c4c7140dc940422dbeefb36c Mon Sep 17 00:00:00 2001 From: broothie Date: Tue, 25 Feb 2025 18:57:37 -0800 Subject: [PATCH 2/2] add tests --- cli_test.go | 19 +++++++++++++++++++ command_test.go | 5 +++-- context.go | 10 ++++++++++ flag_options.go | 16 ++-------------- go.mod | 2 +- go.sum | 4 ++-- help.go | 6 +++--- options_test.go | 25 +++++++++++++++++++++++++ 8 files changed, 65 insertions(+), 22 deletions(-) diff --git a/cli_test.go b/cli_test.go index cb441fd..543f510 100644 --- a/cli_test.go +++ b/cli_test.go @@ -180,13 +180,32 @@ func Test_git(t *testing.T) { } }, }, + "env var based flag is evaluated": { + gitHandler: func(t *testing.T) Handler { + called := ensureCalled(t) + + return func(ctx context.Context) error { + called() + + globalGitignore, err := FlagValue[string](ctx, "global-gitignore") + test.Nil(t, err) + test.Equal(t, globalGitignore, "path/to/some/.gitignore") + return nil + } + }, + }, } for name, testCase := range testCases { t.Run(name, func(t *testing.T) { + t.Setenv("GLOBAL_GITIGNORE", "path/to/some/.gitignore") + command, err := NewCommand("git", "the stupid content tracker", SetVersion("v0.1.0"), AddFlag("git-dir", "Git directory to use"), + AddFlag("global-gitignore", "Global .gitignore file to use.", + SetFlagDefaultEnv("GLOBAL_GITIGNORE"), + ), AddSubCmd("commit", "Record changes to the repository", AddFlag("message", "commit message", AddFlagAlias("msg"), diff --git a/command_test.go b/command_test.go index dbe5708..c175251 100644 --- a/command_test.go +++ b/command_test.go @@ -8,8 +8,9 @@ func ExampleNewCommand() { AddVersionFlag(AddFlagShort('V')), AddHelpFlag(AddFlagShort('h')), AddFlag("port", "Port to run server on.", - SetFlagDefault(3000), AddFlagShort('p'), + SetFlagDefault(3000), + SetFlagDefaultEnv("PORT"), ), AddFlag("auth-required", "Whether to require authentication.", SetFlagDefault(true), @@ -34,6 +35,6 @@ func ExampleNewCommand() { // Flags: // --version -V Print version. (type: bool, default: "false") // --help -h Print help. (type: bool, default: "false") - // --port -p Port to run server on. (type: int, default: "3000") + // --port -p Port to run server on. (type: int, default: $PORT, "3000") // --auth-required Whether to require authentication. (type: bool, default: "true") } diff --git a/context.go b/context.go index 8e4d16d..8a88dc8 100644 --- a/context.go +++ b/context.go @@ -2,6 +2,7 @@ package cli import ( "context" + "os" "github.com/bobg/errors" ) @@ -29,6 +30,15 @@ func FlagValue[T any](ctx context.Context, name string) (T, error) { return flag.value.(T), nil } + if flag.defaultEnvName != "" { + value, err := flag.parser.Parse(os.Getenv(flag.defaultEnvName)) + if err != nil { + return zero, err + } + + return value.(T), nil + } + return flag.defaultValue.(T), nil } diff --git a/flag_options.go b/flag_options.go index 300c4fd..c77b78b 100644 --- a/flag_options.go +++ b/flag_options.go @@ -1,9 +1,6 @@ package cli import ( - "os" - - "github.com/bobg/errors" "github.com/broothie/option" ) @@ -49,7 +46,6 @@ func SetFlagDefault[T Parseable](defaultValue T) option.Func[*Flag] { flag.parser = argParser flag.defaultValue = defaultValue - flag.defaultEnvName = "" return flag, nil } } @@ -59,21 +55,13 @@ func SetFlagDefaultAndParser[T any](defaultValue T, argParser ArgParser[T]) opti return func(flag *Flag) (*Flag, error) { flag.parser = argParser flag.defaultValue = defaultValue - flag.defaultEnvName = "" return flag, nil } } -// SetFlagDefaultEnvAndParser sets the default value to that of the corresponding environment variable, and parser of the flag. -func SetFlagDefaultEnvAndParser[T any](name string, argParser ArgParser[T]) option.Func[*Flag] { +// SetFlagDefaultEnv sets the default value to that of the corresponding environment variable, and parser of the flag. +func SetFlagDefaultEnv(name string) option.Func[*Flag] { return func(flag *Flag) (*Flag, error) { - defaultValue, err := argParser(os.Getenv(name)) - if err != nil { - return nil, errors.Wrapf(err, "parsing value of $%s", name) - } - - flag.parser = argParser - flag.defaultValue = defaultValue flag.defaultEnvName = name return flag, nil } diff --git a/go.mod b/go.mod index 3b66321..665bf0c 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/MakeNowJust/heredoc/v2 v2.0.1 github.com/bobg/errors v1.1.0 github.com/broothie/option v0.1.0 - github.com/broothie/test v0.1.6 + github.com/broothie/test v0.1.8 github.com/samber/lo v1.49.1 ) diff --git a/go.sum b/go.sum index 6adad8b..1b934c1 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/bobg/errors v1.1.0 h1:gsVanPzJMpZQpwY+27/GQYElZez5CuMYwiIpk2A3RGw= github.com/bobg/errors v1.1.0/go.mod h1:Q4775qBZpnte7EGFJqmvnlB1U4pkI1XmU3qxqdp7Zcc= github.com/broothie/option v0.1.0 h1:5l6qdv9g1Ajxn7821brKVzOZxIjlVB0gA1MU8QbW8Fw= github.com/broothie/option v0.1.0/go.mod h1:doEn1r1TpaaBJRdHLZlsdjvnrnH0u1WW+FZA018hE2g= -github.com/broothie/test v0.1.6 h1:lvGSA1O2lFdYJuKDgj9IDpX9AA+rQtY/eYGMvui3PyM= -github.com/broothie/test v0.1.6/go.mod h1:txzDcP9OHro3y7goC+ZznIkASy8NJks4dUcdbwsP0HM= +github.com/broothie/test v0.1.8 h1:II6ZE0z2ZV0y4GJlLl/oBinG6X58mwyioSd/jxNMea0= +github.com/broothie/test v0.1.8/go.mod h1:txzDcP9OHro3y7goC+ZznIkASy8NJks4dUcdbwsP0HM= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= diff --git a/help.go b/help.go index a21840b..a91cdf9 100644 --- a/help.go +++ b/help.go @@ -104,9 +104,9 @@ func (h helpContext) FlagTable() (string, error) { shorts = fmt.Sprintf("-%s", string(flag.shorts)) } - defaultValueHelp := fmt.Sprintf("%q", fmt.Sprint(flag.defaultValue)) + helpValues := []string{fmt.Sprintf("%q", fmt.Sprint(flag.defaultValue))} if flag.defaultEnvName != "" { - defaultValueHelp = fmt.Sprintf("$%s", flag.defaultEnvName) + helpValues = append([]string{fmt.Sprintf("$%s", flag.defaultEnvName)}, helpValues...) } return []string{ @@ -114,7 +114,7 @@ func (h helpContext) FlagTable() (string, error) { strings.Join(longs, " "), shorts, flag.description, - fmt.Sprintf("(type: %T, default: %s)", flag.parser.Type(), defaultValueHelp), + fmt.Sprintf("(type: %T, default: %s)", flag.parser.Type(), strings.Join(helpValues, ", ")), }, true })) } diff --git a/options_test.go b/options_test.go index f5cb930..5ee6c1a 100644 --- a/options_test.go +++ b/options_test.go @@ -19,6 +19,7 @@ func Test_options(t *testing.T) { AddFlagAlias("addr"), AddFlagShort('p'), SetFlagDefault(3000), + SetFlagDefaultEnv("PORT"), ), AddSubCmd("proxy", "Proxy requests", AddAlias("p"), @@ -59,6 +60,7 @@ func Test_options(t *testing.T) { test.DeepEqual(t, []string{"addr"}, portFlag.aliases) test.DeepEqual(t, []rune{'p'}, portFlag.shorts) test.Equal(t, 3000, portFlag.defaultValue) + test.Equal(t, "PORT", portFlag.defaultEnvName) test.Nil(t, portFlag.value) test.Equal(t, reflect.ValueOf(IntParser).Pointer(), reflect.ValueOf(portFlag.parser).Pointer()) test.False(t, portFlag.isBool()) @@ -151,6 +153,29 @@ func ExampleAddFlag() { // --port -p Port to run server on (type: int, default: "3000") } +func ExampleAddFlag_with_env() { + os.Setenv("PORT", "8080") + defer os.Unsetenv("PORT") + + command, _ := NewCommand("server", "An http server.", + AddFlag("port", "Port to run server on", + AddFlagShort('p'), + SetFlagDefault(3000), + SetFlagDefaultEnv("PORT"), + ), + ) + + command.renderHelp(os.Stdout) + // Output: + // server: An http server. + // + // Usage: + // server [flags] + // + // Flags: + // --port -p Port to run server on (type: int, default: $PORT, "3000") +} + func ExampleAddArg() { command, _ := NewCommand("server", "An http server.", AddArg("port", "Port to run server on", SetArgParser(IntParser)),