From f0b7b98489bf14bf901a755cfecb46622b1f5e4e Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Sat, 14 Dec 2024 16:56:43 +0100 Subject: [PATCH 01/50] Merge atmos specific and terraform help documentation issue: https://linear.app/cloudposse/issue/DEV-2821/atmos-terraform-help-should-also-show-terraform-help --- internal/exec/help.go | 87 +++++++++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/internal/exec/help.go b/internal/exec/help.go index 60267c509..df79ec5ee 100644 --- a/internal/exec/help.go +++ b/internal/exec/help.go @@ -5,7 +5,6 @@ import ( "github.com/cloudposse/atmos/pkg/schema" u "github.com/cloudposse/atmos/pkg/utils" - "github.com/cloudposse/atmos/pkg/version" ) // processHelp processes help commands @@ -21,41 +20,57 @@ func processHelp( u.PrintMessage(fmt.Sprintf("atmos %s --stack [options]", componentType)) if componentType == "terraform" { - u.PrintMessage("\nAdditions and differences from native terraform:") - u.PrintMessage(" - before executing other 'terraform' commands, 'atmos' runs 'terraform init'") - u.PrintMessage(" - you can skip over atmos calling 'terraform init' if you know your project is already in a good working state by using " + - "the '--skip-init' flag like so 'atmos terraform -s --skip-init") - u.PrintMessage(" - 'atmos terraform deploy' command executes 'terraform apply -auto-approve' (sets the '-auto-approve' flag when running 'terraform apply')") - u.PrintMessage(" - 'atmos terraform deploy' command supports '--deploy-run-init=true/false' flag to enable/disable running 'terraform init' " + - "before executing the command") - u.PrintMessage(" - 'atmos terraform apply' and 'atmos terraform deploy' commands support '--from-plan' flag. If the flag is specified, " + - "the commands will use the planfile previously generated by 'atmos terraform plan' command instead of generating a new planfile") - u.PrintMessage(" - 'atmos terraform apply' and 'atmos terraform deploy' commands commands support '--planfile' flag to specify the path " + - "to a planfile. The '--planfile' flag should be used instead of the planfile argument in the native 'terraform apply ' command") - u.PrintMessage(" - 'atmos terraform clean' command deletes the '.terraform' folder, '.terraform.lock.hcl' lock file, " + - "and the previously generated 'planfile', 'varfile', and 'backend.tf.json' file for the specified component and stack. " + - "Use the --everything flag to also delete the Terraform state files and directories for the component. " + - "Note: State files store the local state of your infrastructure and should be handled with care, if not using a remote backend.\n\n" + - "Additional flags:\n" + - " --force Forcefully delete Terraform state files and directories without interaction\n" + - " --skip-lock-file Skip deleting the '.terraform.lock.hcl' file\n\n" + - "If no component or stack is specified, the clean operation will apply globally to all components.") - u.PrintMessage(" - 'atmos terraform workspace' command first runs 'terraform init -reconfigure', then 'terraform workspace select', " + - "and if the workspace was not created before, it then runs 'terraform workspace new'") - u.PrintMessage(" - 'atmos terraform import' command searches for 'region' in the variables for the specified component and stack, " + - "and if it finds it, sets 'AWS_REGION=' ENV var before executing the command") - u.PrintMessage(" - 'atmos terraform generate backend' command generates a backend config file for an 'atmos' component in a stack") - u.PrintMessage(" - 'atmos terraform generate backends' command generates backend config files for all 'atmos' components in all stacks") - u.PrintMessage(" - 'atmos terraform generate varfile' command generates a varfile for an 'atmos' component in a stack") - u.PrintMessage(" - 'atmos terraform generate varfiles' command generates varfiles for all 'atmos' components in all stacks") - u.PrintMessage(" - 'atmos terraform shell' command configures an environment for an 'atmos' component in a stack and starts a new shell " + - "allowing executing all native terraform commands inside the shell without using atmos-specific arguments and flags") - u.PrintMessage(" - double-dash '--' can be used to signify the end of the options for Atmos and the start of the additional " + - "native arguments and flags for the 'terraform' commands. " + - "For example: atmos terraform plan -s -- -refresh=false -lock=false") - u.PrintMessage(fmt.Sprintf(" - '--append-user-agent' flag sets the TF_APPEND_USER_AGENT environment variable to customize the User-Agent string in Terraform provider requests. "+ - "Example: 'Atmos/%s (Cloud Posse; +https://atmos.tools)'. "+ - "If not specified, defaults to 'atmos %s'\n", version.Version, version.Version)) + u.PrintMessage(` +Usage: atmos terraform [global options] [args] + +The available commands for execution are listed below. +The primary workflow commands are given first, followed by +less common or more advanced commands. + +Atmos commands: + generate backend Command generates a backend config file for an 'atmos' component in a stack + generate backends Command generates backend config files for all 'atmos' components in all stacks + generate varfile Command generates a varfile for an 'atmos' component in a stack + generate varfiles Command generates varfiles for all 'atmos' components in all stacks + shell Command configures an environment for an 'atmos' component in a stack and starts a new shell allowing executing all native terraform commands inside the shell without using atmos-specific arguments and flags + double-dash '--' Can be used to signify the end of the options for Atmos and the start of the additional native arguments and flags for the 'terraform' commands. For example: atmos terraform plan -s -- -refresh=false -lock=false + '--append-user-agent' Flag sets the TF_APPEND_USER_AGENT environment variable to customize the User-Agent string in Terraform provider requests. Example: 'Atmos/0.0.1 (Cloud Posse; +https://atmos.tools)'. If not specified, defaults to 'atmos 0.0.1' + +Main commands: + init Prepare your working directory for other commands + validate Check whether the configuration is valid + plan Show changes required by the current configuration + apply Create or update infrastructure + destroy Destroy previously-created infrastructure + +All other commands: + console Try Terraform expressions at an interactive command prompt + fmt Reformat your configuration in the standard style + force-unlock Release a stuck lock on the current workspace + get Install or upgrade remote Terraform modules + graph Generate a Graphviz graph of the steps in an operation + import Associate existing infrastructure with a Terraform resource + login Obtain and save credentials for a remote host + logout Remove locally-stored credentials for a remote host + metadata Metadata related commands + modules Show all declared modules in a working directory + output Show output values from your root module + providers Show the providers required for this configuration + refresh Update the state to match remote systems + show Show the current state or a saved plan + state Advanced state management + taint Mark a resource instance as not fully functional + test Execute integration tests for Terraform modules + untaint Remove the 'tainted' state from a resource instance + version Show the current Terraform version + workspace Workspace management + +Global options (use these before the subcommand, if any): + -chdir=DIR Switch to a different working directory before executing the + given subcommand. + -help Show this help output, or the help for a specified subcommand. + -version An alias for the "version" subcommand. +`) } if componentType == "helmfile" { From 437e1d42e32d59d557bd65e8a2aaa479b3fdee91 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Tue, 17 Dec 2024 21:27:06 +0100 Subject: [PATCH 02/50] Add help for terraform issue: https://linear.app/cloudposse/issue/DEV-2821/atmos-terraform-help-should-also-show-terraform-help --- cmd/root.go | 19 +- cmd/terraform.go | 91 ++++++--- cmd/terraform_commands.go | 243 ++++++++++++++++++++++++ cmd/terraform_generate.go | 1 + internal/exec/help.go | 55 ------ internal/exec/utils.go | 2 +- internal/tui/templates/base_template.go | 156 +++++++++++++++ internal/tui/templates/templater.go | 105 ++++++---- 8 files changed, 542 insertions(+), 130 deletions(-) create mode 100644 cmd/terraform_commands.go create mode 100644 internal/tui/templates/base_template.go diff --git a/cmd/root.go b/cmd/root.go index c7b043a3f..b1ad251be 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -138,18 +138,25 @@ func init() { func initConfig() { styles := boa.DefaultStyles() b := boa.New(boa.WithStyles(styles)) - + oldUsageFunc := RootCmd.UsageFunc() RootCmd.SetUsageFunc(b.UsageFunc) RootCmd.SetHelpFunc(func(command *cobra.Command, strings []string) { // Print a styled Atmos logo to the terminal fmt.Println() - err := tuiUtils.PrintStyledText("ATMOS") - if err != nil { - u.LogErrorAndExit(schema.CliConfiguration{}, err) + if command.Use != "terraform" { + err := tuiUtils.PrintStyledText("ATMOS") + if err != nil { + u.LogErrorAndExit(schema.CliConfiguration{}, err) + } + } + // TODO: find a better way to do this if possible + if command.Use == "terraform" { + oldUsageFunc(command) + return + } else { + b.HelpFunc(command, strings) } - - b.HelpFunc(command, strings) command.Usage() }) } diff --git a/cmd/terraform.go b/cmd/terraform.go index 586e79d22..74d34b9bf 100644 --- a/cmd/terraform.go +++ b/cmd/terraform.go @@ -5,8 +5,10 @@ import ( "github.com/spf13/cobra" e "github.com/cloudposse/atmos/internal/exec" + "github.com/cloudposse/atmos/internal/tui/templates" "github.com/cloudposse/atmos/pkg/schema" u "github.com/cloudposse/atmos/pkg/utils" + cc "github.com/ivanpirog/coloredcobra" ) // terraformCmd represents the base command for all terraform sub-commands @@ -16,42 +18,69 @@ var terraformCmd = &cobra.Command{ Short: "Execute Terraform commands", Long: `This command executes Terraform commands`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true}, - Run: func(cmd *cobra.Command, args []string) { - // Check Atmos configuration - //checkAtmosConfig() - - var argsAfterDoubleDash []string - var finalArgs = args - - doubleDashIndex := lo.IndexOf(args, "--") - if doubleDashIndex > 0 { - finalArgs = lo.Slice(args, 0, doubleDashIndex) - argsAfterDoubleDash = lo.Slice(args, doubleDashIndex+1, len(args)) - } - info, err := e.ProcessCommandLineArgs("terraform", cmd, finalArgs, argsAfterDoubleDash) - if err != nil { - u.LogErrorAndExit(schema.CliConfiguration{}, err) - } - - // Exit on help - if info.NeedHelp { - // Check for the latest Atmos release on GitHub and print update message - CheckForAtmosUpdateAndPrintMessage(cliConfig) - return - } - // Check Atmos configuration - checkAtmosConfig() - - err = e.ExecuteTerraform(info) - if err != nil { - u.LogErrorAndExit(schema.CliConfiguration{}, err) - } - }, + Run: terraformRun, +} + +func terraformRun(cmd *cobra.Command, args []string) { + // Check Atmos configuration + //checkAtmosConfig() + + var argsAfterDoubleDash []string + var finalArgs = args + + doubleDashIndex := lo.IndexOf(args, "--") + if doubleDashIndex > 0 { + finalArgs = lo.Slice(args, 0, doubleDashIndex) + argsAfterDoubleDash = lo.Slice(args, doubleDashIndex+1, len(args)) + } + info, err := e.ProcessCommandLineArgs("terraform", cmd, finalArgs, argsAfterDoubleDash) + if err != nil { + u.LogErrorAndExit(schema.CliConfiguration{}, err) + } + + // Exit on help + if info.NeedHelp { + // Check for the latest Atmos release on GitHub and print update message + template := templates.GenerateFromBaseTemplate(cmd.Use, []templates.HelpTemplateSections{ + templates.LongDescription, + templates.Usage, + templates.Aliases, + templates.Examples, + templates.AvailableCommands, + templates.Flags, + templates.GlobalFlags, + templates.NativeCommands, + templates.Footer, + }) + + cmd.SetUsageTemplate(template) + cc.Init(&cc.Config{ + RootCmd: cmd, + Headings: cc.HiCyan + cc.Bold + cc.Underline, + Commands: cc.HiGreen + cc.Bold, + Example: cc.Italic, + ExecName: cc.Bold, + Flags: cc.Bold, + }) + + cmd.Help() + CheckForAtmosUpdateAndPrintMessage(cliConfig) + return + } + // Check Atmos configuration + checkAtmosConfig() + + err = e.ExecuteTerraform(info) + if err != nil { + u.LogErrorAndExit(schema.CliConfiguration{}, err) + } + } func init() { // https://github.com/spf13/cobra/issues/739 terraformCmd.DisableFlagParsing = true terraformCmd.PersistentFlags().StringP("stack", "s", "", "atmos terraform -s ") + attachTerraformCommands(terraformCmd) RootCmd.AddCommand(terraformCmd) } diff --git a/cmd/terraform_commands.go b/cmd/terraform_commands.go new file mode 100644 index 000000000..a5d7737e2 --- /dev/null +++ b/cmd/terraform_commands.go @@ -0,0 +1,243 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +// getTerraformCommands returns an array of statically defined Terraform commands with flags +func getTerraformCommands() []*cobra.Command { + // List of Terraform commands + return []*cobra.Command{ + { + Use: "plan", + Short: "Show changes required by the current configuration", + Long: "Generate an execution plan, which shows what actions Terraform will take to reach the desired state of the configuration.", + Example: "atmos terraform plan -s ", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "apply", + Short: "Apply changes to infrastructure", + Long: "Apply the changes required to reach the desired state of the configuration. This will prompt for confirmation before making changes.", + Example: "atmos terraform apply -s ", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "workspace", + Short: "Manage Terraform workspaces", + Long: "Create, list, select, or delete Terraform workspaces, which allow for separate states within the same configuration. Note, Atmos will automatically select the workspace, if configured.", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "clean", + Short: "Clean up resources", + Long: "Remove unused or outdated resources to keep the infrastructure clean and reduce costs.", + Run: terraformRun, + }, + { + Use: "deploy", + Short: "Deploy the specified infrastructure using Terraform", + Long: `Deploy the specified infrastructure by running the Terraform plan and apply commands. +This command automates the deployment process, integrates configuration, and ensures streamlined execution.`, + Run: terraformRun, + }, + { + Use: "shell", + Short: "Configures an 'atmos' environment and starts a shell for native Terraform commands.", + Long: "command configures an environment for an 'atmos' component in a stack and starts a new shell allowing executing all native terraform commands inside the shell without using atmos-specific arguments and flag", + Run: terraformRun, + }, + { + Use: "version", + Short: "Show the current Terraform version", + Long: "Displays the current version of Terraform installed on the system.", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "varfile", + Short: "Load variables from a file", + Long: "Load variable definitions from a specified file and use them in the configuration.", + }, + { + Use: "write varfile", + Short: "Write variables to a file", + Long: "Write the variables used in the configuration to a specified file for later use or modification.", + }, + { + Use: "destroy", + Short: "Destroy previously-created infrastructure", + Long: "Destroy all the infrastructure managed by Terraform, removing resources as defined in the state file.", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "refresh", + Short: "Update the state to match remote systems", + Long: "Refresh the Terraform state, reconciling the local state with the actual infrastructure state.", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "init", + Short: "Prepare your working directory for other commands", + Long: "Initialize the working directory containing Terraform configuration files. It will download necessary provider plugins and set up the backend. Note, that Atmos will automatically call init for you, when running `plan` and `apply` commands.", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "validate", + Short: "Check whether the configuration is valid", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "console", + Short: "Try Terraform expressions at an interactive command prompt", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "fmt", + Short: "Reformat your configuration in the standard style", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "force-unlock", + Short: "Release a stuck lock on the current workspace", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "get", + Short: "Install or upgrade remote Terraform modules", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "graph", + Short: "Generate a Graphviz graph of the steps in an operation", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "import", + Short: "Associate existing infrastructure with a Terraform resource", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "login", + Short: "Obtain and save credentials for a remote host", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "logout", + Short: "Remove locally-stored credentials for a remote host", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "metadata", + Short: "Metadata related commands", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "modules", + Short: "Show all declared modules in a working directory", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "output", + Short: "Show output values from your root module", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "providers", + Short: "Show the providers required for this configuration", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "show", + Short: "Show the current state or a saved plan", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "state", + Short: "Advanced state management", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "taint", + Short: "Mark a resource instance as not fully functional", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "test", + Short: "Execute integration tests for Terraform modules", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + { + Use: "untaint", + Short: "Remove the 'tainted' state from a resource instance", + Annotations: map[string]string{ + "nativeCommand": "true", + }, + }, + } +} + +// attachTerraformCommands attaches static Terraform commands to a provided parent command +func attachTerraformCommands(parentCmd *cobra.Command) { + commands := getTerraformCommands() + fmt.Println(os.Args) + for _, cmd := range commands { + cmd.Run = func(cmd_ *cobra.Command, args []string) { + if len(os.Args) > 3 { + args = os.Args[2:] + } + parentCmd.Run(parentCmd, args) + } + parentCmd.AddCommand(cmd) + } +} diff --git a/cmd/terraform_generate.go b/cmd/terraform_generate.go index 1c51e9acf..4bbbec664 100644 --- a/cmd/terraform_generate.go +++ b/cmd/terraform_generate.go @@ -10,6 +10,7 @@ var terraformGenerateCmd = &cobra.Command{ Short: "Execute 'terraform generate' commands", Long: "This command generates configurations for terraform components", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Run: terraformRun, } func init() { diff --git a/internal/exec/help.go b/internal/exec/help.go index df79ec5ee..20594a0b0 100644 --- a/internal/exec/help.go +++ b/internal/exec/help.go @@ -18,61 +18,6 @@ func processHelp( u.PrintMessage("In addition, the 'component' argument and 'stack' flag are required to generate the variables and backend config for the component in the stack.\n") u.PrintMessage(fmt.Sprintf("atmos %s -s [options]", componentType)) u.PrintMessage(fmt.Sprintf("atmos %s --stack [options]", componentType)) - - if componentType == "terraform" { - u.PrintMessage(` -Usage: atmos terraform [global options] [args] - -The available commands for execution are listed below. -The primary workflow commands are given first, followed by -less common or more advanced commands. - -Atmos commands: - generate backend Command generates a backend config file for an 'atmos' component in a stack - generate backends Command generates backend config files for all 'atmos' components in all stacks - generate varfile Command generates a varfile for an 'atmos' component in a stack - generate varfiles Command generates varfiles for all 'atmos' components in all stacks - shell Command configures an environment for an 'atmos' component in a stack and starts a new shell allowing executing all native terraform commands inside the shell without using atmos-specific arguments and flags - double-dash '--' Can be used to signify the end of the options for Atmos and the start of the additional native arguments and flags for the 'terraform' commands. For example: atmos terraform plan -s -- -refresh=false -lock=false - '--append-user-agent' Flag sets the TF_APPEND_USER_AGENT environment variable to customize the User-Agent string in Terraform provider requests. Example: 'Atmos/0.0.1 (Cloud Posse; +https://atmos.tools)'. If not specified, defaults to 'atmos 0.0.1' - -Main commands: - init Prepare your working directory for other commands - validate Check whether the configuration is valid - plan Show changes required by the current configuration - apply Create or update infrastructure - destroy Destroy previously-created infrastructure - -All other commands: - console Try Terraform expressions at an interactive command prompt - fmt Reformat your configuration in the standard style - force-unlock Release a stuck lock on the current workspace - get Install or upgrade remote Terraform modules - graph Generate a Graphviz graph of the steps in an operation - import Associate existing infrastructure with a Terraform resource - login Obtain and save credentials for a remote host - logout Remove locally-stored credentials for a remote host - metadata Metadata related commands - modules Show all declared modules in a working directory - output Show output values from your root module - providers Show the providers required for this configuration - refresh Update the state to match remote systems - show Show the current state or a saved plan - state Advanced state management - taint Mark a resource instance as not fully functional - test Execute integration tests for Terraform modules - untaint Remove the 'tainted' state from a resource instance - version Show the current Terraform version - workspace Workspace management - -Global options (use these before the subcommand, if any): - -chdir=DIR Switch to a different working directory before executing the - given subcommand. - -help Show this help output, or the help for a specified subcommand. - -version An alias for the "version" subcommand. -`) - } - if componentType == "helmfile" { u.PrintMessage("\nAdditions and differences from native helmfile:") u.PrintMessage(" - 'atmos helmfile generate varfile' command generates a varfile for the component in the stack") diff --git a/internal/exec/utils.go b/internal/exec/utils.go index 46693a4ba..a4737513e 100644 --- a/internal/exec/utils.go +++ b/internal/exec/utils.go @@ -220,7 +220,7 @@ func ProcessCommandLineArgs( configAndStacksInfo.SettingsListMergeStrategy = argsAndFlagsInfo.SettingsListMergeStrategy // Check if `-h` or `--help` flags are specified - if argsAndFlagsInfo.NeedHelp { + if argsAndFlagsInfo.NeedHelp && configAndStacksInfo.ComponentType != "terraform" { // If we're dealing with `-h` or `--help`, // then the SubCommand should be empty. if argsAndFlagsInfo.SubCommand == "-h" || argsAndFlagsInfo.SubCommand == "--help" { diff --git a/internal/tui/templates/base_template.go b/internal/tui/templates/base_template.go new file mode 100644 index 000000000..5281a46aa --- /dev/null +++ b/internal/tui/templates/base_template.go @@ -0,0 +1,156 @@ +package templates + +import "fmt" + +// + +// MainUsageTemplate returns the usage template for the root command and wrap cobra flag usages to the terminal width +// func MainUsageTemplate() string { +// return ` +// {{ .Long }} +// Usage:{{if .Runnable}} +// {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} +// {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} + +// Aliases: +// {{.NameAndAliases}}{{end}}{{if .HasExample}} + +// Examples: +// {{.Example}}{{end}}{{if .HasAvailableSubCommands}} + +// Available Commands: +// {{formatCommands .Commands false}}{{end}}{{if .HasAvailableLocalFlags}} + +// Flags: +// {{wrappedFlagUsages .LocalFlags | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} + +// Global Flags: +// {{wrappedFlagUsages .InheritedFlags | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} + +// Additional help topics: +// {{formatCommands .Commands true}}{{end}}{{if .HasAvailableSubCommands}} + +// Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} +// ` +// } + +// MainUsageTemplate returns the usage template for the root command and wrap cobra flag usages to the terminal width +func MainUsageTemplate(isLongRequired bool) string { + template := ` +Usage:{{if .Runnable}} + {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} + {{.CommandPath}} [command]{{end}} + + +{{if gt (len .Aliases) 0}} +Aliases: + {{.NameAndAliases}} +{{end}} + +{{if .HasExample}} +Examples: +{{.Example}}{{end}} + +{{if .HasAvailableSubCommands}} +Available Commands: +{{formatCommands .Commands false}}{{end}} + +{{if .HasAvailableLocalFlags}} +Flags: +{{wrappedFlagUsages .LocalFlags | trimTrailingWhitespaces}}{{end}} + + +{{if .HasAvailableInheritedFlags}} +Global Flags: +{{wrappedFlagUsages .InheritedFlags | trimTrailingWhitespaces}}{{end}} + +{{if .HasHelpSubCommands}} +Additional help topics: +{{formatCommands .Commands true}}{{end}}{{if .HasAvailableSubCommands}} + +Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} +` + if isLongRequired { + template = ` +{{ .Long }} +` + template + } + return template +} + +type HelpTemplateSections int + +const ( + LongDescription HelpTemplateSections = iota + Usage + Aliases + Examples + AvailableCommands + Flags + GlobalFlags + AdditionalHelpTopics + NativeCommands + Footer +) + +func GenerateFromBaseTemplate(commandName string, parts []HelpTemplateSections) string { + template := "" + for _, value := range parts { + template += getSection(commandName, value) + } + return template +} + +func getSection(commandName string, section HelpTemplateSections) string { + switch section { + case LongDescription: + return `{{ .Long }} +` + case AdditionalHelpTopics: + return `{{if .HasHelpSubCommands}} + +Additional help topics: +{{formatCommands .Commands "additionalHelpTopics"}}{{end}}` + case Aliases: + return `{{if gt (len .Aliases) 0}} + +Aliases: + {{.NameAndAliases}}{{end}}` + case AvailableCommands: + return `{{if .HasAvailableSubCommands}} + +Available Commands: +{{formatCommands .Commands "availableCommands"}}{{end}}` + case Examples: + return `{{if .HasExample}} + +Examples: +{{.Example}}{{end}}` + case Flags: + return `{{if .HasAvailableLocalFlags}} + +Flags: +{{wrappedFlagUsages .LocalFlags | trimTrailingWhitespaces}}{{end}}` + case GlobalFlags: + return `{{if .HasAvailableInheritedFlags}} + +Global Flags: +{{wrappedFlagUsages .InheritedFlags | trimTrailingWhitespaces}}{{end}}` + case NativeCommands: + return fmt.Sprintf(` +{{HeadingStyle "Native %s Commands:"}} + +{{formatCommands .Commands "native"}} +`, commandName) + case Usage: + return `Usage:{{if .Runnable}} + {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} + {{.CommandPath}} [command]{{end}}` + case Footer: + return `{{if .HasAvailableSubCommands}} + +Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}` + default: + return "" + } +} diff --git a/internal/tui/templates/templater.go b/internal/tui/templates/templater.go index 3d5781ac9..8fdeccbc7 100644 --- a/internal/tui/templates/templater.go +++ b/internal/tui/templates/templater.go @@ -3,6 +3,7 @@ package templates import ( "fmt" "os" + "sort" "strings" "github.com/charmbracelet/lipgloss" @@ -24,34 +25,83 @@ var ( commandDescStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("7")) // White color for description + + commandUnsupportedNameStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("8")). + Bold(true) + commandUnsupportedDescStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("8")) ) // formatCommand returns a styled string for a command and its description -func formatCommand(name string, desc string, padding int) string { +func formatCommand(name string, desc string, padding int, IsNotSupported bool) string { paddedName := fmt.Sprintf("%-*s", padding, name) + if IsNotSupported { + styledName := commandUnsupportedNameStyle.Render(paddedName) + styledDesc := commandUnsupportedDescStyle.Render(desc + " [unsupported]") + return fmt.Sprintf(" %-30s %s", styledName, styledDesc) + } styledName := commandNameStyle.Render(paddedName) styledDesc := commandDescStyle.Render(desc) - return fmt.Sprintf(" %s %s", styledName, styledDesc) + return fmt.Sprintf(" %-30s %s", styledName, styledDesc) } // formatCommands formats a slice of cobra commands with proper styling -func formatCommands(cmds []*cobra.Command) string { +func formatCommands(cmds []*cobra.Command, listType string) string { var maxLen int availableCmds := make([]*cobra.Command, 0) // First pass: collect available commands and find max length for _, cmd := range cmds { - if cmd.IsAvailableCommand() || cmd.Name() == "help" { - availableCmds = append(availableCmds, cmd) - if len(cmd.Name()) > maxLen { - maxLen = len(cmd.Name()) + switch listType { + case "additionalHelpTopics": + if cmd.IsAdditionalHelpTopicCommand() { + availableCmds = append(availableCmds, cmd) + if len(cmd.Name()) > maxLen { + maxLen = len(cmd.Name()) + } + continue + } + case "native": + if cmd.Annotations["nativeCommand"] == "true" { + availableCmds = append(availableCmds, cmd) + if len(cmd.Name()) > maxLen { + maxLen = len(cmd.Name()) + } + continue + } + default: + if cmd.Annotations["nativeCommand"] == "true" { + continue + } + if cmd.IsAvailableCommand() || cmd.Name() == "help" { + availableCmds = append(availableCmds, cmd) + if len(cmd.Name()) > maxLen { + maxLen = len(cmd.Name()) + } } } } var lines []string + // Sorting by whether "IsNotSupported" is present in the Annotations map + sort.Slice(availableCmds, func(i, j int) bool { + // Check if "IsNotSupported" is present for commands[i] and commands[j] + iHasKey := availableCmds[i].Annotations["IsNotSupported"] != "true" + jHasKey := availableCmds[j].Annotations["IsNotSupported"] != "true" + + // Place commands with "IsNotSupported" at the top + if iHasKey && !jHasKey { + return true + } + if !iHasKey && jHasKey { + return false + } + // If both or neither have the key, maintain original order + return i < j + }) for _, cmd := range availableCmds { - lines = append(lines, formatCommand(cmd.Name(), cmd.Short, maxLen)) + lines = append(lines, formatCommand(cmd.Name(), cmd.Short, maxLen, cmd.Annotations["IsNotSupported"] == "true")) } return strings.Join(lines, "\n") @@ -64,7 +114,16 @@ func SetCustomUsageFunc(cmd *cobra.Command) error { return fmt.Errorf("command cannot be nil") } t := &Templater{ - UsageTemplate: MainUsageTemplate(), + UsageTemplate: GenerateFromBaseTemplate(cmd.Use, []HelpTemplateSections{ + Usage, + Aliases, + Examples, + AvailableCommands, + Flags, + GlobalFlags, + AdditionalHelpTopics, + Footer, + }), } cmd.SetUsageTemplate(t.UsageTemplate) @@ -88,34 +147,6 @@ func getTerminalWidth() int { return screenWidth } -// MainUsageTemplate returns the usage template for the root command and wrap cobra flag usages to the terminal width -func MainUsageTemplate() string { - return `Usage:{{if .Runnable}} - {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} - {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} - -Aliases: - {{.NameAndAliases}}{{end}}{{if .HasExample}} - -Examples: -{{.Example}}{{end}}{{if .HasAvailableSubCommands}} - -Available Commands: -{{formatCommands .Commands}}{{end}}{{if .HasAvailableLocalFlags}} - -Flags: -{{wrappedFlagUsages .LocalFlags | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} - -Global Flags: -{{wrappedFlagUsages .InheritedFlags | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} - -Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} - {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} - -Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} -` -} - // WrappedFlagUsages formats the flag usage string to fit within the terminal width func WrappedFlagUsages(f *pflag.FlagSet) string { var builder strings.Builder From 87fd2fd031b4ea79856eaf00b5241a0870769e24 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Mon, 23 Dec 2024 10:57:30 +0100 Subject: [PATCH 03/50] Updated help content --- cmd/terraform_commands.go | 61 ++++++++++++++++++++++++++++++++------- cmd/terraform_generate.go | 14 +++++++-- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/cmd/terraform_commands.go b/cmd/terraform_commands.go index a5d7737e2..863cf0f00 100644 --- a/cmd/terraform_commands.go +++ b/cmd/terraform_commands.go @@ -32,29 +32,37 @@ func getTerraformCommands() []*cobra.Command { { Use: "workspace", Short: "Manage Terraform workspaces", - Long: "Create, list, select, or delete Terraform workspaces, which allow for separate states within the same configuration. Note, Atmos will automatically select the workspace, if configured.", + Long: `The 'atmos terraform workspace' command initializes Terraform for the current configuration, selects the specified workspace, and creates it if it does not already exist. + +It runs the following sequence of Terraform commands: +1. 'terraform init -reconfigure' to initialize the working directory. +2. 'terraform workspace select' to switch to the specified workspace. +3. If the workspace does not exist, it runs 'terraform workspace new' to create and select it. + +This ensures that the workspace is properly set up for Terraform operations.`, Annotations: map[string]string{ "nativeCommand": "true", }, }, { Use: "clean", - Short: "Clean up resources", - Long: "Remove unused or outdated resources to keep the infrastructure clean and reduce costs.", - Run: terraformRun, + Short: "Clean up Terraform state and artifacts.", + Long: `The 'atmos terraform clean' command removes temporary files, state locks, and other artifacts created during Terraform operations. This helps reset the environment and ensures no leftover data interferes with subsequent runs. + +Common use cases: +- Releasing locks on Terraform state files. +- Cleaning up temporary workspaces or plans. +- Preparing the environment for a fresh deployment.`, }, { Use: "deploy", Short: "Deploy the specified infrastructure using Terraform", - Long: `Deploy the specified infrastructure by running the Terraform plan and apply commands. -This command automates the deployment process, integrates configuration, and ensures streamlined execution.`, - Run: terraformRun, + Long: `Deploys infrastructure by running the Terraform apply command with automatic approval. This ensures that the changes defined in your Terraform configuration are applied without requiring manual confirmation, streamlining the deployment process.`, }, { Use: "shell", - Short: "Configures an 'atmos' environment and starts a shell for native Terraform commands.", - Long: "command configures an environment for an 'atmos' component in a stack and starts a new shell allowing executing all native terraform commands inside the shell without using atmos-specific arguments and flag", - Run: terraformRun, + Short: "Configure an environment for an Atmos component and start a new shell.", + Long: `The 'atmos terraform shell' command configures an environment for a specific Atmos component in a stack and then starts a new shell. In this shell, you can execute all native Terraform commands directly without the need to use Atmos-specific arguments and flags. This allows you to interact with Terraform as you would in a typical setup, but within the configured Atmos environment.`, }, { Use: "version", @@ -142,7 +150,17 @@ This command automates the deployment process, integrates configuration, and ens }, { Use: "import", - Short: "Associate existing infrastructure with a Terraform resource", + Short: "Import existing infrastructure into Terraform state.", + Long: `The 'atmos terraform import' command imports existing infrastructure resources into Terraform's state. + +Before executing the command, it searches for the 'region' variable in the specified component and stack configuration. +If the 'region' variable is found, it sets the 'AWS_REGION' environment variable with the corresponding value before executing the import command. + +The import command runs: 'terraform import [ADDRESS] [ID]' + +Arguments: +- ADDRESS: The Terraform address of the resource to import. +- ID: The ID of the resource to import.`, Annotations: map[string]string{ "nativeCommand": "true", }, @@ -229,9 +247,13 @@ This command automates the deployment process, integrates configuration, and ens // attachTerraformCommands attaches static Terraform commands to a provided parent command func attachTerraformCommands(parentCmd *cobra.Command) { + parentCmd.PersistentFlags().String("append-user-agent", "", "Sets the TF_APPEND_USER_AGENT environment variable to customize the User-Agent string in Terraform provider requests. Example: 'Atmos/%s (Cloud Posse; +https://atmos.tools)'. This flag works with almost all commands.") commands := getTerraformCommands() fmt.Println(os.Args) for _, cmd := range commands { + if setFlags, ok := commandMaps[cmd.Use]; ok { + setFlags(cmd) + } cmd.Run = func(cmd_ *cobra.Command, args []string) { if len(os.Args) > 3 { args = os.Args[2:] @@ -241,3 +263,20 @@ func attachTerraformCommands(parentCmd *cobra.Command) { parentCmd.AddCommand(cmd) } } + +var commandMaps = map[string]func(cmd *cobra.Command){ + "deploy": func(cmd *cobra.Command) { + cmd.PersistentFlags().Bool("deploy-run-init", false, "If set atmos will run `terraform init` before executing the command") + cmd.PersistentFlags().Bool("from-plan", false, "If set atmos will use the previously generated plan file") + cmd.PersistentFlags().String("planfile", "", "Set the plan file to use") + }, + "apply": func(cmd *cobra.Command) { + cmd.PersistentFlags().Bool("from-plan", false, "If set atmos will use the previously generated plan file") + cmd.PersistentFlags().String("planfile", "", "Set the plan file to use") + }, + "clean": func(cmd *cobra.Command) { + cmd.PersistentFlags().Bool("everything", false, "If set atmos will also delete the Terraform state files and directories for the component.") + cmd.PersistentFlags().Bool("force", false, "Forcefully delete Terraform state files and directories without interaction") + cmd.PersistentFlags().Bool("skip-lock-file", false, "Skip deleting the `.terraform.lock.hcl` file") + }, +} diff --git a/cmd/terraform_generate.go b/cmd/terraform_generate.go index 4bbbec664..88cb165e3 100644 --- a/cmd/terraform_generate.go +++ b/cmd/terraform_generate.go @@ -6,9 +6,17 @@ import ( // terraformGenerateCmd generates configurations for terraform components var terraformGenerateCmd = &cobra.Command{ - Use: "generate", - Short: "Execute 'terraform generate' commands", - Long: "This command generates configurations for terraform components", + Use: "generate", + Short: "Generate Terraform configuration files for Atmos components and stacks.", + Long: `The 'atmos terraform generate' command is used to generate Terraform configuration files +for specific components and stacks within your Atmos setup. + +This command supports the following subcommands: +- 'backend' to generate a backend configuration file for an Atmos component in a stack. +- 'backends' to generate backend configuration files for all Atmos components in all stacks. +- 'varfile' to generate a variable file (varfile) for an Atmos component in a stack. +- 'varfiles' to generate varfiles for all Atmos components in all stacks.`, + FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: terraformRun, } From 95304aa644c85680dc41520bf5935f4597838392 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Mon, 23 Dec 2024 18:33:50 +0100 Subject: [PATCH 04/50] test fix for auto approve --- internal/exec/terraform.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/exec/terraform.go b/internal/exec/terraform.go index fcf65bdb3..bd403925f 100644 --- a/internal/exec/terraform.go +++ b/internal/exec/terraform.go @@ -17,7 +17,7 @@ import ( ) const ( - autoApproveFlag = "-auto-approve" + autoApproveFlag = "--auto-approve" outFlag = "-out" varFileFlag = "-var-file" skipTerraformLockFileFlag = "--skip-lock-file" From 0f3f84ea8cc12da8ea8b60e560d8115c10c46feb Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Tue, 24 Dec 2024 12:07:16 +0100 Subject: [PATCH 05/50] Update message for invalid command --- cmd/terraform.go | 14 +++++++++++--- cmd/terraform_generate.go | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/cmd/terraform.go b/cmd/terraform.go index 79781900a..cee89f77d 100644 --- a/cmd/terraform.go +++ b/cmd/terraform.go @@ -1,6 +1,8 @@ package cmd import ( + "fmt" + "github.com/samber/lo" "github.com/spf13/cobra" @@ -18,10 +20,10 @@ var terraformCmd = &cobra.Command{ Short: "Execute Terraform commands", Long: `This command executes Terraform commands`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true}, - Run: terraformRun, + RunE: terraformRun, } -func terraformRun(cmd *cobra.Command, args []string) { +func terraformRun(cmd *cobra.Command, args []string) error { // Check Atmos configuration //checkAtmosConfig() @@ -40,6 +42,11 @@ func terraformRun(cmd *cobra.Command, args []string) { // Exit on help if info.NeedHelp { + if info.SubCommand != "" { + fmt.Printf(`Error: Unknkown command %q for %q`+"\n", args[0], cmd.CommandPath()) + fmt.Printf(`Run '%s --help' for usage`+"\n", cmd.CommandPath()) + return fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath()) + } // Check for the latest Atmos release on GitHub and print update message template := templates.GenerateFromBaseTemplate(cmd.Use, []templates.HelpTemplateSections{ templates.LongDescription, @@ -65,7 +72,7 @@ func terraformRun(cmd *cobra.Command, args []string) { cmd.Help() CheckForAtmosUpdateAndPrintMessage(atmosConfig) - return + return nil } // Check Atmos configuration checkAtmosConfig() @@ -74,6 +81,7 @@ func terraformRun(cmd *cobra.Command, args []string) { if err != nil { u.LogErrorAndExit(schema.AtmosConfiguration{}, err) } + return nil } func init() { diff --git a/cmd/terraform_generate.go b/cmd/terraform_generate.go index 88cb165e3..ad3bad8df 100644 --- a/cmd/terraform_generate.go +++ b/cmd/terraform_generate.go @@ -18,7 +18,7 @@ This command supports the following subcommands: - 'varfiles' to generate varfiles for all Atmos components in all stacks.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, - Run: terraformRun, + RunE: terraformRun, } func init() { From c719d86f2b4cb19662304669c5c9b9aae0037383 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Tue, 24 Dec 2024 12:18:50 +0100 Subject: [PATCH 06/50] remove old help code --- cmd/terraform.go | 10 ++++++++- cmd/terraform_commands.go | 7 ++++-- internal/exec/help.go | 46 --------------------------------------- 3 files changed, 14 insertions(+), 49 deletions(-) diff --git a/cmd/terraform.go b/cmd/terraform.go index cee89f77d..9cda9fbf2 100644 --- a/cmd/terraform.go +++ b/cmd/terraform.go @@ -43,7 +43,15 @@ func terraformRun(cmd *cobra.Command, args []string) error { // Exit on help if info.NeedHelp { if info.SubCommand != "" { - fmt.Printf(`Error: Unknkown command %q for %q`+"\n", args[0], cmd.CommandPath()) + suggestions := cmd.SuggestionsFor(args[0]) + if len(suggestions) > 0 { + fmt.Printf("Unknown command: '%s'\n\nDid you mean this?\n", args[0]) + for _, suggestion := range suggestions { + fmt.Printf(" %s\n", suggestion) + } + } else { + fmt.Printf(`Error: Unknkown command %q for %q`+"\n", args[0], cmd.CommandPath()) + } fmt.Printf(`Run '%s --help' for usage`+"\n", cmd.CommandPath()) return fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath()) } diff --git a/cmd/terraform_commands.go b/cmd/terraform_commands.go index 4802e7f40..902ee2e31 100644 --- a/cmd/terraform_commands.go +++ b/cmd/terraform_commands.go @@ -253,11 +253,14 @@ func attachTerraformCommands(parentCmd *cobra.Command) { if setFlags, ok := commandMaps[cmd.Use]; ok { setFlags(cmd) } - cmd.Run = func(cmd_ *cobra.Command, args []string) { + cmd.RunE = func(cmd_ *cobra.Command, args []string) error { if len(os.Args) > 3 { args = os.Args[2:] } - parentCmd.Run(parentCmd, args) + if parentCmd.Run != nil { + return parentCmd.RunE(parentCmd, args) + } + return nil } parentCmd.AddCommand(cmd) } diff --git a/internal/exec/help.go b/internal/exec/help.go index 507439ddb..628f09fa0 100644 --- a/internal/exec/help.go +++ b/internal/exec/help.go @@ -29,52 +29,6 @@ func processHelp( u.PrintMessage(" - double-dash '--' can be used to signify the end of the options for Atmos and the start of the additional " + "native arguments and flags for the 'helmfile' commands") } - } else if componentType == "terraform" && command == "clean" { - u.PrintMessage("\n'atmos terraform clean' command deletes the following folders and files from the component's directory:\n\n" + - " - '.terraform' folder\n" + - " - folder that the 'TF_DATA_DIR' ENV var points to\n" + - " - '.terraform.lock.hcl' file\n" + - " - generated varfile for the component in the stack\n" + - " - generated planfile for the component in the stack\n" + - " - generated 'backend.tf.json' file\n" + - " - 'terraform.tfstate.d' folder (if '--everything' flag is used)\n\n" + - "Usage: atmos terraform clean -s \n\n" + - "Use '--everything' flag to also delete the Terraform state files and and directories with confirm message.\n\n" + - "Use --force to forcefully delete Terraform state files and directories for the component.\n\n" + - "- If no component is specified, the command will apply to all components and stacks.\n" + - "- If no stack is specified, the command will apply to all stacks for the specified component.\n" + - "Use '--skip-lock-file' flag to skip deleting the '.terraform.lock.hcl' file.\n\n" + - "If no component or stack is specified, the clean operation will apply globally to all components.\n\n" + - "For more details refer to https://atmos.tools/cli/commands/terraform/clean\n") - - } else if componentType == "terraform" && command == "deploy" { - u.PrintMessage("\n'atmos terraform deploy' command executes 'terraform apply -auto-approve' on an Atmos component in an Atmos stack.\n\n" + - "Usage: atmos terraform deploy -s \n\n" + - "The command automatically sets '-auto-approve' flag when running 'terraform apply'.\n\n" + - "It supports '--deploy-run-init=true|false' flag to enable/disable running terraform init before executing the command.\n\n" + - "It supports '--from-plan' flag. If the flag is specified, the command will use the planfile previously generated by 'atmos terraform plan' " + - "command instead of generating a new planfile.\nNote that in this case, the planfile name is in the format supported by Atmos and is " + - "saved to the component's folder.\n\n" + - "It supports '--planfile' flag to specify the path to a planfile.\nThe '--planfile' flag should be used instead of the 'planfile' " + - "argument in the native 'terraform apply ' command.\n\n" + - "For more details refer to https://atmos.tools/cli/commands/terraform/deploy\n") - } else if componentType == "terraform" && command == "shell" { - u.PrintMessage("\n'atmos terraform shell' command starts a new SHELL configured with the environment for an Atmos component " + - "in a Stack to allow executing all native terraform commands\ninside the shell without using the atmos-specific arguments and flags.\n\n" + - "Usage: atmos terraform shell -s \n\n" + - "The command does the following:\n\n" + - " - Processes the stack config files, generates the required variables for the Atmos component in the stack, and writes them to a file in the component's folder\n" + - " - Generates a backend config file for the Atmos component in the stack and writes it to a file in the component's folder (or as specified by the Atmos configuration setting)\n" + - " - Creates a Terraform workspace for the component in the stack\n" + - " - Drops the user into a separate shell (process) with all the required paths and ENV vars set\n" + - " - Inside the shell, the user can execute all Terraform commands using the native syntax\n\n" + - "For more details refer to https://atmos.tools/cli/commands/terraform/shell\n") - } else if componentType == "terraform" && command == "workspace" { - u.PrintMessage("\n'atmos terraform workspace' command calculates the Terraform workspace for an Atmos component,\n" + - "and then executes 'terraform init -reconfigure' and selects the Terraform workspace by executing the 'terraform workspace select' command.\n" + - "If the workspace does not exist, the command creates it by executing the 'terraform workspace new' command.\n\n" + - "Usage: atmos terraform workspace -s \n\n" + - "For more details refer to https://atmos.tools/cli/commands/terraform/workspace\n") } else { u.PrintMessage(fmt.Sprintf("\nAtmos supports native '%s' commands with all the options, arguments and flags.\n", componentType)) u.PrintMessage("In addition, 'component' and 'stack' are required in order to generate variables for the component in the stack.\n") From fba6986baf4060e9819a0aed102a70c1b7428a92 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Tue, 24 Dec 2024 17:33:53 +0100 Subject: [PATCH 07/50] testing if auto-approve should be with two - --- examples/demo-localstack/atmos.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/demo-localstack/atmos.yaml b/examples/demo-localstack/atmos.yaml index d63a92c36..07f817f6d 100644 --- a/examples/demo-localstack/atmos.yaml +++ b/examples/demo-localstack/atmos.yaml @@ -34,16 +34,16 @@ commands: - atmos validate stacks # Test dev stack - atmos terraform plan demo -s dev - - atmos terraform apply demo -s dev -auto-approve - - atmos terraform destroy demo -s dev -auto-approve + - atmos terraform apply demo -s dev --auto-approve + - atmos terraform destroy demo -s dev --auto-approve # Test staging stack - atmos terraform plan demo -s staging - - atmos terraform apply demo -s staging -auto-approve - - atmos terraform destroy demo -s staging -auto-approve + - atmos terraform apply demo -s staging --auto-approve + - atmos terraform destroy demo -s staging --auto-approve # Test prod stack - atmos terraform plan demo -s prod - - atmos terraform apply demo -s prod -auto-approve - - atmos terraform destroy demo -s prod -auto-approve + - atmos terraform apply demo -s prod --auto-approve + - atmos terraform destroy demo -s prod --auto-approve # Use Nested Custom Commands to provide easier interface for Docker Compose - name: "localstack" From 51bbb79b5aebd691b382139103825c7e5cca892e Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Thu, 26 Dec 2024 21:10:31 +0100 Subject: [PATCH 08/50] check error after executing old usage func --- cmd/root.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index 5aebd5b2c..a71f9c1de 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -152,7 +152,9 @@ func initConfig() { } // TODO: find a better way to do this if possible if command.Use == "terraform" { - oldUsageFunc(command) + if err := oldUsageFunc(command); err != nil { + u.LogErrorAndExit(schema.AtmosConfiguration{}, err) + } return } else { b.HelpFunc(command, strings) From ce5bef1bf5df70d7cc9db2778101cba7991c9f5c Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Tue, 31 Dec 2024 10:06:42 +0100 Subject: [PATCH 09/50] rebase help.go --- internal/exec/help.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/exec/help.go b/internal/exec/help.go index 628f09fa0..a9bf9a271 100644 --- a/internal/exec/help.go +++ b/internal/exec/help.go @@ -29,7 +29,7 @@ func processHelp( u.PrintMessage(" - double-dash '--' can be used to signify the end of the options for Atmos and the start of the additional " + "native arguments and flags for the 'helmfile' commands") } - } else { + } else if componentType == "terraform" || componentType == "helmfile" { u.PrintMessage(fmt.Sprintf("\nAtmos supports native '%s' commands with all the options, arguments and flags.\n", componentType)) u.PrintMessage("In addition, 'component' and 'stack' are required in order to generate variables for the component in the stack.\n") u.PrintMessage(fmt.Sprintf("atmos %s -s [options]", componentType)) From 1d8fe0e6590b2f7ddef8b5a743976543458cbf74 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Tue, 31 Dec 2024 14:57:35 +0100 Subject: [PATCH 10/50] Fix terraform subcommand help --- cmd/root.go | 17 ++++---- cmd/terraform.go | 52 ++++++++++++++----------- cmd/terraform_commands.go | 6 +-- cmd/terraform_generate.go | 4 +- internal/exec/utils.go | 3 +- internal/tui/templates/base_template.go | 8 ++++ 6 files changed, 52 insertions(+), 38 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index a71f9c1de..44ca4279e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -144,22 +144,19 @@ func initConfig() { RootCmd.SetHelpFunc(func(command *cobra.Command, strings []string) { // Print a styled Atmos logo to the terminal fmt.Println() - if command.Use != "terraform" { - err := tuiUtils.PrintStyledText("ATMOS") - if err != nil { - u.LogErrorAndExit(schema.AtmosConfiguration{}, err) - } - } - // TODO: find a better way to do this if possible - if command.Use == "terraform" { + if command.Use != "atmos" { if err := oldUsageFunc(command); err != nil { u.LogErrorAndExit(schema.AtmosConfiguration{}, err) } - return } else { + err := tuiUtils.PrintStyledText("ATMOS") + if err != nil { + u.LogErrorAndExit(schema.AtmosConfiguration{}, err) + } b.HelpFunc(command, strings) + command.Usage() } - command.Usage() + CheckForAtmosUpdateAndPrintMessage(atmosConfig) }) } diff --git a/cmd/terraform.go b/cmd/terraform.go index 758428222..3e018c825 100644 --- a/cmd/terraform.go +++ b/cmd/terraform.go @@ -20,13 +20,22 @@ var terraformCmd = &cobra.Command{ Short: "Execute Terraform commands", Long: `This command executes Terraform commands`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true}, - RunE: terraformRun, + RunE: func(cmd *cobra.Command, args []string) error { + return terraformRun(cmd, cmd, args) + }, } -func terraformRun(cmd *cobra.Command, args []string) error { - // Check Atmos configuration - //checkAtmosConfig() +// Contains checks if a slice of strings contains an exact match for the target string. +func Contains(slice []string, target string) bool { + for _, item := range slice { + if item == target { + return true + } + } + return false +} +func terraformRun(cmd *cobra.Command, actualCmd *cobra.Command, args []string) error { var argsAfterDoubleDash []string var finalArgs = args @@ -35,28 +44,27 @@ func terraformRun(cmd *cobra.Command, args []string) error { finalArgs = lo.Slice(args, 0, doubleDashIndex) argsAfterDoubleDash = lo.Slice(args, doubleDashIndex+1, len(args)) } - info, err := e.ProcessCommandLineArgs("terraform", cmd, finalArgs, argsAfterDoubleDash) - if err != nil { - u.LogErrorAndExit(schema.AtmosConfiguration{}, err) - } + info, _ := e.ProcessCommandLineArgs("terraform", cmd, finalArgs, argsAfterDoubleDash) // Exit on help if info.NeedHelp { if info.SubCommand != "" && info.SubCommand != "--help" && info.SubCommand != "help" { suggestions := cmd.SuggestionsFor(args[0]) - if len(suggestions) > 0 { - fmt.Printf("Unknown command: '%s'\n\nDid you mean this?\n", args[0]) - for _, suggestion := range suggestions { - fmt.Printf(" %s\n", suggestion) + if !Contains(suggestions, args[0]) { + if len(suggestions) > 0 { + fmt.Printf("Unknown command: '%s'\n\nDid you mean this?\n", args[0]) + for _, suggestion := range suggestions { + fmt.Printf(" %s\n", suggestion) + } + } else { + fmt.Printf(`Error: Unknkown command %q for %q`+"\n", args[0], cmd.CommandPath()) } - } else { - fmt.Printf(`Error: Unknkown command %q for %q`+"\n", args[0], cmd.CommandPath()) + fmt.Printf(`Run '%s --help' for usage`+"\n", cmd.CommandPath()) + return fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath()) } - fmt.Printf(`Run '%s --help' for usage`+"\n", cmd.CommandPath()) - return fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath()) } // Check for the latest Atmos release on GitHub and print update message - template := templates.GenerateFromBaseTemplate(cmd.Use, []templates.HelpTemplateSections{ + template := templates.GenerateFromBaseTemplate(actualCmd.Use, []templates.HelpTemplateSections{ templates.LongDescription, templates.Usage, templates.Aliases, @@ -65,12 +73,13 @@ func terraformRun(cmd *cobra.Command, args []string) error { templates.Flags, templates.GlobalFlags, templates.NativeCommands, + templates.DoubleDashHelp, templates.Footer, }) - cmd.SetUsageTemplate(template) + actualCmd.SetUsageTemplate(template) cc.Init(&cc.Config{ - RootCmd: cmd, + RootCmd: actualCmd, Headings: cc.HiCyan + cc.Bold + cc.Underline, Commands: cc.HiGreen + cc.Bold, Example: cc.Italic, @@ -78,14 +87,13 @@ func terraformRun(cmd *cobra.Command, args []string) error { Flags: cc.Bold, }) - cmd.Help() - CheckForAtmosUpdateAndPrintMessage(atmosConfig) + actualCmd.Help() return nil } // Check Atmos configuration checkAtmosConfig() - err = e.ExecuteTerraform(info) + err := e.ExecuteTerraform(info) if err != nil { u.LogErrorAndExit(schema.AtmosConfiguration{}, err) } diff --git a/cmd/terraform_commands.go b/cmd/terraform_commands.go index 3d8d8ad50..d84c84f4b 100644 --- a/cmd/terraform_commands.go +++ b/cmd/terraform_commands.go @@ -253,6 +253,7 @@ func attachTerraformCommands(parentCmd *cobra.Command) { for _, cmd := range commands { cmd.FParseErrWhitelist.UnknownFlags = true + cmd.DisableFlagParsing = true if setFlags, ok := commandMaps[cmd.Use]; ok { setFlags(cmd) } @@ -260,10 +261,7 @@ func attachTerraformCommands(parentCmd *cobra.Command) { if len(os.Args) > 3 { args = os.Args[2:] } - if parentCmd.RunE != nil { - return parentCmd.RunE(parentCmd, args) - } - return nil + return terraformRun(parentCmd, cmd_, args) } parentCmd.AddCommand(cmd) } diff --git a/cmd/terraform_generate.go b/cmd/terraform_generate.go index ad3bad8df..1a635b229 100644 --- a/cmd/terraform_generate.go +++ b/cmd/terraform_generate.go @@ -18,7 +18,9 @@ This command supports the following subcommands: - 'varfiles' to generate varfiles for all Atmos components in all stacks.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, - RunE: terraformRun, + RunE: func(cmd *cobra.Command, args []string) error { + return terraformRun(cmd, cmd, args) + }, } func init() { diff --git a/internal/exec/utils.go b/internal/exec/utils.go index 772b3fd3b..1ae283af9 100644 --- a/internal/exec/utils.go +++ b/internal/exec/utils.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-config-inspect/tfconfig" "github.com/mitchellh/mapstructure" "github.com/spf13/cobra" + "github.com/spf13/pflag" cfg "github.com/cloudposse/atmos/pkg/config" "github.com/cloudposse/atmos/pkg/schema" @@ -174,7 +175,7 @@ func ProcessCommandLineArgs( cmd.DisableFlagParsing = false err := cmd.ParseFlags(args) - if err != nil { + if err != nil && !errors.Is(err, pflag.ErrHelp) { return configAndStacksInfo, err } diff --git a/internal/tui/templates/base_template.go b/internal/tui/templates/base_template.go index 879a79e1e..bf68b04eb 100644 --- a/internal/tui/templates/base_template.go +++ b/internal/tui/templates/base_template.go @@ -14,6 +14,7 @@ const ( GlobalFlags AdditionalHelpTopics NativeCommands + DoubleDashHelp Footer ) @@ -70,6 +71,13 @@ Global Flags: return `Usage:{{if .Runnable}} {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} {{.CommandPath}} [command]{{end}}` + case DoubleDashHelp: + return fmt.Sprintf(` +The '--' (double-dash) can be used to signify the end of Atmos-specific options +and the beginning of additional native arguments and flags for the specific command being run. + +Example: + atmos %s -s -- `, commandName) case Footer: return `{{if .HasAvailableSubCommands}} From a8d6345402c5efbbd79a86f1b28f7e1f24666161 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Tue, 31 Dec 2024 15:24:52 +0100 Subject: [PATCH 11/50] Remove unwanted setHelpFunc --- cmd/cmd_utils.go | 5 --- cmd/root.go | 11 ------ cmd/terraform.go | 46 +++++++++++++------------ internal/tui/templates/base_template.go | 1 + internal/tui/templates/templater.go | 1 + 5 files changed, 26 insertions(+), 38 deletions(-) diff --git a/cmd/cmd_utils.go b/cmd/cmd_utils.go index 963f4ad9b..1c7310a91 100644 --- a/cmd/cmd_utils.go +++ b/cmd/cmd_utils.go @@ -481,11 +481,6 @@ func CheckForAtmosUpdateAndPrintMessage(atmosConfig schema.AtmosConfiguration) { } } -func customHelpMessageToUpgradeToAtmosLatestRelease(cmd *cobra.Command, args []string) { - originalHelpFunc(cmd, args) - CheckForAtmosUpdateAndPrintMessage(atmosConfig) -} - // Check Atmos is version command func isVersionCommand() bool { return len(os.Args) > 1 && os.Args[1] == "version" diff --git a/cmd/root.go b/cmd/root.go index 44ca4279e..b24458e4f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -20,9 +20,6 @@ import ( var atmosConfig schema.AtmosConfiguration -// originalHelpFunc holds Cobra's original help function to avoid recursion. -var originalHelpFunc func(*cobra.Command, []string) - // RootCmd represents the base command when called without any subcommands var RootCmd = &cobra.Command{ Use: "atmos", @@ -96,14 +93,6 @@ func Execute() error { } } - // Save the original help function to prevent infinite recursion when overriding it. - // This allows us to call the original help functionality within our custom help function. - originalHelpFunc = RootCmd.HelpFunc() - - // Override the help function with a custom one that adds an upgrade message after displaying help. - // This custom help function will call the original help function and then display the bordered message. - RootCmd.SetHelpFunc(customHelpMessageToUpgradeToAtmosLatestRelease) - // If CLI configuration was found, process its custom commands and command aliases if initErr == nil { err = processCustomCommands(atmosConfig, atmosConfig.Commands, RootCmd, true) diff --git a/cmd/terraform.go b/cmd/terraform.go index 3e018c825..2b23eabe7 100644 --- a/cmd/terraform.go +++ b/cmd/terraform.go @@ -63,29 +63,31 @@ func terraformRun(cmd *cobra.Command, actualCmd *cobra.Command, args []string) e return fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath()) } } - // Check for the latest Atmos release on GitHub and print update message - template := templates.GenerateFromBaseTemplate(actualCmd.Use, []templates.HelpTemplateSections{ - templates.LongDescription, - templates.Usage, - templates.Aliases, - templates.Examples, - templates.AvailableCommands, - templates.Flags, - templates.GlobalFlags, - templates.NativeCommands, - templates.DoubleDashHelp, - templates.Footer, - }) + // check if this is terraform --help command. TODO: check if this is the best way to do + if cmd == actualCmd { + template := templates.GenerateFromBaseTemplate(actualCmd.Use, []templates.HelpTemplateSections{ + templates.LongDescription, + templates.Usage, + templates.Aliases, + templates.Examples, + templates.AvailableCommands, + templates.Flags, + templates.GlobalFlags, + templates.NativeCommands, + templates.DoubleDashHelp, + templates.Footer, + }) + actualCmd.SetUsageTemplate(template) + cc.Init(&cc.Config{ + RootCmd: actualCmd, + Headings: cc.HiCyan + cc.Bold + cc.Underline, + Commands: cc.HiGreen + cc.Bold, + Example: cc.Italic, + ExecName: cc.Bold, + Flags: cc.Bold, + }) - actualCmd.SetUsageTemplate(template) - cc.Init(&cc.Config{ - RootCmd: actualCmd, - Headings: cc.HiCyan + cc.Bold + cc.Underline, - Commands: cc.HiGreen + cc.Bold, - Example: cc.Italic, - ExecName: cc.Bold, - Flags: cc.Bold, - }) + } actualCmd.Help() return nil diff --git a/internal/tui/templates/base_template.go b/internal/tui/templates/base_template.go index bf68b04eb..e03d392fd 100644 --- a/internal/tui/templates/base_template.go +++ b/internal/tui/templates/base_template.go @@ -73,6 +73,7 @@ Global Flags: {{.CommandPath}} [command]{{end}}` case DoubleDashHelp: return fmt.Sprintf(` + The '--' (double-dash) can be used to signify the end of Atmos-specific options and the beginning of additional native arguments and flags for the specific command being run. diff --git a/internal/tui/templates/templater.go b/internal/tui/templates/templater.go index 8fdeccbc7..c297277d2 100644 --- a/internal/tui/templates/templater.go +++ b/internal/tui/templates/templater.go @@ -122,6 +122,7 @@ func SetCustomUsageFunc(cmd *cobra.Command) error { Flags, GlobalFlags, AdditionalHelpTopics, + DoubleDashHelp, Footer, }), } From ed8b9f8b4b333dba4e11de8fbe0184915fe86e13 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Tue, 31 Dec 2024 16:47:30 +0100 Subject: [PATCH 12/50] terraform,helmfile empty sub command should redirect to help --- cmd/helmfile.go | 4 ++-- cmd/terraform.go | 2 +- internal/exec/utils.go | 14 -------------- 3 files changed, 3 insertions(+), 17 deletions(-) diff --git a/cmd/helmfile.go b/cmd/helmfile.go index 8bd20dbde..7afba1c5f 100644 --- a/cmd/helmfile.go +++ b/cmd/helmfile.go @@ -32,9 +32,9 @@ var helmfileCmd = &cobra.Command{ u.LogErrorAndExit(schema.AtmosConfiguration{}, err) } // Exit on help - if info.NeedHelp { + if info.NeedHelp || (info.SubCommand == "" && info.SubCommand2 == "") { // Check for the latest Atmos release on GitHub and print update message - CheckForAtmosUpdateAndPrintMessage(atmosConfig) + cmd.Help() return } // Check Atmos configuration diff --git a/cmd/terraform.go b/cmd/terraform.go index 2b23eabe7..aaef9e6e5 100644 --- a/cmd/terraform.go +++ b/cmd/terraform.go @@ -47,7 +47,7 @@ func terraformRun(cmd *cobra.Command, actualCmd *cobra.Command, args []string) e info, _ := e.ProcessCommandLineArgs("terraform", cmd, finalArgs, argsAfterDoubleDash) // Exit on help - if info.NeedHelp { + if info.NeedHelp || (info.SubCommand == "" && info.SubCommand2 == "") { if info.SubCommand != "" && info.SubCommand != "--help" && info.SubCommand != "help" { suggestions := cmd.SuggestionsFor(args[0]) if !Contains(suggestions, args[0]) { diff --git a/internal/exec/utils.go b/internal/exec/utils.go index 1ae283af9..f1baec3cd 100644 --- a/internal/exec/utils.go +++ b/internal/exec/utils.go @@ -220,20 +220,6 @@ func ProcessCommandLineArgs( configAndStacksInfo.LogsFile = argsAndFlagsInfo.LogsFile configAndStacksInfo.SettingsListMergeStrategy = argsAndFlagsInfo.SettingsListMergeStrategy - // Check if `-h` or `--help` flags are specified - if argsAndFlagsInfo.NeedHelp && configAndStacksInfo.ComponentType != "terraform" { - // If we're dealing with `-h` or `--help`, - // then the SubCommand should be empty. - if argsAndFlagsInfo.SubCommand == "-h" || argsAndFlagsInfo.SubCommand == "--help" { - argsAndFlagsInfo.SubCommand = "" - } - err = processHelp(schema.AtmosConfiguration{}, componentType, argsAndFlagsInfo.SubCommand) - if err != nil { - return configAndStacksInfo, err - } - return configAndStacksInfo, nil - } - flags := cmd.Flags() stack, err := flags.GetString("stack") From 18a6d96d05c487ed17d6a9cc1f04097aa1ec5701 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Tue, 31 Dec 2024 17:08:57 +0100 Subject: [PATCH 13/50] check errors for usage and help --- cmd/helmfile.go | 4 +++- cmd/root.go | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/helmfile.go b/cmd/helmfile.go index 7afba1c5f..9477be218 100644 --- a/cmd/helmfile.go +++ b/cmd/helmfile.go @@ -34,7 +34,9 @@ var helmfileCmd = &cobra.Command{ // Exit on help if info.NeedHelp || (info.SubCommand == "" && info.SubCommand2 == "") { // Check for the latest Atmos release on GitHub and print update message - cmd.Help() + if err := cmd.Help(); err != nil { + u.LogErrorAndExit(schema.AtmosConfiguration{}, err) + } return } // Check Atmos configuration diff --git a/cmd/root.go b/cmd/root.go index b24458e4f..24a7b18cb 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -143,7 +143,9 @@ func initConfig() { u.LogErrorAndExit(schema.AtmosConfiguration{}, err) } b.HelpFunc(command, strings) - command.Usage() + if err := command.Usage(); err != nil { + u.LogErrorAndExit(schema.AtmosConfiguration{}, err) + } } CheckForAtmosUpdateAndPrintMessage(atmosConfig) }) From 40b4dd3d828b9b3b4b4dd102bcedfc6f7b214ff2 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Tue, 31 Dec 2024 17:35:41 +0100 Subject: [PATCH 14/50] Added space in between Native commands --- internal/tui/templates/base_template.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/tui/templates/base_template.go b/internal/tui/templates/base_template.go index e03d392fd..5e3ce2269 100644 --- a/internal/tui/templates/base_template.go +++ b/internal/tui/templates/base_template.go @@ -63,6 +63,7 @@ Global Flags: {{wrappedFlagUsages .InheritedFlags | trimTrailingWhitespaces}}{{end}}` case NativeCommands: return fmt.Sprintf(` + {{HeadingStyle "Native %s Commands:"}} {{formatCommands .Commands "native"}} From 80c4b8a138b54ab54c57c733d6a85c64ee156748 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Thu, 2 Jan 2025 22:12:22 +0100 Subject: [PATCH 15/50] Removed unwanted code --- cmd/root.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 24a7b18cb..43b2397d0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -73,12 +73,8 @@ func Execute() error { // Check if the `help` flag is passed and print a styled Atmos logo to the terminal before printing the help err := RootCmd.ParseFlags(os.Args) - if err != nil && errors.Is(err, pflag.ErrHelp) { - fmt.Println() - err = tuiUtils.PrintStyledText("ATMOS") - if err != nil { - u.LogErrorAndExit(schema.AtmosConfiguration{}, err) - } + if err != nil && !errors.Is(err, pflag.ErrHelp) { + u.LogErrorAndExit(atmosConfig, err) } // InitCliConfig finds and merges CLI configurations in the following order: // system dir, home dir, current dir, ENV vars, command-line arguments @@ -121,10 +117,10 @@ func init() { // Set custom usage template templates.SetCustomUsageFunc(RootCmd) - cobra.OnInitialize(initConfig) + initCobraConfig() } -func initConfig() { +func initCobraConfig() { styles := boa.DefaultStyles() b := boa.New(boa.WithStyles(styles)) oldUsageFunc := RootCmd.UsageFunc() @@ -134,17 +130,21 @@ func initConfig() { // Print a styled Atmos logo to the terminal fmt.Println() if command.Use != "atmos" { + err := tuiUtils.PrintStyledText("ATMOS") + if err != nil { + u.LogErrorAndExit(atmosConfig, err) + } if err := oldUsageFunc(command); err != nil { - u.LogErrorAndExit(schema.AtmosConfiguration{}, err) + u.LogErrorAndExit(atmosConfig, err) } } else { err := tuiUtils.PrintStyledText("ATMOS") if err != nil { - u.LogErrorAndExit(schema.AtmosConfiguration{}, err) + u.LogErrorAndExit(atmosConfig, err) } b.HelpFunc(command, strings) if err := command.Usage(); err != nil { - u.LogErrorAndExit(schema.AtmosConfiguration{}, err) + u.LogErrorAndExit(atmosConfig, err) } } CheckForAtmosUpdateAndPrintMessage(atmosConfig) From 68233792d36d7cb454cf1da337257427d2e25df1 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Sun, 5 Jan 2025 20:23:06 +0100 Subject: [PATCH 16/50] Add consistent usage and help for all commands isssue: https://linear.app/cloudposse/issue/DEV-2896/incorrect-output-for-atmos-validate --- cmd/atlantis.go | 1 + cmd/atlantis_generate.go | 1 + cmd/atlantis_generate_repo_config.go | 6 +- cmd/aws.go | 1 + cmd/aws_eks.go | 1 + cmd/aws_eks_update_kubeconfig.go | 1 + cmd/cmd_utils.go | 100 +++++++++++++++++++++++++++ cmd/cmd_utils_test.go | 72 +++++++++++++++++++ cmd/completion.go | 57 +++++++++------ cmd/describe.go | 1 + cmd/describe_affected.go | 5 ++ cmd/describe_component.go | 1 + cmd/describe_config.go | 5 ++ cmd/describe_dependents.go | 1 + cmd/describe_stacks.go | 5 ++ cmd/describe_workflows.go | 5 ++ cmd/docs.go | 2 + cmd/helmfile.go | 37 +--------- cmd/helmfile_generate.go | 1 + cmd/helmfile_generate_varfile.go | 1 + cmd/list.go | 1 + cmd/list_components.go | 4 ++ cmd/list_stacks.go | 4 ++ cmd/pro.go | 1 + cmd/pro_lock.go | 4 ++ cmd/pro_unlock.go | 4 ++ cmd/root.go | 11 ++- cmd/terraform.go | 55 +-------------- cmd/terraform_generate.go | 4 +- cmd/terraform_generate_backend.go | 1 + cmd/terraform_generate_backends.go | 4 ++ cmd/terraform_generate_varfile.go | 1 + cmd/terraform_generate_varfiles.go | 4 ++ cmd/validate.go | 1 + cmd/validate_component.go | 1 + cmd/validate_stacks.go | 4 ++ cmd/vendor.go | 1 + cmd/vendor_diff.go | 3 + cmd/vendor_pull.go | 4 ++ cmd/version.go | 3 + cmd/workflow.go | 1 + 41 files changed, 303 insertions(+), 117 deletions(-) create mode 100644 cmd/cmd_utils_test.go diff --git a/cmd/atlantis.go b/cmd/atlantis.go index cefefa8b5..11873d1e4 100644 --- a/cmd/atlantis.go +++ b/cmd/atlantis.go @@ -13,5 +13,6 @@ var atlantisCmd = &cobra.Command{ } func init() { + addUsageCommand(atlantisCmd, false) RootCmd.AddCommand(atlantisCmd) } diff --git a/cmd/atlantis_generate.go b/cmd/atlantis_generate.go index 97688d568..b83609b47 100644 --- a/cmd/atlantis_generate.go +++ b/cmd/atlantis_generate.go @@ -13,5 +13,6 @@ var atlantisGenerateCmd = &cobra.Command{ } func init() { + addUsageCommand(atlantisGenerateCmd, false) atlantisCmd.AddCommand(atlantisGenerateCmd) } diff --git a/cmd/atlantis_generate_repo_config.go b/cmd/atlantis_generate_repo_config.go index 0ff322dc2..7313c2f4f 100644 --- a/cmd/atlantis_generate_repo_config.go +++ b/cmd/atlantis_generate_repo_config.go @@ -15,9 +15,13 @@ var atlantisGenerateRepoConfigCmd = &cobra.Command{ Long: "This command generates repository configuration for Atlantis", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) + if hasPositionalArgs(args) { + showUsageAndExit(cmd, args, false) + } + // Check Atmos configuration checkAtmosConfig() - err := e.ExecuteAtlantisGenerateRepoConfigCmd(cmd, args) if err != nil { u.LogErrorAndExit(schema.AtmosConfiguration{}, err) diff --git a/cmd/aws.go b/cmd/aws.go index 4976dee10..6186959b7 100644 --- a/cmd/aws.go +++ b/cmd/aws.go @@ -13,5 +13,6 @@ var awsCmd = &cobra.Command{ } func init() { + addUsageCommand(awsCmd, false) RootCmd.AddCommand(awsCmd) } diff --git a/cmd/aws_eks.go b/cmd/aws_eks.go index 9b80f7262..e641de9dc 100644 --- a/cmd/aws_eks.go +++ b/cmd/aws_eks.go @@ -13,5 +13,6 @@ var awsEksCmd = &cobra.Command{ } func init() { + addUsageCommand(awsEksCmd, false) awsCmd.AddCommand(awsEksCmd) } diff --git a/cmd/aws_eks_update_kubeconfig.go b/cmd/aws_eks_update_kubeconfig.go index 38363d303..a768c0649 100644 --- a/cmd/aws_eks_update_kubeconfig.go +++ b/cmd/aws_eks_update_kubeconfig.go @@ -35,6 +35,7 @@ See https://docs.aws.amazon.com/cli/latest/reference/eks/update-kubeconfig.html FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) err := e.ExecuteAwsEksUpdateKubeconfigCommand(cmd, args) if err != nil { u.LogErrorAndExit(schema.AtmosConfiguration{}, err) diff --git a/cmd/cmd_utils.go b/cmd/cmd_utils.go index 1c7310a91..e713c25ac 100644 --- a/cmd/cmd_utils.go +++ b/cmd/cmd_utils.go @@ -10,9 +10,11 @@ import ( "time" "github.com/fatih/color" + cc "github.com/ivanpirog/coloredcobra" "github.com/spf13/cobra" e "github.com/cloudposse/atmos/internal/exec" + "github.com/cloudposse/atmos/internal/tui/templates" tuiUtils "github.com/cloudposse/atmos/internal/tui/utils" cfg "github.com/cloudposse/atmos/pkg/config" "github.com/cloudposse/atmos/pkg/schema" @@ -485,3 +487,101 @@ func CheckForAtmosUpdateAndPrintMessage(atmosConfig schema.AtmosConfiguration) { func isVersionCommand() bool { return len(os.Args) > 1 && os.Args[1] == "version" } + +// handleHelpRequest shows help content and exits only if the first argument is "help" or "--help" or "-h" +func handleHelpRequest(cmd *cobra.Command, args []string, isNativeCommandsAvailable bool) { + if (len(args) > 0 && args[0] == "help") || Contains(args, "--help") || Contains(args, "-h") { + if isNativeCommandsAvailable { + template := templates.GenerateFromBaseTemplate(cmd.Use, []templates.HelpTemplateSections{ + templates.LongDescription, + templates.Usage, + templates.Aliases, + templates.Examples, + templates.AvailableCommands, + templates.Flags, + templates.GlobalFlags, + templates.NativeCommands, + templates.DoubleDashHelp, + templates.Footer, + }) + cmd.SetUsageTemplate(template) + cc.Init(&cc.Config{ + RootCmd: cmd, + Headings: cc.HiCyan + cc.Bold + cc.Underline, + Commands: cc.HiGreen + cc.Bold, + Example: cc.Italic, + ExecName: cc.Bold, + Flags: cc.Bold, + }) + } + cmd.Help() + os.Exit(0) + } +} + +func showUsageAndExit(cmd *cobra.Command, args []string, isNativeCommandsAvailable bool) { + + var suggestions []string + var subCommand string = "" + unkonwnCommand := fmt.Sprintf("Unknown command: %q", cmd.CommandPath()) + + if len(args) > 0 { + // Show help if the first argument is "help" + handleHelpRequest(cmd, args, isNativeCommandsAvailable) + suggestions = cmd.SuggestionsFor(args[0]) + subCommand = args[0] + unkonwnCommand = fmt.Sprintf(`Error: Unknkown command %q for %q`+"\n", subCommand, cmd.CommandPath()) + } + if len(suggestions) > 0 { + u.PrintMessage(fmt.Sprintf("%s\n\nDid you mean this?", unkonwnCommand)) + for _, suggestion := range suggestions { + u.PrintMessage(fmt.Sprintf(" %s\n", suggestion)) + } + } else { + // Retrieve valid subcommands dynamically + validSubcommands := []string{} + for _, subCmd := range cmd.Commands() { + validSubcommands = append(validSubcommands, subCmd.Name()) + } + u.PrintMessage(unkonwnCommand) + if len(validSubcommands) > 0 { + u.PrintMessage("Valid subcommands are:") + for _, sub := range validSubcommands { + u.PrintMessage(fmt.Sprintf(" %s", sub)) + } + } else { + u.PrintMessage("No valid subcommands found") + } + } + u.PrintMessage(fmt.Sprintf(`Run '%s --help' for usage`, cmd.CommandPath())) + u.LogErrorAndExit(atmosConfig, errors.New(unkonwnCommand)) +} + +func addUsageCommand(cmd *cobra.Command, isNativeCommandsAvailable bool) { + cmd.Run = func(cmd *cobra.Command, args []string) { + showUsageAndExit(cmd, args, isNativeCommandsAvailable) + } +} + +// hasPositionalArgs checks if a slice of strings contains an exact match for the target string. +func hasPositionalArgs(args []string) bool { + for i := 0; i < len(args); i++ { + arg := args[i] + + if strings.HasPrefix(arg, "-") { + // Handle "--flag=value" syntax + if strings.Contains(arg, "=") { + continue + } + + // Skip the next argument if it looks like a value for a flag + if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") { + i++ // Skip the value + } + } else { + // If it's not a flag and not a value for a flag, it's positional + return true + } + } + return false +} diff --git a/cmd/cmd_utils_test.go b/cmd/cmd_utils_test.go new file mode 100644 index 000000000..5affdab0e --- /dev/null +++ b/cmd/cmd_utils_test.go @@ -0,0 +1,72 @@ +package cmd + +import ( + "testing" +) + +func TestHasPositionalArgs(t *testing.T) { + testCases := []struct { + name string + args []string + want bool + }{ + { + name: "NoArgs", + args: []string{}, + want: false, + }, + { + name: "OneArg", + args: []string{"arg1"}, + want: true, + }, + { + name: "TwoArgs", + args: []string{"arg1", "arg2"}, + want: true, + }, + { + name: "ArgAndFlagWithParam", + args: []string{"arg1", "--flag", "param"}, + want: true, + }, + { + name: "Flag", + args: []string{"--flag"}, + want: false, + }, + { + name: "FlagAndArg", + args: []string{"--flag", "arg1"}, + want: false, + }, + { + name: "FlagAndArgAndFlag", + args: []string{"--flag", "arg1", "--flag"}, + want: false, + }, + { + name: "FlagsWithArgsWithPositionalArg", + args: []string{"--flag=something", "ted"}, + want: true, + }, + { + name: "FlagsWithoutArgsWithEqual", + args: []string{"--flag=something"}, + want: false, + }, + { + name: "terrraformLikeArgs", + args: []string{"-flag1=hello", "-flag2"}, + want: false, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := hasPositionalArgs(tc.args) + if got != tc.want { + t.Errorf("HasPositionalArgs() = %v, want %v", got, tc.want) + } + }) + } +} diff --git a/cmd/completion.go b/cmd/completion.go index ab74194d1..7bd8c3548 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -3,8 +3,6 @@ package cmd import ( "os" - "github.com/cloudposse/atmos/pkg/schema" - "github.com/spf13/cobra" u "github.com/cloudposse/atmos/pkg/utils" @@ -15,28 +13,43 @@ var completionCmd = &cobra.Command{ Short: "Generate completion script for Bash, Zsh, Fish and PowerShell", Long: "This command generates completion scripts for Bash, Zsh, Fish and PowerShell", DisableFlagsInUseLine: true, - ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, - Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), - Run: func(cmd *cobra.Command, args []string) { - var err error - - switch args[0] { - case "bash": - err = cmd.Root().GenBashCompletion(os.Stdout) - case "zsh": - err = cmd.Root().GenZshCompletion(os.Stdout) - case "fish": - err = cmd.Root().GenFishCompletion(os.Stdout, true) - case "powershell": - err = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) - } - - if err != nil { - u.LogErrorAndExit(schema.AtmosConfiguration{}, err) - } - }, + // Why I am not using cobra inbuilt validation for Args: + // Because we have our own custom validation for Args + // Why we have our own custom validation for Args: + // Because we want to show custom error message when user provides invalid shell name + // ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, + // Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), +} + +func runCompletion(cmd *cobra.Command, args []string) { + var err error + + switch cmd.Use { + case "bash": + err = cmd.Root().GenBashCompletion(os.Stdout) + case "zsh": + err = cmd.Root().GenZshCompletion(os.Stdout) + case "fish": + err = cmd.Root().GenFishCompletion(os.Stdout, true) + case "powershell": + err = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) + } + + if err != nil { + u.LogErrorAndExit(atmosConfig, err) + } } func init() { + shellNames := []string{"bash", "zsh", "fish", "powershell"} + for _, shellName := range shellNames { + completionCmd.AddCommand(&cobra.Command{ + Use: shellName, + Short: "Generate completion script for " + shellName, + Long: "This command generates completion scripts for " + shellName, + Run: runCompletion, + }) + } + addUsageCommand(completionCmd, false) RootCmd.AddCommand(completionCmd) } diff --git a/cmd/describe.go b/cmd/describe.go index 4e134f584..6e63055fb 100644 --- a/cmd/describe.go +++ b/cmd/describe.go @@ -13,5 +13,6 @@ var describeCmd = &cobra.Command{ } func init() { + addUsageCommand(describeCmd, false) RootCmd.AddCommand(describeCmd) } diff --git a/cmd/describe_affected.go b/cmd/describe_affected.go index df05090a7..73b8d92c5 100644 --- a/cmd/describe_affected.go +++ b/cmd/describe_affected.go @@ -15,6 +15,11 @@ var describeAffectedCmd = &cobra.Command{ Long: `This command produces a list of the affected Atmos components and stacks given two Git commits: atmos describe affected [options]`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) + if hasPositionalArgs(args) { + showUsageAndExit(cmd, args, false) + } + // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/describe_component.go b/cmd/describe_component.go index b40de68cd..75062f029 100644 --- a/cmd/describe_component.go +++ b/cmd/describe_component.go @@ -15,6 +15,7 @@ var describeComponentCmd = &cobra.Command{ Long: `This command shows configuration for an Atmos component in an Atmos stack: atmos describe component -s `, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/describe_config.go b/cmd/describe_config.go index 3f201ee9f..33a6fd1a0 100644 --- a/cmd/describe_config.go +++ b/cmd/describe_config.go @@ -15,6 +15,11 @@ var describeConfigCmd = &cobra.Command{ Long: `This command shows the final (deep-merged) CLI configuration: atmos describe config`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) + if hasPositionalArgs(args) { + showUsageAndExit(cmd, args, false) + } + err := e.ExecuteDescribeConfigCmd(cmd, args) if err != nil { u.LogErrorAndExit(schema.AtmosConfiguration{}, err) diff --git a/cmd/describe_dependents.go b/cmd/describe_dependents.go index bad419954..79bf14963 100644 --- a/cmd/describe_dependents.go +++ b/cmd/describe_dependents.go @@ -16,6 +16,7 @@ var describeDependentsCmd = &cobra.Command{ Long: `This command produces a list of Atmos components in Atmos stacks that depend on the provided Atmos component: atmos describe dependents [options]`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/describe_stacks.go b/cmd/describe_stacks.go index 6cf497739..a148c8e8c 100644 --- a/cmd/describe_stacks.go +++ b/cmd/describe_stacks.go @@ -15,6 +15,11 @@ var describeStacksCmd = &cobra.Command{ Long: `This command shows configuration for atmos stacks and components in the stacks: atmos describe stacks [options]`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) + if hasPositionalArgs(args) { + showUsageAndExit(cmd, args, false) + } + // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/describe_workflows.go b/cmd/describe_workflows.go index fe644155c..b22379c7d 100644 --- a/cmd/describe_workflows.go +++ b/cmd/describe_workflows.go @@ -22,6 +22,11 @@ var describeWorkflowsCmd = &cobra.Command{ "describe workflows -o all", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) + if hasPositionalArgs(args) { + showUsageAndExit(cmd, args, false) + } + err := e.ExecuteDescribeWorkflowsCmd(cmd, args) if err != nil { u.LogErrorAndExit(schema.AtmosConfiguration{}, err) diff --git a/cmd/docs.go b/cmd/docs.go index 650a74d25..f954ec944 100644 --- a/cmd/docs.go +++ b/cmd/docs.go @@ -28,6 +28,7 @@ var docsCmd = &cobra.Command{ Args: cobra.MaximumNArgs(1), FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) if len(args) == 1 { info := schema.ConfigAndStacksInfo{ Component: args[0], @@ -124,5 +125,6 @@ var docsCmd = &cobra.Command{ } func init() { + // TODO: Check what this command does RootCmd.AddCommand(docsCmd) } diff --git a/cmd/helmfile.go b/cmd/helmfile.go index 9477be218..5c720781a 100644 --- a/cmd/helmfile.go +++ b/cmd/helmfile.go @@ -1,12 +1,7 @@ package cmd import ( - "github.com/samber/lo" "github.com/spf13/cobra" - - e "github.com/cloudposse/atmos/internal/exec" - "github.com/cloudposse/atmos/pkg/schema" - u "github.com/cloudposse/atmos/pkg/utils" ) // helmfileCmd represents the base command for all helmfile sub-commands @@ -16,42 +11,12 @@ var helmfileCmd = &cobra.Command{ Short: "Execute 'helmfile' commands", Long: `This command runs Helmfile commands`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true}, - Run: func(cmd *cobra.Command, args []string) { - - var argsAfterDoubleDash []string - var finalArgs = args - - doubleDashIndex := lo.IndexOf(args, "--") - if doubleDashIndex > 0 { - finalArgs = lo.Slice(args, 0, doubleDashIndex) - argsAfterDoubleDash = lo.Slice(args, doubleDashIndex+1, len(args)) - } - - info, err := e.ProcessCommandLineArgs("helmfile", cmd, finalArgs, argsAfterDoubleDash) - if err != nil { - u.LogErrorAndExit(schema.AtmosConfiguration{}, err) - } - // Exit on help - if info.NeedHelp || (info.SubCommand == "" && info.SubCommand2 == "") { - // Check for the latest Atmos release on GitHub and print update message - if err := cmd.Help(); err != nil { - u.LogErrorAndExit(schema.AtmosConfiguration{}, err) - } - return - } - // Check Atmos configuration - checkAtmosConfig() - - err = e.ExecuteHelmfile(info) - if err != nil { - u.LogErrorAndExit(schema.AtmosConfiguration{}, err) - } - }, } func init() { // https://github.com/spf13/cobra/issues/739 helmfileCmd.DisableFlagParsing = true helmfileCmd.PersistentFlags().StringP("stack", "s", "", "atmos helmfile -s ") + addUsageCommand(helmfileCmd, false) RootCmd.AddCommand(helmfileCmd) } diff --git a/cmd/helmfile_generate.go b/cmd/helmfile_generate.go index 39532c7ae..ed407d2ee 100644 --- a/cmd/helmfile_generate.go +++ b/cmd/helmfile_generate.go @@ -13,5 +13,6 @@ var helmfileGenerateCmd = &cobra.Command{ } func init() { + addUsageCommand(helmfileGenerateCmd, false) helmfileCmd.AddCommand(helmfileGenerateCmd) } diff --git a/cmd/helmfile_generate_varfile.go b/cmd/helmfile_generate_varfile.go index 0d04b6207..49bb44f98 100644 --- a/cmd/helmfile_generate_varfile.go +++ b/cmd/helmfile_generate_varfile.go @@ -15,6 +15,7 @@ var helmfileGenerateVarfileCmd = &cobra.Command{ Long: `This command generates a varfile for an atmos helmfile component: atmos helmfile generate varfile -s -f `, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/list.go b/cmd/list.go index 62862c8b2..193e66257 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -13,5 +13,6 @@ var listCmd = &cobra.Command{ } func init() { + addUsageCommand(listCmd, false) RootCmd.AddCommand(listCmd) } diff --git a/cmd/list_components.go b/cmd/list_components.go index cce734c28..375de0028 100644 --- a/cmd/list_components.go +++ b/cmd/list_components.go @@ -20,6 +20,10 @@ var listComponentsCmd = &cobra.Command{ Example: "atmos list components\n" + "atmos list components -s ", Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) + if hasPositionalArgs(args) { + showUsageAndExit(cmd, args, false) + } // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/list_stacks.go b/cmd/list_stacks.go index 1ce6c3f61..2f1bad409 100644 --- a/cmd/list_stacks.go +++ b/cmd/list_stacks.go @@ -21,6 +21,10 @@ var listStacksCmd = &cobra.Command{ "atmos list stacks -c ", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) + if hasPositionalArgs(args) { + showUsageAndExit(cmd, args, false) + } // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/pro.go b/cmd/pro.go index f619604d8..00161a74a 100644 --- a/cmd/pro.go +++ b/cmd/pro.go @@ -13,5 +13,6 @@ var proCmd = &cobra.Command{ } func init() { + addUsageCommand(proCmd, false) RootCmd.AddCommand(proCmd) } diff --git a/cmd/pro_lock.go b/cmd/pro_lock.go index fc37aaa90..8b9f9a982 100644 --- a/cmd/pro_lock.go +++ b/cmd/pro_lock.go @@ -15,6 +15,10 @@ var proLockCmd = &cobra.Command{ Long: `This command calls the atmos pro API and locks a stack`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) + if hasPositionalArgs(args) { + showUsageAndExit(cmd, args, false) + } // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/pro_unlock.go b/cmd/pro_unlock.go index b612306cd..dfa6344f7 100644 --- a/cmd/pro_unlock.go +++ b/cmd/pro_unlock.go @@ -15,6 +15,10 @@ var proUnlockCmd = &cobra.Command{ Long: `This command calls the atmos pro API and unlocks a stack`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) + if hasPositionalArgs(args) { + showUsageAndExit(cmd, args, false) + } // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/root.go b/cmd/root.go index 43b2397d0..b006c3d1e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -124,8 +124,15 @@ func initCobraConfig() { styles := boa.DefaultStyles() b := boa.New(boa.WithStyles(styles)) oldUsageFunc := RootCmd.UsageFunc() - RootCmd.SetUsageFunc(b.UsageFunc) - + RootCmd.SetUsageFunc(func(c *cobra.Command) error { + fmt.Println("Usage:", c.Use) + if c.Use == "atmos" { + b.UsageFunc(c) + return nil + } + oldUsageFunc(c) + return nil + }) RootCmd.SetHelpFunc(func(command *cobra.Command, strings []string) { // Print a styled Atmos logo to the terminal fmt.Println() diff --git a/cmd/terraform.go b/cmd/terraform.go index aaef9e6e5..9ed9e2399 100644 --- a/cmd/terraform.go +++ b/cmd/terraform.go @@ -1,16 +1,12 @@ package cmd import ( - "fmt" - "github.com/samber/lo" "github.com/spf13/cobra" e "github.com/cloudposse/atmos/internal/exec" - "github.com/cloudposse/atmos/internal/tui/templates" "github.com/cloudposse/atmos/pkg/schema" u "github.com/cloudposse/atmos/pkg/utils" - cc "github.com/ivanpirog/coloredcobra" ) // terraformCmd represents the base command for all terraform sub-commands @@ -20,9 +16,6 @@ var terraformCmd = &cobra.Command{ Short: "Execute Terraform commands", Long: `This command executes Terraform commands`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true}, - RunE: func(cmd *cobra.Command, args []string) error { - return terraformRun(cmd, cmd, args) - }, } // Contains checks if a slice of strings contains an exact match for the target string. @@ -36,6 +29,7 @@ func Contains(slice []string, target string) bool { } func terraformRun(cmd *cobra.Command, actualCmd *cobra.Command, args []string) error { + handleHelpRequest(cmd, args, false) var argsAfterDoubleDash []string var finalArgs = args @@ -46,52 +40,6 @@ func terraformRun(cmd *cobra.Command, actualCmd *cobra.Command, args []string) e } info, _ := e.ProcessCommandLineArgs("terraform", cmd, finalArgs, argsAfterDoubleDash) - // Exit on help - if info.NeedHelp || (info.SubCommand == "" && info.SubCommand2 == "") { - if info.SubCommand != "" && info.SubCommand != "--help" && info.SubCommand != "help" { - suggestions := cmd.SuggestionsFor(args[0]) - if !Contains(suggestions, args[0]) { - if len(suggestions) > 0 { - fmt.Printf("Unknown command: '%s'\n\nDid you mean this?\n", args[0]) - for _, suggestion := range suggestions { - fmt.Printf(" %s\n", suggestion) - } - } else { - fmt.Printf(`Error: Unknkown command %q for %q`+"\n", args[0], cmd.CommandPath()) - } - fmt.Printf(`Run '%s --help' for usage`+"\n", cmd.CommandPath()) - return fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath()) - } - } - // check if this is terraform --help command. TODO: check if this is the best way to do - if cmd == actualCmd { - template := templates.GenerateFromBaseTemplate(actualCmd.Use, []templates.HelpTemplateSections{ - templates.LongDescription, - templates.Usage, - templates.Aliases, - templates.Examples, - templates.AvailableCommands, - templates.Flags, - templates.GlobalFlags, - templates.NativeCommands, - templates.DoubleDashHelp, - templates.Footer, - }) - actualCmd.SetUsageTemplate(template) - cc.Init(&cc.Config{ - RootCmd: actualCmd, - Headings: cc.HiCyan + cc.Bold + cc.Underline, - Commands: cc.HiGreen + cc.Bold, - Example: cc.Italic, - ExecName: cc.Bold, - Flags: cc.Bold, - }) - - } - - actualCmd.Help() - return nil - } // Check Atmos configuration checkAtmosConfig() @@ -107,5 +55,6 @@ func init() { terraformCmd.DisableFlagParsing = true terraformCmd.PersistentFlags().StringP("stack", "s", "", "atmos terraform -s ") attachTerraformCommands(terraformCmd) + addUsageCommand(terraformCmd, true) RootCmd.AddCommand(terraformCmd) } diff --git a/cmd/terraform_generate.go b/cmd/terraform_generate.go index 1a635b229..b4792d526 100644 --- a/cmd/terraform_generate.go +++ b/cmd/terraform_generate.go @@ -18,11 +18,9 @@ This command supports the following subcommands: - 'varfiles' to generate varfiles for all Atmos components in all stacks.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, - RunE: func(cmd *cobra.Command, args []string) error { - return terraformRun(cmd, cmd, args) - }, } func init() { + addUsageCommand(terraformGenerateCmd, false) terraformCmd.AddCommand(terraformGenerateCmd) } diff --git a/cmd/terraform_generate_backend.go b/cmd/terraform_generate_backend.go index 823d32bd6..94cc1edfb 100644 --- a/cmd/terraform_generate_backend.go +++ b/cmd/terraform_generate_backend.go @@ -15,6 +15,7 @@ var terraformGenerateBackendCmd = &cobra.Command{ Long: `This command generates the backend config for a terraform component: atmos terraform generate backend -s `, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/terraform_generate_backends.go b/cmd/terraform_generate_backends.go index ee7aff947..7a8a10d88 100644 --- a/cmd/terraform_generate_backends.go +++ b/cmd/terraform_generate_backends.go @@ -15,6 +15,10 @@ var terraformGenerateBackendsCmd = &cobra.Command{ Long: `This command generates backend configs for all terraform components`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) + if hasPositionalArgs(args) { + showUsageAndExit(cmd, args, false) + } // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/terraform_generate_varfile.go b/cmd/terraform_generate_varfile.go index 7cf5f6b17..a25c349cf 100644 --- a/cmd/terraform_generate_varfile.go +++ b/cmd/terraform_generate_varfile.go @@ -15,6 +15,7 @@ var terraformGenerateVarfileCmd = &cobra.Command{ Long: `This command generates a varfile for an atmos terraform component: atmos terraform generate varfile -s -f `, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/terraform_generate_varfiles.go b/cmd/terraform_generate_varfiles.go index c41708c05..3c7528096 100644 --- a/cmd/terraform_generate_varfiles.go +++ b/cmd/terraform_generate_varfiles.go @@ -15,6 +15,10 @@ var terraformGenerateVarfilesCmd = &cobra.Command{ Long: `This command generates varfiles for all atmos terraform components in all stacks`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) + if hasPositionalArgs(args) { + showUsageAndExit(cmd, args, false) + } // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/validate.go b/cmd/validate.go index c5a3abda1..8e4b626f7 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -13,5 +13,6 @@ var validateCmd = &cobra.Command{ } func init() { + addUsageCommand(validateCmd, false) RootCmd.AddCommand(validateCmd) } diff --git a/cmd/validate_component.go b/cmd/validate_component.go index 8c68fb866..e1ff00477 100644 --- a/cmd/validate_component.go +++ b/cmd/validate_component.go @@ -21,6 +21,7 @@ var validateComponentCmd = &cobra.Command{ "atmos validate component -s --schema-path --schema-type opa --module-paths catalog", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/validate_stacks.go b/cmd/validate_stacks.go index ab8cb92d3..a5490665e 100644 --- a/cmd/validate_stacks.go +++ b/cmd/validate_stacks.go @@ -17,6 +17,10 @@ var ValidateStacksCmd = &cobra.Command{ Example: "validate stacks", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) + if hasPositionalArgs(args) { + showUsageAndExit(cmd, args, false) + } // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/vendor.go b/cmd/vendor.go index 096a17330..a3aa025e4 100644 --- a/cmd/vendor.go +++ b/cmd/vendor.go @@ -13,5 +13,6 @@ var vendorCmd = &cobra.Command{ } func init() { + addUsageCommand(vendorCmd, false) RootCmd.AddCommand(vendorCmd) } diff --git a/cmd/vendor_diff.go b/cmd/vendor_diff.go index a9bbf17de..c408bd08f 100644 --- a/cmd/vendor_diff.go +++ b/cmd/vendor_diff.go @@ -15,6 +15,9 @@ var vendorDiffCmd = &cobra.Command{ Long: `This command executes 'atmos vendor diff' CLI commands`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) + // TODO: There was no documentation here:https://atmos.tools/cli/commands/vendor we need to know what this command requires to check if we should add usage help + // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/vendor_pull.go b/cmd/vendor_pull.go index 20bab4490..a95f42b3d 100644 --- a/cmd/vendor_pull.go +++ b/cmd/vendor_pull.go @@ -15,6 +15,10 @@ var vendorPullCmd = &cobra.Command{ Long: `This command executes 'atmos vendor pull' CLI commands`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) + if hasPositionalArgs(args) { + showUsageAndExit(cmd, args, false) + } // WithStackValidation is a functional option that enables/disables stack configuration validation // based on whether the --stack flag is provided checkAtmosConfig(WithStackValidation(cmd.Flag("stack").Changed)) diff --git a/cmd/version.go b/cmd/version.go index f188fdc34..a83f8ca54 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -21,6 +21,9 @@ var versionCmd = &cobra.Command{ Long: `This command prints the CLI version`, Example: "atmos version", Run: func(cmd *cobra.Command, args []string) { + if len(args) > 0 { + showUsageAndExit(cmd, args, false) + } // Print a styled Atmos logo to the terminal fmt.Println() err := tuiUtils.PrintStyledText("ATMOS") diff --git a/cmd/workflow.go b/cmd/workflow.go index cc657a6c9..5d9359d24 100644 --- a/cmd/workflow.go +++ b/cmd/workflow.go @@ -22,6 +22,7 @@ var workflowCmd = &cobra.Command{ "For more details refer to https://atmos.tools/cli/commands/workflow/", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, false) err := e.ExecuteWorkflowCmd(cmd, args) if err != nil { u.LogErrorAndExit(schema.AtmosConfiguration{}, err) From ea2732fa6158b50ad35a13b66d01a6032ca6fe15 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Sun, 5 Jan 2025 21:16:20 +0100 Subject: [PATCH 17/50] Removed unwanted help check --- cmd/root.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 43b2397d0..0f28f6c3a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,12 +3,10 @@ package cmd import ( "errors" "fmt" - "os" "github.com/elewis787/boa" cc "github.com/ivanpirog/coloredcobra" "github.com/spf13/cobra" - "github.com/spf13/pflag" e "github.com/cloudposse/atmos/internal/exec" "github.com/cloudposse/atmos/internal/tui/templates" @@ -71,11 +69,6 @@ func Execute() error { Flags: cc.Bold, }) - // Check if the `help` flag is passed and print a styled Atmos logo to the terminal before printing the help - err := RootCmd.ParseFlags(os.Args) - if err != nil && !errors.Is(err, pflag.ErrHelp) { - u.LogErrorAndExit(atmosConfig, err) - } // InitCliConfig finds and merges CLI configurations in the following order: // system dir, home dir, current dir, ENV vars, command-line arguments // Here we need the custom commands from the config @@ -88,7 +81,7 @@ func Execute() error { u.LogErrorAndExit(schema.AtmosConfiguration{}, initErr) } } - + var err error // If CLI configuration was found, process its custom commands and command aliases if initErr == nil { err = processCustomCommands(atmosConfig, atmosConfig.Commands, RootCmd, true) From 5766d45c718c8552d6380b3ff5b1e24a67fd8b7c Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Sun, 5 Jan 2025 23:02:03 +0100 Subject: [PATCH 18/50] Fix atmos help --- cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index 0f28f6c3a..05af8afcd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -122,7 +122,7 @@ func initCobraConfig() { RootCmd.SetHelpFunc(func(command *cobra.Command, strings []string) { // Print a styled Atmos logo to the terminal fmt.Println() - if command.Use != "atmos" { + if command.Use != "atmos" || command.Flags().Changed("help") { err := tuiUtils.PrintStyledText("ATMOS") if err != nil { u.LogErrorAndExit(atmosConfig, err) From 18aaa77ee9b52a3092195e931d5ef1fc7725c064 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Sun, 5 Jan 2025 23:53:26 +0100 Subject: [PATCH 19/50] Help should be in stdout terminal --- cmd/root.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/root.go b/cmd/root.go index 05af8afcd..589de637f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,6 +3,7 @@ package cmd import ( "errors" "fmt" + "os" "github.com/elewis787/boa" cc "github.com/ivanpirog/coloredcobra" @@ -114,6 +115,7 @@ func init() { } func initCobraConfig() { + RootCmd.SetOut(os.Stdout) styles := boa.DefaultStyles() b := boa.New(boa.WithStyles(styles)) oldUsageFunc := RootCmd.UsageFunc() From b9209740dd9809dbd1f4bdab0902a6cb74d895dd Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Mon, 6 Jan 2025 22:02:20 +0100 Subject: [PATCH 20/50] fix terraform args --- cmd/root.go | 7 ++++--- internal/exec/terraform.go | 17 ----------------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index b006c3d1e..c67eadb2e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -22,9 +22,10 @@ var atmosConfig schema.AtmosConfiguration // RootCmd represents the base command when called without any subcommands var RootCmd = &cobra.Command{ - Use: "atmos", - Short: "Universal Tool for DevOps and Cloud Automation", - Long: `Atmos is a universal tool for DevOps and cloud automation used for provisioning, managing and orchestrating workflows across various toolchains`, + Use: "atmos", + Short: "Universal Tool for DevOps and Cloud Automation", + Long: `Atmos is a universal tool for DevOps and cloud automation used for provisioning, managing and orchestrating workflows across various toolchains`, + FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true}, PersistentPreRun: func(cmd *cobra.Command, args []string) { // Determine if the command is a help command or if the help flag is set isHelpCommand := cmd.Name() == "help" diff --git a/internal/exec/terraform.go b/internal/exec/terraform.go index fcf65bdb3..757252731 100644 --- a/internal/exec/terraform.go +++ b/internal/exec/terraform.go @@ -10,7 +10,6 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - tuiUtils "github.com/cloudposse/atmos/internal/tui/utils" cfg "github.com/cloudposse/atmos/pkg/config" "github.com/cloudposse/atmos/pkg/schema" u "github.com/cloudposse/atmos/pkg/utils" @@ -45,22 +44,6 @@ func ExecuteTerraform(info schema.ConfigAndStacksInfo) error { return nil } - // If the user just types `atmos terraform`, print Atmos logo and show terraform help - if info.SubCommand == "" { - fmt.Println() - err = tuiUtils.PrintStyledText("ATMOS") - if err != nil { - return err - } - - err = processHelp(atmosConfig, "terraform", "") - if err != nil { - return err - } - - fmt.Println() - return nil - } if info.SubCommand == "version" { return ExecuteShellCommand(atmosConfig, "terraform", From 7b56200f9e88512888434806f6a0f7a9d72e6a2a Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Mon, 6 Jan 2025 23:22:19 +0100 Subject: [PATCH 21/50] fixed terraform command --- cmd/terraform.go | 9 +++++++-- cmd/terraform_commands.go | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cmd/terraform.go b/cmd/terraform.go index 9ed9e2399..294b52a06 100644 --- a/cmd/terraform.go +++ b/cmd/terraform.go @@ -16,6 +16,13 @@ var terraformCmd = &cobra.Command{ Short: "Execute Terraform commands", Long: `This command executes Terraform commands`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true}, + Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args, true) + if hasPositionalArgs(args) { + addUsageCommand(cmd, true) + } + terraformRun(cmd, cmd, args) + }, } // Contains checks if a slice of strings contains an exact match for the target string. @@ -29,7 +36,6 @@ func Contains(slice []string, target string) bool { } func terraformRun(cmd *cobra.Command, actualCmd *cobra.Command, args []string) error { - handleHelpRequest(cmd, args, false) var argsAfterDoubleDash []string var finalArgs = args @@ -55,6 +61,5 @@ func init() { terraformCmd.DisableFlagParsing = true terraformCmd.PersistentFlags().StringP("stack", "s", "", "atmos terraform -s ") attachTerraformCommands(terraformCmd) - addUsageCommand(terraformCmd, true) RootCmd.AddCommand(terraformCmd) } diff --git a/cmd/terraform_commands.go b/cmd/terraform_commands.go index d84c84f4b..576a110aa 100644 --- a/cmd/terraform_commands.go +++ b/cmd/terraform_commands.go @@ -261,6 +261,7 @@ func attachTerraformCommands(parentCmd *cobra.Command) { if len(os.Args) > 3 { args = os.Args[2:] } + handleHelpRequest(cmd_, args, false) return terraformRun(parentCmd, cmd_, args) } parentCmd.AddCommand(cmd) From 33bd5933d84fd9f36c69ca4f5858a5858d104cf5 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Thu, 9 Jan 2025 21:26:24 +0100 Subject: [PATCH 22/50] fix helm command --- cmd/cmd_utils.go | 22 ++++++++++++++++++ cmd/docs.go | 1 - cmd/helmfile.go | 49 +++++++++++++++++++++++++++++++++++++++++ cmd/helmfile_apply.go | 21 ++++++++++++++++++ cmd/helmfile_destroy.go | 19 ++++++++++++++++ cmd/helmfile_diff.go | 21 ++++++++++++++++++ cmd/helmfile_sync.go | 19 ++++++++++++++++ cmd/terraform.go | 15 +------------ 8 files changed, 152 insertions(+), 15 deletions(-) create mode 100644 cmd/helmfile_apply.go create mode 100644 cmd/helmfile_destroy.go create mode 100644 cmd/helmfile_diff.go create mode 100644 cmd/helmfile_sync.go diff --git a/cmd/cmd_utils.go b/cmd/cmd_utils.go index 912656584..87e9610e0 100644 --- a/cmd/cmd_utils.go +++ b/cmd/cmd_utils.go @@ -11,6 +11,7 @@ import ( "github.com/fatih/color" cc "github.com/ivanpirog/coloredcobra" + "github.com/samber/lo" "github.com/spf13/cobra" e "github.com/cloudposse/atmos/internal/exec" @@ -643,3 +644,24 @@ func hasPositionalArgs(args []string) bool { } return false } + +// getConfigAndStacksInfo gets the +func getConfigAndStacksInfo(commandName string, cmd *cobra.Command, args []string) schema.ConfigAndStacksInfo { + // Check Atmos configuration + checkAtmosConfig() + + var argsAfterDoubleDash []string + var finalArgs = args + + doubleDashIndex := lo.IndexOf(args, "--") + if doubleDashIndex > 0 { + finalArgs = lo.Slice(args, 0, doubleDashIndex) + argsAfterDoubleDash = lo.Slice(args, doubleDashIndex+1, len(args)) + } + + info, err := e.ProcessCommandLineArgs(commandName, cmd, finalArgs, argsAfterDoubleDash) + if err != nil { + u.LogErrorAndExit(schema.AtmosConfiguration{}, err) + } + return info +} diff --git a/cmd/docs.go b/cmd/docs.go index 12c8c8589..0933881ff 100644 --- a/cmd/docs.go +++ b/cmd/docs.go @@ -135,6 +135,5 @@ var docsCmd = &cobra.Command{ } func init() { - // TODO: Check what this command does RootCmd.AddCommand(docsCmd) } diff --git a/cmd/helmfile.go b/cmd/helmfile.go index 5c720781a..3e11c64bc 100644 --- a/cmd/helmfile.go +++ b/cmd/helmfile.go @@ -1,9 +1,47 @@ package cmd import ( + e "github.com/cloudposse/atmos/internal/exec" + u "github.com/cloudposse/atmos/pkg/utils" "github.com/spf13/cobra" ) +// Command: atmos helmfile diff +var helmfileDiffShort = "Show differences between the desired and actual state of Helm releases." +var helmfileDiffLong = `This command calculates and displays the differences between the desired state of Helm releases +defined in your configurations and the actual state deployed in the cluster. + +Example usage: + atmos helmfile diff echo-server -s tenant1-ue2-dev + atmos helmfile diff echo-server -s tenant1-ue2-dev --redirect-stderr /dev/null` + +// Command: atmos helmfile apply +var helmfileApplyShort = "Apply changes to align the actual state of Helm releases with the desired state." +var helmfileApplyLong = `This command reconciles the actual state of Helm releases in the cluster with the desired state +defined in your configurations by applying the necessary changes. + +Example usage: + atmos helmfile apply echo-server -s tenant1-ue2-dev + atmos helmfile apply echo-server -s tenant1-ue2-dev --redirect-stderr /dev/stdout` + +// Command: atmos helmfile sync +var helmfileSyncShort = "Synchronize the state of Helm releases with the desired state without making changes." +var helmfileSyncLong = `This command ensures that the actual state of Helm releases in the cluster matches the desired +state defined in your configurations without performing destructive actions. + +Example usage: + atmos helmfile sync echo-server --stack tenant1-ue2-dev + atmos helmfile sync echo-server --stack tenant1-ue2-dev --redirect-stderr ./errors.txt` + +// Command: atmos helmfile destroy +var helmfileDestroyShort = "Destroy the Helm releases for the specified stack." +var helmfileDestroyLong = `This command removes the specified Helm releases from the cluster, ensuring a clean state for +the given stack. + +Example usage: + atmos helmfile destroy echo-server --stack=tenant1-ue2-dev + atmos helmfile destroy echo-server --stack=tenant1-ue2-dev --redirect-stderr /dev/stdout` + // helmfileCmd represents the base command for all helmfile sub-commands var helmfileCmd = &cobra.Command{ Use: "helmfile", @@ -20,3 +58,14 @@ func init() { addUsageCommand(helmfileCmd, false) RootCmd.AddCommand(helmfileCmd) } + +func helmfileRun(cmd *cobra.Command, commandName string, args []string) { + handleHelpRequest(cmd, args, false) + diffArgs := []string{commandName} + diffArgs = append(diffArgs, args...) + info := getConfigAndStacksInfo("helmfile", cmd, diffArgs) + err := e.ExecuteHelmfile(info) + if err != nil { + u.LogErrorAndExit(atmosConfig, err) + } +} diff --git a/cmd/helmfile_apply.go b/cmd/helmfile_apply.go new file mode 100644 index 000000000..8e5271d0c --- /dev/null +++ b/cmd/helmfile_apply.go @@ -0,0 +1,21 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// helmfileApplyCmd represents the base command for all helmfile sub-commands +var helmfileApplyCmd = &cobra.Command{ + Use: "apply", + Aliases: []string{}, + Short: helmfileApplyShort, + Long: helmfileApplyLong, + FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true}, + Run: func(cmd *cobra.Command, args []string) { + helmfileRun(cmd, "apply", args) + }, +} + +func init() { + helmfileCmd.AddCommand(helmfileApplyCmd) +} diff --git a/cmd/helmfile_destroy.go b/cmd/helmfile_destroy.go new file mode 100644 index 000000000..765a99e73 --- /dev/null +++ b/cmd/helmfile_destroy.go @@ -0,0 +1,19 @@ +package cmd + +import "github.com/spf13/cobra" + +// helmfileDestroyCmd represents the base command for all helmfile sub-commands +var helmfileDestroyCmd = &cobra.Command{ + Use: "destroy", + Aliases: []string{}, + Short: helmfileDestroyShort, + Long: helmfileDestroyLong, + FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true}, + Run: func(cmd *cobra.Command, args []string) { + helmfileRun(cmd, "destroy", args) + }, +} + +func init() { + helmfileCmd.AddCommand(helmfileDestroyCmd) +} diff --git a/cmd/helmfile_diff.go b/cmd/helmfile_diff.go new file mode 100644 index 000000000..16426c28c --- /dev/null +++ b/cmd/helmfile_diff.go @@ -0,0 +1,21 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// helmfileDiffCmd represents the base command for all helmfile sub-commands +var helmfileDiffCmd = &cobra.Command{ + Use: "diff", + Aliases: []string{}, + Short: helmfileDiffShort, + Long: helmfileDiffLong, + FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true}, + Run: func(cmd *cobra.Command, args []string) { + helmfileRun(cmd, "diff", args) + }, +} + +func init() { + helmfileCmd.AddCommand(helmfileDiffCmd) +} diff --git a/cmd/helmfile_sync.go b/cmd/helmfile_sync.go new file mode 100644 index 000000000..14f61ddd6 --- /dev/null +++ b/cmd/helmfile_sync.go @@ -0,0 +1,19 @@ +package cmd + +import "github.com/spf13/cobra" + +// helmfileSyncCmd represents the base command for all helmfile sub-commands +var helmfileSyncCmd = &cobra.Command{ + Use: "sync", + Aliases: []string{}, + Short: helmfileSyncShort, + Long: helmfileSyncLong, + FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true}, + Run: func(cmd *cobra.Command, args []string) { + helmfileRun(cmd, "sync", args) + }, +} + +func init() { + helmfileCmd.AddCommand(helmfileSyncCmd) +} diff --git a/cmd/terraform.go b/cmd/terraform.go index 004415cfe..c1a2fd5c7 100644 --- a/cmd/terraform.go +++ b/cmd/terraform.go @@ -1,7 +1,6 @@ package cmd import ( - "github.com/samber/lo" "github.com/spf13/cobra" e "github.com/cloudposse/atmos/internal/exec" @@ -29,19 +28,7 @@ func Contains(slice []string, target string) bool { } func terraformRun(cmd *cobra.Command, actualCmd *cobra.Command, args []string) error { - var argsAfterDoubleDash []string - var finalArgs = args - - doubleDashIndex := lo.IndexOf(args, "--") - if doubleDashIndex > 0 { - finalArgs = lo.Slice(args, 0, doubleDashIndex) - argsAfterDoubleDash = lo.Slice(args, doubleDashIndex+1, len(args)) - } - - info, _ := e.ProcessCommandLineArgs("terraform", cmd, finalArgs, argsAfterDoubleDash) - // Check Atmos configuration - checkAtmosConfig() - + info := getConfigAndStacksInfo("terraform", cmd, args) err := e.ExecuteTerraform(info) if err != nil { u.LogErrorAndExit(schema.AtmosConfiguration{}, err) From 969d988bdbc50223c0eb85665b53fa397e1a2afd Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Thu, 9 Jan 2025 23:30:27 +0100 Subject: [PATCH 23/50] rearrange the help strings --- cmd/helmfile.go | 36 ------------------------------------ cmd/helmfile_apply.go | 9 +++++++++ cmd/helmfile_destroy.go | 9 +++++++++ cmd/helmfile_diff.go | 9 +++++++++ cmd/helmfile_sync.go | 9 +++++++++ 5 files changed, 36 insertions(+), 36 deletions(-) diff --git a/cmd/helmfile.go b/cmd/helmfile.go index 5ece2ed61..58d5c44cc 100644 --- a/cmd/helmfile.go +++ b/cmd/helmfile.go @@ -6,42 +6,6 @@ import ( "github.com/spf13/cobra" ) -// Command: atmos helmfile diff -var helmfileDiffShort = "Show differences between the desired and actual state of Helm releases." -var helmfileDiffLong = `This command calculates and displays the differences between the desired state of Helm releases -defined in your configurations and the actual state deployed in the cluster. - -Example usage: - atmos helmfile diff echo-server -s tenant1-ue2-dev - atmos helmfile diff echo-server -s tenant1-ue2-dev --redirect-stderr /dev/null` - -// Command: atmos helmfile apply -var helmfileApplyShort = "Apply changes to align the actual state of Helm releases with the desired state." -var helmfileApplyLong = `This command reconciles the actual state of Helm releases in the cluster with the desired state -defined in your configurations by applying the necessary changes. - -Example usage: - atmos helmfile apply echo-server -s tenant1-ue2-dev - atmos helmfile apply echo-server -s tenant1-ue2-dev --redirect-stderr /dev/stdout` - -// Command: atmos helmfile sync -var helmfileSyncShort = "Synchronize the state of Helm releases with the desired state without making changes." -var helmfileSyncLong = `This command ensures that the actual state of Helm releases in the cluster matches the desired -state defined in your configurations without performing destructive actions. - -Example usage: - atmos helmfile sync echo-server --stack tenant1-ue2-dev - atmos helmfile sync echo-server --stack tenant1-ue2-dev --redirect-stderr ./errors.txt` - -// Command: atmos helmfile destroy -var helmfileDestroyShort = "Destroy the Helm releases for the specified stack." -var helmfileDestroyLong = `This command removes the specified Helm releases from the cluster, ensuring a clean state for -the given stack. - -Example usage: - atmos helmfile destroy echo-server --stack=tenant1-ue2-dev - atmos helmfile destroy echo-server --stack=tenant1-ue2-dev --redirect-stderr /dev/stdout` - // helmfileCmd represents the base command for all helmfile sub-commands var helmfileCmd = &cobra.Command{ Use: "helmfile", diff --git a/cmd/helmfile_apply.go b/cmd/helmfile_apply.go index 8e5271d0c..fad4c8b9d 100644 --- a/cmd/helmfile_apply.go +++ b/cmd/helmfile_apply.go @@ -4,6 +4,15 @@ import ( "github.com/spf13/cobra" ) +// Command: atmos helmfile apply +var helmfileApplyShort = "Apply changes to align the actual state of Helm releases with the desired state." +var helmfileApplyLong = `This command reconciles the actual state of Helm releases in the cluster with the desired state +defined in your configurations by applying the necessary changes. + +Example usage: + atmos helmfile apply echo-server -s tenant1-ue2-dev + atmos helmfile apply echo-server -s tenant1-ue2-dev --redirect-stderr /dev/stdout` + // helmfileApplyCmd represents the base command for all helmfile sub-commands var helmfileApplyCmd = &cobra.Command{ Use: "apply", diff --git a/cmd/helmfile_destroy.go b/cmd/helmfile_destroy.go index 765a99e73..8f92953bd 100644 --- a/cmd/helmfile_destroy.go +++ b/cmd/helmfile_destroy.go @@ -2,6 +2,15 @@ package cmd import "github.com/spf13/cobra" +// Command: atmos helmfile destroy +var helmfileDestroyShort = "Destroy the Helm releases for the specified stack." +var helmfileDestroyLong = `This command removes the specified Helm releases from the cluster, ensuring a clean state for +the given stack. + +Example usage: + atmos helmfile destroy echo-server --stack=tenant1-ue2-dev + atmos helmfile destroy echo-server --stack=tenant1-ue2-dev --redirect-stderr /dev/stdout` + // helmfileDestroyCmd represents the base command for all helmfile sub-commands var helmfileDestroyCmd = &cobra.Command{ Use: "destroy", diff --git a/cmd/helmfile_diff.go b/cmd/helmfile_diff.go index 16426c28c..13280ddd0 100644 --- a/cmd/helmfile_diff.go +++ b/cmd/helmfile_diff.go @@ -4,6 +4,15 @@ import ( "github.com/spf13/cobra" ) +// Command: atmos helmfile diff +var helmfileDiffShort = "Show differences between the desired and actual state of Helm releases." +var helmfileDiffLong = `This command calculates and displays the differences between the desired state of Helm releases +defined in your configurations and the actual state deployed in the cluster. + +Example usage: + atmos helmfile diff echo-server -s tenant1-ue2-dev + atmos helmfile diff echo-server -s tenant1-ue2-dev --redirect-stderr /dev/null` + // helmfileDiffCmd represents the base command for all helmfile sub-commands var helmfileDiffCmd = &cobra.Command{ Use: "diff", diff --git a/cmd/helmfile_sync.go b/cmd/helmfile_sync.go index 14f61ddd6..8a836af59 100644 --- a/cmd/helmfile_sync.go +++ b/cmd/helmfile_sync.go @@ -2,6 +2,15 @@ package cmd import "github.com/spf13/cobra" +// Command: atmos helmfile sync +var helmfileSyncShort = "Synchronize the state of Helm releases with the desired state without making changes." +var helmfileSyncLong = `This command ensures that the actual state of Helm releases in the cluster matches the desired +state defined in your configurations without performing destructive actions. + +Example usage: + atmos helmfile sync echo-server --stack tenant1-ue2-dev + atmos helmfile sync echo-server --stack tenant1-ue2-dev --redirect-stderr ./errors.txt` + // helmfileSyncCmd represents the base command for all helmfile sub-commands var helmfileSyncCmd = &cobra.Command{ Use: "sync", From e22e144ba80f850de514dbd23061a719abbf8f6b Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Thu, 9 Jan 2025 23:44:38 +0100 Subject: [PATCH 24/50] simplified the code --- cmd/root.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 62da163ca..db50c97ce 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -17,6 +17,7 @@ import ( u "github.com/cloudposse/atmos/pkg/utils" ) +// atmosConfig This is initialized before everything in the Execute function. So we can directly use this. var atmosConfig schema.AtmosConfiguration // RootCmd represents the base command when called without any subcommands @@ -126,11 +127,9 @@ func initCobraConfig() { oldUsageFunc := RootCmd.UsageFunc() RootCmd.SetUsageFunc(func(c *cobra.Command) error { if c.Use == "atmos" { - b.UsageFunc(c) - return nil + return b.UsageFunc(c) } - oldUsageFunc(c) - return nil + return oldUsageFunc(c) }) RootCmd.SetHelpFunc(func(command *cobra.Command, strings []string) { // Print a styled Atmos logo to the terminal From 9814e038b5c62046196201c3866ad0142d58ed05 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Sat, 11 Jan 2025 02:27:59 +0100 Subject: [PATCH 25/50] Updated version with the help and usage as per expectations --- cmd/version.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/version.go b/cmd/version.go index f29149d63..95b3c01e8 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -21,7 +21,8 @@ var versionCmd = &cobra.Command{ Long: `This command shows the version of the Atmos CLI you are currently running and checks if a newer version is available. Use this command to verify your installation and ensure you are up to date.`, Example: "atmos version", Run: func(cmd *cobra.Command, args []string) { - if len(args) > 0 { + handleHelpRequest(cmd, args, false) + if hasPositionalArgs(args) { showUsageAndExit(cmd, args, false) } // Print a styled Atmos logo to the terminal From d1dc47c2a2b63e56751e678c93298e5191107830 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Sun, 12 Jan 2025 14:23:26 +0100 Subject: [PATCH 26/50] fix atmos command usage --- cmd/cmd_utils.go | 2 +- cmd/root.go | 28 +++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/cmd/cmd_utils.go b/cmd/cmd_utils.go index 21c19f028..a5aa58239 100644 --- a/cmd/cmd_utils.go +++ b/cmd/cmd_utils.go @@ -589,7 +589,7 @@ func showUsageAndExit(cmd *cobra.Command, args []string, isNativeCommandsAvailab handleHelpRequest(cmd, args, isNativeCommandsAvailable) suggestions = cmd.SuggestionsFor(args[0]) subCommand = args[0] - unkonwnCommand = fmt.Sprintf(`Error: Unknkown command %q for %q`+"\n", subCommand, cmd.CommandPath()) + unkonwnCommand = fmt.Sprintf(`Error: Unkown command %q for %q`+"\n", subCommand, cmd.CommandPath()) } if len(suggestions) > 0 { u.PrintMessage(fmt.Sprintf("%s\n\nDid you mean this?", unkonwnCommand)) diff --git a/cmd/root.go b/cmd/root.go index db50c97ce..dc5e34144 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" "os" + "regexp" + "strings" "github.com/elewis787/boa" cc "github.com/ivanpirog/coloredcobra" @@ -98,7 +100,31 @@ func Execute() error { } } - return RootCmd.Execute() + // Cobra for some reason handles root command in such a way that custom usage and help command don't work as per expectations + RootCmd.SilenceErrors = true + err = RootCmd.Execute() + if err != nil { + if strings.Contains(err.Error(), "unknown command") { + command := getInvalidCommandName(err.Error()) + showUsageAndExit(RootCmd, []string{command}, false) + } + } + return err +} + +func getInvalidCommandName(input string) string { + // Regular expression to match the command name inside quotes + re := regexp.MustCompile(`unknown command "([^"]+)"`) + + // Find the match + match := re.FindStringSubmatch(input) + + // Check if a match is found + if len(match) > 1 { + command := match[1] // The first capturing group contains the command + return command + } + return "" } func init() { From c1153f13af0af630ea97281013342dc4ad5bff06 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Sun, 12 Jan 2025 14:38:44 +0100 Subject: [PATCH 27/50] Fix atmos usage error assertion --- tests/test-cases/core.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test-cases/core.yaml b/tests/test-cases/core.yaml index 7b022f333..c8f12fb9d 100644 --- a/tests/test-cases/core.yaml +++ b/tests/test-cases/core.yaml @@ -47,7 +47,7 @@ tests: - "non-existent" expect: stderr: - - 'unknown command "non-existent" for "atmos"' + - 'Error: Unkown command \"non-existent\" for \"atmos\"' exit_code: 1 - name: atmos terraform non-existent From e6f9ba8ec61bf68e9cdad2a3e2963502c687eea4 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Sun, 12 Jan 2025 17:19:53 +0100 Subject: [PATCH 28/50] Add SubCommand Aliases: --- cmd/cmd_utils.go | 14 +- cmd/colored/colored.go | 323 ++++++++++++++++++++++++ cmd/root.go | 14 +- go.mod | 3 +- go.sum | 5 - internal/tui/templates/base_template.go | 34 ++- internal/tui/templates/templater.go | 51 +++- 7 files changed, 413 insertions(+), 31 deletions(-) create mode 100644 cmd/colored/colored.go diff --git a/cmd/cmd_utils.go b/cmd/cmd_utils.go index a5aa58239..68e2231f6 100644 --- a/cmd/cmd_utils.go +++ b/cmd/cmd_utils.go @@ -9,10 +9,10 @@ import ( "strings" "time" - cc "github.com/ivanpirog/coloredcobra" "github.com/samber/lo" "github.com/spf13/cobra" + "github.com/cloudposse/atmos/cmd/colored" e "github.com/cloudposse/atmos/internal/exec" "github.com/cloudposse/atmos/internal/tui/templates" tuiUtils "github.com/cloudposse/atmos/internal/tui/utils" @@ -564,13 +564,13 @@ func handleHelpRequest(cmd *cobra.Command, args []string, isNativeCommandsAvaila templates.Footer, }) cmd.SetUsageTemplate(template) - cc.Init(&cc.Config{ + colored.Init(&colored.Config{ RootCmd: cmd, - Headings: cc.HiCyan + cc.Bold + cc.Underline, - Commands: cc.HiGreen + cc.Bold, - Example: cc.Italic, - ExecName: cc.Bold, - Flags: cc.Bold, + Headings: colored.HiCyan + colored.Bold + colored.Underline, + Commands: colored.HiGreen + colored.Bold, + Example: colored.Italic, + ExecName: colored.Bold, + Flags: colored.Bold, }) } cmd.Help() diff --git a/cmd/colored/colored.go b/cmd/colored/colored.go new file mode 100644 index 000000000..381ff7212 --- /dev/null +++ b/cmd/colored/colored.go @@ -0,0 +1,323 @@ +// ColoredCobra allows you to colorize Cobra's text output, +// making it look better using simple settings to customize +// individual parts of console output. +// +// Usage example: +// +// 1. Insert in cmd/root.go file of your project : +// +// import cc "github.com/ivanpirog/coloredcobra" +// +// 2. Put the following code to the beginning of the Execute() function: +// +// cc.Init(&cc.Config{ +// RootCmd: rootCmd, +// Headings: cc.Bold + cc.Underline, +// Commands: cc.Yellow + cc.Bold, +// ExecName: cc.Bold, +// Flags: cc.Bold, +// }) +// +// 3. Build & execute your code. +// +// Copyright © 2022 Ivan Pirog . +// Released under the MIT license. +// Project home: https://github.com/ivanpirog/coloredcobra +package colored + +import ( + "regexp" + "strings" + + "github.com/fatih/color" + "github.com/spf13/cobra" +) + +// Config is a settings structure which sets styles for individual parts of Cobra text output. +// +// Note that RootCmd is required. +// +// Example: +// +// c := &cc.Config{ +// RootCmd: rootCmd, +// Headings: cc.HiWhite + cc.Bold + cc.Underline, +// Commands: cc.Yellow + cc.Bold, +// CmdShortDescr: cc.Cyan, +// ExecName: cc.Bold, +// Flags: cc.Bold, +// Aliases: cc.Bold, +// Example: cc.Italic, +// } +type Config struct { + RootCmd *cobra.Command + Headings uint8 + Commands uint8 + CmdShortDescr uint8 + ExecName uint8 + Flags uint8 + FlagsDataType uint8 + FlagsDescr uint8 + Aliases uint8 + Example uint8 + NoExtraNewlines bool + NoBottomNewline bool +} + +// Constants for colors and B, I, U +const ( + None = 0 + Black = 1 + Red = 2 + Green = 3 + Yellow = 4 + Blue = 5 + Magenta = 6 + Cyan = 7 + White = 8 + HiRed = 9 + HiGreen = 10 + HiYellow = 11 + HiBlue = 12 + HiMagenta = 13 + HiCyan = 14 + HiWhite = 15 + Bold = 16 + Italic = 32 + Underline = 64 +) + +// Init patches Cobra's usage template with configuration provided. +func Init(cfg *Config) { + + if cfg.RootCmd == nil { + panic("coloredcobra: Root command pointer is missing.") + } + + // Get usage template + tpl := cfg.RootCmd.UsageTemplate() + + // + // Add extra line breaks for headings + // + if cfg.NoExtraNewlines == false { + tpl = strings.NewReplacer( + "Use \"", "\nUse \"", + ).Replace(tpl) + } + + // + // Styling headers + // + if cfg.Headings != None { + ch := getColor(cfg.Headings) + // Add template function to style the headers + cobra.AddTemplateFunc("HeadingStyle", ch.SprintFunc()) + } + + // + // Styling commands + // + if cfg.Commands != None { + cc := getColor(cfg.Commands) + + // Add template function to style commands + cobra.AddTemplateFunc("CommandStyle", cc.SprintFunc()) + cobra.AddTemplateFunc("sum", func(a, b int) int { + return a + b + }) + + // Patch usage template + re := regexp.MustCompile(`(?i){{\s*rpad\s+.Name\s+.NamePadding\s*}}`) + tpl = re.ReplaceAllLiteralString(tpl, "{{rpad (CommandStyle .Name) (sum .NamePadding 12)}}") + + re = regexp.MustCompile(`(?i){{\s*rpad\s+.CommandPath\s+.CommandPathPadding\s*}}`) + tpl = re.ReplaceAllLiteralString(tpl, "{{rpad (CommandStyle .CommandPath) (sum .CommandPathPadding 12)}}") + } + + // + // Styling a short desription of commands + // + if cfg.CmdShortDescr != None { + csd := getColor(cfg.CmdShortDescr) + + cobra.AddTemplateFunc("CmdShortStyle", csd.SprintFunc()) + + re := regexp.MustCompile(`(?ism)({{\s*range\s+.Commands\s*}}.*?){{\s*.Short\s*}}`) + tpl = re.ReplaceAllString(tpl, `$1{{CmdShortStyle .Short}}`) + } + + // + // Styling executable file name + // + if cfg.ExecName != None { + cen := getColor(cfg.ExecName) + + // Add template functions + cobra.AddTemplateFunc("ExecStyle", cen.SprintFunc()) + cobra.AddTemplateFunc("UseLineStyle", func(s string) string { + spl := strings.Split(s, " ") + spl[0] = cen.Sprint(spl[0]) + return strings.Join(spl, " ") + }) + + // Patch usage template + re := regexp.MustCompile(`(?i){{\s*.CommandPath\s*}}`) + tpl = re.ReplaceAllLiteralString(tpl, "{{ExecStyle .CommandPath}}") + + re = regexp.MustCompile(`(?i){{\s*.UseLine\s*}}`) + tpl = re.ReplaceAllLiteralString(tpl, "{{UseLineStyle .UseLine}}") + } + + // + // Styling flags + // + var cf, cfd, cfdt *color.Color + if cfg.Flags != None { + cf = getColor(cfg.Flags) + } + if cfg.FlagsDescr != None { + cfd = getColor(cfg.FlagsDescr) + } + if cfg.FlagsDataType != None { + cfdt = getColor(cfg.FlagsDataType) + } + if cf != nil || cfd != nil || cfdt != nil { + + cobra.AddTemplateFunc("FlagStyle", func(s string) string { + + // Flags info section is multi-line. + // Let's split these lines and iterate them. + lines := strings.Split(s, "\n") + for k := range lines { + + // Styling short and full flags (-f, --flag) + if cf != nil { + re := regexp.MustCompile(`(--?\S+)`) + for _, flag := range re.FindAllString(lines[k], 2) { + lines[k] = strings.Replace(lines[k], flag, cf.Sprint(flag), 1) + } + } + + // If no styles for flag data types and description - continue + if cfd == nil && cfdt == nil { + continue + } + + // Split line into two parts: flag data type and description + // Tip: Use debugger to understand the logic + re := regexp.MustCompile(`\s{2,}`) + spl := re.Split(lines[k], -1) + if len(spl) != 3 { + continue + } + + // Styling the flag description + if cfd != nil { + lines[k] = strings.Replace(lines[k], spl[2], cfd.Sprint(spl[2]), 1) + } + + // Styling flag data type + // Tip: Use debugger to understand the logic + if cfdt != nil { + re = regexp.MustCompile(`\s+(\w+)$`) // the last word after spaces is the flag data type + m := re.FindAllStringSubmatch(spl[1], -1) + if len(m) == 1 && len(m[0]) == 2 { + lines[k] = strings.Replace(lines[k], m[0][1], cfdt.Sprint(m[0][1]), 1) + } + } + + } + s = strings.Join(lines, "\n") + + return s + + }) + + // Patch usage template + re := regexp.MustCompile(`(?i)(\.(InheritedFlags|LocalFlags)\.FlagUsages)`) + tpl = re.ReplaceAllString(tpl, "FlagStyle $1") + } + + // + // Styling aliases + // + if cfg.Aliases != None { + ca := getColor(cfg.Aliases) + cobra.AddTemplateFunc("AliasStyle", ca.SprintFunc()) + + re := regexp.MustCompile(`(?i){{\s*.NameAndAliases\s*}}`) + tpl = re.ReplaceAllLiteralString(tpl, "{{AliasStyle .NameAndAliases}}") + } + + // + // Styling the example text + // + if cfg.Example != None { + ce := getColor(cfg.Example) + cobra.AddTemplateFunc("ExampleStyle", ce.SprintFunc()) + + re := regexp.MustCompile(`(?i){{\s*.Example\s*}}`) + tpl = re.ReplaceAllLiteralString(tpl, "{{ExampleStyle .Example}}") + } + + // Adding a new line to the end + if !cfg.NoBottomNewline { + tpl += "\n" + } + // Apply patched template + cfg.RootCmd.SetUsageTemplate(tpl) + // Debug line, uncomment when needed + // fmt.Println(tpl) +} + +// getColor decodes color param and returns color.Color object +func getColor(param uint8) (c *color.Color) { + + switch param & 15 { + case None: + c = color.New(color.FgWhite) + case Black: + c = color.New(color.FgBlack) + case Red: + c = color.New(color.FgRed) + case Green: + c = color.New(color.FgGreen) + case Yellow: + c = color.New(color.FgYellow) + case Blue: + c = color.New(color.FgBlue) + case Magenta: + c = color.New(color.FgMagenta) + case Cyan: + c = color.New(color.FgCyan) + case White: + c = color.New(color.FgWhite) + case HiRed: + c = color.New(color.FgHiRed) + case HiGreen: + c = color.New(color.FgHiGreen) + case HiYellow: + c = color.New(color.FgHiYellow) + case HiBlue: + c = color.New(color.FgHiBlue) + case HiMagenta: + c = color.New(color.FgHiMagenta) + case HiCyan: + c = color.New(color.FgHiCyan) + case HiWhite: + c = color.New(color.FgHiWhite) + } + + if param&Bold == Bold { + c.Add(color.Bold) + } + if param&Italic == Italic { + c.Add(color.Italic) + } + if param&Underline == Underline { + c.Add(color.Underline) + } + + return +} diff --git a/cmd/root.go b/cmd/root.go index dc5e34144..60bf4300a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,9 +8,9 @@ import ( "strings" "github.com/elewis787/boa" - cc "github.com/ivanpirog/coloredcobra" "github.com/spf13/cobra" + "github.com/cloudposse/atmos/cmd/colored" e "github.com/cloudposse/atmos/internal/exec" "github.com/cloudposse/atmos/internal/tui/templates" tuiUtils "github.com/cloudposse/atmos/internal/tui/utils" @@ -65,13 +65,13 @@ var RootCmd = &cobra.Command{ // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the RootCmd. func Execute() error { - cc.Init(&cc.Config{ + colored.Init(&colored.Config{ RootCmd: RootCmd, - Headings: cc.HiCyan + cc.Bold + cc.Underline, - Commands: cc.HiGreen + cc.Bold, - Example: cc.Italic, - ExecName: cc.Bold, - Flags: cc.Bold, + Headings: colored.HiCyan + colored.Bold + colored.Underline, + Commands: colored.HiGreen + colored.Bold, + Example: colored.Italic, + ExecName: colored.Bold, + Flags: colored.Bold, }) // InitCliConfig finds and merges CLI configurations in the following order: diff --git a/go.mod b/go.mod index 2a6d5429e..35fa294f9 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,6 @@ require ( github.com/hashicorp/hcl/v2 v2.23.0 github.com/hashicorp/terraform-config-inspect v0.0.0-20241129133400-c404f8227ea6 github.com/hashicorp/terraform-exec v0.21.0 - github.com/ivanpirog/coloredcobra v1.0.1 github.com/json-iterator/go v1.1.12 github.com/jwalton/go-supportscolor v1.2.0 github.com/kubescape/go-git-url v0.0.30 @@ -51,6 +50,7 @@ require ( github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 github.com/zclconf/go-cty v1.16.0 + golang.org/x/oauth2 v0.24.0 golang.org/x/term v0.28.0 gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 gopkg.in/yaml.v2 v2.4.0 @@ -264,7 +264,6 @@ require ( golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.34.0 // indirect - golang.org/x/oauth2 v0.24.0 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect diff --git a/go.sum b/go.sum index b75178c2d..c3ddce922 100644 --- a/go.sum +++ b/go.sum @@ -911,7 +911,6 @@ github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNA github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= @@ -1349,11 +1348,8 @@ github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/ivanpirog/coloredcobra v1.0.1 h1:aURSdEmlR90/tSiWS0dMjdwOvCVUeYLfltLfbgNxrN4= -github.com/ivanpirog/coloredcobra v1.0.1/go.mod h1:iho4nEKcnwZFiniGSdcgdvRgZNjxm+h20acv8vqmN6Q= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= @@ -1698,7 +1694,6 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= diff --git a/internal/tui/templates/base_template.go b/internal/tui/templates/base_template.go index 5e3ce2269..74febc3ab 100644 --- a/internal/tui/templates/base_template.go +++ b/internal/tui/templates/base_template.go @@ -8,6 +8,7 @@ const ( LongDescription HelpTemplateSections = iota Usage Aliases + SubCommandAliases Examples AvailableCommands Flags @@ -34,32 +35,49 @@ func getSection(commandName string, section HelpTemplateSections) string { case AdditionalHelpTopics: return `{{if .HasHelpSubCommands}} -Additional help topics: + +{{HeadingStyle "Additional help topics:"}} + {{formatCommands .Commands "additionalHelpTopics"}}{{end}}` case Aliases: return `{{if gt (len .Aliases) 0}} -Aliases: +{{HeadingStyle "Aliases:"}} + {{.NameAndAliases}}{{end}}` + case SubCommandAliases: + return `{{if (isAliasesPresent .Commands)}} + +{{HeadingStyle "SubCommand Aliases:"}} + +{{formatCommands .Commands "subcommandAliases"}}{{end}}` case AvailableCommands: return `{{if .HasAvailableSubCommands}} -Available Commands: + +{{HeadingStyle "Available Commands:"}} + {{formatCommands .Commands "availableCommands"}}{{end}}` case Examples: return `{{if .HasExample}} -Examples: + +{{HeadingStyle "Examples:"}} + {{.Example}}{{end}}` case Flags: return `{{if .HasAvailableLocalFlags}} -Flags: + +{{HeadingStyle "Flags:"}} + {{wrappedFlagUsages .LocalFlags | trimTrailingWhitespaces}}{{end}}` case GlobalFlags: return `{{if .HasAvailableInheritedFlags}} -Global Flags: + +{{HeadingStyle "Global Flags:"}} + {{wrappedFlagUsages .InheritedFlags | trimTrailingWhitespaces}}{{end}}` case NativeCommands: return fmt.Sprintf(` @@ -69,7 +87,9 @@ Global Flags: {{formatCommands .Commands "native"}} `, commandName) case Usage: - return `Usage:{{if .Runnable}} + return ` +{{HeadingStyle "Usage:"}} +{{if .Runnable}} {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} {{.CommandPath}} [command]{{end}}` case DoubleDashHelp: diff --git a/internal/tui/templates/templater.go b/internal/tui/templates/templater.go index c4539a6ea..6f38935aa 100644 --- a/internal/tui/templates/templater.go +++ b/internal/tui/templates/templater.go @@ -48,12 +48,45 @@ var customHelpShortMessage = map[string]string{ "tf": "Alias for `terraform` commands", } +// filterCommands returns only commands or aliases based on returnOnlyAliases boolean +func filterCommands(commands []*cobra.Command, returnOnlyAliases bool) []*cobra.Command { + filtered := []*cobra.Command{} + for _, cmd := range commands { + isAlias := false + for _, parentCmd := range commands { + if cmd != parentCmd { + for _, alias := range parentCmd.Aliases { + if cmd.Name() == alias { + isAlias = true + break + } + } + } + if isAlias { + break + } + } + // Add to the filtered list based on the includeAliases flag + if returnOnlyAliases && isAlias { + filtered = append(filtered, cmd) // Include only aliases + } else if !returnOnlyAliases && !isAlias { + filtered = append(filtered, cmd) // Include only primary commands + } + } + return filtered +} + +func isAliasesPresent(cmds []*cobra.Command) bool { + return len(filterCommands(cmds, true)) > 0 +} + // formatCommands formats a slice of cobra commands with proper styling func formatCommands(cmds []*cobra.Command, listType string) string { var maxLen int availableCmds := make([]*cobra.Command, 0) // First pass: collect available commands and find max length + cmds = filterCommands(cmds, listType == "subcommandAliases") for _, cmd := range cmds { switch listType { case "additionalHelpTopics": @@ -72,6 +105,19 @@ func formatCommands(cmds []*cobra.Command, listType string) string { } continue } + case "subcommandAliases": + if v, ok := customHelpShortMessage[cmd.Name()]; ok { + cmd.Short = v + } + if cmd.Annotations["nativeCommand"] == "true" { + continue + } + if cmd.IsAvailableCommand() || cmd.Name() == "help" { + availableCmds = append(availableCmds, cmd) + if len(cmd.Name()) > maxLen { + maxLen = len(cmd.Name()) + } + } default: if cmd.Annotations["nativeCommand"] == "true" { continue @@ -83,9 +129,6 @@ func formatCommands(cmds []*cobra.Command, listType string) string { } } } - if v, ok := customHelpShortMessage[cmd.Name()]; ok { - cmd.Short = v - } } var lines []string @@ -124,6 +167,7 @@ func SetCustomUsageFunc(cmd *cobra.Command) error { Aliases, Examples, AvailableCommands, + SubCommandAliases, Flags, GlobalFlags, AdditionalHelpTopics, @@ -133,6 +177,7 @@ func SetCustomUsageFunc(cmd *cobra.Command) error { } cmd.SetUsageTemplate(t.UsageTemplate) + cobra.AddTemplateFunc("isAliasesPresent", isAliasesPresent) cobra.AddTemplateFunc("formatCommands", formatCommands) return nil } From 35c8182780ce5b23976a5656192767b5c5c0c3ed Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Mon, 13 Jan 2025 07:09:47 +0100 Subject: [PATCH 29/50] Updated template to be more dynamic --- cmd/atlantis_generate_repo_config.go | 4 +-- cmd/aws_eks_update_kubeconfig.go | 2 +- cmd/cmd_utils.go | 33 +++---------------------- cmd/describe_affected.go | 4 +-- cmd/describe_component.go | 2 +- cmd/describe_config.go | 4 +-- cmd/describe_dependents.go | 2 +- cmd/describe_stacks.go | 4 +-- cmd/describe_workflows.go | 4 +-- cmd/docs.go | 2 +- cmd/helmfile.go | 2 +- cmd/helmfile_generate_varfile.go | 2 +- cmd/list_components.go | 4 +-- cmd/list_stacks.go | 4 +-- cmd/pro_lock.go | 4 +-- cmd/pro_unlock.go | 4 +-- cmd/root.go | 2 +- cmd/terraform_commands.go | 2 +- cmd/terraform_generate_backend.go | 2 +- cmd/terraform_generate_backends.go | 4 +-- cmd/terraform_generate_varfile.go | 2 +- cmd/terraform_generate_varfiles.go | 4 +-- cmd/validate_component.go | 2 +- cmd/validate_stacks.go | 4 +-- cmd/vendor_diff.go | 2 +- cmd/vendor_pull.go | 4 +-- cmd/version.go | 4 +-- cmd/workflow.go | 2 +- internal/tui/templates/base_template.go | 21 +++++++--------- internal/tui/templates/templater.go | 13 +++++++++- 30 files changed, 66 insertions(+), 83 deletions(-) diff --git a/cmd/atlantis_generate_repo_config.go b/cmd/atlantis_generate_repo_config.go index 0dcf7c714..0c340aef9 100644 --- a/cmd/atlantis_generate_repo_config.go +++ b/cmd/atlantis_generate_repo_config.go @@ -15,9 +15,9 @@ var atlantisGenerateRepoConfigCmd = &cobra.Command{ Long: "Generate the repository configuration file required for Atlantis to manage Terraform repositories.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) if hasPositionalArgs(args) { - showUsageAndExit(cmd, args, false) + showUsageAndExit(cmd, args) } // Check Atmos configuration diff --git a/cmd/aws_eks_update_kubeconfig.go b/cmd/aws_eks_update_kubeconfig.go index 431d05b5a..3016938c9 100644 --- a/cmd/aws_eks_update_kubeconfig.go +++ b/cmd/aws_eks_update_kubeconfig.go @@ -34,7 +34,7 @@ See https://docs.aws.amazon.com/cli/latest/reference/eks/update-kubeconfig.html FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) err := e.ExecuteAwsEksUpdateKubeconfigCommand(cmd, args) if err != nil { u.LogErrorAndExit(schema.AtmosConfiguration{}, err) diff --git a/cmd/cmd_utils.go b/cmd/cmd_utils.go index 68e2231f6..d28beb4c6 100644 --- a/cmd/cmd_utils.go +++ b/cmd/cmd_utils.go @@ -12,9 +12,7 @@ import ( "github.com/samber/lo" "github.com/spf13/cobra" - "github.com/cloudposse/atmos/cmd/colored" e "github.com/cloudposse/atmos/internal/exec" - "github.com/cloudposse/atmos/internal/tui/templates" tuiUtils "github.com/cloudposse/atmos/internal/tui/utils" cfg "github.com/cloudposse/atmos/pkg/config" "github.com/cloudposse/atmos/pkg/schema" @@ -548,37 +546,14 @@ func isVersionCommand() bool { } // handleHelpRequest shows help content and exits only if the first argument is "help" or "--help" or "-h" -func handleHelpRequest(cmd *cobra.Command, args []string, isNativeCommandsAvailable bool) { +func handleHelpRequest(cmd *cobra.Command, args []string) { if (len(args) > 0 && args[0] == "help") || Contains(args, "--help") || Contains(args, "-h") { - if isNativeCommandsAvailable { - template := templates.GenerateFromBaseTemplate(cmd.Use, []templates.HelpTemplateSections{ - templates.LongDescription, - templates.Usage, - templates.Aliases, - templates.Examples, - templates.AvailableCommands, - templates.Flags, - templates.GlobalFlags, - templates.NativeCommands, - templates.DoubleDashHelp, - templates.Footer, - }) - cmd.SetUsageTemplate(template) - colored.Init(&colored.Config{ - RootCmd: cmd, - Headings: colored.HiCyan + colored.Bold + colored.Underline, - Commands: colored.HiGreen + colored.Bold, - Example: colored.Italic, - ExecName: colored.Bold, - Flags: colored.Bold, - }) - } cmd.Help() os.Exit(0) } } -func showUsageAndExit(cmd *cobra.Command, args []string, isNativeCommandsAvailable bool) { +func showUsageAndExit(cmd *cobra.Command, args []string) { var suggestions []string var subCommand string = "" @@ -586,7 +561,7 @@ func showUsageAndExit(cmd *cobra.Command, args []string, isNativeCommandsAvailab if len(args) > 0 { // Show help if the first argument is "help" - handleHelpRequest(cmd, args, isNativeCommandsAvailable) + handleHelpRequest(cmd, args) suggestions = cmd.SuggestionsFor(args[0]) subCommand = args[0] unkonwnCommand = fmt.Sprintf(`Error: Unkown command %q for %q`+"\n", subCommand, cmd.CommandPath()) @@ -618,7 +593,7 @@ func showUsageAndExit(cmd *cobra.Command, args []string, isNativeCommandsAvailab func addUsageCommand(cmd *cobra.Command, isNativeCommandsAvailable bool) { cmd.Run = func(cmd *cobra.Command, args []string) { - showUsageAndExit(cmd, args, isNativeCommandsAvailable) + showUsageAndExit(cmd, args) } } diff --git a/cmd/describe_affected.go b/cmd/describe_affected.go index 6f55906f9..c02301ba5 100644 --- a/cmd/describe_affected.go +++ b/cmd/describe_affected.go @@ -15,9 +15,9 @@ var describeAffectedCmd = &cobra.Command{ Long: "Identify and list Atmos components and stacks impacted by changes between two Git commits.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) if hasPositionalArgs(args) { - showUsageAndExit(cmd, args, false) + showUsageAndExit(cmd, args) } // Check Atmos configuration diff --git a/cmd/describe_component.go b/cmd/describe_component.go index 61eeb7b51..e014e809a 100644 --- a/cmd/describe_component.go +++ b/cmd/describe_component.go @@ -15,7 +15,7 @@ var describeComponentCmd = &cobra.Command{ Long: `Display the configuration details for a specific Atmos component within a designated Atmos stack, including its dependencies, settings, and overrides.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/describe_config.go b/cmd/describe_config.go index ea16436bc..8138c226d 100644 --- a/cmd/describe_config.go +++ b/cmd/describe_config.go @@ -15,9 +15,9 @@ var describeConfigCmd = &cobra.Command{ Long: "This command displays the final, deep-merged CLI configuration after combining all relevant configuration files.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) if hasPositionalArgs(args) { - showUsageAndExit(cmd, args, false) + showUsageAndExit(cmd, args) } err := e.ExecuteDescribeConfigCmd(cmd, args) diff --git a/cmd/describe_dependents.go b/cmd/describe_dependents.go index 3e3c953d7..199883705 100644 --- a/cmd/describe_dependents.go +++ b/cmd/describe_dependents.go @@ -16,7 +16,7 @@ var describeDependentsCmd = &cobra.Command{ Long: "This command generates a list of Atmos components within stacks that depend on the specified Atmos component.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/describe_stacks.go b/cmd/describe_stacks.go index 287a5ea8b..5ca50d4a6 100644 --- a/cmd/describe_stacks.go +++ b/cmd/describe_stacks.go @@ -15,9 +15,9 @@ var describeStacksCmd = &cobra.Command{ Long: "This command shows the configuration details for Atmos stacks and the components within those stacks.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) if hasPositionalArgs(args) { - showUsageAndExit(cmd, args, false) + showUsageAndExit(cmd, args) } // Check Atmos configuration diff --git a/cmd/describe_workflows.go b/cmd/describe_workflows.go index 183df07b9..0d610d220 100644 --- a/cmd/describe_workflows.go +++ b/cmd/describe_workflows.go @@ -22,9 +22,9 @@ var describeWorkflowsCmd = &cobra.Command{ "describe workflows -o all", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) if hasPositionalArgs(args) { - showUsageAndExit(cmd, args, false) + showUsageAndExit(cmd, args) } err := e.ExecuteDescribeWorkflowsCmd(cmd, args) diff --git a/cmd/docs.go b/cmd/docs.go index 64e7080eb..4c254fe11 100644 --- a/cmd/docs.go +++ b/cmd/docs.go @@ -28,7 +28,7 @@ var docsCmd = &cobra.Command{ Args: cobra.MaximumNArgs(1), FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) if len(args) == 1 { info := schema.ConfigAndStacksInfo{ Component: args[0], diff --git a/cmd/helmfile.go b/cmd/helmfile.go index 58d5c44cc..39a62fbe0 100644 --- a/cmd/helmfile.go +++ b/cmd/helmfile.go @@ -24,7 +24,7 @@ func init() { } func helmfileRun(cmd *cobra.Command, commandName string, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) diffArgs := []string{commandName} diffArgs = append(diffArgs, args...) info := getConfigAndStacksInfo("helmfile", cmd, diffArgs) diff --git a/cmd/helmfile_generate_varfile.go b/cmd/helmfile_generate_varfile.go index 30f2ffb80..acaac887e 100644 --- a/cmd/helmfile_generate_varfile.go +++ b/cmd/helmfile_generate_varfile.go @@ -15,7 +15,7 @@ var helmfileGenerateVarfileCmd = &cobra.Command{ Long: "This command generates a values file for a specified Helmfile component.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/list_components.go b/cmd/list_components.go index 18b97ccb7..3dc44621a 100644 --- a/cmd/list_components.go +++ b/cmd/list_components.go @@ -22,9 +22,9 @@ var listComponentsCmd = &cobra.Command{ Example: "atmos list components\n" + "atmos list components -s ", Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) if hasPositionalArgs(args) { - showUsageAndExit(cmd, args, false) + showUsageAndExit(cmd, args) } // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/list_stacks.go b/cmd/list_stacks.go index 73e2be46e..5e11f4242 100644 --- a/cmd/list_stacks.go +++ b/cmd/list_stacks.go @@ -22,9 +22,9 @@ var listStacksCmd = &cobra.Command{ "atmos list stacks -c ", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) if hasPositionalArgs(args) { - showUsageAndExit(cmd, args, false) + showUsageAndExit(cmd, args) } // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/pro_lock.go b/cmd/pro_lock.go index 8b9f9a982..a6998a53d 100644 --- a/cmd/pro_lock.go +++ b/cmd/pro_lock.go @@ -15,9 +15,9 @@ var proLockCmd = &cobra.Command{ Long: `This command calls the atmos pro API and locks a stack`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) if hasPositionalArgs(args) { - showUsageAndExit(cmd, args, false) + showUsageAndExit(cmd, args) } // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/pro_unlock.go b/cmd/pro_unlock.go index dfa6344f7..f97d46cb6 100644 --- a/cmd/pro_unlock.go +++ b/cmd/pro_unlock.go @@ -15,9 +15,9 @@ var proUnlockCmd = &cobra.Command{ Long: `This command calls the atmos pro API and unlocks a stack`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) if hasPositionalArgs(args) { - showUsageAndExit(cmd, args, false) + showUsageAndExit(cmd, args) } // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/root.go b/cmd/root.go index 60bf4300a..b9eb24fee 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -106,7 +106,7 @@ func Execute() error { if err != nil { if strings.Contains(err.Error(), "unknown command") { command := getInvalidCommandName(err.Error()) - showUsageAndExit(RootCmd, []string{command}, false) + showUsageAndExit(RootCmd, []string{command}) } } return err diff --git a/cmd/terraform_commands.go b/cmd/terraform_commands.go index 592e831ed..8c8cff50b 100644 --- a/cmd/terraform_commands.go +++ b/cmd/terraform_commands.go @@ -261,7 +261,7 @@ func attachTerraformCommands(parentCmd *cobra.Command) { if len(os.Args) > 3 { args = os.Args[2:] } - handleHelpRequest(cmd_, args, false) + handleHelpRequest(cmd_, args) terraformRun(parentCmd, cmd_, args) } parentCmd.AddCommand(cmd) diff --git a/cmd/terraform_generate_backend.go b/cmd/terraform_generate_backend.go index 3cd7527dd..d65f9ddd4 100644 --- a/cmd/terraform_generate_backend.go +++ b/cmd/terraform_generate_backend.go @@ -16,7 +16,7 @@ var terraformGenerateBackendCmd = &cobra.Command{ Example: `atmos terraform generate backend -s `, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/terraform_generate_backends.go b/cmd/terraform_generate_backends.go index e1599d425..8337b4fa1 100644 --- a/cmd/terraform_generate_backends.go +++ b/cmd/terraform_generate_backends.go @@ -15,9 +15,9 @@ var terraformGenerateBackendsCmd = &cobra.Command{ Long: "This command generates the backend configuration files for all Terraform components in the Atmos environment.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) if hasPositionalArgs(args) { - showUsageAndExit(cmd, args, false) + showUsageAndExit(cmd, args) } // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/terraform_generate_varfile.go b/cmd/terraform_generate_varfile.go index 94f672ab3..6d1fa2327 100644 --- a/cmd/terraform_generate_varfile.go +++ b/cmd/terraform_generate_varfile.go @@ -15,7 +15,7 @@ var terraformGenerateVarfileCmd = &cobra.Command{ Long: "This command generates a `varfile` for a specified Atmos Terraform component.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/terraform_generate_varfiles.go b/cmd/terraform_generate_varfiles.go index ca48d2781..01e369bb4 100644 --- a/cmd/terraform_generate_varfiles.go +++ b/cmd/terraform_generate_varfiles.go @@ -15,9 +15,9 @@ var terraformGenerateVarfilesCmd = &cobra.Command{ Long: "This command generates varfiles for all Atmos Terraform components across all stacks.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) if hasPositionalArgs(args) { - showUsageAndExit(cmd, args, false) + showUsageAndExit(cmd, args) } // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/validate_component.go b/cmd/validate_component.go index 46428c7f6..ece24430d 100644 --- a/cmd/validate_component.go +++ b/cmd/validate_component.go @@ -21,7 +21,7 @@ var validateComponentCmd = &cobra.Command{ "atmos validate component -s --schema-path --schema-type opa --module-paths catalog", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/validate_stacks.go b/cmd/validate_stacks.go index 0a242ba5d..23995b06d 100644 --- a/cmd/validate_stacks.go +++ b/cmd/validate_stacks.go @@ -17,9 +17,9 @@ var ValidateStacksCmd = &cobra.Command{ Example: "validate stacks", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) if hasPositionalArgs(args) { - showUsageAndExit(cmd, args, false) + showUsageAndExit(cmd, args) } // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/vendor_diff.go b/cmd/vendor_diff.go index 48b4f1ab9..f208c98d8 100644 --- a/cmd/vendor_diff.go +++ b/cmd/vendor_diff.go @@ -15,7 +15,7 @@ var vendorDiffCmd = &cobra.Command{ Long: "This command compares and displays the differences in vendor-specific configurations or dependencies.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) // TODO: There was no documentation here:https://atmos.tools/cli/commands/vendor we need to know what this command requires to check if we should add usage help // Check Atmos configuration diff --git a/cmd/vendor_pull.go b/cmd/vendor_pull.go index a6b024178..20a73a5bd 100644 --- a/cmd/vendor_pull.go +++ b/cmd/vendor_pull.go @@ -15,9 +15,9 @@ var vendorPullCmd = &cobra.Command{ Long: "Pull and update vendor-specific configurations or dependencies to ensure the project has the latest required resources.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) if hasPositionalArgs(args) { - showUsageAndExit(cmd, args, false) + showUsageAndExit(cmd, args) } // WithStackValidation is a functional option that enables/disables stack configuration validation // based on whether the --stack flag is provided diff --git a/cmd/version.go b/cmd/version.go index 95b3c01e8..c0fb06eaf 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -21,9 +21,9 @@ var versionCmd = &cobra.Command{ Long: `This command shows the version of the Atmos CLI you are currently running and checks if a newer version is available. Use this command to verify your installation and ensure you are up to date.`, Example: "atmos version", Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) if hasPositionalArgs(args) { - showUsageAndExit(cmd, args, false) + showUsageAndExit(cmd, args) } // Print a styled Atmos logo to the terminal fmt.Println() diff --git a/cmd/workflow.go b/cmd/workflow.go index 522987e0a..434287417 100644 --- a/cmd/workflow.go +++ b/cmd/workflow.go @@ -83,7 +83,7 @@ var workflowCmd = &cobra.Command{ "For more details refer to https://atmos.tools/cli/commands/workflow/", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args, false) + handleHelpRequest(cmd, args) // If no arguments are provided, start the workflow UI if len(args) == 0 { err := e.ExecuteWorkflowCmd(cmd, args) diff --git a/internal/tui/templates/base_template.go b/internal/tui/templates/base_template.go index 74febc3ab..85fbabedf 100644 --- a/internal/tui/templates/base_template.go +++ b/internal/tui/templates/base_template.go @@ -1,7 +1,5 @@ package templates -import "fmt" - type HelpTemplateSections int const ( @@ -19,15 +17,15 @@ const ( Footer ) -func GenerateFromBaseTemplate(commandName string, parts []HelpTemplateSections) string { +func GenerateFromBaseTemplate(parts []HelpTemplateSections) string { template := "" for _, value := range parts { - template += getSection(commandName, value) + template += getSection(value) } return template } -func getSection(commandName string, section HelpTemplateSections) string { +func getSection(section HelpTemplateSections) string { switch section { case LongDescription: return `{{ .Long }} @@ -80,12 +78,11 @@ func getSection(commandName string, section HelpTemplateSections) string { {{wrappedFlagUsages .InheritedFlags | trimTrailingWhitespaces}}{{end}}` case NativeCommands: - return fmt.Sprintf(` + return `{{if (isNativeCommandsAvailable .Commands)}} -{{HeadingStyle "Native %s Commands:"}} +{{HeadingStyle "Native "}}{{HeadingStyle .Use}}{{HeadingStyle " Commands:"}} -{{formatCommands .Commands "native"}} -`, commandName) +{{formatCommands .Commands "native"}}{{end}}` case Usage: return ` {{HeadingStyle "Usage:"}} @@ -93,17 +90,17 @@ func getSection(commandName string, section HelpTemplateSections) string { {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} {{.CommandPath}} [command]{{end}}` case DoubleDashHelp: - return fmt.Sprintf(` + return ` The '--' (double-dash) can be used to signify the end of Atmos-specific options and the beginning of additional native arguments and flags for the specific command being run. Example: - atmos %s -s -- `, commandName) + atmos {{.CommandPath}} {{if gt (len .Commands) 0}}[subcommand]{{end}} -s -- ` case Footer: return `{{if .HasAvailableSubCommands}} -Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}` +Use "{{.CommandPath}} {{if gt (len .Commands) 0}}[subcommand]{{end}} --help" for more information about a command.{{end}}` default: return "" } diff --git a/internal/tui/templates/templater.go b/internal/tui/templates/templater.go index 6f38935aa..d3596c84a 100644 --- a/internal/tui/templates/templater.go +++ b/internal/tui/templates/templater.go @@ -76,6 +76,15 @@ func filterCommands(commands []*cobra.Command, returnOnlyAliases bool) []*cobra. return filtered } +func isNativeCommandsAvailable(cmds []*cobra.Command) bool { + for _, cmd := range cmds { + if cmd.Annotations["nativeCommand"] == "true" { + return true + } + } + return false +} + func isAliasesPresent(cmds []*cobra.Command) bool { return len(filterCommands(cmds, true)) > 0 } @@ -162,11 +171,12 @@ func SetCustomUsageFunc(cmd *cobra.Command) error { return fmt.Errorf("command cannot be nil") } t := &Templater{ - UsageTemplate: GenerateFromBaseTemplate(cmd.Use, []HelpTemplateSections{ + UsageTemplate: GenerateFromBaseTemplate([]HelpTemplateSections{ Usage, Aliases, Examples, AvailableCommands, + NativeCommands, SubCommandAliases, Flags, GlobalFlags, @@ -178,6 +188,7 @@ func SetCustomUsageFunc(cmd *cobra.Command) error { cmd.SetUsageTemplate(t.UsageTemplate) cobra.AddTemplateFunc("isAliasesPresent", isAliasesPresent) + cobra.AddTemplateFunc("isNativeCommandsAvailable", isNativeCommandsAvailable) cobra.AddTemplateFunc("formatCommands", formatCommands) return nil } From 55c1362db46c8be37bec39644331f2bbafbc5bb2 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Mon, 13 Jan 2025 09:46:03 +0100 Subject: [PATCH 30/50] use cobra.NoArgs instead --- cmd/atlantis.go | 2 +- cmd/atlantis_generate.go | 2 +- cmd/atlantis_generate_repo_config.go | 6 +-- cmd/aws.go | 2 +- cmd/aws_eks.go | 2 +- cmd/aws_eks_update_kubeconfig.go | 1 - cmd/cmd_utils.go | 10 +--- cmd/cmd_utils_test.go | 72 ---------------------------- cmd/completion.go | 9 +--- cmd/describe.go | 2 +- cmd/describe_affected.go | 6 +-- cmd/describe_component.go | 1 - cmd/describe_config.go | 5 +- cmd/describe_dependents.go | 1 - cmd/describe_stacks.go | 5 +- cmd/describe_workflows.go | 6 +-- cmd/docs.go | 1 - cmd/helmfile.go | 3 +- cmd/helmfile_generate.go | 2 +- cmd/list.go | 2 +- cmd/list_components.go | 5 +- cmd/list_stacks.go | 5 +- cmd/pro.go | 2 +- cmd/pro_lock.go | 5 +- cmd/pro_unlock.go | 5 +- cmd/root.go | 10 +++- cmd/terraform.go | 8 ++-- cmd/terraform_commands.go | 4 +- cmd/terraform_generate.go | 2 +- cmd/terraform_generate_backends.go | 5 +- cmd/terraform_generate_varfiles.go | 5 +- cmd/validate.go | 2 +- cmd/validate_stacks.go | 5 +- cmd/vendor.go | 2 +- cmd/vendor_pull.go | 5 +- cmd/version.go | 5 +- internal/exec/helmfile.go | 22 --------- 37 files changed, 43 insertions(+), 194 deletions(-) delete mode 100644 cmd/cmd_utils_test.go diff --git a/cmd/atlantis.go b/cmd/atlantis.go index 19d991449..560dc7da9 100644 --- a/cmd/atlantis.go +++ b/cmd/atlantis.go @@ -10,9 +10,9 @@ var atlantisCmd = &cobra.Command{ Short: "Generate and manage Atlantis configurations", Long: `Generate and manage Atlantis configurations that use Atmos under the hood to run Terraform workflows, bringing the power of Atmos to Atlantis for streamlined infrastructure automation.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, } func init() { - addUsageCommand(atlantisCmd, false) RootCmd.AddCommand(atlantisCmd) } diff --git a/cmd/atlantis_generate.go b/cmd/atlantis_generate.go index 7f7c4f6fb..7f23d72fb 100644 --- a/cmd/atlantis_generate.go +++ b/cmd/atlantis_generate.go @@ -10,9 +10,9 @@ var atlantisGenerateCmd = &cobra.Command{ Short: "Generate Atlantis configuration files", Long: "This command generates configuration files to automate and streamline Terraform workflows with Atlantis.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, } func init() { - addUsageCommand(atlantisGenerateCmd, false) atlantisCmd.AddCommand(atlantisGenerateCmd) } diff --git a/cmd/atlantis_generate_repo_config.go b/cmd/atlantis_generate_repo_config.go index 0c340aef9..bbab3cafd 100644 --- a/cmd/atlantis_generate_repo_config.go +++ b/cmd/atlantis_generate_repo_config.go @@ -14,12 +14,8 @@ var atlantisGenerateRepoConfigCmd = &cobra.Command{ Short: "Generate repository configuration for Atlantis", Long: "Generate the repository configuration file required for Atlantis to manage Terraform repositories.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args) - if hasPositionalArgs(args) { - showUsageAndExit(cmd, args) - } - // Check Atmos configuration checkAtmosConfig() err := e.ExecuteAtlantisGenerateRepoConfigCmd(cmd, args) diff --git a/cmd/aws.go b/cmd/aws.go index 2df54368b..a65b13fdd 100644 --- a/cmd/aws.go +++ b/cmd/aws.go @@ -10,9 +10,9 @@ var awsCmd = &cobra.Command{ Short: "Run AWS-specific commands for interacting with cloud resources", Long: `This command allows interaction with AWS resources through various CLI commands.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, } func init() { - addUsageCommand(awsCmd, false) RootCmd.AddCommand(awsCmd) } diff --git a/cmd/aws_eks.go b/cmd/aws_eks.go index 237c43db9..2e6cba71d 100644 --- a/cmd/aws_eks.go +++ b/cmd/aws_eks.go @@ -15,9 +15,9 @@ You can use this command to interact with AWS EKS, including operations like con For a list of available AWS EKS commands, refer to the Atmos documentation: https://atmos.tools/cli/commands/aws/eks-update-kubeconfig`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, } func init() { - addUsageCommand(awsEksCmd, false) awsCmd.AddCommand(awsEksCmd) } diff --git a/cmd/aws_eks_update_kubeconfig.go b/cmd/aws_eks_update_kubeconfig.go index 3016938c9..b421a0b21 100644 --- a/cmd/aws_eks_update_kubeconfig.go +++ b/cmd/aws_eks_update_kubeconfig.go @@ -34,7 +34,6 @@ See https://docs.aws.amazon.com/cli/latest/reference/eks/update-kubeconfig.html FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args) err := e.ExecuteAwsEksUpdateKubeconfigCommand(cmd, args) if err != nil { u.LogErrorAndExit(schema.AtmosConfiguration{}, err) diff --git a/cmd/cmd_utils.go b/cmd/cmd_utils.go index d28beb4c6..e95d0b846 100644 --- a/cmd/cmd_utils.go +++ b/cmd/cmd_utils.go @@ -442,7 +442,7 @@ func checkAtmosConfig(opts ...AtmosValidateOption) { atmosConfigExists, err := u.IsDirectory(atmosConfig.StacksBaseAbsolutePath) if !atmosConfigExists || err != nil { printMessageForMissingAtmosConfig(atmosConfig) - os.Exit(0) + os.Exit(1) } } } @@ -560,8 +560,6 @@ func showUsageAndExit(cmd *cobra.Command, args []string) { unkonwnCommand := fmt.Sprintf("Unknown command: %q", cmd.CommandPath()) if len(args) > 0 { - // Show help if the first argument is "help" - handleHelpRequest(cmd, args) suggestions = cmd.SuggestionsFor(args[0]) subCommand = args[0] unkonwnCommand = fmt.Sprintf(`Error: Unkown command %q for %q`+"\n", subCommand, cmd.CommandPath()) @@ -591,12 +589,6 @@ func showUsageAndExit(cmd *cobra.Command, args []string) { u.LogErrorAndExit(atmosConfig, errors.New(unkonwnCommand)) } -func addUsageCommand(cmd *cobra.Command, isNativeCommandsAvailable bool) { - cmd.Run = func(cmd *cobra.Command, args []string) { - showUsageAndExit(cmd, args) - } -} - // hasPositionalArgs checks if a slice of strings contains an exact match for the target string. func hasPositionalArgs(args []string) bool { for i := 0; i < len(args); i++ { diff --git a/cmd/cmd_utils_test.go b/cmd/cmd_utils_test.go deleted file mode 100644 index 5affdab0e..000000000 --- a/cmd/cmd_utils_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package cmd - -import ( - "testing" -) - -func TestHasPositionalArgs(t *testing.T) { - testCases := []struct { - name string - args []string - want bool - }{ - { - name: "NoArgs", - args: []string{}, - want: false, - }, - { - name: "OneArg", - args: []string{"arg1"}, - want: true, - }, - { - name: "TwoArgs", - args: []string{"arg1", "arg2"}, - want: true, - }, - { - name: "ArgAndFlagWithParam", - args: []string{"arg1", "--flag", "param"}, - want: true, - }, - { - name: "Flag", - args: []string{"--flag"}, - want: false, - }, - { - name: "FlagAndArg", - args: []string{"--flag", "arg1"}, - want: false, - }, - { - name: "FlagAndArgAndFlag", - args: []string{"--flag", "arg1", "--flag"}, - want: false, - }, - { - name: "FlagsWithArgsWithPositionalArg", - args: []string{"--flag=something", "ted"}, - want: true, - }, - { - name: "FlagsWithoutArgsWithEqual", - args: []string{"--flag=something"}, - want: false, - }, - { - name: "terrraformLikeArgs", - args: []string{"-flag1=hello", "-flag2"}, - want: false, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - got := hasPositionalArgs(tc.args) - if got != tc.want { - t.Errorf("HasPositionalArgs() = %v, want %v", got, tc.want) - } - }) - } -} diff --git a/cmd/completion.go b/cmd/completion.go index 668f108fa..93375e578 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -13,12 +13,7 @@ var completionCmd = &cobra.Command{ Short: "Generate autocompletion scripts for Bash, Zsh, Fish, and PowerShell", Long: "This command generates completion scripts for Bash, Zsh, Fish and PowerShell", DisableFlagsInUseLine: true, - // Why I am not using cobra inbuilt validation for Args: - // Because we have our own custom validation for Args - // Why we have our own custom validation for Args: - // Because we want to show custom error message when user provides invalid shell name - // ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, - // Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + Args: cobra.NoArgs, } func runCompletion(cmd *cobra.Command, args []string) { @@ -48,8 +43,8 @@ func init() { Short: "Generate completion script for " + shellName, Long: "This command generates completion scripts for " + shellName, Run: runCompletion, + Args: cobra.NoArgs, }) } - addUsageCommand(completionCmd, false) RootCmd.AddCommand(completionCmd) } diff --git a/cmd/describe.go b/cmd/describe.go index be444cf08..8de4c0c1e 100644 --- a/cmd/describe.go +++ b/cmd/describe.go @@ -10,10 +10,10 @@ var describeCmd = &cobra.Command{ Short: "Show details about Atmos configurations and components", Long: `Display configuration details for Atmos CLI, stacks, and components.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, } func init() { - addUsageCommand(describeCmd, false) describeCmd.PersistentFlags().StringP("query", "q", "", "Query the results of an 'atmos describe' command using 'yq' expressions: atmos describe --query ") RootCmd.AddCommand(describeCmd) } diff --git a/cmd/describe_affected.go b/cmd/describe_affected.go index c02301ba5..c32337923 100644 --- a/cmd/describe_affected.go +++ b/cmd/describe_affected.go @@ -14,12 +14,8 @@ var describeAffectedCmd = &cobra.Command{ Short: "List Atmos components and stacks affected by two Git commits", Long: "Identify and list Atmos components and stacks impacted by changes between two Git commits.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args) - if hasPositionalArgs(args) { - showUsageAndExit(cmd, args) - } - // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/describe_component.go b/cmd/describe_component.go index e014e809a..d6eea1317 100644 --- a/cmd/describe_component.go +++ b/cmd/describe_component.go @@ -15,7 +15,6 @@ var describeComponentCmd = &cobra.Command{ Long: `Display the configuration details for a specific Atmos component within a designated Atmos stack, including its dependencies, settings, and overrides.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args) // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/describe_config.go b/cmd/describe_config.go index 8138c226d..e258e7d4c 100644 --- a/cmd/describe_config.go +++ b/cmd/describe_config.go @@ -14,11 +14,8 @@ var describeConfigCmd = &cobra.Command{ Short: "Display the final merged CLI configuration", Long: "This command displays the final, deep-merged CLI configuration after combining all relevant configuration files.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args) - if hasPositionalArgs(args) { - showUsageAndExit(cmd, args) - } err := e.ExecuteDescribeConfigCmd(cmd, args) if err != nil { diff --git a/cmd/describe_dependents.go b/cmd/describe_dependents.go index 199883705..ca50fd9d4 100644 --- a/cmd/describe_dependents.go +++ b/cmd/describe_dependents.go @@ -16,7 +16,6 @@ var describeDependentsCmd = &cobra.Command{ Long: "This command generates a list of Atmos components within stacks that depend on the specified Atmos component.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args) // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/describe_stacks.go b/cmd/describe_stacks.go index 5ca50d4a6..001d6ab72 100644 --- a/cmd/describe_stacks.go +++ b/cmd/describe_stacks.go @@ -14,11 +14,8 @@ var describeStacksCmd = &cobra.Command{ Short: "Display configuration for Atmos stacks and their components", Long: "This command shows the configuration details for Atmos stacks and the components within those stacks.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args) - if hasPositionalArgs(args) { - showUsageAndExit(cmd, args) - } // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/describe_workflows.go b/cmd/describe_workflows.go index 0d610d220..855af276f 100644 --- a/cmd/describe_workflows.go +++ b/cmd/describe_workflows.go @@ -21,12 +21,8 @@ var describeWorkflowsCmd = &cobra.Command{ "describe workflows -o map\n" + "describe workflows -o all", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args) - if hasPositionalArgs(args) { - showUsageAndExit(cmd, args) - } - err := e.ExecuteDescribeWorkflowsCmd(cmd, args) if err != nil { u.LogErrorAndExit(schema.AtmosConfiguration{}, err) diff --git a/cmd/docs.go b/cmd/docs.go index 4c254fe11..25ded076c 100644 --- a/cmd/docs.go +++ b/cmd/docs.go @@ -28,7 +28,6 @@ var docsCmd = &cobra.Command{ Args: cobra.MaximumNArgs(1), FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args) if len(args) == 1 { info := schema.ConfigAndStacksInfo{ Component: args[0], diff --git a/cmd/helmfile.go b/cmd/helmfile.go index 39a62fbe0..3d27ac07e 100644 --- a/cmd/helmfile.go +++ b/cmd/helmfile.go @@ -13,18 +13,17 @@ var helmfileCmd = &cobra.Command{ Short: "Manage Helmfile-based Kubernetes deployments", Long: `This command runs Helmfile commands to manage Kubernetes deployments using Helmfile.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true}, + Args: cobra.NoArgs, } func init() { // https://github.com/spf13/cobra/issues/739 helmfileCmd.DisableFlagParsing = true helmfileCmd.PersistentFlags().StringP("stack", "s", "", "atmos helmfile -s ") - addUsageCommand(helmfileCmd, false) RootCmd.AddCommand(helmfileCmd) } func helmfileRun(cmd *cobra.Command, commandName string, args []string) { - handleHelpRequest(cmd, args) diffArgs := []string{commandName} diffArgs = append(diffArgs, args...) info := getConfigAndStacksInfo("helmfile", cmd, diffArgs) diff --git a/cmd/helmfile_generate.go b/cmd/helmfile_generate.go index e79e96cad..7aba7df13 100644 --- a/cmd/helmfile_generate.go +++ b/cmd/helmfile_generate.go @@ -10,9 +10,9 @@ var helmfileGenerateCmd = &cobra.Command{ Short: "Generate configurations for Helmfile components", Long: "This command generates various configuration files for Helmfile components in Atmos.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, } func init() { - addUsageCommand(helmfileGenerateCmd, false) helmfileCmd.AddCommand(helmfileGenerateCmd) } diff --git a/cmd/list.go b/cmd/list.go index 26f599b32..3e95c5791 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -10,9 +10,9 @@ var listCmd = &cobra.Command{ Short: "List available stacks and components", Long: `Display a list of all available stacks and components defined in your project.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, } func init() { - addUsageCommand(listCmd, false) RootCmd.AddCommand(listCmd) } diff --git a/cmd/list_components.go b/cmd/list_components.go index 3dc44621a..3800a590e 100644 --- a/cmd/list_components.go +++ b/cmd/list_components.go @@ -21,11 +21,8 @@ var listComponentsCmd = &cobra.Command{ Long: "List Atmos components, with options to filter results by specific stacks.", Example: "atmos list components\n" + "atmos list components -s ", + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args) - if hasPositionalArgs(args) { - showUsageAndExit(cmd, args) - } // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/list_stacks.go b/cmd/list_stacks.go index 5e11f4242..9dcd8ac91 100644 --- a/cmd/list_stacks.go +++ b/cmd/list_stacks.go @@ -21,11 +21,8 @@ var listStacksCmd = &cobra.Command{ Example: "atmos list stacks\n" + "atmos list stacks -c ", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args) - if hasPositionalArgs(args) { - showUsageAndExit(cmd, args) - } // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/pro.go b/cmd/pro.go index 737f97bb1..0e0262d38 100644 --- a/cmd/pro.go +++ b/cmd/pro.go @@ -10,9 +10,9 @@ var proCmd = &cobra.Command{ Short: "Access premium features integrated with app.cloudposse.com", Long: `This command allows you to manage and configure premium features available through app.cloudposse.com.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, } func init() { - addUsageCommand(proCmd, false) RootCmd.AddCommand(proCmd) } diff --git a/cmd/pro_lock.go b/cmd/pro_lock.go index a6998a53d..68191669f 100644 --- a/cmd/pro_lock.go +++ b/cmd/pro_lock.go @@ -14,11 +14,8 @@ var proLockCmd = &cobra.Command{ Short: "Lock a stack", Long: `This command calls the atmos pro API and locks a stack`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args) - if hasPositionalArgs(args) { - showUsageAndExit(cmd, args) - } // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/pro_unlock.go b/cmd/pro_unlock.go index f97d46cb6..feb625a60 100644 --- a/cmd/pro_unlock.go +++ b/cmd/pro_unlock.go @@ -14,11 +14,8 @@ var proUnlockCmd = &cobra.Command{ Short: "Unlock a stack", Long: `This command calls the atmos pro API and unlocks a stack`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args) - if hasPositionalArgs(args) { - showUsageAndExit(cmd, args) - } // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/root.go b/cmd/root.go index b9eb24fee..5b6d36983 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -43,6 +43,8 @@ var RootCmd = &cobra.Command{ cmd.SilenceUsage = true cmd.SilenceErrors = true } + handleHelpRequest(cmd, args) + checkAtmosConfig() }, Run: func(cmd *cobra.Command, args []string) { // Check Atmos configuration @@ -106,6 +108,7 @@ func Execute() error { if err != nil { if strings.Contains(err.Error(), "unknown command") { command := getInvalidCommandName(err.Error()) + fmt.Println("_______REOO__________") showUsageAndExit(RootCmd, []string{command}) } } @@ -155,9 +158,14 @@ func initCobraConfig() { if c.Use == "atmos" { return b.UsageFunc(c) } - return oldUsageFunc(c) + showUsageAndExit(c, c.Flags().Args()) + return nil }) RootCmd.SetHelpFunc(func(command *cobra.Command, strings []string) { + + if !(Contains(os.Args, "help") || Contains(os.Args, "--help") || Contains(os.Args, "-h")) { + showUsageAndExit(command, command.Flags().Args()) + } // Print a styled Atmos logo to the terminal fmt.Println() if command.Use != "atmos" || command.Flags().Changed("help") { diff --git a/cmd/terraform.go b/cmd/terraform.go index 7fcfb41e3..ea7a7f2aa 100644 --- a/cmd/terraform.go +++ b/cmd/terraform.go @@ -21,7 +21,7 @@ var terraformCmd = &cobra.Command{ Short: "Execute Terraform commands (e.g., plan, apply, destroy) using Atmos stack configurations", Long: `This command allows you to execute Terraform commands, such as plan, apply, and destroy, using Atmos stack configurations for consistent infrastructure management.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true}, - PostRunE: func(cmd *cobra.Command, args []string) error { + PostRun: func(cmd *cobra.Command, args []string) { info := getConfigAndStacksInfo("terraform", cmd, args) sections, err := e.ExecuteDescribeComponent(info.ComponentFromArg, info.Stack, true) @@ -55,19 +55,18 @@ var terraformCmd = &cobra.Command{ store := atmosConfig.Stores[hook.Name] if store == nil { - return fmt.Errorf("store %q not found in configuration", hook.Name) + u.LogErrorAndExit(atmosConfig, fmt.Errorf("store %q not found in configuration", hook.Name)) } u.LogInfo(atmosConfig, fmt.Sprintf(" storing terraform output '%s' in store '%s' with key '%s' and value %v", outputKey, hook.Name, key, outputValue)) err = store.Set(info.Stack, info.ComponentFromArg, key, outputValue) if err != nil { - return err + u.LogErrorAndExit(atmosConfig, err) } } } } } - return nil }, } @@ -93,7 +92,6 @@ func init() { // https://github.com/spf13/cobra/issues/739 terraformCmd.DisableFlagParsing = true terraformCmd.PersistentFlags().StringP("stack", "s", "", "atmos terraform -s ") - addUsageCommand(terraformCmd, true) attachTerraformCommands(terraformCmd) RootCmd.AddCommand(terraformCmd) } diff --git a/cmd/terraform_commands.go b/cmd/terraform_commands.go index 8c8cff50b..6739cad5e 100644 --- a/cmd/terraform_commands.go +++ b/cmd/terraform_commands.go @@ -258,10 +258,10 @@ func attachTerraformCommands(parentCmd *cobra.Command) { setFlags(cmd) } cmd.Run = func(cmd_ *cobra.Command, args []string) { - if len(os.Args) > 3 { + if len(os.Args) > 2 { args = os.Args[2:] } - handleHelpRequest(cmd_, args) + terraformRun(parentCmd, cmd_, args) } parentCmd.AddCommand(cmd) diff --git a/cmd/terraform_generate.go b/cmd/terraform_generate.go index de1216fbc..789046353 100644 --- a/cmd/terraform_generate.go +++ b/cmd/terraform_generate.go @@ -16,6 +16,7 @@ This command supports the following subcommands: - 'backends' to generate backend configuration files for all Atmos components in all stacks. - 'varfile' to generate a variable file (varfile) for an Atmos component in a stack. - 'varfiles' to generate varfiles for all Atmos components in all stacks.`, + Args: cobra.NoArgs, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { terraformRun(cmd, cmd, args) @@ -23,6 +24,5 @@ This command supports the following subcommands: } func init() { - addUsageCommand(terraformGenerateCmd, false) terraformCmd.AddCommand(terraformGenerateCmd) } diff --git a/cmd/terraform_generate_backends.go b/cmd/terraform_generate_backends.go index 8337b4fa1..8f9f11f6d 100644 --- a/cmd/terraform_generate_backends.go +++ b/cmd/terraform_generate_backends.go @@ -14,11 +14,8 @@ var terraformGenerateBackendsCmd = &cobra.Command{ Short: "Generate backend configurations for all Terraform components", Long: "This command generates the backend configuration files for all Terraform components in the Atmos environment.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args) - if hasPositionalArgs(args) { - showUsageAndExit(cmd, args) - } // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/terraform_generate_varfiles.go b/cmd/terraform_generate_varfiles.go index 01e369bb4..25e756454 100644 --- a/cmd/terraform_generate_varfiles.go +++ b/cmd/terraform_generate_varfiles.go @@ -14,11 +14,8 @@ var terraformGenerateVarfilesCmd = &cobra.Command{ Short: "Generate varfiles for all Terraform components in all stacks", Long: "This command generates varfiles for all Atmos Terraform components across all stacks.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args) - if hasPositionalArgs(args) { - showUsageAndExit(cmd, args) - } // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/validate.go b/cmd/validate.go index 10c8be25c..5588be093 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -10,9 +10,9 @@ var validateCmd = &cobra.Command{ Short: "Validate configurations against OPA policies and JSON schemas", Long: `This command validates stacks and components by checking their configurations against Open Policy Agent (OPA) policies and JSON schemas.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, } func init() { - addUsageCommand(validateCmd, false) RootCmd.AddCommand(validateCmd) } diff --git a/cmd/validate_stacks.go b/cmd/validate_stacks.go index 23995b06d..ef8cdb210 100644 --- a/cmd/validate_stacks.go +++ b/cmd/validate_stacks.go @@ -16,11 +16,8 @@ var ValidateStacksCmd = &cobra.Command{ Long: "This command validates the configuration of stack manifests in Atmos to ensure proper setup and compliance.", Example: "validate stacks", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args) - if hasPositionalArgs(args) { - showUsageAndExit(cmd, args) - } // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/vendor.go b/cmd/vendor.go index 35b95e1ce..9ff2d4da6 100644 --- a/cmd/vendor.go +++ b/cmd/vendor.go @@ -10,9 +10,9 @@ var vendorCmd = &cobra.Command{ Short: "Manage external dependencies for components or stacks", Long: `This command manages external dependencies for Atmos components or stacks by vendoring them. Vendoring involves copying and locking required dependencies locally, ensuring consistency, reliability, and alignment with the principles of immutable infrastructure.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, } func init() { - addUsageCommand(vendorCmd, false) RootCmd.AddCommand(vendorCmd) } diff --git a/cmd/vendor_pull.go b/cmd/vendor_pull.go index 20a73a5bd..f860c556f 100644 --- a/cmd/vendor_pull.go +++ b/cmd/vendor_pull.go @@ -14,11 +14,8 @@ var vendorPullCmd = &cobra.Command{ Short: "Pull the latest vendor configurations or dependencies", Long: "Pull and update vendor-specific configurations or dependencies to ensure the project has the latest required resources.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args) - if hasPositionalArgs(args) { - showUsageAndExit(cmd, args) - } // WithStackValidation is a functional option that enables/disables stack configuration validation // based on whether the --stack flag is provided checkAtmosConfig(WithStackValidation(cmd.Flag("stack").Changed)) diff --git a/cmd/version.go b/cmd/version.go index c0fb06eaf..f18dc98af 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -20,11 +20,8 @@ var versionCmd = &cobra.Command{ Short: "Display the version of Atmos you are running and check for updates", Long: `This command shows the version of the Atmos CLI you are currently running and checks if a newer version is available. Use this command to verify your installation and ensure you are up to date.`, Example: "atmos version", + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - handleHelpRequest(cmd, args) - if hasPositionalArgs(args) { - showUsageAndExit(cmd, args) - } // Print a styled Atmos logo to the terminal fmt.Println() err := tuiUtils.PrintStyledText("ATMOS") diff --git a/internal/exec/helmfile.go b/internal/exec/helmfile.go index 734cbdf68..ed5c9668a 100644 --- a/internal/exec/helmfile.go +++ b/internal/exec/helmfile.go @@ -10,7 +10,6 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - tuiUtils "github.com/cloudposse/atmos/internal/tui/utils" cfg "github.com/cloudposse/atmos/pkg/config" "github.com/cloudposse/atmos/pkg/schema" u "github.com/cloudposse/atmos/pkg/utils" @@ -33,27 +32,6 @@ func ExecuteHelmfile(info schema.ConfigAndStacksInfo) error { return err } - if info.NeedHelp { - return nil - } - - // If the user just types `atmos helmfile`, print Atmos logo and show helmfile help - if info.SubCommand == "" { - fmt.Println() - err = tuiUtils.PrintStyledText("ATMOS") - if err != nil { - return err - } - - err = processHelp(atmosConfig, "helmfile", "") - if err != nil { - return err - } - - fmt.Println() - return nil - } - if info.SubCommand == "version" { return ExecuteShellCommand(atmosConfig, "helmfile", []string{info.SubCommand}, "", nil, false, info.RedirectStdErr) } From 6a893e91c9133a5da0b262bead3ce746a875a969 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Mon, 13 Jan 2025 20:20:37 +0100 Subject: [PATCH 31/50] fix atmos version command --- cmd/root.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 5b6d36983..63391ed49 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -43,8 +43,6 @@ var RootCmd = &cobra.Command{ cmd.SilenceUsage = true cmd.SilenceErrors = true } - handleHelpRequest(cmd, args) - checkAtmosConfig() }, Run: func(cmd *cobra.Command, args []string) { // Check Atmos configuration @@ -108,7 +106,6 @@ func Execute() error { if err != nil { if strings.Contains(err.Error(), "unknown command") { command := getInvalidCommandName(err.Error()) - fmt.Println("_______REOO__________") showUsageAndExit(RootCmd, []string{command}) } } From 73de411e8fd139a6d29288e2231114b25b5c84b3 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Mon, 13 Jan 2025 21:02:36 +0100 Subject: [PATCH 32/50] update atmos test case --- tests/test-cases/core.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test-cases/core.yaml b/tests/test-cases/core.yaml index c8f12fb9d..56592a34b 100644 --- a/tests/test-cases/core.yaml +++ b/tests/test-cases/core.yaml @@ -24,7 +24,7 @@ tests: - "but the directory does not exist." stderr: - "^$" - exit_code: 0 + exit_code: 1 - name: atmos docs enabled: true From e51577fb0da6fc31b4b638935eb84bec3202c764 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Tue, 14 Jan 2025 07:25:28 +0100 Subject: [PATCH 33/50] Fix help and add test cases --- cmd/cmd_utils.go | 2 +- cmd/root.go | 10 +- cmd/terraform.go | 4 + cmd/terraform_commands.go | 2 + cmd/terraform_generate.go | 3 - tests/test-cases/help-and-usage.yaml | 177 +++++++++++++++++++++++++++ 6 files changed, 191 insertions(+), 7 deletions(-) create mode 100644 tests/test-cases/help-and-usage.yaml diff --git a/cmd/cmd_utils.go b/cmd/cmd_utils.go index e95d0b846..ec104eb2b 100644 --- a/cmd/cmd_utils.go +++ b/cmd/cmd_utils.go @@ -562,7 +562,7 @@ func showUsageAndExit(cmd *cobra.Command, args []string) { if len(args) > 0 { suggestions = cmd.SuggestionsFor(args[0]) subCommand = args[0] - unkonwnCommand = fmt.Sprintf(`Error: Unkown command %q for %q`+"\n", subCommand, cmd.CommandPath()) + unkonwnCommand = fmt.Sprintf(`Error: Unknown command %q for %q`+"\n", subCommand, cmd.CommandPath()) } if len(suggestions) > 0 { u.PrintMessage(fmt.Sprintf("%s\n\nDid you mean this?", unkonwnCommand)) diff --git a/cmd/root.go b/cmd/root.go index 63391ed49..65341d9a0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -158,10 +158,14 @@ func initCobraConfig() { showUsageAndExit(c, c.Flags().Args()) return nil }) - RootCmd.SetHelpFunc(func(command *cobra.Command, strings []string) { + RootCmd.SetHelpFunc(func(command *cobra.Command, args []string) { if !(Contains(os.Args, "help") || Contains(os.Args, "--help") || Contains(os.Args, "-h")) { - showUsageAndExit(command, command.Flags().Args()) + arguments := os.Args[len(strings.Split(command.CommandPath(), " ")):] + if len(command.Flags().Args()) > 0 { + arguments = command.Flags().Args() + } + showUsageAndExit(command, arguments) } // Print a styled Atmos logo to the terminal fmt.Println() @@ -178,7 +182,7 @@ func initCobraConfig() { if err != nil { u.LogErrorAndExit(atmosConfig, err) } - b.HelpFunc(command, strings) + b.HelpFunc(command, args) if err := command.Usage(); err != nil { u.LogErrorAndExit(atmosConfig, err) } diff --git a/cmd/terraform.go b/cmd/terraform.go index ea7a7f2aa..6a81d4232 100644 --- a/cmd/terraform.go +++ b/cmd/terraform.go @@ -82,6 +82,10 @@ func Contains(slice []string, target string) bool { func terraformRun(cmd *cobra.Command, actualCmd *cobra.Command, args []string) { info := getConfigAndStacksInfo("terraform", cmd, args) + if info.NeedHelp { + actualCmd.Usage() + return + } err := e.ExecuteTerraform(info) if err != nil { u.LogErrorAndExit(atmosConfig, err) diff --git a/cmd/terraform_commands.go b/cmd/terraform_commands.go index 6739cad5e..1bb077145 100644 --- a/cmd/terraform_commands.go +++ b/cmd/terraform_commands.go @@ -258,6 +258,8 @@ func attachTerraformCommands(parentCmd *cobra.Command) { setFlags(cmd) } cmd.Run = func(cmd_ *cobra.Command, args []string) { + // Because we disable flag parsing we require manual handle help Request + handleHelpRequest(cmd, args) if len(os.Args) > 2 { args = os.Args[2:] } diff --git a/cmd/terraform_generate.go b/cmd/terraform_generate.go index 789046353..609f8d7d2 100644 --- a/cmd/terraform_generate.go +++ b/cmd/terraform_generate.go @@ -18,9 +18,6 @@ This command supports the following subcommands: - 'varfiles' to generate varfiles for all Atmos components in all stacks.`, Args: cobra.NoArgs, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, - Run: func(cmd *cobra.Command, args []string) { - terraformRun(cmd, cmd, args) - }, } func init() { diff --git a/tests/test-cases/help-and-usage.yaml b/tests/test-cases/help-and-usage.yaml new file mode 100644 index 000000000..14b1a301c --- /dev/null +++ b/tests/test-cases/help-and-usage.yaml @@ -0,0 +1,177 @@ +tests: + - name: atmos non-existent + enabled: true + description: "Ensure atmos CLI returns an error for a non-existent command." + workdir: "../" + command: "atmos" + args: + - "non-existent" + expect: + stderr: + - 'Error: Unknown command \"non-existent\" for \"atmos\"' + exit_code: 1 + - name: atmos --help + enabled: true + description: "Ensure atmos CLI help command lists available commands." + workdir: "./" + command: "atmos" + args: + - "--help" + expect: + stdout: + - "Usage:" + - "Available Commands:" + - "\\batlantis\\b" + - "\\baws\\b" + - "\\bcompletion\\b" + - "\\bdescribe\\b" + - "\\bdocs\\b" + - "\\bhelmfile\\b" + - "\\bhelp\\b" + - "\\blist\\b" + - "\\bpro\\b" + - "\\bterraform\\b" + - "\\bvalidate\\b" + - "\\bvendor\\b" + - "\\bversion\\b" + - "\\bworkflow\\b" + - "SubCommand Aliases:" + - "\\bhf\\b" + - "Flags:" + - "\\batmos \\[subcommand\\] [<]component[>] -s [<]stack[>] -- [<]native-flags[>]" + - "for more information about a command" + stderr: + - "^$" + exit_code: 0 + - name: atmos + enabled: true + description: "Ensure atmos CLI help command shows atmos config " + workdir: "./" + command: "atmos" + expect: + stdout: + - "atmos.yaml CLI config file was not found" + - "The default Atmos stacks directory is set to stacks," + stderr: + - "^$" + exit_code: 1 + - name: atmos terraform + enabled: true + description: "Ensure atmos terraform cli shows usage command" + workdir: "./" + command: "atmos" + args: + - "terraform" + expect: + stdout: + - "Unknown command: \"atmos terraform\"" + - "Valid subcommands are:" + - "apply" + - "clean" + - "console" + stderr: + - "Unknown command: \"atmos terraform\"" + exit_code: 1 + - name: atmos terraform help + enabled: true + description: "Ensure 'atmos terraform help' shows help output" + workdir: "./" + command: "atmos" + args: + - "terraform" + - "help" + expect: + stdout: + - "\\bapply\\b" + - "\\bgenerate\\b" + stderr: + - "^$" + exit_code: 0 + - name: atmos terraform --help + enabled: true + description: "Ensure atmos terraform --help shows help output" + workdir: "./" + command: "atmos" + args: + - "terraform" + - "--help" + expect: + stdout: + - "\\bapply\\b" + - "\\bgenerate\\b" + stderr: + - "^$" + exit_code: 0 + - name: atmos terraform apply --help + enabled: true + description: "Ensure atmos terraform apply --help shows help output" + workdir: "./" + command: "atmos" + args: + - "terraform" + - "apply" + - "--help" + expect: + stdout: + - "\\bUsage\\b" + - "Flags:" + - "and the beginning of additional native arguments and flags for the specific command bei" + stderr: + - "^$" + exit_code: 0 + - name: atmos terraform apply help + enabled: true + description: "Ensure atmos terraform help shows help output" + workdir: "./" + command: "atmos" + args: + - "terraform" + - "apply" + - "--help" + expect: + stdout: + - "\\bUsage\\b" + - "Flags:" + - "and the beginning of additional native arguments and flags for the specific command bei" + stderr: + - "^$" + exit_code: 0 + - name: atmos terraform non-existent + enabled: true + description: "Ensure atmos terraform CLI returns an error for a non-existent command." + workdir: "../" + command: "atmos" + args: + - "terraform" + - "non-existent" + expect: + stderr: + - 'Error: Unknown command \"non-existent\" for \"atmos terraform\"' + exit_code: 1 + - name: atmos terraform plan non-existent in non workspace + enabled: true + description: "Ensure atmos terraform CLI returns an error for a non-existent command in non existing workspace." + workdir: "../" + command: "atmos" + args: + - "terraform" + - "plan" + - "non-existent" + expect: + stdout: + - "CLI config file specifies the directory for Atmos stack" + - "Quick Start" + exit_code: 1 + - name: atmos terraform plan non-existent in workspace + enabled: true + description: "Ensure atmos terraform CLI returns an error for a non-existent command in existing workspace." + workdir: "../examples/quick-start-simple" + command: "atmos" + args: + - "terraform" + - "plan" + - "non-existent" + expect: + stderr: + - "'stack' is required. Usage: atmos terraform -s " + exit_code: 1 From e88e989e07daf4761e9fd27f844c7acb153cda57 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Tue, 14 Jan 2025 20:44:05 +0100 Subject: [PATCH 34/50] added more test cases --- tests/test-cases/help-and-usage.yaml | 225 ++++++++++++++++++++++++++- 1 file changed, 223 insertions(+), 2 deletions(-) diff --git a/tests/test-cases/help-and-usage.yaml b/tests/test-cases/help-and-usage.yaml index 14b1a301c..ae4ecce8b 100644 --- a/tests/test-cases/help-and-usage.yaml +++ b/tests/test-cases/help-and-usage.yaml @@ -64,13 +64,13 @@ tests: - "terraform" expect: stdout: - - "Unknown command: \"atmos terraform\"" + - "Error: Unknown command: \"atmos terraform\"" - "Valid subcommands are:" - "apply" - "clean" - "console" stderr: - - "Unknown command: \"atmos terraform\"" + - "Error: Unknown command: \"atmos terraform\"" exit_code: 1 - name: atmos terraform help enabled: true @@ -175,3 +175,224 @@ tests: stderr: - "'stack' is required. Usage: atmos terraform -s " exit_code: 1 + - name: atmos helmfile + enabled: true + description: "Should show usage for helmfile" + workdir: "../" + command: "atmos" + args: + - "helmfile" + expect: + stderr: + - "Error: Unknown command: \"atmos helmfile\"" + stdout: + - "Valid subcommands are:" + - "apply" + - "destroy" + exit_code: 1 + - name: atmos helmfile non-existant + enabled: true + description: "Should show usage for helmfile and non-existant subcommand" + workdir: "../" + command: "atmos" + args: + - "helmfile" + - "non-existant" + expect: + stderr: + - "Error: Unknown command \"non-existant\" for \"atmos helmfile\"" + stdout: + - "Valid subcommands are:" + - "apply" + - "destroy" + exit_code: 1 + - name: atmos helmfile help + enabled: true + description: "Should show help for helmfile" + workdir: "../" + command: "atmos" + args: + - "helmfile" + - "help" + expect: + stdout: + - "Available Commands:" + - "apply" + - "destroy" + exit_code: 0 + - name: atmos helmfile --help + enabled: true + description: "Should show help for helmfile when using help flag" + workdir: "../" + command: "atmos" + args: + - "helmfile" + - "help" + expect: + stdout: + - "Available Commands:" + - "apply" + - "destroy" + exit_code: 0 + - name: atmos helmfile apply + enabled: true + description: "Should show error in non atmos workspace" + workdir: "../" + command: "atmos" + args: + - "helmfile" + - "apply" + - "non-existent" + expect: + stdout: + - "CLI config file specifies the directory for Atmos stack" + - "Quick Start" + exit_code: 1 + - name: atmos helmfile apply + enabled: true + description: "Should show error in non atmos workspace" + workdir: "../" + command: "atmos" + args: + - "helmfile" + - "apply" + - "non-existent" + expect: + stdout: + - "CLI config file specifies the directory for Atmos stack" + - "Quick Start" + exit_code: 1 + - name: atmos helmfile apply + enabled: true + description: "Should show error in atmos workspace" + workdir: "../examples/demo-helmfile" + command: "atmos" + args: + - "helmfile" + - "apply" + expect: + stderr: + - "'stack' is required. Usage: atmos helmfile -s " + exit_code: 1 + - name: atmos helmfile apply help + enabled: true + description: "Should show help for atmos helmfile apply" + workdir: "../" + command: "atmos" + args: + - "helmfile" + - "apply" + - "help" + expect: + stdout: + - "Flags:" + - "--logs-file" + exit_code: 0 + - name: atmos helmfile apply --help + enabled: true + description: "Should show help for atmos helmfile apply --help" + workdir: "../" + command: "atmos" + args: + - "helmfile" + - "apply" + - "--help" + expect: + stdout: + - "Flags:" + - "--logs-file" + exit_code: 0 + - name: atmos atlantis + enabled: true + description: "Should show usage atmos atlantis" + workdir: "../" + command: "atmos" + args: + - "atlantis" + expect: + stderr: + - "Error: Unknown command: \"atmos atlantis\"" + exit_code: 1 + - name: atmos atlantis help + enabled: true + description: "Should show help 'atmos atlantis help'" + workdir: "../" + command: "atmos" + args: + - "atlantis" + - "help" + expect: + stdout: + - "Available Commands:" + - "generate" + exit_code: 0 + - name: atmos atlantis --help + enabled: true + description: "Should show help 'atmos atlantis --help'" + workdir: "../" + command: "atmos" + args: + - "atlantis" + - "--help" + expect: + stdout: + - "Available Commands:" + - "generate" + exit_code: 0 + + - name: atmos atlantis generate + enabled: true + description: "Should show usage atmos atlantis" + workdir: "../" + command: "atmos" + args: + - "atlantis" + - "generate" + expect: + stderr: + - "Error: Unknown command: \"atmos atlantis generate\"" + exit_code: 1 + - name: atmos atlantis generate help + enabled: true + description: "Should show help 'atmos atlantis generate help'" + workdir: "../" + command: "atmos" + args: + - "atlantis" + - "generate" + - "help" + expect: + stdout: + - "Available Commands:" + - "repo-config" + exit_code: 0 + - name: atmos atlantis generate --help + enabled: true + description: "Should show help 'atmos atlantis --help'" + workdir: "../" + command: "atmos" + args: + - "atlantis" + - "generate" + - "--help" + expect: + stdout: + - "Available Commands:" + - "repo-config" + exit_code: 0 + - name: atmos atlantis generate repo-config + enabled: true + description: "Should show config missing in non atmos workspace 'atmos atlantis generate repo-config'" + workdir: "../" + command: "atmos" + args: + - "atlantis" + - "generate" + - "repo-config" + expect: + stdout: + - "atmos.yaml CLI config file specifies the directory for Atmos stacks as stacks," + - "To configure and start using Atmos, refer to the following documents:" + stderr: + - "^$" + exit_code: 1 From b1e0b4bd61e142bfe6b1e59a86a659a6305026ce Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Tue, 14 Jan 2025 20:44:52 +0100 Subject: [PATCH 35/50] filter commands added --- cmd/cmd_utils.go | 2 +- cmd/helmfile.go | 1 + internal/tui/templates/templater.go | 33 ++++++++++++----------------- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/cmd/cmd_utils.go b/cmd/cmd_utils.go index ec104eb2b..9da2ee368 100644 --- a/cmd/cmd_utils.go +++ b/cmd/cmd_utils.go @@ -557,7 +557,7 @@ func showUsageAndExit(cmd *cobra.Command, args []string) { var suggestions []string var subCommand string = "" - unkonwnCommand := fmt.Sprintf("Unknown command: %q", cmd.CommandPath()) + unkonwnCommand := fmt.Sprintf("Error: Unknown command: %q", cmd.CommandPath()) if len(args) > 0 { suggestions = cmd.SuggestionsFor(args[0]) diff --git a/cmd/helmfile.go b/cmd/helmfile.go index 3d27ac07e..0c62ba872 100644 --- a/cmd/helmfile.go +++ b/cmd/helmfile.go @@ -24,6 +24,7 @@ func init() { } func helmfileRun(cmd *cobra.Command, commandName string, args []string) { + handleHelpRequest(cmd, args) diffArgs := []string{commandName} diffArgs = append(diffArgs, args...) info := getConfigAndStacksInfo("helmfile", cmd, diffArgs) diff --git a/internal/tui/templates/templater.go b/internal/tui/templates/templater.go index d3596c84a..d8fba15b3 100644 --- a/internal/tui/templates/templater.go +++ b/internal/tui/templates/templater.go @@ -45,32 +45,27 @@ func formatCommand(name string, desc string, padding int, IsNotSupported bool) s var customHelpShortMessage = map[string]string{ "help": "Display help information for Atmos commands", - "tf": "Alias for `terraform` commands", } // filterCommands returns only commands or aliases based on returnOnlyAliases boolean func filterCommands(commands []*cobra.Command, returnOnlyAliases bool) []*cobra.Command { + if !returnOnlyAliases { + return commands + } filtered := []*cobra.Command{} + cmdMap := make(map[string]struct{}) for _, cmd := range commands { - isAlias := false - for _, parentCmd := range commands { - if cmd != parentCmd { - for _, alias := range parentCmd.Aliases { - if cmd.Name() == alias { - isAlias = true - break - } - } - } - if isAlias { - break + cmdMap[cmd.Use] = struct{}{} + } + for _, cmd := range commands { + for _, alias := range cmd.Aliases { + if _, ok := cmdMap[alias]; ok { + continue } - } - // Add to the filtered list based on the includeAliases flag - if returnOnlyAliases && isAlias { - filtered = append(filtered, cmd) // Include only aliases - } else if !returnOnlyAliases && !isAlias { - filtered = append(filtered, cmd) // Include only primary commands + copyCmd := *cmd + copyCmd.Use = alias + copyCmd.Short = fmt.Sprintf("Alias of %q command", cmd.CommandPath()) + filtered = append(filtered, ©Cmd) } } return filtered From c74a2d4a5437c570609f1308de2baa124a0b6ee7 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Tue, 14 Jan 2025 20:48:02 +0100 Subject: [PATCH 36/50] fixed typo --- cmd/cmd_utils.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/cmd_utils.go b/cmd/cmd_utils.go index 9da2ee368..57538f585 100644 --- a/cmd/cmd_utils.go +++ b/cmd/cmd_utils.go @@ -557,15 +557,15 @@ func showUsageAndExit(cmd *cobra.Command, args []string) { var suggestions []string var subCommand string = "" - unkonwnCommand := fmt.Sprintf("Error: Unknown command: %q", cmd.CommandPath()) + unknownCommand := fmt.Sprintf("Error: Unknown command: %q", cmd.CommandPath()) if len(args) > 0 { suggestions = cmd.SuggestionsFor(args[0]) subCommand = args[0] - unkonwnCommand = fmt.Sprintf(`Error: Unknown command %q for %q`+"\n", subCommand, cmd.CommandPath()) + unknownCommand = fmt.Sprintf(`Error: Unknown command %q for %q`+"\n", subCommand, cmd.CommandPath()) } if len(suggestions) > 0 { - u.PrintMessage(fmt.Sprintf("%s\n\nDid you mean this?", unkonwnCommand)) + u.PrintMessage(fmt.Sprintf("%s\n\nDid you mean this?", unknownCommand)) for _, suggestion := range suggestions { u.PrintMessage(fmt.Sprintf(" %s\n", suggestion)) } @@ -575,7 +575,7 @@ func showUsageAndExit(cmd *cobra.Command, args []string) { for _, subCmd := range cmd.Commands() { validSubcommands = append(validSubcommands, subCmd.Name()) } - u.PrintMessage(unkonwnCommand) + u.PrintMessage(unknownCommand) if len(validSubcommands) > 0 { u.PrintMessage("Valid subcommands are:") for _, sub := range validSubcommands { @@ -586,7 +586,7 @@ func showUsageAndExit(cmd *cobra.Command, args []string) { } } u.PrintMessage(fmt.Sprintf(`Run '%s --help' for usage`, cmd.CommandPath())) - u.LogErrorAndExit(atmosConfig, errors.New(unkonwnCommand)) + u.LogErrorAndExit(atmosConfig, errors.New(unknownCommand)) } // hasPositionalArgs checks if a slice of strings contains an exact match for the target string. From f4fe81c6caacbe3e0bcef4d3eb914fb7b31c333c Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Tue, 14 Jan 2025 20:49:13 +0100 Subject: [PATCH 37/50] removed unwanted function --- cmd/cmd_utils.go | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/cmd/cmd_utils.go b/cmd/cmd_utils.go index 57538f585..c5cfc9117 100644 --- a/cmd/cmd_utils.go +++ b/cmd/cmd_utils.go @@ -589,29 +589,6 @@ func showUsageAndExit(cmd *cobra.Command, args []string) { u.LogErrorAndExit(atmosConfig, errors.New(unknownCommand)) } -// hasPositionalArgs checks if a slice of strings contains an exact match for the target string. -func hasPositionalArgs(args []string) bool { - for i := 0; i < len(args); i++ { - arg := args[i] - - if strings.HasPrefix(arg, "-") { - // Handle "--flag=value" syntax - if strings.Contains(arg, "=") { - continue - } - - // Skip the next argument if it looks like a value for a flag - if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") { - i++ // Skip the value - } - } else { - // If it's not a flag and not a value for a flag, it's positional - return true - } - } - return false -} - // getConfigAndStacksInfo gets the func getConfigAndStacksInfo(commandName string, cmd *cobra.Command, args []string) schema.ConfigAndStacksInfo { // Check Atmos configuration From be6b4b59ced8c1ef199108862d2d1b43e7b7ac47 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Tue, 14 Jan 2025 21:01:33 +0100 Subject: [PATCH 38/50] remove duplicated test --- tests/test-cases/core.yaml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests/test-cases/core.yaml b/tests/test-cases/core.yaml index 56592a34b..75404a120 100644 --- a/tests/test-cases/core.yaml +++ b/tests/test-cases/core.yaml @@ -37,19 +37,6 @@ tests: exit_code: 0 stderr: - "^$" - - - name: atmos non-existent - enabled: true - description: "Ensure atmos CLI returns an error for a non-existent command." - workdir: "../" - command: "atmos" - args: - - "non-existent" - expect: - stderr: - - 'Error: Unkown command \"non-existent\" for \"atmos\"' - exit_code: 1 - - name: atmos terraform non-existent enabled: false description: "Ensure atmos CLI returns an error for a non-existent command." From db28636775b9385dbce38e33adbcf74910c3e475 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Wed, 15 Jan 2025 21:36:31 +0100 Subject: [PATCH 39/50] use existing theme color package --- cmd/colored/colored.go | 99 +++++++++--------------------------------- cmd/root.go | 7 +-- pkg/ui/theme/colors.go | 20 +++++++++ 3 files changed, 41 insertions(+), 85 deletions(-) diff --git a/cmd/colored/colored.go b/cmd/colored/colored.go index 381ff7212..e50ccbdda 100644 --- a/cmd/colored/colored.go +++ b/cmd/colored/colored.go @@ -29,6 +29,7 @@ import ( "regexp" "strings" + "github.com/cloudposse/atmos/pkg/ui/theme" "github.com/fatih/color" "github.com/spf13/cobra" ) @@ -51,15 +52,6 @@ import ( // } type Config struct { RootCmd *cobra.Command - Headings uint8 - Commands uint8 - CmdShortDescr uint8 - ExecName uint8 - Flags uint8 - FlagsDataType uint8 - FlagsDescr uint8 - Aliases uint8 - Example uint8 NoExtraNewlines bool NoBottomNewline bool } @@ -100,7 +92,7 @@ func Init(cfg *Config) { // // Add extra line breaks for headings // - if cfg.NoExtraNewlines == false { + if !cfg.NoExtraNewlines { tpl = strings.NewReplacer( "Use \"", "\nUse \"", ).Replace(tpl) @@ -109,8 +101,8 @@ func Init(cfg *Config) { // // Styling headers // - if cfg.Headings != None { - ch := getColor(cfg.Headings) + if theme.Styles.Help.Headings != nil { + ch := theme.Styles.Help.Headings // Add template function to style the headers cobra.AddTemplateFunc("HeadingStyle", ch.SprintFunc()) } @@ -118,8 +110,8 @@ func Init(cfg *Config) { // // Styling commands // - if cfg.Commands != None { - cc := getColor(cfg.Commands) + if theme.Styles.Help.Commands != nil { + cc := theme.Styles.Help.Commands // Add template function to style commands cobra.AddTemplateFunc("CommandStyle", cc.SprintFunc()) @@ -138,8 +130,8 @@ func Init(cfg *Config) { // // Styling a short desription of commands // - if cfg.CmdShortDescr != None { - csd := getColor(cfg.CmdShortDescr) + if theme.Styles.Help.CmdShortDescr != nil { + csd := theme.Styles.Help.CmdShortDescr cobra.AddTemplateFunc("CmdShortStyle", csd.SprintFunc()) @@ -150,8 +142,8 @@ func Init(cfg *Config) { // // Styling executable file name // - if cfg.ExecName != None { - cen := getColor(cfg.ExecName) + if theme.Styles.Help.ExecName != nil { + cen := theme.Styles.Help.ExecName // Add template functions cobra.AddTemplateFunc("ExecStyle", cen.SprintFunc()) @@ -173,14 +165,14 @@ func Init(cfg *Config) { // Styling flags // var cf, cfd, cfdt *color.Color - if cfg.Flags != None { - cf = getColor(cfg.Flags) + if theme.Styles.Help.Flags != nil { + cf = theme.Styles.Help.Flags } - if cfg.FlagsDescr != None { - cfd = getColor(cfg.FlagsDescr) + if theme.Styles.Help.FlagsDescr != nil { + cfd = theme.Styles.Help.FlagsDescr } - if cfg.FlagsDataType != None { - cfdt = getColor(cfg.FlagsDataType) + if theme.Styles.Help.FlagsDataType != nil { + cfdt = theme.Styles.Help.FlagsDataType } if cf != nil || cfd != nil || cfdt != nil { @@ -242,8 +234,8 @@ func Init(cfg *Config) { // // Styling aliases // - if cfg.Aliases != None { - ca := getColor(cfg.Aliases) + if theme.Styles.Help.Aliases != nil { + ca := theme.Styles.Help.Aliases cobra.AddTemplateFunc("AliasStyle", ca.SprintFunc()) re := regexp.MustCompile(`(?i){{\s*.NameAndAliases\s*}}`) @@ -253,8 +245,8 @@ func Init(cfg *Config) { // // Styling the example text // - if cfg.Example != None { - ce := getColor(cfg.Example) + if theme.Styles.Help.Example != nil { + ce := theme.Styles.Help.Example cobra.AddTemplateFunc("ExampleStyle", ce.SprintFunc()) re := regexp.MustCompile(`(?i){{\s*.Example\s*}}`) @@ -270,54 +262,3 @@ func Init(cfg *Config) { // Debug line, uncomment when needed // fmt.Println(tpl) } - -// getColor decodes color param and returns color.Color object -func getColor(param uint8) (c *color.Color) { - - switch param & 15 { - case None: - c = color.New(color.FgWhite) - case Black: - c = color.New(color.FgBlack) - case Red: - c = color.New(color.FgRed) - case Green: - c = color.New(color.FgGreen) - case Yellow: - c = color.New(color.FgYellow) - case Blue: - c = color.New(color.FgBlue) - case Magenta: - c = color.New(color.FgMagenta) - case Cyan: - c = color.New(color.FgCyan) - case White: - c = color.New(color.FgWhite) - case HiRed: - c = color.New(color.FgHiRed) - case HiGreen: - c = color.New(color.FgHiGreen) - case HiYellow: - c = color.New(color.FgHiYellow) - case HiBlue: - c = color.New(color.FgHiBlue) - case HiMagenta: - c = color.New(color.FgHiMagenta) - case HiCyan: - c = color.New(color.FgHiCyan) - case HiWhite: - c = color.New(color.FgHiWhite) - } - - if param&Bold == Bold { - c.Add(color.Bold) - } - if param&Italic == Italic { - c.Add(color.Italic) - } - if param&Underline == Underline { - c.Add(color.Underline) - } - - return -} diff --git a/cmd/root.go b/cmd/root.go index 65341d9a0..5dbe1d57d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -66,12 +66,7 @@ var RootCmd = &cobra.Command{ // This is called by main.main(). It only needs to happen once to the RootCmd. func Execute() error { colored.Init(&colored.Config{ - RootCmd: RootCmd, - Headings: colored.HiCyan + colored.Bold + colored.Underline, - Commands: colored.HiGreen + colored.Bold, - Example: colored.Italic, - ExecName: colored.Bold, - Flags: colored.Bold, + RootCmd: RootCmd, }) // InitCliConfig finds and merges CLI configurations in the following order: diff --git a/pkg/ui/theme/colors.go b/pkg/ui/theme/colors.go index aba957436..1e7fe1115 100644 --- a/pkg/ui/theme/colors.go +++ b/pkg/ui/theme/colors.go @@ -21,6 +21,18 @@ const ( ColorBorder = "#5F5FD7" // UI borders ) +type HelpStyle struct { + Headings *color.Color + Commands *color.Color + Example *color.Color + ExecName *color.Color + Flags *color.Color + CmdShortDescr *color.Color + FlagsDescr *color.Color + FlagsDataType *color.Color + Aliases *color.Color +} + // Styles provides pre-configured lipgloss styles for common UI elements var Styles = struct { VersionNumber lipgloss.Style @@ -34,6 +46,7 @@ var Styles = struct { CommandName lipgloss.Style Description lipgloss.Style Border lipgloss.Style + Help HelpStyle }{ VersionNumber: lipgloss.NewStyle().Foreground(lipgloss.Color(ColorGray)), NewVersion: lipgloss.NewStyle().Foreground(lipgloss.Color(ColorGreen)), @@ -46,6 +59,13 @@ var Styles = struct { CommandName: lipgloss.NewStyle().Foreground(lipgloss.Color(ColorGreen)), Description: lipgloss.NewStyle().Foreground(lipgloss.Color(ColorWhite)), Border: lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color(ColorBorder)), + Help: HelpStyle{ + Headings: color.New(color.FgHiCyan).Add(color.Bold).Add(color.Underline), + Commands: color.New(color.FgHiGreen).Add(color.Bold), + Example: color.New(color.Italic), + ExecName: color.New(color.Bold), + Flags: color.New(color.Bold), + }, } // Colors provides color.Attribute mappings for the old color.New style From 27d6ae042c1574b08e16a1f2b115828e314db8a9 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Wed, 15 Jan 2025 21:36:31 +0100 Subject: [PATCH 40/50] use existing theme color package --- cmd/colored/colored.go | 129 +++++++---------------------------------- cmd/root.go | 7 +-- pkg/ui/theme/colors.go | 20 +++++++ 3 files changed, 41 insertions(+), 115 deletions(-) diff --git a/cmd/colored/colored.go b/cmd/colored/colored.go index 381ff7212..07f447e75 100644 --- a/cmd/colored/colored.go +++ b/cmd/colored/colored.go @@ -29,6 +29,7 @@ import ( "regexp" "strings" + "github.com/cloudposse/atmos/pkg/ui/theme" "github.com/fatih/color" "github.com/spf13/cobra" ) @@ -41,52 +42,13 @@ import ( // // c := &cc.Config{ // RootCmd: rootCmd, -// Headings: cc.HiWhite + cc.Bold + cc.Underline, -// Commands: cc.Yellow + cc.Bold, -// CmdShortDescr: cc.Cyan, -// ExecName: cc.Bold, -// Flags: cc.Bold, -// Aliases: cc.Bold, -// Example: cc.Italic, // } type Config struct { RootCmd *cobra.Command - Headings uint8 - Commands uint8 - CmdShortDescr uint8 - ExecName uint8 - Flags uint8 - FlagsDataType uint8 - FlagsDescr uint8 - Aliases uint8 - Example uint8 NoExtraNewlines bool NoBottomNewline bool } -// Constants for colors and B, I, U -const ( - None = 0 - Black = 1 - Red = 2 - Green = 3 - Yellow = 4 - Blue = 5 - Magenta = 6 - Cyan = 7 - White = 8 - HiRed = 9 - HiGreen = 10 - HiYellow = 11 - HiBlue = 12 - HiMagenta = 13 - HiCyan = 14 - HiWhite = 15 - Bold = 16 - Italic = 32 - Underline = 64 -) - // Init patches Cobra's usage template with configuration provided. func Init(cfg *Config) { @@ -100,7 +62,7 @@ func Init(cfg *Config) { // // Add extra line breaks for headings // - if cfg.NoExtraNewlines == false { + if !cfg.NoExtraNewlines { tpl = strings.NewReplacer( "Use \"", "\nUse \"", ).Replace(tpl) @@ -109,8 +71,8 @@ func Init(cfg *Config) { // // Styling headers // - if cfg.Headings != None { - ch := getColor(cfg.Headings) + if theme.Styles.Help.Headings != nil { + ch := theme.Styles.Help.Headings // Add template function to style the headers cobra.AddTemplateFunc("HeadingStyle", ch.SprintFunc()) } @@ -118,8 +80,8 @@ func Init(cfg *Config) { // // Styling commands // - if cfg.Commands != None { - cc := getColor(cfg.Commands) + if theme.Styles.Help.Commands != nil { + cc := theme.Styles.Help.Commands // Add template function to style commands cobra.AddTemplateFunc("CommandStyle", cc.SprintFunc()) @@ -138,8 +100,8 @@ func Init(cfg *Config) { // // Styling a short desription of commands // - if cfg.CmdShortDescr != None { - csd := getColor(cfg.CmdShortDescr) + if theme.Styles.Help.CmdShortDescr != nil { + csd := theme.Styles.Help.CmdShortDescr cobra.AddTemplateFunc("CmdShortStyle", csd.SprintFunc()) @@ -150,8 +112,8 @@ func Init(cfg *Config) { // // Styling executable file name // - if cfg.ExecName != None { - cen := getColor(cfg.ExecName) + if theme.Styles.Help.ExecName != nil { + cen := theme.Styles.Help.ExecName // Add template functions cobra.AddTemplateFunc("ExecStyle", cen.SprintFunc()) @@ -173,14 +135,14 @@ func Init(cfg *Config) { // Styling flags // var cf, cfd, cfdt *color.Color - if cfg.Flags != None { - cf = getColor(cfg.Flags) + if theme.Styles.Help.Flags != nil { + cf = theme.Styles.Help.Flags } - if cfg.FlagsDescr != None { - cfd = getColor(cfg.FlagsDescr) + if theme.Styles.Help.FlagsDescr != nil { + cfd = theme.Styles.Help.FlagsDescr } - if cfg.FlagsDataType != None { - cfdt = getColor(cfg.FlagsDataType) + if theme.Styles.Help.FlagsDataType != nil { + cfdt = theme.Styles.Help.FlagsDataType } if cf != nil || cfd != nil || cfdt != nil { @@ -242,8 +204,8 @@ func Init(cfg *Config) { // // Styling aliases // - if cfg.Aliases != None { - ca := getColor(cfg.Aliases) + if theme.Styles.Help.Aliases != nil { + ca := theme.Styles.Help.Aliases cobra.AddTemplateFunc("AliasStyle", ca.SprintFunc()) re := regexp.MustCompile(`(?i){{\s*.NameAndAliases\s*}}`) @@ -253,8 +215,8 @@ func Init(cfg *Config) { // // Styling the example text // - if cfg.Example != None { - ce := getColor(cfg.Example) + if theme.Styles.Help.Example != nil { + ce := theme.Styles.Help.Example cobra.AddTemplateFunc("ExampleStyle", ce.SprintFunc()) re := regexp.MustCompile(`(?i){{\s*.Example\s*}}`) @@ -270,54 +232,3 @@ func Init(cfg *Config) { // Debug line, uncomment when needed // fmt.Println(tpl) } - -// getColor decodes color param and returns color.Color object -func getColor(param uint8) (c *color.Color) { - - switch param & 15 { - case None: - c = color.New(color.FgWhite) - case Black: - c = color.New(color.FgBlack) - case Red: - c = color.New(color.FgRed) - case Green: - c = color.New(color.FgGreen) - case Yellow: - c = color.New(color.FgYellow) - case Blue: - c = color.New(color.FgBlue) - case Magenta: - c = color.New(color.FgMagenta) - case Cyan: - c = color.New(color.FgCyan) - case White: - c = color.New(color.FgWhite) - case HiRed: - c = color.New(color.FgHiRed) - case HiGreen: - c = color.New(color.FgHiGreen) - case HiYellow: - c = color.New(color.FgHiYellow) - case HiBlue: - c = color.New(color.FgHiBlue) - case HiMagenta: - c = color.New(color.FgHiMagenta) - case HiCyan: - c = color.New(color.FgHiCyan) - case HiWhite: - c = color.New(color.FgHiWhite) - } - - if param&Bold == Bold { - c.Add(color.Bold) - } - if param&Italic == Italic { - c.Add(color.Italic) - } - if param&Underline == Underline { - c.Add(color.Underline) - } - - return -} diff --git a/cmd/root.go b/cmd/root.go index 65341d9a0..5dbe1d57d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -66,12 +66,7 @@ var RootCmd = &cobra.Command{ // This is called by main.main(). It only needs to happen once to the RootCmd. func Execute() error { colored.Init(&colored.Config{ - RootCmd: RootCmd, - Headings: colored.HiCyan + colored.Bold + colored.Underline, - Commands: colored.HiGreen + colored.Bold, - Example: colored.Italic, - ExecName: colored.Bold, - Flags: colored.Bold, + RootCmd: RootCmd, }) // InitCliConfig finds and merges CLI configurations in the following order: diff --git a/pkg/ui/theme/colors.go b/pkg/ui/theme/colors.go index aba957436..1e7fe1115 100644 --- a/pkg/ui/theme/colors.go +++ b/pkg/ui/theme/colors.go @@ -21,6 +21,18 @@ const ( ColorBorder = "#5F5FD7" // UI borders ) +type HelpStyle struct { + Headings *color.Color + Commands *color.Color + Example *color.Color + ExecName *color.Color + Flags *color.Color + CmdShortDescr *color.Color + FlagsDescr *color.Color + FlagsDataType *color.Color + Aliases *color.Color +} + // Styles provides pre-configured lipgloss styles for common UI elements var Styles = struct { VersionNumber lipgloss.Style @@ -34,6 +46,7 @@ var Styles = struct { CommandName lipgloss.Style Description lipgloss.Style Border lipgloss.Style + Help HelpStyle }{ VersionNumber: lipgloss.NewStyle().Foreground(lipgloss.Color(ColorGray)), NewVersion: lipgloss.NewStyle().Foreground(lipgloss.Color(ColorGreen)), @@ -46,6 +59,13 @@ var Styles = struct { CommandName: lipgloss.NewStyle().Foreground(lipgloss.Color(ColorGreen)), Description: lipgloss.NewStyle().Foreground(lipgloss.Color(ColorWhite)), Border: lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color(ColorBorder)), + Help: HelpStyle{ + Headings: color.New(color.FgHiCyan).Add(color.Bold).Add(color.Underline), + Commands: color.New(color.FgHiGreen).Add(color.Bold), + Example: color.New(color.Italic), + ExecName: color.New(color.Bold), + Flags: color.New(color.Bold), + }, } // Colors provides color.Attribute mappings for the old color.New style From 4780d79738ae4a075a774f45965d2cc0c5a84ca9 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Wed, 15 Jan 2025 22:31:39 +0100 Subject: [PATCH 41/50] Update cmd/cmd_utils.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- cmd/cmd_utils.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cmd/cmd_utils.go b/cmd/cmd_utils.go index c5cfc9117..b718d73fd 100644 --- a/cmd/cmd_utils.go +++ b/cmd/cmd_utils.go @@ -556,13 +556,11 @@ func handleHelpRequest(cmd *cobra.Command, args []string) { func showUsageAndExit(cmd *cobra.Command, args []string) { var suggestions []string - var subCommand string = "" unknownCommand := fmt.Sprintf("Error: Unknown command: %q", cmd.CommandPath()) if len(args) > 0 { suggestions = cmd.SuggestionsFor(args[0]) - subCommand = args[0] - unknownCommand = fmt.Sprintf(`Error: Unknown command %q for %q`+"\n", subCommand, cmd.CommandPath()) + unknownCommand = fmt.Sprintf(`Error: Unknown command %q for %q`+"\n", args[0], cmd.CommandPath()) } if len(suggestions) > 0 { u.PrintMessage(fmt.Sprintf("%s\n\nDid you mean this?", unknownCommand)) From bd91befe2449afafe6049954cd3c1ff2e6f00617 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Wed, 15 Jan 2025 23:09:08 +0100 Subject: [PATCH 42/50] Fix error message ux --- cmd/cmd_utils.go | 12 ++++++------ pkg/utils/log_utils.go | 5 +++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/cmd/cmd_utils.go b/cmd/cmd_utils.go index c5cfc9117..4b58926a6 100644 --- a/cmd/cmd_utils.go +++ b/cmd/cmd_utils.go @@ -557,15 +557,16 @@ func showUsageAndExit(cmd *cobra.Command, args []string) { var suggestions []string var subCommand string = "" - unknownCommand := fmt.Sprintf("Error: Unknown command: %q", cmd.CommandPath()) + unknownCommand := fmt.Sprintf("Error: Unknown command: %q\n\n", cmd.CommandPath()) if len(args) > 0 { suggestions = cmd.SuggestionsFor(args[0]) subCommand = args[0] - unknownCommand = fmt.Sprintf(`Error: Unknown command %q for %q`+"\n", subCommand, cmd.CommandPath()) + unknownCommand = fmt.Sprintf("Error: Unknown command %q for %q\n\n", subCommand, cmd.CommandPath()) } + u.PrintErrorInColor(unknownCommand) if len(suggestions) > 0 { - u.PrintMessage(fmt.Sprintf("%s\n\nDid you mean this?", unknownCommand)) + u.PrintMessage("Did you mean this?") for _, suggestion := range suggestions { u.PrintMessage(fmt.Sprintf(" %s\n", suggestion)) } @@ -575,7 +576,6 @@ func showUsageAndExit(cmd *cobra.Command, args []string) { for _, subCmd := range cmd.Commands() { validSubcommands = append(validSubcommands, subCmd.Name()) } - u.PrintMessage(unknownCommand) if len(validSubcommands) > 0 { u.PrintMessage("Valid subcommands are:") for _, sub := range validSubcommands { @@ -585,8 +585,8 @@ func showUsageAndExit(cmd *cobra.Command, args []string) { u.PrintMessage("No valid subcommands found") } } - u.PrintMessage(fmt.Sprintf(`Run '%s --help' for usage`, cmd.CommandPath())) - u.LogErrorAndExit(atmosConfig, errors.New(unknownCommand)) + u.PrintMessage(fmt.Sprintf("\nRun '%s --help' for usage", cmd.CommandPath())) + os.Exit(1) } // getConfigAndStacksInfo gets the diff --git a/pkg/utils/log_utils.go b/pkg/utils/log_utils.go index af3007f18..3f38349af 100644 --- a/pkg/utils/log_utils.go +++ b/pkg/utils/log_utils.go @@ -29,6 +29,11 @@ func PrintMessageInColor(message string, messageColor *color.Color) { _, _ = messageColor.Fprint(os.Stdout, message) } +func PrintErrorInColor(message string) { + messageColor := theme.Colors.Error + _, _ = messageColor.Fprint(os.Stderr, message) +} + // LogErrorAndExit logs errors to std.Error and exits with an error code func LogErrorAndExit(atmosConfig schema.AtmosConfiguration, err error) { if err != nil { From f1e2b9b8ff3f3a392fbcb910039d8a7693d43ba3 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Wed, 15 Jan 2025 23:29:06 +0100 Subject: [PATCH 43/50] fix template --- internal/tui/templates/base_template.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/tui/templates/base_template.go b/internal/tui/templates/base_template.go index 85fbabedf..a4d648f2e 100644 --- a/internal/tui/templates/base_template.go +++ b/internal/tui/templates/base_template.go @@ -88,7 +88,7 @@ func getSection(section HelpTemplateSections) string { {{HeadingStyle "Usage:"}} {{if .Runnable}} {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} - {{.CommandPath}} [command]{{end}}` + {{.CommandPath}} [sub-command] [flags]{{end}}` case DoubleDashHelp: return ` From 36357af300658ace31ce25820156a340a7e2ef1b Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Wed, 15 Jan 2025 23:52:55 +0100 Subject: [PATCH 44/50] fix usage test case for atmos terraform --- tests/test-cases/help-and-usage.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test-cases/help-and-usage.yaml b/tests/test-cases/help-and-usage.yaml index ae4ecce8b..cc0e4162b 100644 --- a/tests/test-cases/help-and-usage.yaml +++ b/tests/test-cases/help-and-usage.yaml @@ -64,7 +64,6 @@ tests: - "terraform" expect: stdout: - - "Error: Unknown command: \"atmos terraform\"" - "Valid subcommands are:" - "apply" - "clean" From 00c43578bd07a4e67543d102b4c232769fb7bc8d Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Thu, 16 Jan 2025 00:04:31 +0100 Subject: [PATCH 45/50] fix atlantis help --- cmd/atlantis_generate_repo_config.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/atlantis_generate_repo_config.go b/cmd/atlantis_generate_repo_config.go index bbab3cafd..65f6d253e 100644 --- a/cmd/atlantis_generate_repo_config.go +++ b/cmd/atlantis_generate_repo_config.go @@ -16,6 +16,7 @@ var atlantisGenerateRepoConfigCmd = &cobra.Command{ FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args) // Check Atmos configuration checkAtmosConfig() err := e.ExecuteAtlantisGenerateRepoConfigCmd(cmd, args) From 8fded4b3b45857463e830fac66bbdf8614c52a97 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Thu, 16 Jan 2025 00:08:10 +0100 Subject: [PATCH 46/50] Add more test cases for atlantis --- tests/test-cases/help-and-usage.yaml | 36 ++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/test-cases/help-and-usage.yaml b/tests/test-cases/help-and-usage.yaml index cc0e4162b..060e8adbc 100644 --- a/tests/test-cases/help-and-usage.yaml +++ b/tests/test-cases/help-and-usage.yaml @@ -395,3 +395,39 @@ tests: stderr: - "^$" exit_code: 1 + - name: atmos atlantis generate repo-config help + enabled: true + description: "Should show help for 'atmos atlantis generate repo-config help'" + workdir: "../" + command: "atmos" + args: + - "atlantis" + - "generate" + - "repo-config" + - "help" + expect: + stdout: + - "Flags:" + - "--affected-only" + - "--config-template" + stderr: + - "^$" + exit_code: 0 + - name: atmos atlantis generate repo-config --help + enabled: true + description: "Should show help for 'atmos atlantis generate repo-config --help'" + workdir: "../" + command: "atmos" + args: + - "atlantis" + - "generate" + - "repo-config" + - "--help" + expect: + stdout: + - "Flags:" + - "--affected-only" + - "--config-template" + stderr: + - "^$" + exit_code: 0 From c3a6b6e5f5fe861009f9f7fc282edcd222279b5b Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Thu, 16 Jan 2025 01:17:43 +0100 Subject: [PATCH 47/50] fix atlantis command help and usage --- cmd/atlantis_generate_repo_config.go | 4 +++- tests/test-cases/help-and-usage.yaml | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/cmd/atlantis_generate_repo_config.go b/cmd/atlantis_generate_repo_config.go index 65f6d253e..47e29a64b 100644 --- a/cmd/atlantis_generate_repo_config.go +++ b/cmd/atlantis_generate_repo_config.go @@ -14,9 +14,11 @@ var atlantisGenerateRepoConfigCmd = &cobra.Command{ Short: "Generate repository configuration for Atlantis", Long: "Generate the repository configuration file required for Atlantis to manage Terraform repositories.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, - Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { handleHelpRequest(cmd, args) + if len(args) > 0 { + showUsageAndExit(cmd, args) + } // Check Atmos configuration checkAtmosConfig() err := e.ExecuteAtlantisGenerateRepoConfigCmd(cmd, args) diff --git a/tests/test-cases/help-and-usage.yaml b/tests/test-cases/help-and-usage.yaml index 060e8adbc..88b89489d 100644 --- a/tests/test-cases/help-and-usage.yaml +++ b/tests/test-cases/help-and-usage.yaml @@ -431,3 +431,17 @@ tests: stderr: - "^$" exit_code: 0 + - name: atmos atlantis generate repo-config non-existant + enabled: true + description: "Should show usage for 'atmos atlantis generate repo-config non-existant'" + workdir: "../" + command: "atmos" + args: + - "atlantis" + - "generate" + - "repo-config" + - "non-existant" + expect: + stderr: + - "Error: Unknown command \"non-existant\" for \"atmos atlantis generate repo-config\"" + exit_code: 1 From 33bdded9839dfb7a3bf0fddf4581ff541cbca50d Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Thu, 16 Jan 2025 01:17:43 +0100 Subject: [PATCH 48/50] fix atlantis command help and usage --- cmd/atlantis_generate_repo_config.go | 4 +++- tests/test-cases/help-and-usage.yaml | 34 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/cmd/atlantis_generate_repo_config.go b/cmd/atlantis_generate_repo_config.go index 65f6d253e..47e29a64b 100644 --- a/cmd/atlantis_generate_repo_config.go +++ b/cmd/atlantis_generate_repo_config.go @@ -14,9 +14,11 @@ var atlantisGenerateRepoConfigCmd = &cobra.Command{ Short: "Generate repository configuration for Atlantis", Long: "Generate the repository configuration file required for Atlantis to manage Terraform repositories.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, - Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { handleHelpRequest(cmd, args) + if len(args) > 0 { + showUsageAndExit(cmd, args) + } // Check Atmos configuration checkAtmosConfig() err := e.ExecuteAtlantisGenerateRepoConfigCmd(cmd, args) diff --git a/tests/test-cases/help-and-usage.yaml b/tests/test-cases/help-and-usage.yaml index 060e8adbc..c209133b8 100644 --- a/tests/test-cases/help-and-usage.yaml +++ b/tests/test-cases/help-and-usage.yaml @@ -114,7 +114,7 @@ tests: stdout: - "\\bUsage\\b" - "Flags:" - - "and the beginning of additional native arguments and flags for the specific command bei" + - "and the beginning of additional native arguments and flags for the specific command being run" stderr: - "^$" exit_code: 0 @@ -131,7 +131,7 @@ tests: stdout: - "\\bUsage\\b" - "Flags:" - - "and the beginning of additional native arguments and flags for the specific command bei" + - "and the beginning of additional native arguments and flags for the specific command being run" stderr: - "^$" exit_code: 0 @@ -233,21 +233,7 @@ tests: - "apply" - "destroy" exit_code: 0 - - name: atmos helmfile apply - enabled: true - description: "Should show error in non atmos workspace" - workdir: "../" - command: "atmos" - args: - - "helmfile" - - "apply" - - "non-existent" - expect: - stdout: - - "CLI config file specifies the directory for Atmos stack" - - "Quick Start" - exit_code: 1 - - name: atmos helmfile apply + - name: atmos helmfile apply non-existant enabled: true description: "Should show error in non atmos workspace" workdir: "../" @@ -431,3 +417,17 @@ tests: stderr: - "^$" exit_code: 0 + - name: atmos atlantis generate repo-config non-existant + enabled: true + description: "Should show usage for 'atmos atlantis generate repo-config non-existant'" + workdir: "../" + command: "atmos" + args: + - "atlantis" + - "generate" + - "repo-config" + - "non-existant" + expect: + stderr: + - "Error: Unknown command \"non-existant\" for \"atmos atlantis generate repo-config\"" + exit_code: 1 From ac22b726970529b5c653c093fee4ef34ee9c56ae Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Sat, 18 Jan 2025 15:51:54 +0100 Subject: [PATCH 49/50] fix test and update golden screenshots --- go.mod | 1 - go.sum | 2 -- internal/tui/templates/templater.go | 6 +++--- ...ands_check_atmos_--help_in_empty-dir.stdout.golden | 11 ++++++++--- tests/test-cases/empty-dir.yaml | 4 ++-- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index c13309fea..3b42ab9f1 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,6 @@ require ( github.com/hashicorp/terraform-config-inspect v0.0.0-20241129133400-c404f8227ea6 github.com/hashicorp/terraform-exec v0.21.0 github.com/hexops/gotextdiff v1.0.3 - github.com/ivanpirog/coloredcobra v1.0.1 github.com/jfrog/jfrog-client-go v1.49.0 github.com/json-iterator/go v1.1.12 github.com/jwalton/go-supportscolor v1.2.0 diff --git a/go.sum b/go.sum index 217013264..98e6a450a 100644 --- a/go.sum +++ b/go.sum @@ -916,8 +916,6 @@ github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= diff --git a/internal/tui/templates/templater.go b/internal/tui/templates/templater.go index aa9fefc06..f96ea3245 100644 --- a/internal/tui/templates/templater.go +++ b/internal/tui/templates/templater.go @@ -92,6 +92,9 @@ func formatCommands(cmds []*cobra.Command, listType string) string { // First pass: collect available commands and find max length cmds = filterCommands(cmds, listType == "subcommandAliases") for _, cmd := range cmds { + if v, ok := customHelpShortMessage[cmd.Name()]; ok { + cmd.Short = v + } switch listType { case "additionalHelpTopics": if cmd.IsAdditionalHelpTopicCommand() { @@ -110,9 +113,6 @@ func formatCommands(cmds []*cobra.Command, listType string) string { continue } case "subcommandAliases": - if v, ok := customHelpShortMessage[cmd.Name()]; ok { - cmd.Short = v - } if cmd.Annotations["nativeCommand"] == "true" { continue } diff --git a/tests/snapshots/TestCLICommands_check_atmos_--help_in_empty-dir.stdout.golden b/tests/snapshots/TestCLICommands_check_atmos_--help_in_empty-dir.stdout.golden index 485f99f73..49c614a05 100644 --- a/tests/snapshots/TestCLICommands_check_atmos_--help_in_empty-dir.stdout.golden +++ b/tests/snapshots/TestCLICommands_check_atmos_--help_in_empty-dir.stdout.golden @@ -3,7 +3,7 @@ Usage: atmos [flags] - atmos [command] + atmos [sub-command] [flags] Available Commands: @@ -25,6 +25,11 @@ Available Commands: version Display the version of Atmos you are running and check for updates workflow Run predefined tasks using workflows +SubCommand Aliases: + + hf Alias of "atmos helmfile" command + tf Alias of "atmos terraform" command + Flags: @@ -51,7 +56,7 @@ The '--' (double-dash) can be used to signify the end of Atmos-specific options and the beginning of additional native arguments and flags for the specific command being run. Example: - atmos atmos -s -- + atmos atmos [subcommand] -s -- -Use "atmos [command] --help" for more information about a command. +Use "atmos [subcommand] --help" for more information about a command. diff --git a/tests/test-cases/empty-dir.yaml b/tests/test-cases/empty-dir.yaml index f0e1eabaa..29c051d51 100644 --- a/tests/test-cases/empty-dir.yaml +++ b/tests/test-cases/empty-dir.yaml @@ -28,7 +28,7 @@ tests: - "https://atmos.tools/cli/configuration" stderr: - "^$" - exit_code: 0 + exit_code: 1 - name: check atmos --help in empty-dir enabled: true @@ -42,7 +42,7 @@ tests: stdout: - "Available Commands:" - "Flags:" - - 'Use "atmos \[command\] --help" for more information about a command\.' + - 'Use "atmos \[subcommand\] --help" for more information about a command\.' stderr: - "^$" exit_code: 0 From 9d5d4070dee774671f0c2b4dd5f4878b4416ea2a Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Sat, 18 Jan 2025 16:11:53 +0100 Subject: [PATCH 50/50] fix tests for atmos help --- .../TestCLICommands_atmos_--help.stdout.golden | 11 ++++++++--- tests/test-cases/log-level-validation.yaml | 6 +++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/snapshots/TestCLICommands_atmos_--help.stdout.golden b/tests/snapshots/TestCLICommands_atmos_--help.stdout.golden index 485f99f73..49c614a05 100644 --- a/tests/snapshots/TestCLICommands_atmos_--help.stdout.golden +++ b/tests/snapshots/TestCLICommands_atmos_--help.stdout.golden @@ -3,7 +3,7 @@ Usage: atmos [flags] - atmos [command] + atmos [sub-command] [flags] Available Commands: @@ -25,6 +25,11 @@ Available Commands: version Display the version of Atmos you are running and check for updates workflow Run predefined tasks using workflows +SubCommand Aliases: + + hf Alias of "atmos helmfile" command + tf Alias of "atmos terraform" command + Flags: @@ -51,7 +56,7 @@ The '--' (double-dash) can be used to signify the end of Atmos-specific options and the beginning of additional native arguments and flags for the specific command being run. Example: - atmos atmos -s -- + atmos atmos [subcommand] -s -- -Use "atmos [command] --help" for more information about a command. +Use "atmos [subcommand] --help" for more information about a command. diff --git a/tests/test-cases/log-level-validation.yaml b/tests/test-cases/log-level-validation.yaml index e2742dc6d..7d5b12e9c 100644 --- a/tests/test-cases/log-level-validation.yaml +++ b/tests/test-cases/log-level-validation.yaml @@ -45,7 +45,7 @@ tests: - -s - test expect: - exit_code: 0 + exit_code: 1 - name: "Valid Log Level in Environment Variable" enabled: true @@ -61,7 +61,7 @@ tests: env: ATMOS_LOGS_LEVEL: Debug expect: - exit_code: 0 + exit_code: 1 - name: "Valid Log Level in Command Line Flag" enabled: true @@ -77,4 +77,4 @@ tests: - -s - test expect: - exit_code: 0 + exit_code: 1