Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions cmd/cmd_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
Expand Down Expand Up @@ -237,6 +238,9 @@ func processCommandAliases(
Short: aliasFor,
Long: aliasFor,
FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true},
Annotations: map[string]string{
"configAlias": aliasCmd, // marks as CLI config alias, stores target command
},
Run: func(cmd *cobra.Command, args []string) {
err := cmd.ParseFlags(args)
errUtils.CheckErrorPrintAndExit(err, "", "")
Expand All @@ -257,9 +261,28 @@ func processCommandAliases(
// from re-applying the parent's chdir directive.
filteredEnv := filterChdirEnv(os.Environ())

commandToRun := fmt.Sprintf("%s %s %s", execPath, aliasCmd, strings.Join(filteredArgs, " "))
err = e.ExecuteShell(commandToRun, commandToRun, currentDirPath, filteredEnv, false)
errUtils.CheckErrorPrintAndExit(err, "", "")
// Build command arguments: split aliasCmd into parts and append filteredArgs.
// Use direct process execution instead of shell to avoid path escaping
// issues on Windows where backslashes in paths are misinterpreted.
cmdArgs := strings.Fields(aliasCmd)
cmdArgs = append(cmdArgs, filteredArgs...)

execCmd := exec.Command(execPath, cmdArgs...)
execCmd.Dir = currentDirPath
execCmd.Env = filteredEnv
execCmd.Stdin = os.Stdin
execCmd.Stdout = os.Stdout
execCmd.Stderr = os.Stderr

err = execCmd.Run()
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
// Preserve the subprocess exit code.
err = errUtils.WithExitCode(err, exitErr.ExitCode())
}
errUtils.CheckErrorPrintAndExit(err, "", "")
}
},
}

Expand Down
164 changes: 164 additions & 0 deletions cmd/cmd_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1436,3 +1436,167 @@ func TestGetConfigAndStacksInfo_PathResolutionWithValidPath(t *testing.T) {
// If we got here, verify the path was resolved.
assert.Equal(t, "top-level-component1", info.ComponentFromArg, "Path should be resolved to component name")
}

// TestProcessCommandAliases tests the processCommandAliases function.
// NOTE: We use unique alias names (e.g., "test-alias-tp") to avoid conflicts
// with existing RootCmd commands, since processCommandAliases internally checks
// getTopLevelCommands() which reads from the global RootCmd.
func TestProcessCommandAliases(t *testing.T) {
tests := []struct {
name string
aliases schema.CommandAliases
topLevel bool
expectedAliases []string
}{
{
name: "single alias",
aliases: schema.CommandAliases{
"test-alias-tp": "terraform plan",
},
topLevel: true,
expectedAliases: []string{"test-alias-tp"},
},
{
name: "multiple aliases",
aliases: schema.CommandAliases{
"test-alias-tp": "terraform plan",
"test-alias-ta": "terraform apply",
"test-alias-td": "terraform destroy",
},
topLevel: true,
expectedAliases: []string{"test-alias-tp", "test-alias-ta", "test-alias-td"},
},
{
name: "empty aliases",
aliases: schema.CommandAliases{},
topLevel: true,
expectedAliases: []string{},
},
{
name: "alias with whitespace",
aliases: schema.CommandAliases{
" test-alias-ws ": " terraform plan ",
},
topLevel: true,
expectedAliases: []string{"test-alias-ws"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Use test kit to reset RootCmd state.
_ = NewTestKit(t)

// Create a parent command.
parentCmd := &cobra.Command{
Use: "atmos",
Short: "Test parent command",
}

// Process aliases.
err := processCommandAliases(schema.AtmosConfiguration{}, tt.aliases, parentCmd, tt.topLevel)
require.NoError(t, err)

// Verify the expected aliases were added.
for _, expectedAlias := range tt.expectedAliases {
found := false
for _, cmd := range parentCmd.Commands() {
if cmd.Name() != expectedAlias {
continue
}
found = true

// Verify the command has the configAlias annotation.
assert.NotNil(t, cmd.Annotations, "Alias command should have annotations")
_, hasConfigAlias := cmd.Annotations["configAlias"]
assert.True(t, hasConfigAlias, "Alias command should have configAlias annotation")

// Verify the Short description contains "alias for".
assert.Contains(t, cmd.Short, "alias for", "Alias command should have 'alias for' in Short")

// Verify DisableFlagParsing is true.
assert.True(t, cmd.DisableFlagParsing, "Alias command should have DisableFlagParsing=true")

break
}
assert.True(t, found, "Expected alias %q to be added to parent command", expectedAlias)
}
})
}
}

// TestProcessCommandAliases_DoesNotOverrideExistingCommands tests that aliases don't override existing RootCmd commands.
// NOTE: The processCommandAliases function checks getTopLevelCommands() which reads from global RootCmd,
// so we test that it doesn't add aliases that conflict with existing RootCmd commands like "version".
func TestProcessCommandAliases_DoesNotOverrideExistingCommands(t *testing.T) {
_ = NewTestKit(t)

// Create a parent command to receive aliases.
parentCmd := &cobra.Command{
Use: "atmos",
Short: "Test parent command",
}

// Try to add aliases - "version" should be skipped (exists in RootCmd),
// but "test-alias-new" should be added (unique name).
aliases := schema.CommandAliases{
"version": "terraform version", // Should NOT be added (exists in RootCmd).
"test-alias-new2": "terraform plan", // Should be added (unique name).
}

err := processCommandAliases(schema.AtmosConfiguration{}, aliases, parentCmd, true)
require.NoError(t, err)

// Verify "version" alias was NOT added to parentCmd (because it exists in RootCmd).
var versionFound bool
for _, cmd := range parentCmd.Commands() {
if cmd.Name() == "version" {
versionFound = true
break
}
}
assert.False(t, versionFound, "version alias should not be added because it conflicts with existing RootCmd command")

// Verify "test-alias-new2" alias was added.
var newAliasFound bool
for _, cmd := range parentCmd.Commands() {
if cmd.Name() == "test-alias-new2" {
newAliasFound = true
assert.Contains(t, cmd.Short, "alias for")
break
}
}
assert.True(t, newAliasFound, "test-alias-new2 should be added")
}

