Skip to content

Commit

Permalink
Standardize Help and Usage content for all commands and sub commands (#…
Browse files Browse the repository at this point in the history
…914)

* Merge atmos specific and terraform help documentation

issue: https://linear.app/cloudposse/issue/DEV-2821/atmos-terraform-help-should-also-show-terraform-help

* Add help for terraform

issue: https://linear.app/cloudposse/issue/DEV-2821/atmos-terraform-help-should-also-show-terraform-help

* Updated help content

* test fix for auto approve

* Update message for invalid command

* remove old help code

* testing if auto-approve should be with two -

* check error after executing old usage func

* rebase help.go

* Fix terraform subcommand help

* Remove unwanted setHelpFunc

* terraform,helmfile empty sub command should redirect to help

* check errors for usage and help

* Added space in between Native commands

* Removed unwanted code

* Add consistent usage and help for all commands

isssue: https://linear.app/cloudposse/issue/DEV-2896/incorrect-output-for-atmos-validate

* Removed unwanted help check

* Fix atmos help

* Help should be in stdout terminal

* fix terraform args

* fixed terraform command

* fix helm command

* rearrange the help strings

* simplified the code

* Updated version with the help and usage as per expectations

* fix atmos command usage

* Fix atmos usage error assertion

* Add SubCommand Aliases:

* Updated template to be more dynamic

* use cobra.NoArgs instead

* fix atmos version command

* update atmos test case

* Fix help and add test cases

* added more test cases

* filter commands added

* fixed typo

* removed unwanted function

* remove duplicated test

* use existing theme color package

* use existing theme color package

* Update cmd/cmd_utils.go

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Fix error message ux

* fix template

* fix usage test case for atmos terraform

* fix atlantis help

* Add more test cases for atlantis

* fix atlantis command help and usage

* fix atlantis command help and usage

* fix test and update golden screenshots

* fix tests for atmos help

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
  • Loading branch information
samtholiya and coderabbitai[bot] authored Jan 18, 2025
1 parent 14bea36 commit b8f480f
Show file tree
Hide file tree
Showing 56 changed files with 1,134 additions and 288 deletions.
1 change: 1 addition & 0 deletions cmd/atlantis.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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() {
Expand Down
1 change: 1 addition & 0 deletions cmd/atlantis_generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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() {
Expand Down
5 changes: 4 additions & 1 deletion cmd/atlantis_generate_repo_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ 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)
if len(args) > 0 {
showUsageAndExit(cmd, args)
}
// Check Atmos configuration
checkAtmosConfig()

err := e.ExecuteAtlantisGenerateRepoConfigCmd(cmd, args)
if err != nil {
u.LogErrorAndExit(schema.AtmosConfiguration{}, err)
Expand Down
1 change: 1 addition & 0 deletions cmd/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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() {
Expand Down
1 change: 1 addition & 0 deletions cmd/aws_eks.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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() {
Expand Down
66 changes: 65 additions & 1 deletion cmd/cmd_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strings"
"time"

"github.com/samber/lo"
"github.com/spf13/cobra"

e "github.com/cloudposse/atmos/internal/exec"
Expand Down Expand Up @@ -441,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)
}
}
}
Expand Down Expand Up @@ -543,3 +544,66 @@ 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) {
if (len(args) > 0 && args[0] == "help") || Contains(args, "--help") || Contains(args, "-h") {
cmd.Help()
os.Exit(0)
}
}

func showUsageAndExit(cmd *cobra.Command, args []string) {

var suggestions []string
unknownCommand := fmt.Sprintf("Error: Unknown command: %q\n\n", cmd.CommandPath())

if len(args) > 0 {
suggestions = cmd.SuggestionsFor(args[0])
unknownCommand = fmt.Sprintf("Error: Unknown command %q for %q\n\n", args[0], cmd.CommandPath())
}
u.PrintErrorInColor(unknownCommand)
if len(suggestions) > 0 {
u.PrintMessage("Did you mean this?")
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())
}
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("\nRun '%s --help' for usage", cmd.CommandPath()))
os.Exit(1)
}

// 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
}
234 changes: 234 additions & 0 deletions cmd/colored/colored.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
// 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 <[email protected]>.
// Released under the MIT license.
// Project home: https://github.com/ivanpirog/coloredcobra
package colored

import (
"regexp"
"strings"

"github.com/cloudposse/atmos/pkg/ui/theme"
"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,
// }
type Config struct {
RootCmd *cobra.Command
NoExtraNewlines bool
NoBottomNewline bool
}

// 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 {
tpl = strings.NewReplacer(
"Use \"", "\nUse \"",
).Replace(tpl)
}

//
// Styling headers
//
if theme.Styles.Help.Headings != nil {
ch := theme.Styles.Help.Headings
// Add template function to style the headers
cobra.AddTemplateFunc("HeadingStyle", ch.SprintFunc())
}

//
// Styling commands
//
if theme.Styles.Help.Commands != nil {
cc := theme.Styles.Help.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 theme.Styles.Help.CmdShortDescr != nil {
csd := theme.Styles.Help.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 theme.Styles.Help.ExecName != nil {
cen := theme.Styles.Help.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 theme.Styles.Help.Flags != nil {
cf = theme.Styles.Help.Flags
}
if theme.Styles.Help.FlagsDescr != nil {
cfd = theme.Styles.Help.FlagsDescr
}
if theme.Styles.Help.FlagsDataType != nil {
cfdt = theme.Styles.Help.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 theme.Styles.Help.Aliases != nil {
ca := theme.Styles.Help.Aliases
cobra.AddTemplateFunc("AliasStyle", ca.SprintFunc())

re := regexp.MustCompile(`(?i){{\s*.NameAndAliases\s*}}`)
tpl = re.ReplaceAllLiteralString(tpl, "{{AliasStyle .NameAndAliases}}")
}

//
// Styling the example text
//
if theme.Styles.Help.Example != nil {
ce := theme.Styles.Help.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)
}
Loading

0 comments on commit b8f480f

Please sign in to comment.