diff --git a/command_run.go b/command_run.go index 8a135a7732..10ff42e537 100644 --- a/command_run.go +++ b/command_run.go @@ -333,6 +333,17 @@ func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context } } + // Handle interactive mode if enabled + for _, c := range cmdChain { + if c.shouldRunInteractive() { + tracef("running interactive mode (cmd=%[1]q)", c.Name) + if err := c.handleInteractiveMode(ctx); err != nil { + deferErr = c.handleExitCoder(ctx, err) + return ctx, deferErr + } + } + } + if err := cmd.checkAllRequiredFlags(); err != nil { cmd.isInError = true if cmd.OnUsageError != nil { diff --git a/docs/v3/examples/flags/advanced.md b/docs/v3/examples/flags/advanced.md index e5a5fff7e5..96a41e66c5 100644 --- a/docs/v3/examples/flags/advanced.md +++ b/docs/v3/examples/flags/advanced.md @@ -532,3 +532,139 @@ Will result in help output like: ``` Flag port value 70000 out of range[0-65535] ``` + +#### Interactive Mode + +`urfave/cli` supports an interactive mode that allows users to input missing parameters +through a series of prompts. This is particularly useful for creating user-friendly +CLI tools that guide users through the configuration process. + +##### Enabling Interactive Mode + +To enable interactive mode, add the `--interactive` (or `-i`) flag to your command: + +```go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/urfave/cli/v3" +) + +func main() { + cmd := &cli.Command{ + Name: "user-config", + Usage: "Configure user settings interactively", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "interactive", + Aliases: []string{"i"}, + Usage: "Enable interactive mode", + }, + &cli.InteractiveStringFlag{ + StringFlag: cli.StringFlag{ + Name: "name", + Value: "Guest", + Usage: "Your name", + }, + Prompt: "Enter your name", + Required: true, + }, + &cli.InteractiveIntFlag{ + Int64Flag: cli.Int64Flag{ + Name: "age", + Value: 18, + Usage: "Your age", + }, + Prompt: "Enter your age", + Required: true, + }, + &cli.InteractiveBoolFlag{ + BoolFlag: cli.BoolFlag{ + Name: "subscribe", + Value: false, + Usage: "Subscribe to newsletter", + }, + Prompt: "Do you want to subscribe to our newsletter", + }, + }, + Action: func(ctx context.Context, cmd *cli.Command) error { + fmt.Printf("Name: %s\n", cmd.String("name")) + fmt.Printf("Age: %d\n", cmd.Int("age")) + fmt.Printf("Subscribed: %v\n", cmd.Bool("subscribe")) + return nil + }, + } + + if err := cmd.Run(context.Background(), os.Args); err != nil { + log.Fatal(err) + } +} +``` + +##### Using Interactive Mode + +When you run the command with the `--interactive` or `-i` flag: + +```sh-session +$ user-config --interactive +Enter your name [Guest]: John +Enter your age [18]: 25 +Do you want to subscribe to our newsletter [y/N]: y + +Name: John +Age: 25 +Subscribed: true +``` + +If you press Enter without typing anything, the default value will be used. + +##### Interactive Flag Types + +The following interactive flag types are available: + +- `InteractiveStringFlag` - For string inputs +- `InteractiveIntFlag` - For integer inputs +- `InteractiveBoolFlag` - For yes/no confirmations +- `InteractiveFloatFlag` - For float inputs +- `InteractiveDurationFlag` - For duration inputs (e.g., 1s, 2m, 3h) + +Each interactive flag has these additional fields: + +- `Prompt` - The text to display when prompting the user +- `Required` - Whether the user must provide a value (cannot use default) + +##### Required Fields + +When `Required` is set to `true`, the user cannot skip the prompt by pressing Enter: + +```go +&cli.InteractiveStringFlag{ + StringFlag: cli.StringFlag{ + Name: "email", + Usage: "Your email address", + }, + Prompt: "Enter your email", + Required: true, +} +``` + +##### Custom Prompter + +You can also use the `InteractivePrompter` interface to create custom prompt behavior: + +```go +type InteractivePrompter interface { + Prompt(prompt string, defaultValue string) (string, error) + PromptRequired(prompt string) (string, error) + PromptConfirm(prompt string, defaultValue bool) (bool, error) + PromptSelect(prompt string, options []string) (int, string, error) +} +``` + +The `DefaultPrompter` uses standard input/output, but you can implement your own +prompter for testing or custom UI scenarios. diff --git a/examples/example-interactive/example-interactive.go b/examples/example-interactive/example-interactive.go new file mode 100644 index 0000000000..e10f6d704d --- /dev/null +++ b/examples/example-interactive/example-interactive.go @@ -0,0 +1,128 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/urfave/cli/v3" +) + +func main() { + cmd := &cli.Command{ + Name: "interactive-demo", + Usage: "A demonstration of interactive mode in urfave/cli", + Description: `This example demonstrates how to use the interactive mode +to prompt users for missing parameters. Use --interactive or -i flag +to enable interactive prompting.`, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "interactive", + Aliases: []string{"i"}, + Usage: "Enable interactive mode for missing parameters", + }, + &cli.InteractiveStringFlag{ + StringFlag: cli.StringFlag{ + Name: "name", + Value: "Guest", + Usage: "Your name", + }, + Prompt: "Enter your name", + Required: true, + }, + &cli.InteractiveIntFlag{ + Int64Flag: cli.Int64Flag{ + Name: "age", + Value: 18, + Usage: "Your age", + }, + Prompt: "Enter your age", + Required: true, + }, + &cli.InteractiveStringFlag{ + StringFlag: cli.StringFlag{ + Name: "email", + Usage: "Your email address", + }, + Prompt: "Enter your email (optional)", + Required: false, + }, + &cli.InteractiveBoolFlag{ + BoolFlag: cli.BoolFlag{ + Name: "subscribe", + Value: false, + Usage: "Subscribe to newsletter", + }, + Prompt: "Do you want to subscribe to our newsletter", + }, + }, + Action: func(ctx context.Context, cmd *cli.Command) error { + fmt.Println("\n=== User Profile ===") + fmt.Printf("Name: %s\n", cmd.String("name")) + fmt.Printf("Age: %d\n", cmd.Int("age")) + if email := cmd.String("email"); email != "" { + fmt.Printf("Email: %s\n", email) + } else { + fmt.Println("Email: Not provided") + } + fmt.Printf("Subscribed: %v\n", cmd.Bool("subscribe")) + + if cmd.Bool("subscribe") { + fmt.Println("\nThank you for subscribing!") + } + + return nil + }, + Commands: []*cli.Command{ + { + Name: "create", + Usage: "Create a new project interactively", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "interactive", + Aliases: []string{"i"}, + Usage: "Enable interactive mode", + }, + &cli.InteractiveStringFlag{ + StringFlag: cli.StringFlag{ + Name: "project-name", + Usage: "Name of the project", + }, + Prompt: "Enter project name", + Required: true, + }, + &cli.InteractiveStringFlag{ + StringFlag: cli.StringFlag{ + Name: "description", + Value: "A new project", + Usage: "Project description", + }, + Prompt: "Enter project description", + Required: false, + }, + &cli.InteractiveStringFlag{ + StringFlag: cli.StringFlag{ + Name: "language", + Value: "go", + Usage: "Programming language", + }, + Prompt: "Enter programming language", + Required: false, + }, + }, + Action: func(ctx context.Context, cmd *cli.Command) error { + fmt.Println("\n=== Project Created ===") + fmt.Printf("Project Name: %s\n", cmd.String("project-name")) + fmt.Printf("Description: %s\n", cmd.String("description")) + fmt.Printf("Language: %s\n", cmd.String("language")) + return nil + }, + }, + }, + } + + if err := cmd.Run(context.Background(), os.Args); err != nil { + log.Fatal(err) + } +} diff --git a/flag_interactive.go b/flag_interactive.go new file mode 100644 index 0000000000..3e0a206eee --- /dev/null +++ b/flag_interactive.go @@ -0,0 +1,460 @@ +package cli + +import ( + "bufio" + "context" + "fmt" + "io" + "os" + "strconv" + "strings" + "time" +) + +type InteractiveFlag = FlagBase[bool, InteractiveConfig, interactiveValue] + +type InteractiveConfig struct { + Prompt string + DefaultValue string + Required bool +} + +type interactiveValue struct { + destination *bool +} + +func (iv interactiveValue) Create(val bool, p *bool, c InteractiveConfig) Value { + *p = val + return &interactiveValue{ + destination: p, + } +} + +func (iv interactiveValue) ToString(value bool) string { + iv.destination = &value + return iv.String() +} + +func (iv *interactiveValue) Set(s string) error { + v, err := strconv.ParseBool(s) + if err != nil { + return err + } + *iv.destination = v + return nil +} + +func (iv *interactiveValue) Get() any { return *iv.destination } + +func (iv *interactiveValue) String() string { + return strconv.FormatBool(*iv.destination) +} + +func (iv *interactiveValue) IsBoolFlag() bool { return true } + +func (cmd *Command) Interactive(name string) bool { + if v, ok := cmd.Value(name).(bool); ok { + tracef("interactive available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) + return v + } + tracef("interactive NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) + return false +} + +type InteractivePrompter interface { + Prompt(prompt string, defaultValue string) (string, error) + PromptRequired(prompt string) (string, error) + PromptConfirm(prompt string, defaultValue bool) (bool, error) + PromptSelect(prompt string, options []string) (int, string, error) +} + +type DefaultPrompter struct { + Reader io.Reader + Writer io.Writer +} + +func NewDefaultPrompter() *DefaultPrompter { + return &DefaultPrompter{ + Reader: os.Stdin, + Writer: os.Stdout, + } +} + +func (p *DefaultPrompter) Prompt(prompt string, defaultValue string) (string, error) { + if defaultValue != "" { + fmt.Fprintf(p.Writer, "%s [%s]: ", prompt, defaultValue) + } else { + fmt.Fprintf(p.Writer, "%s: ", prompt) + } + + scanner := bufio.NewScanner(p.Reader) + if scanner.Scan() { + input := strings.TrimSpace(scanner.Text()) + if input == "" { + return defaultValue, nil + } + return input, nil + } + + if err := scanner.Err(); err != nil { + return "", err + } + + return defaultValue, nil +} + +func (p *DefaultPrompter) PromptRequired(prompt string) (string, error) { + for { + result, err := p.Prompt(prompt, "") + if err != nil { + return "", err + } + if result != "" { + return result, nil + } + fmt.Fprintln(p.Writer, "This field is required. Please enter a value.") + } +} + +func (p *DefaultPrompter) PromptConfirm(prompt string, defaultValue bool) (bool, error) { + options := "y/N" + if defaultValue { + options = "Y/n" + } + + for { + fmt.Fprintf(p.Writer, "%s [%s]: ", prompt, options) + + scanner := bufio.NewScanner(p.Reader) + if scanner.Scan() { + input := strings.ToLower(strings.TrimSpace(scanner.Text())) + if input == "" { + return defaultValue, nil + } + if input == "y" || input == "yes" { + return true, nil + } + if input == "n" || input == "no" { + return false, nil + } + fmt.Fprintln(p.Writer, "Please enter 'y' or 'n'.") + } + + if err := scanner.Err(); err != nil { + return false, err + } + } +} + +func (p *DefaultPrompter) PromptSelect(prompt string, options []string) (int, string, error) { + if len(options) == 0 { + return -1, "", fmt.Errorf("no options provided") + } + + fmt.Fprintln(p.Writer, prompt) + for i, opt := range options { + fmt.Fprintf(p.Writer, " %d. %s\n", i+1, opt) + } + + for { + fmt.Fprintf(p.Writer, "Please select an option (1-%d): ", len(options)) + + scanner := bufio.NewScanner(p.Reader) + if scanner.Scan() { + input := strings.TrimSpace(scanner.Text()) + index, err := strconv.Atoi(input) + if err == nil && index >= 1 && index <= len(options) { + return index - 1, options[index-1], nil + } + fmt.Fprintf(p.Writer, "Invalid selection. Please enter a number between 1 and %d.\n", len(options)) + } + + if err := scanner.Err(); err != nil { + return -1, "", err + } + } +} + +func (cmd *Command) RunInteractive(ctx context.Context, prompter InteractivePrompter) error { + if prompter == nil { + prompter = NewDefaultPrompter() + } + + for _, flag := range cmd.allFlags() { + if !flag.IsSet() { + if ip, ok := flag.(InteractivePrompterFlag); ok { + if ip.ShouldPrompt() { + err := ip.PromptValue(prompter) + if err != nil { + return err + } + } + } + } + } + + return nil +} + +type InteractivePrompterFlag interface { + Flag + ShouldPrompt() bool + PromptValue(prompter InteractivePrompter) error + GetPrompt() string + GetDefaultValue() string + IsPromptRequired() bool +} + +type InteractiveStringFlag struct { + StringFlag + Prompt string + Required bool +} + +func (f *InteractiveStringFlag) ShouldPrompt() bool { + return f.Prompt != "" +} + +func (f *InteractiveStringFlag) GetPrompt() string { + return f.Prompt +} + +func (f *InteractiveStringFlag) GetDefaultValue() string { + return f.Value +} + +func (f *InteractiveStringFlag) IsPromptRequired() bool { + return f.Required +} + +func (f *InteractiveStringFlag) PromptValue(prompter InteractivePrompter) error { + var value string + var err error + + if f.Required { + value, err = prompter.PromptRequired(f.Prompt) + } else { + value, err = prompter.Prompt(f.Prompt, f.Value) + } + + if err != nil { + return err + } + + return f.Set(f.Name, value) +} + +type InteractiveIntFlag struct { + Int64Flag + Prompt string + Required bool +} + +func (f *InteractiveIntFlag) ShouldPrompt() bool { + return f.Prompt != "" +} + +func (f *InteractiveIntFlag) GetPrompt() string { + return f.Prompt +} + +func (f *InteractiveIntFlag) GetDefaultValue() string { + return strconv.FormatInt(f.Value, 10) +} + +func (f *InteractiveIntFlag) IsPromptRequired() bool { + return f.Required +} + +func (f *InteractiveIntFlag) PromptValue(prompter InteractivePrompter) error { + for { + var value string + var err error + + if f.Required { + value, err = prompter.PromptRequired(f.Prompt) + } else { + value, err = prompter.Prompt(f.Prompt, strconv.FormatInt(f.Value, 10)) + } + + if err != nil { + return err + } + + if value == "" && !f.Required { + return nil + } + + _, err = strconv.ParseInt(value, 10, 64) + if err == nil { + return f.Set(f.Name, value) + } + + fmt.Println("Invalid integer value. Please try again.") + } +} + +type InteractiveBoolFlag struct { + BoolFlag + Prompt string + Required bool +} + +func (f *InteractiveBoolFlag) ShouldPrompt() bool { + return f.Prompt != "" +} + +func (f *InteractiveBoolFlag) GetPrompt() string { + return f.Prompt +} + +func (f *InteractiveBoolFlag) GetDefaultValue() string { + return strconv.FormatBool(f.Value) +} + +func (f *InteractiveBoolFlag) IsPromptRequired() bool { + return f.Required +} + +func (f *InteractiveBoolFlag) PromptValue(prompter InteractivePrompter) error { + value, err := prompter.PromptConfirm(f.Prompt, f.Value) + if err != nil { + return err + } + + return f.Set(f.Name, strconv.FormatBool(value)) +} + +type InteractiveFloatFlag struct { + FloatFlag + Prompt string + Required bool +} + +func (f *InteractiveFloatFlag) ShouldPrompt() bool { + return f.Prompt != "" +} + +func (f *InteractiveFloatFlag) GetPrompt() string { + return f.Prompt +} + +func (f *InteractiveFloatFlag) GetDefaultValue() string { + return strconv.FormatFloat(f.Value, 'f', -1, 64) +} + +func (f *InteractiveFloatFlag) IsPromptRequired() bool { + return f.Required +} + +func (f *InteractiveFloatFlag) PromptValue(prompter InteractivePrompter) error { + for { + var value string + var err error + + if f.Required { + value, err = prompter.PromptRequired(f.Prompt) + } else { + value, err = prompter.Prompt(f.Prompt, strconv.FormatFloat(f.Value, 'f', -1, 64)) + } + + if err != nil { + return err + } + + if value == "" && !f.Required { + return nil + } + + _, err = strconv.ParseFloat(value, 64) + if err == nil { + return f.Set(f.Name, value) + } + + fmt.Println("Invalid float value. Please try again.") + } +} + +type InteractiveDurationFlag struct { + DurationFlag + Prompt string + Required bool +} + +func (f *InteractiveDurationFlag) ShouldPrompt() bool { + return f.Prompt != "" +} + +func (f *InteractiveDurationFlag) GetPrompt() string { + return f.Prompt +} + +func (f *InteractiveDurationFlag) GetDefaultValue() string { + return f.Value.String() +} + +func (f *InteractiveDurationFlag) IsPromptRequired() bool { + return f.Required +} + +func (f *InteractiveDurationFlag) PromptValue(prompter InteractivePrompter) error { + for { + var value string + var err error + + if f.Required { + value, err = prompter.PromptRequired(f.Prompt) + } else { + value, err = prompter.Prompt(f.Prompt, f.Value.String()) + } + + if err != nil { + return err + } + + if value == "" && !f.Required { + return nil + } + + _, err = time.ParseDuration(value) + if err == nil { + return f.Set(f.Name, value) + } + + fmt.Println("Invalid duration value (e.g., 1s, 2m, 3h). Please try again.") + } +} + +var InteractiveFlagInstance Flag = &BoolFlag{ + Name: "interactive", + Aliases: []string{"i"}, + Usage: "enable interactive mode for missing parameters", +} + +func (cmd *Command) shouldRunInteractive() bool { + for _, flag := range cmd.allFlags() { + for _, name := range flag.Names() { + if name == "interactive" || name == "i" { + if flag.IsSet() { + if v, ok := flag.Get().(bool); ok { + return v + } + } + } + } + } + return false +} + +func (cmd *Command) handleInteractiveMode(ctx context.Context) error { + prompter := NewDefaultPrompter() + + if cmd.Reader != nil { + prompter.Reader = cmd.Reader + } + if cmd.Writer != nil { + prompter.Writer = cmd.Writer + } + + return cmd.RunInteractive(ctx, prompter) +} diff --git a/flag_interactive_test.go b/flag_interactive_test.go new file mode 100644 index 0000000000..66044940f8 --- /dev/null +++ b/flag_interactive_test.go @@ -0,0 +1,445 @@ +package cli + +import ( + "bytes" + "context" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type MockPrompter struct { + Inputs []string + InputIndex int + Outputs []string +} + +func NewMockPrompter(inputs ...string) *MockPrompter { + return &MockPrompter{ + Inputs: inputs, + InputIndex: 0, + Outputs: []string{}, + } +} + +func (p *MockPrompter) nextInput() string { + if p.InputIndex < len(p.Inputs) { + input := p.Inputs[p.InputIndex] + p.InputIndex++ + return input + } + return "" +} + +func (p *MockPrompter) Prompt(prompt string, defaultValue string) (string, error) { + p.Outputs = append(p.Outputs, prompt) + input := p.nextInput() + if input == "" { + return defaultValue, nil + } + return input, nil +} + +func (p *MockPrompter) PromptRequired(prompt string) (string, error) { + for { + p.Outputs = append(p.Outputs, prompt) + input := p.nextInput() + if input != "" { + return input, nil + } + } +} + +func (p *MockPrompter) PromptConfirm(prompt string, defaultValue bool) (bool, error) { + p.Outputs = append(p.Outputs, prompt) + input := p.nextInput() + if input == "" { + return defaultValue, nil + } + if input == "y" || input == "Y" || input == "yes" || input == "Yes" { + return true, nil + } + return false, nil +} + +func (p *MockPrompter) PromptSelect(prompt string, options []string) (int, string, error) { + p.Outputs = append(p.Outputs, prompt) + input := p.nextInput() + if input == "" { + return 0, options[0], nil + } + index, err := strconv.Atoi(input) + if err != nil { + return 0, options[0], nil + } + if index >= 1 && index <= len(options) { + return index - 1, options[index-1], nil + } + return 0, options[0], nil +} + +func TestDefaultPrompter_Prompt(t *testing.T) { + var output bytes.Buffer + input := bytes.NewBufferString("test input\n") + + prompter := &DefaultPrompter{ + Reader: input, + Writer: &output, + } + + result, err := prompter.Prompt("Enter something", "default") + require.NoError(t, err) + assert.Equal(t, "test input", result) + assert.Contains(t, output.String(), "Enter something [default]") +} + +func TestDefaultPrompter_Prompt_WithDefault(t *testing.T) { + var output bytes.Buffer + input := bytes.NewBufferString("\n") + + prompter := &DefaultPrompter{ + Reader: input, + Writer: &output, + } + + result, err := prompter.Prompt("Enter something", "default value") + require.NoError(t, err) + assert.Equal(t, "default value", result) +} + +func TestDefaultPrompter_PromptConfirm(t *testing.T) { + testCases := []struct { + name string + input string + defaultValue bool + expected bool + }{ + {"yes input", "y\n", false, true}, + {"no input", "n\n", true, false}, + {"empty with default true", "\n", true, true}, + {"empty with default false", "\n", false, false}, + {"Yes input", "Yes\n", false, true}, + {"NO input", "NO\n", true, false}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var output bytes.Buffer + input := bytes.NewBufferString(tc.input) + + prompter := &DefaultPrompter{ + Reader: input, + Writer: &output, + } + + result, err := prompter.PromptConfirm("Confirm?", tc.defaultValue) + require.NoError(t, err) + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestDefaultPrompter_PromptSelect(t *testing.T) { + options := []string{"Option 1", "Option 2", "Option 3"} + + testCases := []struct { + name string + input string + expectedIndex int + expectedValue string + }{ + {"select first", "1\n", 0, "Option 1"}, + {"select second", "2\n", 1, "Option 2"}, + {"select third", "3\n", 2, "Option 3"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var output bytes.Buffer + input := bytes.NewBufferString(tc.input) + + prompter := &DefaultPrompter{ + Reader: input, + Writer: &output, + } + + index, value, err := prompter.PromptSelect("Choose an option", options) + require.NoError(t, err) + assert.Equal(t, tc.expectedIndex, index) + assert.Equal(t, tc.expectedValue, value) + assert.Contains(t, output.String(), "Choose an option") + }) + } +} + +func TestInteractiveStringFlag_PromptValue(t *testing.T) { + flag := &InteractiveStringFlag{ + StringFlag: StringFlag{ + Name: "username", + Value: "default_user", + }, + Prompt: "Enter username", + Required: false, + } + + err := flag.PreParse() + require.NoError(t, err) + + mockPrompter := NewMockPrompter("test_user") + + err = flag.PromptValue(mockPrompter) + require.NoError(t, err) + + assert.Equal(t, "test_user", flag.Get()) + assert.Contains(t, mockPrompter.Outputs[0], "Enter username") +} + +func TestInteractiveStringFlag_PromptValue_WithDefault(t *testing.T) { + flag := &InteractiveStringFlag{ + StringFlag: StringFlag{ + Name: "username", + Value: "default_user", + }, + Prompt: "Enter username", + Required: false, + } + + err := flag.PreParse() + require.NoError(t, err) + + mockPrompter := NewMockPrompter("") + + err = flag.PromptValue(mockPrompter) + require.NoError(t, err) + + assert.Equal(t, "default_user", flag.Get()) +} + +func TestInteractiveIntFlag_PromptValue(t *testing.T) { + flag := &InteractiveIntFlag{ + Int64Flag: Int64Flag{ + Name: "age", + Value: 18, + }, + Prompt: "Enter age", + Required: false, + } + + err := flag.PreParse() + require.NoError(t, err) + + mockPrompter := NewMockPrompter("25") + + err = flag.PromptValue(mockPrompter) + require.NoError(t, err) + + assert.Equal(t, int64(25), flag.Get()) +} + +func TestInteractiveBoolFlag_PromptValue(t *testing.T) { + flag := &InteractiveBoolFlag{ + BoolFlag: BoolFlag{ + Name: "subscribe", + Value: false, + }, + Prompt: "Subscribe to newsletter", + } + + err := flag.PreParse() + require.NoError(t, err) + + mockPrompter := NewMockPrompter("y") + + err = flag.PromptValue(mockPrompter) + require.NoError(t, err) + + assert.Equal(t, true, flag.Get()) +} + +func TestInteractiveFloatFlag_PromptValue(t *testing.T) { + flag := &InteractiveFloatFlag{ + FloatFlag: FloatFlag{ + Name: "price", + Value: 9.99, + }, + Prompt: "Enter price", + Required: false, + } + + err := flag.PreParse() + require.NoError(t, err) + + mockPrompter := NewMockPrompter("19.99") + + err = flag.PromptValue(mockPrompter) + require.NoError(t, err) + + assert.Equal(t, 19.99, flag.Get()) +} + +func TestInteractiveDurationFlag_PromptValue(t *testing.T) { + flag := &InteractiveDurationFlag{ + DurationFlag: DurationFlag{ + Name: "timeout", + Value: 30 * time.Second, + }, + Prompt: "Enter timeout", + Required: false, + } + + err := flag.PreParse() + require.NoError(t, err) + + mockPrompter := NewMockPrompter("1m") + + err = flag.PromptValue(mockPrompter) + require.NoError(t, err) + + assert.Equal(t, 1*time.Minute, flag.Get()) +} + +func TestCommand_RunInteractive(t *testing.T) { + cmd := &Command{ + Flags: []Flag{ + &InteractiveStringFlag{ + StringFlag: StringFlag{ + Name: "name", + }, + Prompt: "Enter your name", + Required: true, + }, + &InteractiveIntFlag{ + Int64Flag: Int64Flag{ + Name: "age", + Value: 18, + }, + Prompt: "Enter your age", + Required: false, + }, + }, + Action: func(ctx context.Context, cmd *Command) error { + return nil + }, + } + + mockPrompter := NewMockPrompter("John", "25") + + err := cmd.RunInteractive(context.Background(), mockPrompter) + require.NoError(t, err) + + assert.Equal(t, 2, len(mockPrompter.Outputs)) +} + +func TestInteractivePrompterFlag_Interface(t *testing.T) { + var _ InteractivePrompterFlag = &InteractiveStringFlag{} + var _ InteractivePrompterFlag = &InteractiveIntFlag{} + var _ InteractivePrompterFlag = &InteractiveBoolFlag{} + var _ InteractivePrompterFlag = &InteractiveFloatFlag{} + var _ InteractivePrompterFlag = &InteractiveDurationFlag{} +} + +func TestInteractiveStringFlag_ShouldPrompt(t *testing.T) { + flagWithPrompt := &InteractiveStringFlag{ + Prompt: "Enter something", + } + assert.True(t, flagWithPrompt.ShouldPrompt()) + + flagWithoutPrompt := &InteractiveStringFlag{} + assert.False(t, flagWithoutPrompt.ShouldPrompt()) +} + +func TestCommand_InteractiveMethod(t *testing.T) { + cmd := &Command{ + Flags: []Flag{ + &BoolFlag{ + Name: "interactive", + Value: true, + }, + }, + } + + for _, f := range cmd.allFlags() { + err := f.PreParse() + require.NoError(t, err) + err = f.Set("interactive", "true") + require.NoError(t, err) + } + + assert.True(t, cmd.shouldRunInteractive()) +} + +func TestCommand_InteractiveMethod_NotSet(t *testing.T) { + cmd := &Command{ + Flags: []Flag{ + &StringFlag{ + Name: "name", + Value: "test", + }, + }, + } + + for _, f := range cmd.allFlags() { + err := f.PreParse() + require.NoError(t, err) + } + + assert.False(t, cmd.shouldRunInteractive()) +} + +func TestInteractiveStringFlag_GetDefaultValue(t *testing.T) { + flag := &InteractiveStringFlag{ + StringFlag: StringFlag{ + Name: "test", + Value: "default_value", + }, + } + + assert.Equal(t, "default_value", flag.GetDefaultValue()) +} + +func TestInteractiveIntFlag_GetDefaultValue(t *testing.T) { + flag := &InteractiveIntFlag{ + Int64Flag: Int64Flag{ + Name: "test", + Value: 42, + }, + } + + assert.Equal(t, "42", flag.GetDefaultValue()) +} + +func TestInteractiveBoolFlag_GetDefaultValue(t *testing.T) { + flag := &InteractiveBoolFlag{ + BoolFlag: BoolFlag{ + Name: "test", + Value: true, + }, + } + + assert.Equal(t, "true", flag.GetDefaultValue()) +} + +func TestInteractiveFloatFlag_GetDefaultValue(t *testing.T) { + flag := &InteractiveFloatFlag{ + FloatFlag: FloatFlag{ + Name: "test", + Value: 3.14, + }, + } + + assert.Equal(t, "3.14", flag.GetDefaultValue()) +} + +func TestInteractiveDurationFlag_GetDefaultValue(t *testing.T) { + flag := &InteractiveDurationFlag{ + DurationFlag: DurationFlag{ + Name: "test", + Value: 5 * time.Second, + }, + } + + assert.Equal(t, "5s", flag.GetDefaultValue()) +}