Skip to content

Commit

Permalink
feat: Add promptChoice and promptChoiceOnce template functions
Browse files Browse the repository at this point in the history
  • Loading branch information
twpayne committed Aug 13, 2023
1 parent 29e8c30 commit 5e8d2b3
Show file tree
Hide file tree
Showing 13 changed files with 259 additions and 6 deletions.
7 changes: 7 additions & 0 deletions assets/chezmoi.io/docs/reference/commands/execute-template.md
Expand Up @@ -20,6 +20,13 @@ from *pairs*. *pairs* is a comma-separated list of *prompt*`=`*value* pairs. If
`promptBool` is called with a *prompt* that does not match any of *pairs*, then
it returns false.

## `--promptChoice` *pairs*

Simulate the `promptChoice` template function with a function that returns
values from *pairs*. *pairs* is a comma-separated list of *prompt*`=`*value*
pairs. If `promptChoice` is called with a *prompt* that does not match any of
*pairs*, then it returns false.

## `--promptInt` *pairs*

Simulate the `promptInt` template function with a function that returns values
Expand Down
7 changes: 7 additions & 0 deletions assets/chezmoi.io/docs/reference/commands/init.md
Expand Up @@ -66,6 +66,13 @@ a comma-separated list of *prompt*`=`*value* pairs. If `promptBool` is called
with a *prompt* that does not match any of *pairs*, then it prompts the user for
a value.

## `--promptChoice` *pairs*

Populate the `promptChoice` template function with values from *pairs*. *pairs*
is a comma-separated list of *prompt*`=`*value* pairs. If `promptChoice` is
called with a *prompt* that does not match any of *pairs*, then it prompts the
user for a value.

## `--promptDefaults`

Make all `prompt*` template function calls with a default value return that
Expand Down
@@ -0,0 +1,3 @@
# `promptChoice` *prompt* *choices* [*default*]

`promptChoice` prompts the user with *prompt* and *choices* and returns the user's response. *choices* must be a list of strings. If *default* is passed and the user's response is empty then it returns *default*.
@@ -0,0 +1,5 @@
# `promptChoiceOnce` *map* *path* *prompt* *choices* [*default*]