// TestProcessCommandAliases_NonTopLevel tests that non-top-level aliases are NOT added.
// The processCommandAliases function only adds aliases when topLevel=true.
func TestProcessCommandAliases_NonTopLevel(t *testing.T) {
_ = NewTestKit(t)

parentCmd := &cobra.Command{
Use: "terraform",
Short: "Terraform commands",
}

aliases := schema.CommandAliases{
"p": "plan",
"a": "apply",
}

// Process as non-top-level (topLevel=false).
err := processCommandAliases(schema.AtmosConfiguration{}, aliases, parentCmd, false)
require.NoError(t, err)

// Verify aliases were NOT added (because topLevel=false).
// The condition `!exist && topLevel` prevents alias creation when topLevel=false.
// Check directly in Commands() list since Find() returns parent on not found.
hasAliases := false
for _, cmd := range parentCmd.Commands() {
if cmd.Name() == "p" || cmd.Name() == "a" {
hasAliases = true
break
}
}
assert.False(t, hasAliases, "Non-top-level aliases should not be added")
}
55 changes: 53 additions & 2 deletions cmd/help_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,15 @@ func isCommandAvailable(cmd *cobra.Command) bool {
return cmd.IsAvailableCommand() || cmd.Name() == "help"
}

// isConfigAlias checks if a command is a CLI config alias.
func isConfigAlias(cmd *cobra.Command) bool {
if cmd.Annotations == nil {
return false
}
_, ok := cmd.Annotations["configAlias"]
return ok
}

// calculateCommandWidth calculates the display width of a command name including type suffix.
func calculateCommandWidth(cmd *cobra.Command) int {
width := len(cmd.Name())
Expand All @@ -490,10 +499,11 @@ func calculateCommandWidth(cmd *cobra.Command) int {
}

// calculateMaxCommandWidth finds the maximum command name width for alignment.
// Config aliases are excluded from this calculation since they're shown in a separate section.
func calculateMaxCommandWidth(commands []*cobra.Command) int {
maxWidth := 0
for _, c := range commands {
if !isCommandAvailable(c) {
if !isCommandAvailable(c) || isConfigAlias(c) {
continue
}
width := calculateCommandWidth(c)
Expand Down Expand Up @@ -564,14 +574,54 @@ func printAvailableCommands(ctx *helpRenderContext, cmd *cobra.Command) {
maxCmdWidth := calculateMaxCommandWidth(cmd.Commands())

for _, c := range cmd.Commands() {
if !isCommandAvailable(c) {
if !isCommandAvailable(c) || isConfigAlias(c) {
continue
}
formatCommandLine(ctx, c, maxCmdWidth)
}
fmt.Fprintln(ctx.writer)
}

// getConfigAliases returns all available config alias commands.
func getConfigAliases(cmd *cobra.Command) []*cobra.Command {
var aliases []*cobra.Command
for _, c := range cmd.Commands() {
if c.IsAvailableCommand() && isConfigAlias(c) {
aliases = append(aliases, c)
}
}
return aliases
}

// printConfigAliases prints CLI config aliases in a dedicated section.
func printConfigAliases(ctx *helpRenderContext, cmd *cobra.Command) {
defer perf.Track(nil, "cmd.printConfigAliases")()

aliases := getConfigAliases(cmd)
if len(aliases) == 0 {
return
}

fmt.Fprintln(ctx.writer, ctx.styles.heading.Render("ALIASES"))
fmt.Fprintln(ctx.writer)

// Calculate max width for alignment.
maxWidth := 0
for _, c := range aliases {
if len(c.Name()) > maxWidth {
maxWidth = len(c.Name())
}
}

for _, c := range aliases {
name := ctx.styles.commandName.Render(fmt.Sprintf("%-*s", maxWidth, c.Name()))
// c.Short contains "alias for `command`".
desc := renderMarkdownDescription(c.Short)
fmt.Fprintf(ctx.writer, " %s %s\n", name, desc)
}
fmt.Fprintln(ctx.writer)
}

// printFlags prints command flags.
func printFlags(w io.Writer, cmd *cobra.Command, atmosConfig *schema.AtmosConfiguration, styles *helpStyles) {
defer perf.Track(nil, "cmd.printFlags")()
Expand Down Expand Up @@ -642,6 +692,7 @@ func applyColoredHelpTemplate(cmd *cobra.Command) {
printSubcommandAliases(ctx, c)
printExamples(ctx.writer, c, ctx.renderer, ctx.styles)
printAvailableCommands(ctx, c)
printConfigAliases(ctx, c)
printFlags(ctx.writer, c, ctx.atmosConfig, ctx.styles)
printFooter(ctx.writer, c, ctx.styles)
})
Expand Down
Loading
Loading