Skip to content

Commit

Permalink
Support Default Values for Arguments in Custom Commands (#905)
Browse files Browse the repository at this point in the history
* Default value for demo argument should not cause an error

* modifying mdx file

* comment correction

* mdx change

* test cases

* Update website/docs/core-concepts/custom-commands/custom-commands.mdx

Co-authored-by: Erik Osterman (CEO @ Cloud Posse) <[email protected]>

---------

Co-authored-by: Erik Osterman (CEO @ Cloud Posse) <[email protected]>
  • Loading branch information
Listener430 and osterman authored Jan 3, 2025
1 parent 2763512 commit 02ec47e
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 15 deletions.
80 changes: 69 additions & 11 deletions cmd/cmd_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,29 +168,80 @@ func preCustomCommand(
commandConfig *schema.Command,
) {
var sb strings.Builder
if len(args) != len(commandConfig.Arguments) {
if len(commandConfig.Arguments) == 0 {
u.LogError(schema.AtmosConfiguration{}, errors.New("invalid command"))

//checking for zero arguments in config
if len(commandConfig.Arguments) == 0 {
if len(commandConfig.Steps) > 0 {
// do nothing here; let the code proceed
} else if len(commandConfig.Commands) > 0 {
// show sub-commands
sb.WriteString("Available command(s):\n")
for i, c := range commandConfig.Commands {
sb.WriteString(fmt.Sprintf("%d. %s %s %s\n", i+1, parentCommand.Use, commandConfig.Name, c.Name))
sb.WriteString(
fmt.Sprintf("%d. %s %s %s\n", i+1, parentCommand.Use, commandConfig.Name, c.Name),
)
}
u.LogInfo(schema.AtmosConfiguration{}, sb.String())
os.Exit(1)
} else {
// truly invalid, nothing to do
u.LogError(schema.AtmosConfiguration{}, errors.New(
"invalid command: no args, no steps, no sub-commands",
))
os.Exit(1)
}
sb.WriteString(fmt.Sprintf("Command requires %d argument(s):\n", len(commandConfig.Arguments)))
for i, arg := range commandConfig.Arguments {
if arg.Name == "" {
u.LogErrorAndExit(schema.AtmosConfiguration{}, errors.New("invalid argument configuration: empty argument name"))
}

//Check on many arguments required and have no default value
requiredNoDefaultCount := 0
for _, arg := range commandConfig.Arguments {
if arg.Required && arg.Default == "" {
requiredNoDefaultCount++
}
}

// Check if the number of arguments provided is less than the required number of arguments
if len(args) < requiredNoDefaultCount {
sb.WriteString(
fmt.Sprintf("Command requires at least %d argument(s) (no defaults provided for them):\n",
requiredNoDefaultCount))

// List out which arguments are missing
missingIndex := 1
for _, arg := range commandConfig.Arguments {
if arg.Required && arg.Default == "" {
sb.WriteString(fmt.Sprintf(" %d. %s\n", missingIndex, arg.Name))
missingIndex++
}
sb.WriteString(fmt.Sprintf(" %d. %s\n", i+1, arg.Name))
}
if len(args) > 0 {
sb.WriteString(fmt.Sprintf("\nReceived %d argument(s): %s", len(args), strings.Join(args, ", ")))
sb.WriteString(fmt.Sprintf("\nReceived %d argument(s): %s\n", len(args), strings.Join(args, ", ")))
}
u.LogErrorAndExit(schema.AtmosConfiguration{}, errors.New(sb.String()))
}

// Merge user-supplied arguments with defaults
finalArgs := make([]string, len(commandConfig.Arguments))

for i, arg := range commandConfig.Arguments {
if i < len(args) {
finalArgs[i] = args[i]
} else {
if arg.Default != "" {
finalArgs[i] = fmt.Sprintf("%v", arg.Default)
} else {
// This theoretically shouldn't happen:
sb.WriteString(fmt.Sprintf("Missing required argument '%s' with no default!\n", arg.Name))
u.LogErrorAndExit(schema.AtmosConfiguration{}, errors.New(sb.String()))
}
}
}
// Set the resolved arguments as annotations on the command
if cmd.Annotations == nil {
cmd.Annotations = make(map[string]string)
}
cmd.Annotations["resolvedArgs"] = strings.Join(finalArgs, ",")

// no "steps" means a sub command should be specified
if len(commandConfig.Steps) == 0 {
_ = cmd.Help()
Expand Down Expand Up @@ -223,12 +274,19 @@ func executeCustomCommand(
atmosConfig.Logs.Level = u.LogLevelTrace
}

mergedArgsStr := cmd.Annotations["resolvedArgs"]
finalArgs := strings.Split(mergedArgsStr, ",")
if mergedArgsStr == "" {
// If for some reason no annotation was set, just fallback
finalArgs = args
}

// Execute custom command's steps
for i, step := range commandConfig.Steps {
// Prepare template data for arguments
argumentsData := map[string]string{}
for ix, arg := range commandConfig.Arguments {
argumentsData[arg.Name] = args[ix]
argumentsData[arg.Name] = finalArgs[ix]
}

// Prepare template data for flags
Expand Down
12 changes: 12 additions & 0 deletions examples/demo-custom-command/atmos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@ commands:
steps:
- curl -s https://api.github.com/repos/{{ .Arguments.repo }} | jq -r .stargazers_count

# # Use positional arguments with default values
- name: greet
description: "Displays a personalized greeting. Defaults to 'John Doe' if no name is provided."
arguments:
- name: name
description: >-
Enter your name as an argument
required: true
default: John Doe
steps:
- "echo Hello, {{ .Arguments.name }}"

# Use flags
- name: weather
description: This command fetches the weather
Expand Down
2 changes: 2 additions & 0 deletions pkg/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@ type Command struct {
type CommandArgument struct {
Name string `yaml:"name" json:"name" mapstructure:"name"`
Description string `yaml:"description" json:"description" mapstructure:"description"`
Required bool `yaml:"required" json:"required" mapstructure:"required"`
Default string `yaml:"default" json:"default" mapstructure:"default"`
}

type CommandFlag struct {
Expand Down
29 changes: 29 additions & 0 deletions tests/test_cases.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,32 @@ tests:
stderr:
- "^$"
exit_code: 0

- name: atmos greet with args
enabled: true
description: "Validate atmos custom command greet runs with argument provided."
workdir: "../examples/demo-custom-command/"
command: "atmos"
args:
- "greet"
- "Andrey"
expect:
stdout:
- "Hello, Andrey\n"
stderr:
- "^$"
exit_code: 0

- name: atmos greet without args
enabled: true
description: "Validate atmos custom command greet runs without argument provided."
workdir: "../examples/demo-custom-command/"
command: "atmos"
args:
- "greet"
expect:
stdout:
- "Hello, John Doe\n"
stderr:
- "^$"
exit_code: 0
17 changes: 13 additions & 4 deletions website/docs/core-concepts/custom-commands/custom-commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,26 +39,35 @@ atmos hello

## Positional Arguments

Atmos also can support positional arguments. Arguments do not support default values and are required if defined.
Atmos also supports positional arguments. If a positional argument is required but not provided by the user,
the command will fail—unless you define a default in your config.

If we add the following to `atmos.yaml`, will introduce a new `hello` command that accepts one `name` argument.
For the example, adding the following to `atmos.yaml` will introduce a new `greet` command that accepts one `name` argument,
but uses a default of "John Doe" if none is provided.

```yaml
# subcommands
commands:
- name: hello
- name: greet
description: This command says hello to the provided name
arguments:
- name: name
description: Name to greet
required: true
default: John Doe
steps:
- "echo Hello {{ .Arguments.name }}!"
```

We can run this example like this:

```shell
atmos hello world
atmos greet Alice
```
or defaulting to "John Doe"

```shell
atmos greet
```

## Passing Flags
Expand Down

0 comments on commit 02ec47e

Please sign in to comment.