Repository: go-go-golems/glazed (go)

Issue:
Properly format arguments in the Use when converting to cobra command
We are currently passing the program name straight to use, which means that there is no documentation about the possible arguments. This should be more explicit.

Pull request:
:sparkles: Properly show arguments in use string 
Add argument info to cobra command Use field 

This PR updates the cobra integration to populate the Use field with a generated string that incorporates the argument definitions. 

The Use string now shows:
- Required vs optional arguments
- Default values 
- List arguments that accept multiple values

Some key points:

- A new function GenerateUseString was added to create the Use string.
- Required args are in <>, optional in [], lists end in ..., defaults show the value.
- The ordering of arguments is maintained from the definition list.

On the implementation side:
- Argument parsing logic was moved to its own function GatherArguments for easier testing.
- Switched to using an OrderedMap instead of a regular map to preserve order.
- Added more parameter type tests for list and file-loading detection.
- Added more tests for different argument parsing scenarios.

Closes #344 

Patch:
diff --git a/pkg/cli/cobra.go b/pkg/cli/cobra.go
index efef0d2..bd780c6 100644
--- a/pkg/cli/cobra.go
+++ b/pkg/cli/cobra.go
@@ -49,7 +49,8 @@ func GatherParametersFromCobraCommand(
 
 	// merge parameters and arguments
 	// arguments take precedence over parameters
-	for k, v := range arguments {
+	for p := arguments.Oldest(); p != nil; p = p.Next() {
+		k, v := p.Key, p.Value
 		ps[k] = v
 	}
 
@@ -296,7 +297,7 @@ func BuildCobraCommandAlias(alias *alias.CommandAlias) (*cobra.Command, error) {
 	provided, err := parameters.GatherArguments(alias.Arguments, argumentDefinitions, true)
 
 	for _, argDef := range argumentDefinitions {
-		_, ok := provided[argDef.Name]
+		_, ok := provided.Get(argDef.Name)
 		if argDef.Required && !ok {
 			minArgs++
 		}
diff --git a/pkg/cmds/parameters/cobra.go b/pkg/cmds/parameters/cobra.go
index d3f2f71..278ca3d 100644
--- a/pkg/cmds/parameters/cobra.go
+++ b/pkg/cmds/parameters/cobra.go
@@ -7,15 +7,24 @@ import (
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
 	"github.com/spf13/viper"
+	orderedmap "github.com/wk8/go-ordered-map/v2"
 	"strings"
 	"time"
 )
 
-// AddArgumentsToCobraCommand adds the arguments (not the flags) of a CommandDescription to a cobra command
-// as positional arguments.
-// An optional argument cannot be followed by a required argument.
-// Similarly, a list of arguments cannot be followed by any argument (since we otherwise wouldn't
-// know how many belong to the list and where to do the cut off).
+// AddArgumentsToCobraCommand adds each ParameterDefinition from `arguments` as positional arguments to the provided `cmd` cobra command.
+// Argument ordering, optionality, and multiplicity constraints are respected:
+//   - Required arguments (argument.Required == true) should come before the optional.
+//   - Only one list argument (either ParameterTypeStringList or ParameterTypeIntegerList) is allowed and it should be the last one.
+//
+// Any violation of these conditions yields an error.
+// This function processes each argument, checks their default values for validity, which if invalid,
+// triggers an error return.
+//
+// It computes the minimum and maximum number of arguments the command can take based on the required, optional,
+// and list arguments.
+// If everything is successful, it assigns an argument validator (either MinimumNArgs or RangeArgs)
+// to the cobra command's Args attribute.
 func AddArgumentsToCobraCommand(cmd *cobra.Command, arguments []*ParameterDefinition) error {
 	minArgs := 0
 	// -1 signifies unbounded
@@ -51,27 +60,80 @@ func AddArgumentsToCobraCommand(cmd *cobra.Command, arguments []*ParameterDefini
 		cmd.Args = cobra.RangeArgs(minArgs, maxArgs)
 	}
 
+	cmd.Use = GenerateUseString(cmd, arguments)
+
 	return nil
 }
 
-// GatherArguments parses the positional arguments passed as a list of strings into a map of
-// parsed values. If onlyProvided is true, then only arguments that are provided are returned
-// (i.e. the default values are not included).
+// GenerateUseString creates a string representation of the 'Use' field for a given cobra command and a list of parameter definitions. The first word of the existing 'Use' field is treated as the verb for the command.
+// The resulting string briefly describes how to use the command respecting the following conventions:
+//   - Required parameters are enclosed in '<>'.
+//   - Optional parameters are enclosed in '[]'.
+//   - Optional parameters that accept multiple input (ParameterTypeStringList or ParameterTypeIntegerList) are followed by '...'.
+//   - If a parameter has a default value, it is specified after parameter name like 'parameter (default: value)'.
+//
+// For example:
+//   - If there is a required parameter 'name', and an optional parameter 'age' with a default value of '30', the resulting string will be: 'verb <name> [age (default: 30)]'.
+//   - If there is a required parameter 'name', and an optional parameter 'colors' of type ParameterTypeStringList, the resulting Use string will be: 'verb <name> [colors...]'
+func GenerateUseString(cmd *cobra.Command, arguments []*ParameterDefinition) string {
+	fields := strings.Fields(cmd.Use)
+	if len(fields) == 0 {
+		return ""
+	}
+	verb := fields[0]
+	useStr := verb
+	var defaultValueStr string
+
+	for _, arg := range arguments {
+		defaultValueStr = ""
+		if arg.Default != nil {
+			defaultValueStr = fmt.Sprintf(" (default: %v)", arg.Default)
+		}
+		left, right := "[", "]"
+		if arg.Required {
+			left, right = "<", ">"
+		}
+		if arg.Type == ParameterTypeStringList || arg.Type == ParameterTypeIntegerList {
+			useStr += " " + left + arg.Name + "..." + defaultValueStr + right
+		} else {
+			useStr += " " + left + arg.Name + defaultValueStr + right
+		}
+	}
+
+	return useStr
+}
+
+// GatherArguments parses positional string arguments into a map of values.
+//
+// It takes the command-line arguments, the expected argument definitions,
+// and a boolean indicating whether to only include explicitly provided arguments,
+// not the default values of missing arguments.
+//
+// Only the last parameter definitions can be a list parameter type.
+//
+// Required arguments missing from the input will result in an error.
+// Arguments with default values can be included based on the onlyProvided flag.
+//
+// The result is a map with argument names as keys and parsed values.
+// Argument order is maintained.
+//
+// Any extra arguments not defined will result in an error.
+// Parsing errors for individual arguments will also return errors.
 func GatherArguments(
 	args []string,
 	arguments []*ParameterDefinition,
 	onlyProvided bool,
-) (map[string]interface{}, error) {
+) (*orderedmap.OrderedMap[string, interface{}], error) {
 	_ = args
-	result := make(map[string]interface{})
+	result := orderedmap.New[string, interface{}]()
 	argsIdx := 0
 	for _, argument := range arguments {
 		if argsIdx >= len(args) {
 			if argument.Required {
-				return nil, errors.Errorf("Argument %s not found", argument.Name)
+				return nil, fmt.Errorf("Argument %s not found", argument.Name)
 			} else {
 				if argument.Default != nil && !onlyProvided {
-					result[argument.Name] = argument.Default
+					result.Set(argument.Name, argument.Default)
 				}
 				continue
 			}
@@ -90,10 +152,10 @@ func GatherArguments(
 			return nil, err
 		}
 
-		result[argument.Name] = i2
+		result.Set(argument.Name, i2)
 	}
 	if argsIdx < len(args) {
-		return nil, errors.Errorf("Too many arguments")
+		return nil, fmt.Errorf("Too many arguments")
 	}
 	return result, nil
 }
diff --git a/pkg/cmds/parameters/parameters.go b/pkg/cmds/parameters/parameters.go
index 9303987..d6f79f8 100644
--- a/pkg/cmds/parameters/parameters.go
+++ b/pkg/cmds/parameters/parameters.go
@@ -479,19 +479,13 @@ const (
 func IsFileLoadingParameter(p ParameterType, v string) bool {
 	//exhaustive:ignore
 	switch p {
-	case ParameterTypeStringFromFile:
-		return true
-	case ParameterTypeObjectListFromFile:
-		return true
-	case ParameterTypeObjectFromFile:
-		return true
-	case ParameterTypeStringListFromFile:
-		return true
-	case ParameterTypeObjectListFromFiles:
-		return true
-	case ParameterTypeStringListFromFiles:
-		return true
-	case ParameterTypeStringFromFiles:
+	case ParameterTypeStringFromFile,
+		ParameterTypeObjectListFromFile,
+		ParameterTypeObjectFromFile,
+		ParameterTypeStringListFromFile,
+		ParameterTypeObjectListFromFiles,
+		ParameterTypeStringListFromFiles,
+		ParameterTypeStringFromFiles:
 		return true
 	case ParameterTypeKeyValue:
 		return strings.HasPrefix(v, "@")
@@ -500,22 +494,19 @@ func IsFileLoadingParameter(p ParameterType, v string) bool {
 	}
 }
 
+// IsListParameter returns if the parameter has to be parsed from a list of strings,
+// not if its value is actually a string.
 func IsListParameter(p ParameterType) bool {
 	//exhaustive:ignore
 	switch p {
-	case ParameterTypeObjectListFromFiles:
-		return true
-	case ParameterTypeStringListFromFiles:
-		return true
-	case ParameterTypeStringFromFiles:
-		return true
-	case ParameterTypeStringList:
-		return true
-	case ParameterTypeIntegerList:
-		return true
-	case ParameterTypeFloatList:
-		return true
-	case ParameterTypeKeyValue:
+	case ParameterTypeObjectListFromFiles,
+		ParameterTypeStringListFromFiles,
+		ParameterTypeStringFromFiles,
+		ParameterTypeStringList,
+		ParameterTypeIntegerList,
+		ParameterTypeFloatList,
+		ParameterTypeChoiceList,
+		ParameterTypeKeyValue:
 		return true
 	default:
 		return false
diff --git a/pkg/doc/topics/06-usage-string.md b/pkg/doc/topics/06-usage-string.md
new file mode 100644
index 0000000..5e49c43
--- /dev/null
+++ b/pkg/doc/topics/06-usage-string.md
@@ -0,0 +1,77 @@
+---
+Title: Understanding Command Usage Strings
+Slug: usage-string
+Short: |
+  Explains how command argument usage is indicated within our application.
+Topics:
+- User Guide
+Commands: []
+Flags: []
+IsTemplate: false
+IsTopLevel: true
+ShowPerDefault: false
+SectionType: GeneralTopic
+---
+
+## Overview
+
+Every command stems around a verb which acts as the main action the command is performing.
+In contrast to parameter flags, which are preceded by `--` for `-`, arguments are 
+passed as normal arguments. They can be interleaved with normal flags.
+
+Arguments can be:
+- required. These need to be provided by the user, or have a default value
+- list arguments, which means that they gobble up the rest of the arguments
+
+When parsing the arguments, the parser will try to match the command-line arguments to the command's arguments.
+Once the arguments run out, the parser will try to match the remaining arguments to their default values.
+
+Finally, leftover arguments are assigned to a potential list argument.
+
+- Required arguments are always placed before optional arguments.
+- Parameters accepting list inputs should not directly follow each other.
+
+### Required Parameters
+
+Required arguments are indicated by angle brackets `<>`.
+These arguments must be specified for a command to run successfully. For example:
+
+```
+command <filename>
+```
+
+This indicates that the command requires a filename to be specified for it to run properly.
+
+### Optional Parameters
+
+Optional arguments can be identified by the square brackets `[]`.
+These arguments may be skipped, and the command may still run successfully. For example:
+
+```
+command <filename> [directory]
+```
+
+Here the `directory` is optional.
+If not provided, the command will still execute, but with certain default settings.
+
+### List Parameters
+
+Some commands may accept a list of inputs for certain arguments. This is symbolized by an ellipsis `...` following the argument.
+
+```
+command <filename> [tags...]
+```
+
+In this case, the `tags` argument can accept a list of values.
+
+### Default Values
+
+Parameters may come with default values. These can be identified by text following the format `default: value`. This means that if you do not provide a value for this argument, the system will use the default value.
+
+```
+command <filename> [directory (default: home)]
+```
+
+This command implies that if no directory is specified,
+the system uses the `home` directory as a default.
+
diff --git a/scripts/param-definitions.sh b/scripts/param-definitions.sh
new file mode 100755
index 0000000..84157ff
--- /dev/null
+++ b/scripts/param-definitions.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+for i in CommandDescription ParameterDefinition ParameterLayer; do
+	oak go definitions --recurse pkg/ --name "$i" --definition-type struct,interface
+done


Test patch:
diff --git a/pkg/cmds/cobra_test.go b/pkg/cmds/cobra_test.go
index 921ae9a..85992aa 100644
--- a/pkg/cmds/cobra_test.go
+++ b/pkg/cmds/cobra_test.go
@@ -8,13 +8,14 @@ import (
 	"github.com/spf13/cobra"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	orderedmap "github.com/wk8/go-ordered-map/v2"
 	"github.com/zenizh/go-capturer"
 	"gopkg.in/yaml.v3"
 	"testing"
 )
 
 func TestAddZeroArguments(t *testing.T) {
-	cmd := &cobra.Command{}
+	cmd := &cobra.Command{Use: "test"}
 	desc := CommandDescription{
 		Arguments: []*parameters.ParameterDefinition{},
 	}
@@ -42,8 +43,10 @@ func TestAddSingleRequiredArgument(t *testing.T) {
 
 	values, err := parameters.GatherArguments([]string{"bar"}, desc.Arguments, false)
 	require.Nil(t, err)
-	assert.Equal(t, 1, len(values))
-	assert.Equal(t, "bar", values["foo"])
+	assert.Equal(t, 1, values.Len())
+	v1, ok := values.Get("foo")
+	require.True(t, ok)
+	assert.Equal(t, "bar", v1)
 
 	_, err = parameters.GatherArguments([]string{}, desc.Arguments, false)
 	assert.Error(t, err)
@@ -77,9 +80,13 @@ func TestAddTwoRequiredArguments(t *testing.T) {
 
 	values, err := parameters.GatherArguments([]string{"bar", "foo"}, desc.Arguments, false)
 	require.Nil(t, err)
-	assert.Equal(t, 2, len(values))
-	assert.Equal(t, "bar", values["foo"])
-	assert.Equal(t, "foo", values["bar"])
+	assert.Equal(t, 2, values.Len())
+	v1, ok := values.Get("foo")
+	require.True(t, ok)
+	assert.Equal(t, "bar", v1)
+	v2, ok := values.Get("bar")
+	require.True(t, ok)
+	assert.Equal(t, "foo", v2)
 
 	_, err = parameters.GatherArguments([]string{}, desc.Arguments, false)
 	assert.Error(t, err)
@@ -116,15 +123,22 @@ func TestOneRequiredOneOptionalArgument(t *testing.T) {
 
 	values, err := parameters.GatherArguments([]string{"bar", "foo"}, desc.Arguments, false)
 	require.Nil(t, err)
-	assert.Equal(t, 2, len(values))
-	assert.Equal(t, "bar", values["foo"])
-	assert.Equal(t, "foo", values["bar"])
+	assert.Equal(t, 2, values.Len())
+	v1, ok := values.Get("foo")
+	require.True(t, ok)
+	assert.Equal(t, "bar", v1)
+	v2, ok := values.Get("bar")
+	require.True(t, ok)
+	assert.Equal(t, "foo", v2)
 
 	values, err = parameters.GatherArguments([]string{"foo"}, desc.Arguments, false)
 	require.Nil(t, err)
-	assert.Equal(t, 2, len(values))
-	assert.Equal(t, "foo", values["foo"])
-	assert.Equal(t, "baz", values["bar"])
+	v1, ok = values.Get("foo")
+	require.True(t, ok)
+	assert.Equal(t, "foo", v1)
+	v2, ok = values.Get("bar")
+	require.True(t, ok)
+	assert.Equal(t, "baz", v2)
 
 	_, err = parameters.GatherArguments([]string{}, desc.Arguments, false)
 	assert.Error(t, err)
@@ -152,13 +166,17 @@ func TestOneOptionalArgument(t *testing.T) {
 
 	values, err := parameters.GatherArguments([]string{"foo"}, desc.Arguments, false)
 	require.Nil(t, err)
-	assert.Equal(t, 1, len(values))
-	assert.Equal(t, "foo", values["foo"])
+	assert.Equal(t, 1, values.Len())
+	v1, ok := values.Get("foo")
+	require.True(t, ok)
+	assert.Equal(t, "foo", v1)
 
 	values, err = parameters.GatherArguments([]string{}, desc.Arguments, false)
 	require.Nil(t, err)
-	assert.Equal(t, 1, len(values))
-	assert.Equal(t, "123", values["foo"])
+	assert.Equal(t, 1, values.Len())
+	v1, ok = values.Get("foo")
+	require.True(t, ok)
+	assert.Equal(t, "123", v1)
 }
 
 func TestDefaultIntValue(t *testing.T) {
@@ -176,13 +194,17 @@ func TestDefaultIntValue(t *testing.T) {
 	require.Nil(t, err)
 	values, err := parameters.GatherArguments([]string{}, desc.Arguments, false)
 	require.Nil(t, err)
-	assert.Equal(t, 1, len(values))
-	assert.Equal(t, 123, values["foo"])
+	assert.Equal(t, 1, values.Len())
+	v1, ok := values.Get("foo")
+	require.True(t, ok)
+	assert.Equal(t, 123, v1)
 
 	values, err = parameters.GatherArguments([]string{"234"}, desc.Arguments, false)
 	require.Nil(t, err)
-	assert.Equal(t, 1, len(values))
-	assert.Equal(t, 234, values["foo"])
+	assert.Equal(t, 1, values.Len())
+	v1, ok = values.Get("foo")
+	require.True(t, ok)
+	assert.Equal(t, 234, v1)
 
 	_, err = parameters.GatherArguments([]string{"foo"}, desc.Arguments, false)
 	assert.Error(t, err)
@@ -303,15 +325,21 @@ func TestAddStringListOptionalArgument(t *testing.T) {
 
 	values, err := parameters.GatherArguments([]string{"bar", "foo"}, desc.Arguments, false)
 	require.Nil(t, err)
-	assert.Equal(t, []string{"bar", "foo"}, values["foo"])
+	v1, ok := values.Get("foo")
+	require.True(t, ok)
+	assert.Equal(t, []string{"bar", "foo"}, v1)
 
 	values, err = parameters.GatherArguments([]string{"foo"}, desc.Arguments, false)
 	require.Nil(t, err)
-	assert.Equal(t, []string{"foo"}, values["foo"])
+	v1, ok = values.Get("foo")
+	require.True(t, ok)
+	assert.Equal(t, []string{"foo"}, v1)
 
 	values, err = parameters.GatherArguments([]string{}, desc.Arguments, false)
 	require.Nil(t, err)
-	assert.Equal(t, []string{"baz"}, values["foo"])
+	v1, ok = values.Get("foo")
+	require.True(t, ok)
+	assert.Equal(t, []string{"baz"}, v1)
 }
 
 func TestFailAddingArgumentAfterStringList(t *testing.T) {
@@ -505,7 +533,7 @@ func testCommandParseHelper(
 	var flagsError error
 	var argsError error
 	var flagParameters map[string]interface{}
-	var argumentParameters map[string]interface{}
+	var argumentParameters *orderedmap.OrderedMap[string, interface{}]
 
 	cmd := &cobra.Command{
 		Run: func(cmd *cobra.Command, args []string) {
diff --git a/pkg/cmds/parameters/cobra-gather-arguments_test.go b/pkg/cmds/parameters/cobra-gather-arguments_test.go
new file mode 100644
index 0000000..a9449c8
--- /dev/null
+++ b/pkg/cmds/parameters/cobra-gather-arguments_test.go
@@ -0,0 +1,442 @@
+package parameters
+
+import (
+	"github.com/spf13/cobra"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"testing"
+)
+
+// Test no arguments are passed to the function
+func TestGatherArguments_NoArguments(t *testing.T) {
+	_, err := GatherArguments([]string{}, []*ParameterDefinition{}, true)
+	assert.NoError(t, err)
+}
+
+// Test missing required argument
+func TestGatherArguments_RequiredMissing(t *testing.T) {
+	arg := []*ParameterDefinition{
+		{
+			Name:     "Test",
+			Required: true,
+		},
+	}
+	_, err := GatherArguments([]string{}, arg, true)
+	assert.EqualError(t, err, "Argument Test not found")
+}
+
+// Test the parsing of every kind of parameter type for provided args
+// This should be broken down into individual tests for each parameter types.
+// However a generic example of such test might look like:
+func TestGatherArguments_ParsingProvidedArguments(t *testing.T) {
+	arg := []*ParameterDefinition{
+		{
+			Name: "Test",
+			Type: ParameterTypeString,
+		},
+	}
+	res, err := GatherArguments([]string{"value"}, arg, true)
+	assert.NoError(t, err)
+	v, present := res.Get("Test")
+	assert.True(t, present)
+	assert.Equal(t, v, "value")
+}
+
+// Test parsing of list-type parameter with multiple arguments
+func TestGatherArguments_ListParameterParsing(t *testing.T) {
+	arg := []*ParameterDefinition{
+		{
+			Name: "Test",
+			Type: ParameterTypeStringList,
+		},
+	}
+	res, err := GatherArguments([]string{"value1", "value2"}, arg, true)
+	assert.NoError(t, err)
+	v, present := res.Get("Test")
+	assert.True(t, present)
+	assert.Equal(t, v, []string{"value1", "value2"})
+}
+
+// Test handling of default values when onlyProvided is set to false
+func TestGatherArguments_DefaultsWhenProvidedFalse(t *testing.T) {
+	arg := []*ParameterDefinition{
+		{
+			Name:    "Test",
+			Type:    ParameterTypeString,
+			Default: "default",
+		},
+	}
+	res, err := GatherArguments([]string{}, arg, false)
+	assert.NoError(t, err)
+	v, present := res.Get("Test")
+	assert.True(t, present)
+	assert.Equal(t, v, "default")
+}
+
+// Test handling of default values when onlyProvided is set to true
+func TestGatherArguments_NoDefaultsWhenProvidedTrue(t *testing.T) {
+	arg := []*ParameterDefinition{
+		{
+			Name:    "Test",
+			Type:    ParameterTypeString,
+			Default: "default",
+		},
+	}
+	v, err := GatherArguments([]string{}, arg, true)
+	assert.NoError(t, err)
+	// check that Test is not in v
+	_, present := v.Get("Test")
+	assert.False(t, present)
+}
+
+// Test the error condition of providing too many arguments
+func TestGatherArguments_TooManyArguments(t *testing.T) {
+	arg := []*ParameterDefinition{
+		{
+			Name: "Test",
+			Type: ParameterTypeString,
+		},
+	}
+	v, err := GatherArguments([]string{"value1", "value2"}, arg, true)
+	_ = v
+	assert.EqualError(t, err, "Too many arguments")
+}
+
+// Test the correct sequencing of arguments
+func TestGatherArguments_CorrectSequence(t *testing.T) {
+	arg := []*ParameterDefinition{
+		{
+			Name: "Test1",
+			Type: ParameterTypeString,
+		},
+		{
+			Name: "Test2",
+			Type: ParameterTypeString,
+		},
+	}
+	res, err := GatherArguments([]string{"value1", "value2"}, arg, true)
+	assert.NoError(t, err)
+	v1, present := res.Get("Test1")
+	assert.True(t, present)
+	assert.Equal(t, v1, "value1")
+	v2, present := res.Get("Test2")
+	assert.True(t, present)
+	assert.Equal(t, v2, "value2")
+}
+
+// Test various combinations of list and non-list arguments
+func TestGatherArguments_CombinationsListNonList(t *testing.T) {
+	arg := []*ParameterDefinition{
+		{
+			Name: "Test1",
+			Type: ParameterTypeString,
+		},
+		{
+			Name: "Test2",
+			Type: ParameterTypeStringList,
+		},
+	}
+	res, err := GatherArguments([]string{"value1", "value2", "value3"}, arg, true)
+	assert.NoError(t, err)
+	v1, present := res.Get("Test1")
+	assert.True(t, present)
+	assert.Equal(t, v1, "value1")
+	v2, present := res.Get("Test2")
+	assert.True(t, present)
+	assert.Equal(t, v2, []string{"value2", "value3"})
+}
+
+func TestListParsingWithDefaults(t *testing.T) {
+	args := []string{"data1", "data2"}
+	arguments := []*ParameterDefinition{
+		{
+			Name:    "arg1",
+			Type:    ParameterTypeStringList,
+			Default: []string{"default1", "default2"},
+		},
+	}
+	result, err := GatherArguments(args, arguments, false)
+	assert.NoError(t, err)
+	v1, present := result.Get("arg1")
+	assert.True(t, present)
+	assert.Equal(t, []string{"data1", "data2"}, v1)
+}
+
+func TestListDefault(t *testing.T) {
+	args := []string{}
+	arguments := []*ParameterDefinition{
+		{
+			Name:    "arg1",
+			Type:    ParameterTypeStringList,
+			Default: []string{"default1", "default2"},
+		},
+	}
+	result, err := GatherArguments(args, arguments, false)
+	assert.NoError(t, err)
+	v1, present := result.Get("arg1")
+	assert.True(t, present)
+	assert.Equal(t, []string{"default1", "default2"}, v1)
+}
+
+func TestIntegerListParsing(t *testing.T) {
+	args := []string{"1", "2", "3"}
+	arguments := []*ParameterDefinition{
+		{
+			Name: "arg1",
+			Type: ParameterTypeIntegerList,
+		},
+	}
+	result, err := GatherArguments(args, arguments, false)
+	assert.NoError(t, err)
+	v2, present := result.Get("arg1")
+	assert.True(t, present)
+	assert.Equal(t, []int{1, 2, 3}, v2)
+}
+
+func TestFloatListParsing(t *testing.T) {
+	args := []string{"1.1", "2.2", "3.3"}
+	arguments := []*ParameterDefinition{
+		{
+			Name: "arg1",
+			Type: ParameterTypeFloatList,
+		},
+	}
+	result, err := GatherArguments(args, arguments, false)
+	assert.NoError(t, err)
+	v2, present := result.Get("arg1")
+	assert.True(t, present)
+	assert.Equal(t, []float64{1.1, 2.2, 3.3}, v2)
+}
+
+func TestChoiceListParsing(t *testing.T) {
+	args := []string{"choice1", "choice2", "choice3"}
+	arguments := []*ParameterDefinition{
+		{
+			Name: "arg1",
+			Type: ParameterTypeChoiceList,
+			Choices: []string{
+				"choice1",
+				"choice2",
+				"choice3",
+			},
+		},
+	}
+	result, err := GatherArguments(args, arguments, false)
+	assert.NoError(t, err)
+	v2, present := result.Get("arg1")
+	assert.True(t, present)
+	assert.Equal(t, []string{"choice1", "choice2", "choice3"}, v2)
+}
+
+func TestParsingErrorInvalidInt(t *testing.T) {
+	args := []string{"1", "2", "3", "notanint"}
+	arguments := []*ParameterDefinition{
+		{
+			Name: "arg1",
+			Type: ParameterTypeIntegerList,
+		},
+	}
+	_, err := GatherArguments(args, arguments, false)
+	assert.Error(t, err)
+}
+
+func TestSingleParametersFollowedByListDefaults(t *testing.T) {
+	args := []string{"1", "2", "3"}
+	arguments := []*ParameterDefinition{
+		{
+			Name: "arg1",
+			Type: ParameterTypeInteger,
+		},
+		{
+			Name:    "arg2",
+			Type:    ParameterTypeIntegerList,
+			Default: []int{4, 5, 6},
+		},
+	}
+	result, err := GatherArguments(args, arguments, false)
+	assert.NoError(t, err)
+	v1, present := result.Get("arg1")
+	assert.True(t, present)
+	assert.Equal(t, 1, v1)
+	v2, present := result.Get("arg2")
+	assert.True(t, present)
+	assert.Equal(t, []int{2, 3}, v2)
+}
+
+func TestThreeSingleParametersFollowedByListDefaults(t *testing.T) {
+	args := []string{"1", "2", "3", "4"}
+	arguments := []*ParameterDefinition{
+		{
+			Name: "arg1",
+			Type: ParameterTypeInteger,
+		},
+		{
+			Name: "arg2",
+			Type: ParameterTypeInteger,
+		},
+		{
+			Name: "arg3",
+			Type: ParameterTypeInteger,
+		},
+		{
+			Name:    "arg4",
+			Type:    ParameterTypeIntegerList,
+			Default: []int{5, 6, 7},
+		},
+	}
+	result, err := GatherArguments(args, arguments, false)
+	assert.NoError(t, err)
+	v1, present := result.Get("arg1")
+	assert.True(t, present)
+	assert.Equal(t, 1, v1)
+	v2, present := result.Get("arg2")
+	assert.True(t, present)
+	assert.Equal(t, 2, v2)
+	v3, present := result.Get("arg3")
+	assert.True(t, present)
+	assert.Equal(t, 3, v3)
+	v4, present := result.Get("arg4")
+	assert.True(t, present)
+	assert.Equal(t, []int{4}, v4)
+}
+
+func TestThreeSingleParametersFollowedByListDefaultsOnlyTwoValues(t *testing.T) {
+	args := []string{"1", "2", "3"}
+	arguments := []*ParameterDefinition{
+		{
+			Name: "arg1",
+			Type: ParameterTypeInteger,
+		},
+		{
+			Name: "arg2",
+			Type: ParameterTypeInteger,
+		},
+		{
+			Name: "arg3",
+			Type: ParameterTypeInteger,
+		},
+		{
+			Name:    "arg4",
+			Type:    ParameterTypeIntegerList,
+			Default: []int{5, 6, 7},
+		},
+	}
+	result, err := GatherArguments(args, arguments, false)
+	assert.NoError(t, err)
+	v1, present := result.Get("arg1")
+	assert.True(t, present)
+	assert.Equal(t, 1, v1)
+	v2, present := result.Get("arg2")
+	assert.True(t, present)
+	assert.Equal(t, 2, v2)
+	v3, present := result.Get("arg3")
+	assert.True(t, present)
+	assert.Equal(t, 3, v3)
+	v4, present := result.Get("arg4")
+	assert.True(t, present)
+	assert.Equal(t, []int{5, 6, 7}, v4)
+}
+
+// Test that an argument of type objectListFromFile from test-data/objectList.json correctly parses the argument
+func TestObjectListFromFileParsing(t *testing.T) {
+	args := []string{"test-data/objectList.json"}
+	arguments := []*ParameterDefinition{
+		{
+			Name: "arg1",
+			Type: ParameterTypeObjectListFromFile,
+		},
+	}
+	result, err := GatherArguments(args, arguments, false)
+	assert.NoError(t, err)
+	v1, present := result.Get("arg1")
+	assert.True(t, present)
+	assert.Equal(t, []interface{}{
+		map[string]interface{}{
+			"name": "objectList1",
+			"type": "object",
+		},
+		map[string]interface{}{
+			"name": "objectList2",
+			"type": "object",
+		},
+	}, v1)
+}
+
+// Test that loading from multiple files with an argument of type objectListFromFiles correctly parses
+// objectList.json objectList2.yaml and objectList3.csv
+func TestObjectListFromFilesParsing(t *testing.T) {
+	args := []string{"test-data/objectList.json", "test-data/objectList2.yaml", "test-data/objectList3.csv"}
+	arguments := []*ParameterDefinition{
+		{
+			Name: "arg1",
+			Type: ParameterTypeObjectListFromFiles,
+		},
+	}
+	result, err := GatherArguments(args, arguments, false)
+	assert.NoError(t, err)
+	v1, present := result.Get("arg1")
+	assert.True(t, present)
+	assert.Equal(t,
+		[]interface{}{map[string]interface{}{"name": "objectList1", "type": "object"},
+			map[string]interface{}{"name": "objectList2", "type": "object"},
+			map[string]interface{}{"name": "objectList3", "type": "object"},
+			map[string]interface{}{"name": "objectList4", "type": "object"},
+			map[string]interface{}{"name": "objectList5", "type": "object"},
+			map[string]interface{}{"name": "objectList6", "type": "object"},
+		}, v1)
+}
+
+func TestGenerateUseString_NoArguments(t *testing.T) {
+	cmd := &cobra.Command{Use: "test"}
+	arguments := []*ParameterDefinition{}
+	result := GenerateUseString(cmd, arguments)
+	require.Equal(t, "test", result)
+}
+
+func TestGenerateUseString_RequiredArguments(t *testing.T) {
+	cmd := &cobra.Command{Use: "test"}
+	arguments := []*ParameterDefinition{{Name: "name", Required: true}}
+	result := GenerateUseString(cmd, arguments)
+	require.Equal(t, "test <name>", result)
+}
+
+func TestGenerateUseString_OptionalArguments(t *testing.T) {
+	cmd := &cobra.Command{Use: "test"}
+	arguments := []*ParameterDefinition{{Name: "name"}}
+	result := GenerateUseString(cmd, arguments)
+	require.Equal(t, "test [name]", result)
+}
+
+func TestGenerateUseString_RequiredAndOptionalArguments(t *testing.T) {
+	cmd := &cobra.Command{Use: "test"}
+	arguments := []*ParameterDefinition{
+		{Name: "name", Required: true},
+		{Name: "age"},
+	}
+	result := GenerateUseString(cmd, arguments)
+	require.Equal(t, "test <name> [age]", result)
+}
+
+func TestGenerateUseString_WithDefaultValue(t *testing.T) {
+	cmd := &cobra.Command{Use: "test"}
+	arguments := []*ParameterDefinition{{Name: "name", Default: "John"}}
+	result := GenerateUseString(cmd, arguments)
+	require.Equal(t, "test [name (default: John)]", result)
+}
+
+func TestGenerateUseString_WithMultipleValues(t *testing.T) {
+	cmd := &cobra.Command{Use: "test"}
+	arguments := []*ParameterDefinition{{Name: "name", Type: ParameterTypeStringList}}
+	result := GenerateUseString(cmd, arguments)
+	require.Equal(t, "test [name...]", result)
+}
+
+func TestGenerateUseString_RequiredWithMultipleValues(t *testing.T) {
+	cmd := &cobra.Command{Use: "test"}
+	arguments := []*ParameterDefinition{
+		{Name: "name", Required: true, Type: ParameterTypeStringList},
+	}
+
+	result := GenerateUseString(cmd, arguments)
+	require.Equal(t, "test <name...>", result)
+}
diff --git a/pkg/cmds/parameters/cobra_test.go b/pkg/cmds/parameters/cobra_test.go
index 1f62b81..8e6b737 100644
--- a/pkg/cmds/parameters/cobra_test.go
+++ b/pkg/cmds/parameters/cobra_test.go
@@ -104,3 +104,7 @@ func TestInvalidChoiceDefaultValue(t *testing.T) {
 		assert.Error(t, err)
 	}
 }
+
+func TestGatherArguments(t *testing.T) {
+
+}
diff --git a/pkg/cmds/parameters/parameters_test.go b/pkg/cmds/parameters/parameters_test.go
index 7ff7b59..aec350b 100644
--- a/pkg/cmds/parameters/parameters_test.go
+++ b/pkg/cmds/parameters/parameters_test.go
@@ -50,6 +50,27 @@ func loadValidityTestDataFromYAML(s []byte) ([]*ValidityTest, error) {
 	return tests, nil
 }
 
+//go:embed "test-data/types.yaml"
+var testParametersTypesYaml []byte
+
+type ParameterTypeTest struct {
+	Type          string `yaml:"type"`
+	IsList        bool   `yaml:"isList"`
+	IsFileLoading bool   `yaml:"isFileLoading"`
+	Value         string `yaml:"value,omitempty"`
+}
+
+func loadParameterTypeTests(yamlData []byte) ([]ParameterTypeTest, error) {
+	var tests []ParameterTypeTest
+	err := yaml.Unmarshal(yamlData, &tests)
+	if err != nil {
+		return nil, err
+	}
+	return tests, nil
+}
+
+var testParameterTypeTests []ParameterTypeTest
+
 func init() {
 	testParameterDefinitions, testParameterDefinitionsList = LoadParameterDefinitionsFromYAML(testFlagsYaml)
 	var err error
@@ -57,6 +78,21 @@ func init() {
 	if err != nil {
 		panic(err)
 	}
+
+	testParameterTypeTests, err = loadParameterTypeTests(testParametersTypesYaml)
+	if err != nil {
+		panic(err)
+	}
+}
+
+func TestParameterTypes(t *testing.T) {
+	for _, test := range testParameterTypeTests {
+		t.Run(test.Type, func(t *testing.T) {
+			type_ := ParameterType(test.Type)
+			assert.Equal(t, test.IsList, IsListParameter(type_))
+			assert.Equal(t, test.IsFileLoading, IsFileLoadingParameter(type_, test.Value))
+		})
+	}
 }
 
 func TestParameterValidity(t *testing.T) {
diff --git a/pkg/cmds/parameters/test-data/types.yaml b/pkg/cmds/parameters/test-data/types.yaml
new file mode 100644
index 0000000..9533d0e
--- /dev/null
+++ b/pkg/cmds/parameters/test-data/types.yaml
@@ -0,0 +1,82 @@
+- type: int
+  isList: false
+  isFileLoading: false
+
+- type: string
+  isList: false
+  isFileLoading: false
+
+- type: stringFromFile
+  isList: false
+  isFileLoading: true
+
+- type: stringFromFiles
+  isList: true
+  isFileLoading: true
+
+- type: objectListFromFile
+  # this is not a list because we actually parse a single file
+  isList: false
+  isFileLoading: true
+
+- type: objectListFromFiles
+  isList: true
+  isFileLoading: true
+
+- type: objectFromFile
+  isList: false
+  isFileLoading: true
+
+- type: stringListFromFile
+  isList: false
+  isFileLoading: true
+
+- type: stringListFromFiles
+  isList: true
+  isFileLoading: true
+
+- type: keyValue
+  isList: true
+  value: "foobar:foo"
+  isFileLoading: false
+
+- type: keyValue
+  isList: true
+  value: "@file.json"
+  isFileLoading: true
+
+- type: int
+  isList: false
+  isFileLoading: false
+
+- type: float
+  isList: false
+  isFileLoading: false
+
+- type: bool
+  isList: false
+  isFileLoading: false
+
+- type: date
+  isList: false
+  isFileLoading: false
+
+- type: stringList
+  isList: true
+  isFileLoading: false
+
+- type: intList
+  isList: true
+  isFileLoading: false
+
+- type: floatList
+  isList: true
+  isFileLoading: false
+
+- type: choice
+  isList: false
+  isFileLoading: false
+
+- type: choiceList
+  isList: true
+  isFileLoading: false
\ No newline at end of file