Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Default Values for Arguments in Custom Commands #905

Merged
merged 6 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
81 changes: 70 additions & 11 deletions cmd/cmd_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,29 +168,81 @@ 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 it still has steps, we allow it:
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 if we've checked requiredNoDefaultCount above but just in case:
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 +275,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 }}"

Listener430 marked this conversation as resolved.
Show resolved Hide resolved
# 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
18 changes: 13 additions & 5 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,34 @@ atmos hello

## Positional Arguments

Atmos also can support positional arguments. Arguments do not support default values and are required if defined.

If we add the following to `atmos.yaml`, will introduce a new `hello` command that accepts one `name` argument.
Atmos also can support positional arguments. If a positional argument is required but not provided by the user,
Listener430 marked this conversation as resolved.
Show resolved Hide resolved
the command will fail—unless you define a default in your config.
If we add 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
Loading