Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions argument.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type Argument struct {
description string
parser argParser
defaultValue any
variadic bool

value any
}
Expand Down Expand Up @@ -44,12 +45,21 @@ func (a *Argument) isOptional() bool {
return !a.isRequired()
}

func (a *Argument) isVariadic() bool {
return a.variadic
}

func (a *Argument) inBrackets() string {
name := a.name
if a.isVariadic() {
name += "..."
}

if a.isOptional() {
return fmt.Sprintf("[<%s>]", a.name)
return fmt.Sprintf("[<%s>]", name)
}

return fmt.Sprintf("<%s>", a.name)
return fmt.Sprintf("<%s>", name)
}

func (c *Command) findArg(name string) (*Argument, bool) {
Expand Down
10 changes: 10 additions & 0 deletions argument_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,13 @@ func SetArgDefault[T Parseable](defaultValue T) option.Func[*Argument] {
return argument, nil
}
}

// SetArgVariadic makes the argument accept a variable number of values.
// When set, this argument will collect all remaining command line arguments
// into a slice. Only the last argument in a command can be variadic.
func SetArgVariadic() option.Func[*Argument] {
return func(argument *Argument) (*Argument, error) {
argument.variadic = true
return argument, nil
}
}
21 changes: 21 additions & 0 deletions command_config_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ func (c *Command) validateConfig() error {
c.validateNoDuplicateArguments,
c.validateNoDuplicateSubCommands,
c.validateEitherCommandsOrArguments,
c.validateVariadicArguments,
}

var errs []error
Expand Down Expand Up @@ -72,3 +73,23 @@ func (c *Command) validateEitherCommandsOrArguments() error {

return nil
}

func (c *Command) validateVariadicArguments() error {
var errs []error

// Check that only the last argument can be variadic
for i, argument := range c.arguments {
if argument.isVariadic() && i != len(c.arguments)-1 {
errs = append(errs, errors.Errorf("only the last argument can be variadic, but argument %q at position %d is variadic", argument.name, i+1))
}
}

// Check that variadic arguments cannot have default values
for _, argument := range c.arguments {
if argument.isVariadic() && argument.isOptional() {
errs = append(errs, errors.Errorf("variadic argument %q cannot have a default value", argument.name))
}
}

return errors.Join(errs...)
}
33 changes: 33 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,39 @@ func ArgValue[T any](ctx context.Context, name string) (T, error) {
return arg.defaultValue.(T), nil
}

func VariadicArgValue[T any](ctx context.Context, name string) ([]T, error) {
command, err := commandFromContext(ctx)
if err != nil {
return nil, errors.Wrapf(err, "finding variadic argument %q", name)
}

arg, found := command.findArg(name)
if !found {
return nil, errors.Wrapf(ArgumentNotFoundError, "finding variadic argument %q", name)
}

if !arg.isVariadic() {
return nil, errors.Errorf("argument %q is not variadic", name)
}

if arg.value == nil {
return []T{}, nil
}

// Convert []any to []T
anySlice := arg.value.([]any)
result := make([]T, len(anySlice))
for i, v := range anySlice {
if val, ok := v.(T); ok {
result[i] = val
} else {
return nil, errors.Errorf("invalid type in variadic argument %q at index %d: expected %T, got %T", name, i, *new(T), v)
}
}

return result, nil
}

type commandContextKeyType struct{}

var commandContextKey = commandContextKeyType{}
Expand Down
94 changes: 94 additions & 0 deletions example_variadic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package cli_test

import (
"context"
"fmt"

"github.com/broothie/cli"
)

// Example demonstrates using variadic arguments to copy multiple files
func Example_variadic_arguments() {
cli.Run("copy", "Copy files to destination",
// Required destination argument
cli.AddArg("destination", "Destination directory"),

// Variadic source files argument
cli.AddArg("sources", "Source files to copy", cli.SetArgVariadic()),

cli.SetHandler(func(ctx context.Context) error {
// Get the destination
dest, err := cli.ArgValue[string](ctx, "destination")
if err != nil {
return err
}

// Get all source files as a slice
sources, err := cli.VariadicArgValue[string](ctx, "sources")
if err != nil {
return err
}

fmt.Printf("Copying %d files to %s:\n", len(sources), dest)
for _, source := range sources {
fmt.Printf(" %s -> %s\n", source, dest)
}

return nil
}),
)
}

// Example demonstrates using variadic arguments for a command that processes multiple numbers
func Example_variadic_with_types() {
cli.Run("sum", "Calculate sum of numbers",
cli.AddArg("numbers", "Numbers to sum",
cli.SetArgParser(cli.IntParser),
cli.SetArgVariadic(),
),

cli.SetHandler(func(ctx context.Context) error {
numbers, err := cli.VariadicArgValue[int](ctx, "numbers")
if err != nil {
return err
}

sum := 0
for _, num := range numbers {
sum += num
}

fmt.Printf("Sum of %v = %d\n", numbers, sum)
return nil
}),
)
}

// Example demonstrates mixing flags with variadic arguments
func Example_variadic_with_flags() {
cli.Run("process", "Process files with options",
cli.AddFlag("verbose", "Enable verbose output", cli.SetFlagDefault(false)),
cli.AddFlag("format", "Output format", cli.SetFlagDefault("text")),
cli.AddArg("files", "Files to process", cli.SetArgVariadic()),

cli.SetHandler(func(ctx context.Context) error {
verbose, _ := cli.FlagValue[bool](ctx, "verbose")
format, _ := cli.FlagValue[string](ctx, "format")
files, _ := cli.VariadicArgValue[string](ctx, "files")

if verbose {
fmt.Printf("Processing %d files in %s format:\n", len(files), format)
}

for _, file := range files {
if verbose {
fmt.Printf("Processing: %s\n", file)
} else {
fmt.Printf("%s\n", file)
}
}

return nil
}),
)
}
42 changes: 41 additions & 1 deletion parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,13 @@ func (p *parser) processArg() error {
return errors.Wrapf(TooManyArgumentsError, "only expected %d arguments", len(p.command.arguments))
}

current, _ := p.current()
argument := p.command.arguments[p.argumentIndex]

if argument.isVariadic() {
return p.processVariadicArg(argument)
}

current, _ := p.current()
value, err := argument.parser.Parse(current)
if err != nil {
return errors.Wrapf(err, "parsing provided value %q for argument %d", current, p.argumentIndex+1)
Expand All @@ -218,6 +223,41 @@ func (p *parser) processArg() error {
return nil
}

func (p *parser) processVariadicArg(argument *Argument) error {
var values []any

// Collect all remaining arguments that aren't flags or subcommands
for p.index < len(p.tokens) {
current, _ := p.current()

// Stop if we encounter a flag
if strings.HasPrefix(current, flagPrefix) {
break
}

// Stop if we encounter a subcommand
if _, found := lo.Find(p.command.subCommands, func(subCommand *Command) bool {
return subCommand.name == current
}); found {
break
}

// Parse the current argument
value, err := argument.parser.Parse(current)
if err != nil {
return errors.Wrapf(err, "parsing provided value %q for variadic argument %q", current, argument.name)
}

values = append(values, value)
p.index += 1
}

// Store the collected values as a slice
argument.value = values
p.argumentIndex += 1
return nil
}

func (c *Command) findLongFlag(name string) (*Flag, bool) {
return c.findFlagUpToRoot(func(flag *Flag) bool { return flag.name == name || lo.Contains(flag.aliases, name) })
}
Expand Down
Loading
Loading