`promptChoiceOnce` returns the value of *map* at *path* if it exists and is a
string, otherwise it prompts the user for one of *choices* with *prompt* and an
optional *default* using `promptChoice`.
2 changes: 2 additions & 0 deletions assets/chezmoi.io/mkdocs.yml
Expand Up @@ -221,6 +221,8 @@ nav:
- exit: reference/templates/init-functions/exit.md
- promptBool: reference/templates/init-functions/promptBool.md
- promptBoolOnce: reference/templates/init-functions/promptBoolOnce.md
- promptChoice: reference/templates/init-functions/promptChoice.md
- promptChoiceOnce: reference/templates/init-functions/promptChoiceOnce.md
- promptInt: reference/templates/init-functions/promptInt.md
- promptIntOnce: reference/templates/init-functions/promptIntOnce.md
- promptString: reference/templates/init-functions/promptString.md
Expand Down
3 changes: 3 additions & 0 deletions internal/cmd/config.go
Expand Up @@ -771,6 +771,8 @@ func (c *Config) createConfigFile(
"exit": c.exitInitTemplateFunc,
"promptBool": c.promptBoolInteractiveTemplateFunc,
"promptBoolOnce": c.promptBoolOnceInteractiveTemplateFunc,
"promptChoice": c.promptChoiceInteractiveTemplateFunc,
"promptChoiceOnce": c.promptChoiceOnceInteractiveTemplateFunc,
"promptInt": c.promptIntInteractiveTemplateFunc,
"promptIntOnce": c.promptIntOnceInteractiveTemplateFunc,
"promptString": c.promptStringInteractiveTemplateFunc,
Expand Down Expand Up @@ -1395,6 +1397,7 @@ func (c *Config) gitAutoCommit(status *git.Status) error {
funcMap := maps.Clone(sprig.TxtFuncMap())
maps.Copy(funcMap, map[string]any{
"promptBool": c.promptBoolInteractiveTemplateFunc,
"promptChoice": c.promptChoiceInteractiveTemplateFunc,
"promptInt": c.promptIntInteractiveTemplateFunc,
"promptString": c.promptStringInteractiveTemplateFunc,
"targetRelPath": func(source string) string {
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/config_test.go
Expand Up @@ -189,7 +189,7 @@ func withDestSystem(destSystem chezmoi.System) configOption {
}
}

func withNoTTY(noTTY bool) configOption {
func withNoTTY(noTTY bool) configOption { //nolint:unparam
return func(c *Config) error {
c.noTTY = noTTY
return nil
Expand Down
59 changes: 55 additions & 4 deletions internal/cmd/executetemplatecmd.go
Expand Up @@ -7,13 +7,15 @@ import (
"strings"

"github.com/spf13/cobra"
"golang.org/x/exp/slices"

"github.com/twpayne/chezmoi/v2/internal/chezmoi"
)

type executeTemplateCmdConfig struct {
init bool
promptBool map[string]string
promptChoice map[string]string
promptInt map[string]int
promptString map[string]string
stdinIsATTY bool
Expand Down Expand Up @@ -47,6 +49,12 @@ func (c *Config) newExecuteTemplateCmd() *cobra.Command {
c.executeTemplate.promptBool,
"Simulate promptBool",
)
flags.StringToStringVar(
&c.executeTemplate.promptChoice,
"promptChoice",
c.executeTemplate.promptChoice,
"Simulate promptChoice",
)
flags.StringToIntVar(
&c.executeTemplate.promptInt,
"promptInt",
Expand Down Expand Up @@ -148,6 +156,47 @@ func (c *Config) runExecuteTemplateCmd(cmd *cobra.Command, args []string) error
return promptBoolInitTemplateFunc(field, args...)
}

promptChoiceInitTemplateFunc := func(prompt string, choices []any, args ...string) string {
choiceStrs, err := anySliceToStringSlice(choices)
if err != nil {
panic(err)
}
switch len(args) {
case 0:
if value, ok := c.executeTemplate.promptChoice[prompt]; ok {
if !slices.Contains(choiceStrs, value) {
panic(fmt.Errorf("%s: invalid choice", value))
}
return value
}
return prompt
case 1:
if value, ok := c.executeTemplate.promptChoice[prompt]; ok {
if !slices.Contains(choiceStrs, value) {
panic(fmt.Errorf("%s: invalid choice", value))
}
return value
}
return args[0]
default:
err := fmt.Errorf("want 2 or 3 arguments, got %d", len(args)+1)
panic(err)
}
}

promptChoiceOnceInitTemplateFunc := func(m map[string]any, path any, prompt string, choices []any, args ...string) string {
nestedMap, lastKey, err := nestedMapAtPath(m, path)
if err != nil {
panic(err)
}
if value, ok := nestedMap[lastKey]; ok {
if stringValue, ok := value.(string); ok {
return stringValue
}
}
return promptChoiceInitTemplateFunc(prompt, choices, args...)
}

promptIntInitTemplateFunc := func(prompt string, args ...int64) int64 {
switch len(args) {
case 0:
Expand All @@ -163,7 +212,7 @@ func (c *Config) runExecuteTemplateCmd(cmd *cobra.Command, args []string) error
}
}

promptIntOnceInitTemplateFunc := func(m map[string]any, path any, field string, args ...int64) int64 {
promptIntOnceInitTemplateFunc := func(m map[string]any, path any, prompt string, args ...int64) int64 {
nestedMap, lastKey, err := nestedMapAtPath(m, path)
if err != nil {
panic(err)
Expand All @@ -173,7 +222,7 @@ func (c *Config) runExecuteTemplateCmd(cmd *cobra.Command, args []string) error
return intValue
}
}
return promptIntInitTemplateFunc(field, args...)
return promptIntInitTemplateFunc(prompt, args...)
}

promptStringInitTemplateFunc := func(prompt string, args ...string) string {
Expand All @@ -194,7 +243,7 @@ func (c *Config) runExecuteTemplateCmd(cmd *cobra.Command, args []string) error
}
}

promptStringOnceInitTemplateFunc := func(m map[string]any, path any, field string, args ...string) string {
promptStringOnceInitTemplateFunc := func(m map[string]any, path any, prompt string, args ...string) string {
nestedMap, lastKey, err := nestedMapAtPath(m, path)
if err != nil {
panic(err)
Expand All @@ -204,7 +253,7 @@ func (c *Config) runExecuteTemplateCmd(cmd *cobra.Command, args []string) error
return stringValue
}
}
return promptStringInitTemplateFunc(field, args...)
return promptStringInitTemplateFunc(prompt, args...)
}

stdinIsATTYInitTemplateFunc := func() bool {
Expand All @@ -215,6 +264,8 @@ func (c *Config) runExecuteTemplateCmd(cmd *cobra.Command, args []string) error
"exit": c.exitInitTemplateFunc,
"promptBool": promptBoolInitTemplateFunc,
"promptBoolOnce": promptBoolOnceInitTemplateFunc,
"promptChoice": promptChoiceInitTemplateFunc,
"promptChoiceOnce": promptChoiceOnceInitTemplateFunc,
"promptInt": promptIntInitTemplateFunc,
"promptIntOnce": promptIntOnceInitTemplateFunc,
"promptString": promptStringInitTemplateFunc,
Expand Down
90 changes: 90 additions & 0 deletions internal/cmd/inittemplatefuncs_test.go
Expand Up @@ -80,6 +80,96 @@ func TestPromptBoolInteractiveTemplateFunc(t *testing.T) {
}
}

func TestPromptChoiceInteractiveTemplateFunc(t *testing.T) {
for _, tc := range []struct {
name string
prompt string
choices []any
args []string
stdinStr string
expectedStdoutStr string
expected string
expectedErr bool
}{
{
name: "response_without_default",
prompt: "choice",
choices: []any{"one", "two", "three"},
stdinStr: "one\n",
expectedStdoutStr: "choice (one/two/three)? ",
expected: "one",
},
{
name: "response_with_default",
prompt: "choice",
choices: []any{"one", "two", "three"},
args: []string{"one"},
stdinStr: "two\n",
expectedStdoutStr: "choice (one/two/three, default one)? ",
expected: "two",
},
{
name: "no_response_with_default",
prompt: "choice",
choices: []any{"one", "two", "three"},
args: []string{"three"},
stdinStr: "\n",
expectedStdoutStr: "choice (one/two/three, default three)? ",
expected: "three",
},
{
name: "invalid_response",
prompt: "choice",
choices: []any{"one", "two", "three"},
stdinStr: "invalid\n",
expectedErr: true,
},
{
name: "invalid_response_with_default",
prompt: "choice",
choices: []any{"one", "two", "three"},
args: []string{"one"},
stdinStr: "invalid\n",
expectedErr: true,
},
{
name: "too_many_args",
prompt: "choice",
choices: []any{"one", "two", "three"},
args: []string{"two", "three"},
stdinStr: "\n",
expectedErr: true,
},
{
name: "invalid_default",
prompt: "choice",
choices: []any{"one", "two", "three"},
args: []string{"four"},
stdinStr: "\n",
expectedErr: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
stdin := strings.NewReader(tc.stdinStr)
stdout := &strings.Builder{}
config, err := newConfig(
withNoTTY(true),
withStdin(stdin),
withStdout(stdout),
)
assert.NoError(t, err)
if tc.expectedErr {
assert.Panics(t, func() {
config.promptChoiceInteractiveTemplateFunc(tc.prompt, tc.choices, tc.args...)
})
} else {
assert.Equal(t, tc.expected, config.promptChoiceInteractiveTemplateFunc(tc.prompt, tc.choices, tc.args...))
assert.Equal(t, tc.expectedStdoutStr, stdout.String())
}
})
}
}

func TestPromptIntInteractiveTemplateFunc(t *testing.T) {
for _, tc := range []struct {
name string
Expand Down
65 changes: 65 additions & 0 deletions internal/cmd/interactivetemplatefuncs.go
Expand Up @@ -11,6 +11,7 @@ import (
type interactiveTemplateFuncsConfig struct {
forcePromptOnce bool
promptBool map[string]string
promptChoice map[string]string
promptDefaults bool
promptInt map[string]int
promptString map[string]string
Expand All @@ -35,6 +36,12 @@ func (c *Config) addInteractiveTemplateFuncFlags(flags *pflag.FlagSet) {
c.interactiveTemplateFuncs.promptBool,
"Populate promptBool",
)
flags.StringToStringVar(
&c.interactiveTemplateFuncs.promptChoice,
"promptChoice",
c.interactiveTemplateFuncs.promptChoice,
"Populate promptChoice",
)
flags.StringToIntVar(
&c.interactiveTemplateFuncs.promptInt,
"promptInt",
Expand Down Expand Up @@ -101,6 +108,49 @@ func (c *Config) promptBoolOnceInteractiveTemplateFunc(
return c.promptBoolInteractiveTemplateFunc(prompt, args...)
}

func (c *Config) promptChoiceInteractiveTemplateFunc(prompt string, choices []any, args ...string) string {
if len(args) > 1 {
err := fmt.Errorf("want 2 or 3 arguments, got %d", len(args)+2)
panic(err)
}

if valueStr, ok := c.interactiveTemplateFuncs.promptChoice[prompt]; ok {
return valueStr
}

choiceStrs, err := anySliceToStringSlice(choices)
if err != nil {
panic(err)
}

value, err := c.promptChoice(prompt, choiceStrs, args...)
if err != nil {
panic(err)
}
return value
}

func (c *Config) promptChoiceOnceInteractiveTemplateFunc(m map[string]any, path any, prompt string, choices []any, args ...string) string {
if len(args) > 1 {
err := fmt.Errorf("want 4 or 5 arguments, got %d", len(args)+4)
panic(err)
}

nestedMap, lastKey, err := nestedMapAtPath(m, path)
if err != nil {
panic(err)
}
if !c.interactiveTemplateFuncs.forcePromptOnce {
if value, ok := nestedMap[lastKey]; ok {
if valueStr, ok := value.(string); ok {
return valueStr
}
}
}

return c.promptChoiceInteractiveTemplateFunc(prompt, choices, args...)
}

func (c *Config) promptIntInteractiveTemplateFunc(prompt string, args ...int64) int64 {
if len(args) > 1 {
err := fmt.Errorf("want 1 or 2 arguments, got %d", len(args)+1)
Expand Down Expand Up @@ -186,3 +236,18 @@ func (c *Config) promptStringOnceInteractiveTemplateFunc(

return c.promptStringInteractiveTemplateFunc(prompt, args...)
}

func anySliceToStringSlice(slice []any) ([]string, error) {
result := make([]string, 0, len(slice))
for _, elem := range slice {
switch elem := elem.(type) {
case []byte:
result = append(result, string(elem))
case string:
result = append(result, elem)
default:
return nil, fmt.Errorf("%v: not a string", elem)
}
}
return result, nil
}

0 comments on commit 5e8d2b3

Please sign in to comment.