Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: exec, run, info strict commands implementation, flags renaming. #3723

Merged
merged 180 commits into from
Feb 10, 2025

Conversation

levkohimins
Copy link
Contributor

@levkohimins levkohimins commented Jan 2, 2025

Description

  1. exec, run commands implementation.
  2. Sorting and renaming CLI flags.
  3. Automatic generation of environment variables.
  4. Code refactoring.

Relates to #3445 .

Fixes #3757.

TODOs

Read the Gruntwork contribution guidelines.

  • Update the docs.
  • Run the relevant tests successfully, including pre-commit checks.
  • Ensure any 3rd party code adheres with our license policy or delete this line if its not applicable.
  • Include release notes. If this PR is backward incompatible, include a migration guide.

Release Notes (draft)

Added / Removed / Updated [X].

Migration Guide

Summary by CodeRabbit

  • New Features

    • Introduced streamlined CLI commands (“run” and “exec”) to simplify command execution.
    • Simplified many command‐line flag names by removing legacy “terragrunt-” prefixes.
    • Added new experimental features and enhanced strict control mechanisms for better management of deprecated commands and flags.
  • Bug Fixes

    • Enhanced logging with improved format handling and more reliable error reporting.
    • Refined strict mode checks to provide clearer warnings and errors regarding deprecated features.
    • Improved error handling in command execution and context management.
  • Documentation

    • Updated documentation to reflect new flag names and configuration file conventions (e.g., using “root.hcl”).
    • Added migration guides to assist users in transitioning to the new CLI structure and flag naming conventions.
  • Tests

    • Improved integration and unit tests for increased reliability and consistency.
    • Enhanced tests for strict controls and command execution to ensure accurate behavior and error reporting.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (13)
internal/cli/slice_flag.go (1)

138-143: Consider adding validation for required fields

The struct looks good, but we might want to validate that values and valueType are not nil in the constructor to prevent potential panics later.

 func newSliceValue[T comparable](valueType FlagVariable[T], valSep string, dest *[]T, setter FlagSetterFunc[T]) *sliceValue[T] {
+	if dest == nil {
+		dest = &[]T{}
+	}
+	if valueType == nil {
+		panic("valueType cannot be nil")
+	}
 	return &sliceValue[T]{
 		values:    dest,
 		valueType: valueType,
internal/cli/generic_flag.go (5)

30-31: Enhance environment variables documentation

The documentation could be clearer about the order of precedence between flag values and environment variables. Let's make it more explicit.

-	// The names of the env variables that are parsed and assigned to `Destination` before the flag value.
+	// The names of environment variables that can set this flag's value. Multiple variables are checked in order,
+	// with the first non-empty value taking precedence. Flag values from command line override environment variables.

32-35: Clarify Action and Setter documentation

The documentation should explain the relationship between Action and Setter, and when each is called.

-	// Action is a function that is called when the flag is specified. It is executed only after all command flags have been parsed.
-	// Setter allows to set a value to any type by calling its `func(bool) error` function.
+	// Action is called after all command flags are parsed, allowing for validation or side effects.
+	// Example: func(ctx *Context, value T) error
+	
+	// Setter provides custom value setting logic, called immediately when the flag value is set.
+	// Example: func(value T) error

44-58: Improve error handling in Apply method

The error handling could be more robust by checking for nil values and providing context.

 func (flag *GenericFlag[T]) Apply(set *libflag.FlagSet) error {
+	if set == nil {
+		return errors.New("flag set cannot be nil")
+	}
+
 	if flag.FlagValue != nil {
 		return ApplyFlag(flag, set)
 	}

 	valueType := &genericVar[T]{dest: flag.Destination}
 	value := newGenericValue(valueType, flag.Setter)

 	flag.FlagValue = &flagValue{
 		value:            value,
 		initialTextValue: value.String(),
 	}

 	return ApplyFlag(flag, set)
 }

125-135: Add validation in Set method

Consider adding input validation before setting the value.

 func (flag *genericValue[T]) Set(str string) error {
+	if str == "" {
+		return errors.New("value cannot be empty")
+	}
+
 	if err := flag.value.Set(str); err != nil {
 		return err
 	}

 	if flag.setter != nil {
 		return flag.setter(flag.Get().(T))
 	}

 	return nil
 }

170-173: Improve error messages for better user experience

The error messages could be more helpful by providing valid value ranges and examples.

-			return errors.New(InvalidValueError{underlyingError: err, msg: `must be one of: "0", "1", "f", "t", "false", "true"`})
+			return errors.New(InvalidValueError{underlyingError: err, msg: `must be a boolean value (e.g., "true", "false", "1", "0")`})

-			return errors.New(InvalidValueError{underlyingError: err, msg: "must be 32-bit integer"})
+			return errors.New(InvalidValueError{underlyingError: err, msg: "must be a 32-bit integer (range: -2147483648 to 2147483647)"})

-			return errors.New(InvalidValueError{underlyingError: err, msg: "must be 32-bit unsigned integer"})
+			return errors.New(InvalidValueError{underlyingError: err, msg: "must be a positive 32-bit integer (range: 0 to 4294967295)"})

-			return errors.New(InvalidValueError{underlyingError: err, msg: "must be 64-bit integer"})
+			return errors.New(InvalidValueError{underlyingError: err, msg: "must be a 64-bit integer (range: -9223372036854775808 to 9223372036854775807)"})

Also applies to: 178-181, 186-189, 194-197

internal/cli/flag.go (4)

14-16: Consider clarifying the split delimiter usage.
Currently, FlagSplitter is set to strings.Split, but there's no explicit mention of how environment variables with certain delimiters (e.g., commas, semicolons) should be handled. Clarifying the expected delimiter pattern in the documentation or variable name might help future developers.


34-36: Tidy up the docstring.
There's a minor grammatical slip in "Executed after flag have been parsed and assigned...". Consider rephrasing to "Executed after the flag has been parsed and assigned..." for clarity.

-// Executed after flag have been parsed  and assigned to...
+// Executed after the flag has been parsed and assigned to...

57-60: Documentation mismatch.
The doc for IsArgSet still says “IsSet returns true...” rather than “IsArgSet returns true…”. Update the docstring to align with the actual function name and behavior.

-// IsSet returns true if the flag was by CLI arg.
+// IsArgSet returns true if the flag was set by CLI arg.

247-249: Unused helper?
SplitValue(val) always returns a single-element slice and doesn’t appear to be used. If it’s for future expansions, consider adding a TODO comment or leaving short usage notes so folks know its purpose.

internal/cli/map_flag.go (3)

31-62: Nice improvements to the MapFlag struct! 👍

The generic type constraints and new fields make the flag handling more flexible and type-safe. The documentation is clear and helpful.

Consider adding an example in the struct documentation showing how to use the new Setter field, as it's a powerful new feature that users might overlook.


125-128: Enhance GetEnvVars documentation

While the implementation is correct, let's make the documentation more helpful.

-// GetEnvVars implements `cli.Flag` interface.
+// GetEnvVars returns the list of environment variable names that can be used to set this flag's value.
+// These environment variables are checked in order before the flag value from command line.

185-208: Consider a small optimization in Set method

The implementation looks great! Here's a tiny optimization to consider:

 func (flag *mapValue[K, V]) Set(str string) error {
 	parts := flag.splitter(str, flag.valSep)
 	if len(parts) != flatPatsCount {
 		return errors.New(NewInvalidKeyValueError(flag.valSep, str))
 	}
+
+	// Trim spaces once and reuse
+	keyStr := strings.TrimSpace(parts[0])
+	valStr := strings.TrimSpace(parts[1])
 
 	key := flag.keyType.Clone(new(K))
-	if err := key.Set(strings.TrimSpace(parts[0])); err != nil {
+	if err := key.Set(keyStr); err != nil {
 		return err
 	}
 
 	val := flag.valType.Clone(new(V))
-	if err := val.Set(strings.TrimSpace(parts[1])); err != nil {
+	if err := val.Set(valStr); err != nil {
 		return err
 	}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3131c90 and 9bb185c.

📒 Files selected for processing (8)
  • cli/commands/run/command.go (1 hunks)
  • cli/help_test.go (1 hunks)
  • internal/cli/bool_flag.go (1 hunks)
  • internal/cli/flag.go (1 hunks)
  • internal/cli/generic_flag.go (5 hunks)
  • internal/cli/map_flag.go (7 hunks)
  • internal/cli/slice_flag.go (6 hunks)
  • test/integration_serial_test.go (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • cli/help_test.go
  • cli/commands/run/command.go
  • test/integration_serial_test.go
  • internal/cli/bool_flag.go
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Pull Request has non-contributor approval
  • GitHub Check: Pull Request has non-contributor approval
  • GitHub Check: build-and-test
  • GitHub Check: Pull Request has non-contributor approval
🔇 Additional comments (10)
internal/cli/slice_flag.go (5)

34-40: Nice improvements to the flag structure! 👍

Good changes here:

  • Renamed to EnvVars for clarity (plural makes more sense)
  • Well-documented new Setter field
  • More consistent action function type

80-82: Consider adding error handling for value initialization

The value initialization looks cleaner, but we might want to handle potential errors from newSliceValue. What happens if the value type initialization fails?


102-105: Clean and simple implementation! 🎯

Nice straightforward update to support multiple environment variables.


166-168: The nil check is redundant here

Since we're already checking if flag.setter != nil, we don't need additional nil handling. The code is safe as is! 👍


1-195: Overall excellent code organization! 🌟

The code is well-structured, follows Go conventions, and has clear documentation. The changes make the flag handling more robust while maintaining readability.

internal/cli/flag.go (3)

223-227: Verify boolean-only flags.
TakesValue() unconditionally returns true, but boolean flags typically don’t consume a value (like --verbose). Consider adding a condition to detect IsBoolFlag() to return the correct behavior.


242-245: Confirm if subcommand scoping is always intended.
AllowedSubcommandScope() consistently returns true, which might be fine, but double-check if you want certain flags restricted to top-level commands. This might help define more specific usage boundaries.


251-269: Empty environment variables are skipped—double-check reasons.
ApplyFlag() discards any empty environment variable values. If you ever want to allow empty values (like indicating a reset), consider adjusting your logic. If not, a short inline comment clarifying the rationale might help.

internal/cli/map_flag.go (2)

74-113: LGTM! Clean initialization and environment handling

The map initialization using make() and the new LookupEnvFunc implementation look great. The code is more robust and handles edge cases well.


160-183: Great work on the mapValue implementation! 🎯

The generic type constraints and the new Reset method make the code more robust and user-friendly. The type handling with FlagVariable is elegant.

Comment on lines 65 to 73
func (flag *MapFlag[K, V]) Apply(set *libflag.FlagSet) error {
if flag.FlagValue != nil {
return ApplyFlag(flag, set)
}

if flag.FlagValue != nil {
return ApplyFlag(flag, set)
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove duplicate code block

There's a duplicate check for flag.FlagValue that should be removed.

func (flag *MapFlag[K, V]) Apply(set *libflag.FlagSet) error {
	if flag.FlagValue != nil {
		return ApplyFlag(flag, set)
	}
-
-	if flag.FlagValue != nil {
-		return ApplyFlag(flag, set)
-	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (flag *MapFlag[K, V]) Apply(set *libflag.FlagSet) error {
if flag.FlagValue != nil {
return ApplyFlag(flag, set)
}
if flag.FlagValue != nil {
return ApplyFlag(flag, set)
}
func (flag *MapFlag[K, V]) Apply(set *libflag.FlagSet) error {
if flag.FlagValue != nil {
return ApplyFlag(flag, set)
}
// (Other code, if any, would follow here)
}

@levkohimins
Copy link
Contributor Author

@coderabbitai util/writer_notifier.go and generate unit testing code.

Copy link
Contributor

coderabbitai bot commented Feb 10, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@levkohimins Here are the unit tests for the WriterNotifier implementation:

package util

import (
	"bytes"
	"errors"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

type mockWriter struct {
	written []byte
	err     error
}

func (w *mockWriter) Write(p []byte) (n int, err error) {
	if w.err != nil {
		return 0, w.err
	}
	w.written = append(w.written, p...)
	return len(p), nil
}

func TestWriterNotifier_Write(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name           string
		writer        *mockWriter
		input         []byte
		expectedBytes int
		expectedErr   error
		notifyCalled  bool
	}{
		{
			name:           "successful write",
			writer:         &mockWriter{},
			input:         []byte("test data"),
			expectedBytes: 9,
			expectedErr:   nil,
			notifyCalled:  true,
		},
		{
			name:           "write error",
			writer:         &mockWriter{err: errors.New("write error")},
			input:         []byte("test data"),
			expectedBytes: 0,
			expectedErr:   errors.New("write error"),
			notifyCalled:  false,
		},
		{
			name:           "empty write",
			writer:         &mockWriter{},
			input:         []byte{},
			expectedBytes: 0,
			expectedErr:   nil,
			notifyCalled:  false,
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			notified := false
			notifier := NewWriterNotifier(tt.writer, func() {
				notified = true
			})

			n, err := notifier.Write(tt.input)

			assert.Equal(t, tt.expectedBytes, n)
			if tt.expectedErr != nil {
				assert.Error(t, err)
				assert.Equal(t, tt.expectedErr.Error(), err.Error())
			} else {
				assert.NoError(t, err)
			}

			if tt.notifyCalled {
				assert.True(t, notified, "notify function should have been called")
				assert.Equal(t, tt.input, tt.writer.written)
			} else {
				assert.False(t, notified, "notify function should not have been called")
			}
		})
	}
}

func TestWriterNotifier_NilWriter(t *testing.T) {
	t.Parallel()

	notifier := NewWriterNotifier(nil, func() {})
	n, err := notifier.Write([]byte("test"))
	
	assert.Equal(t, 0, n)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "nil writer")
}

func TestWriterNotifier_NilNotify(t *testing.T) {
	t.Parallel()

	buf := &bytes.Buffer{}
	notifier := NewWriterNotifier(buf, nil)
	
	testData := []byte("test data")
	n, err := notifier.Write(testData)
	
	assert.Equal(t, len(testData), n)
	assert.NoError(t, err)
	assert.Equal(t, testData, buf.Bytes())
}

func TestWriterNotifier_MultipleWrites(t *testing.T) {
	t.Parallel()

	buf := &bytes.Buffer{}
	notifyCount := 0
	notifier := NewWriterNotifier(buf, func() {
		notifyCount++
	})

	writes := [][]byte{
		[]byte("first "),
		[]byte("second "),
		[]byte("third"),
	}

	for _, data := range writes {
		n, err := notifier.Write(data)
		require.NoError(t, err)
		require.Equal(t, len(data), n)
	}

	assert.Equal(t, 3, notifyCount)
	assert.Equal(t, "first second third", buf.String())
}

These tests cover:

  1. Basic write functionality with successful writes
  2. Error handling for write failures
  3. Edge cases with nil writer and nil notify function
  4. Empty writes
  5. Multiple sequential writes
  6. Notification callback behavior

Each test is run in parallel for efficiency and includes assertions to verify:

  • Correct number of bytes written
  • Error handling
  • Notification callback execution
  • Data integrity

The tests use both mock writers and bytes.Buffer for different scenarios, providing good coverage of the WriterNotifier functionality.


🏁 Script executed:

#!/bin/bash
# Find and read the writer_notifier.go file
fd writer_notifier.go | xargs cat

Length of output: 628


🏁 Script executed:

#!/bin/bash
# Get PR diff to find the file
gh pr diff 3723 | grep -A 50 "util/writer_notifier.go"

Length of output: 54


@levkohimins Here are the corrected unit tests matching the actual implementation:

package util

import (
	"bytes"
	"errors"
	"testing"

	"github.com/stretchr/testify/assert"
)

type mockWriter struct {
	written []byte
	err     error
}

func (w *mockWriter) Write(p []byte) (n int, err error) {
	if w.err != nil {
		return 0, w.err
	}
	w.written = append(w.written, p...)
	return len(p), nil
}

func TestWriterNotifier(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name            string
		writes         [][]byte
		expectedWrites []byte
		writerErr      error
		notifyData     []byte // The data that should be passed to notify function
	}{
		{
			name:            "single write triggers notification",
			writes:         [][]byte{[]byte("test data")},
			expectedWrites: []byte("test data"),
			notifyData:     []byte("test data"),
		},
		{
			name:            "empty write doesn't trigger notification",
			writes:         [][]byte{[]byte("")},
			expectedWrites: []byte(""),
			notifyData:     nil,
		},
		{
			name: "multiple writes only notify on first non-empty",
			writes: [][]byte{
				[]byte(""),
				[]byte("first"),
				[]byte("second"),
			},
			expectedWrites: []byte("firstsecond"),
			notifyData:     []byte("first"),
		},
		{
			name:            "writer error still attempts notification",
			writes:         [][]byte{[]byte("test")},
			writerErr:      errors.New("write failed"),
			expectedWrites: nil,
			notifyData:     []byte("test"),
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			mockWriter := &mockWriter{err: tt.writerErr}
			var notifiedData []byte
			notified := false

			writer := WriterNotifier(mockWriter, func(p []byte) {
				notified = true
				notifiedData = make([]byte, len(p))
				copy(notifiedData, p)
			})

			for _, data := range tt.writes {
				n, err := writer.Write(data)
				if tt.writerErr != nil {
					assert.Error(t, err)
					assert.Equal(t, 0, n)
				} else {
					assert.NoError(t, err)
					assert.Equal(t, len(data), n)
				}
			}

			if tt.notifyData != nil {
				assert.True(t, notified, "notify function should have been called")
				assert.Equal(t, tt.notifyData, notifiedData, "notification data mismatch")
			} else {
				assert.False(t, notified, "notify function should not have been called")
			}

			if tt.writerErr == nil {
				assert.Equal(t, tt.expectedWrites, mockWriter.written)
			}
		})
	}
}

func TestWriterNotifier_NilWriter(t *testing.T) {
	t.Parallel()

	writer := WriterNotifier(nil, func(p []byte) {})
	n, err := writer.Write([]byte("test"))
	assert.Equal(t, 0, n)
	assert.Error(t, err)
}

func TestWriterNotifier_NilNotifyFn(t *testing.T) {
	t.Parallel()

	buf := &bytes.Buffer{}
	writer := WriterNotifier(buf, nil)
	
	data := []byte("test data")
	n, err := writer.Write(data)
	
	assert.Equal(t, len(data), n)
	assert.NoError(t, err)
	assert.Equal(t, data, buf.Bytes())
}

func TestWriterNotifier_ConcurrentWrites(t *testing.T) {
	t.Parallel()

	buf := &bytes.Buffer{}
	notifyCount := 0
	writer := WriterNotifier(buf, func(p []byte) {
		notifyCount++
	})

	// Perform concurrent writes
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			writer.Write([]byte("test"))
		}()
	}
	wg.Wait()

	// Should only notify once despite concurrent writes
	assert.Equal(t, 1, notifyCount)
	assert.Equal(t, "testtesttesttesttesttesttesttesttesttest", buf.String())
}

Key aspects tested:

  1. Notification happens only on first non-empty write
  2. Empty writes don't trigger notification
  3. Notification includes the actual bytes being written
  4. Multiple writes only notify once (sync.Once behavior)
  5. Concurrent writes still only notify once
  6. Error handling for nil writer and nil notify function
  7. Writer errors don't prevent notification
  8. Data is correctly forwarded to underlying writer

The tests are thorough and match the actual implementation's behavior, particularly the important aspects of:

  • One-time notification using sync.Once
  • Passing written bytes to the notification function
  • Only notifying on non-empty writes

@yhakbar yhakbar mentioned this pull request Feb 10, 2025
4 tasks
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (25)
main.go (2)

24-28: Make the error message more helpful

While the error handling is solid, we could make it more user-friendly by including the valid log levels in the error message.

Here's a suggested improvement:

 if err := global.NewLogLevelFlag(opts, nil).Apply(new(flag.FlagSet)); err != nil {
-    opts.Logger.Error(err.Error())
+    opts.Logger.Errorf("Invalid log level in TG_LOG_LEVEL environment variable: %v. Valid levels are: TRACE, DEBUG, INFO, WARN, ERROR", err)
     os.Exit(1)
 }

69-73: Add documentation and improve formatting

The setupContext function looks good but could use some love:

  1. Let's add documentation to explain its purpose
  2. Let's address the formatting suggestion from the linter

Here's how we can improve it:

+// setupContext creates and configures the application context with exit code tracking
+// and logging capabilities.
 func setupContext(opts *options.TerragruntOptions, exitCode *tf.DetailedExitCode) context.Context {
     ctx := context.Background()
     ctx = tf.ContextWithDetailedExitCode(ctx, exitCode)
+
     return log.ContextWithLogger(ctx, opts.Logger)
 }
🧰 Tools
🪛 golangci-lint (1.62.2)

72-72: return statements should not be cuddled if block has more than two lines

(wsl)

internal/strict/view/plaintext/render.go (2)

1-11: Add a package-level comment for clarity.

A brief comment explaining the purpose of this package (e.g., rendering controls in plaintext format) could help maintainers quickly understand its role.


44-64: Avoid panics in the initial template parse.

Using template.Must will panic if Parse fails. Consider handling this error more gracefully, especially if there's a possibility of dynamic or user-provided template strings.

internal/cli/context.go (2)

7-15: Consider renaming the struct to avoid overshadowing the standard library's context.
This type is named Context but it embeds the standard library's context.Context. It's not necessarily wrong, but it might cause confusion in usage or code reading. Renaming could clarify usage and reduce potential mix-ups around method calls on context.Context vs. the local Context type.


40-45: Watch out for overshadowing WithValue.
By providing WithValue(key, val), calls might mask or confuse standard library usage in certain scenarios. This can be valid, but consider clarifying it through docstrings or a renaming strategy for maximum clarity.

pkg/log/formatter.go (1)

9-37: Consider splitting the interface to reduce complexity.
The Formatter interface does a lot: toggling color, output, relative paths, custom format, etc. Breaking it into smaller interfaces can make testing and maintenance simpler in the long run.

internal/strict/controls/control.go (1)

100-129: Double-check subcontrol evaluation ordering.

When ctrl.Enabled is false, we skip returning ctrl.Error but eventually invoke ctrl.Subcontrols.Evaluate(ctx). This can lead to warnings across subcontrols if they're individually enabled. Ensure this order aligns with your intended flow, or consider adjusting the sequence to evaluate subcontrols first if you want consistent error/warning aggregation.

internal/cli/flag.go (1)

108-114: Include specific environment variable in the error message.

Right now, if the same env var is set multiple times, the error reads “setting the env var multiple times” without mentioning which env var. It might help to incorporate the variable name for faster troubleshooting.

- return errors.Errorf("setting the env var multiple times")
+ return errors.Errorf("setting the env var %s multiple times", flag.valueName)
cli/flags/global/flags.go (1)

196-229: Check combined usage of strict-mode and strict-control flags.

Both flags independently enable strict features. If users specify both, their combined effect might need clarifying. Consider logging a single notice summarizing the net effect of enabling both.

internal/cli/exit_code.go (1)

12-13: Consider adding validation for exit code range

While using byte type implicitly limits values to 0-255, consider adding explicit validation when setting exit codes to catch potential overflows early.

internal/strict/controls/deprecated_command.go (2)

9-12: Consider using an enum for command categories

While string constants work, an enumerated type might provide better type safety and prevent typos in category names.


14-22: Great user-friendly error messages! 🎯

The error and warning messages are clear and actionable. They follow a consistent format and provide helpful guidance to users.

However, consider adding a link to documentation about deprecated commands, similar to what's done in the experiments error messages.

internal/experiment/errors.go (1)

18-20: Consider more descriptive error message

The current message could be more helpful. Consider including context about what caused the invalid experiment name.

-  return "allowed experiment(s): " + strings.Join(err.allowedNames, ", ")
+  return "Invalid experiment name. Allowed experiment(s) are: " + strings.Join(err.allowedNames, ", ")
internal/strict/status.go (2)

15-19: Consider using constants for status names

The status names in the map could be defined as constants to ensure consistency across the codebase.


41-45: Consider moving ANSI colors to a separate package

These color constants might be useful in other parts of the codebase. Consider moving them to a shared internal/colors package.

internal/strict/category.go (3)

9-10: Consider using a more descriptive type name.

The comment mentions "DeprecatedFlag Category" but the type name is just "Categories". This could be confusing.

-// Categories is multiple of DeprecatedFlag Category.
-type Categories []*Category
+// Categories represents a collection of DeprecatedFlag categories.
+type Categories []*Category

54-60: Improve the Less implementation for better readability.

The current implementation is a bit hard to follow. Let's make it more explicit.

-func (categories Categories) Less(i, j int) bool {
-	if categories[i].Name == "" || categories[j].Name == "" {
-		return categories[j].Name == ""
-	}
-	return categories[i].Name < categories[j].Name
-}
+func (categories Categories) Less(i, j int) bool {
+	// Handle empty names: empty names should come last
+	if categories[i].Name == "" {
+		return false
+	}
+	if categories[j].Name == "" {
+		return true
+	}
+	// Normal lexicographical comparison
+	return categories[i].Name < categories[j].Name
+}
🧰 Tools
🪛 golangci-lint (1.62.2)

59-59: return statements should not be cuddled if block has more than two lines

(wsl)


74-80: Consider adding validation for the Category struct.

The Category struct could benefit from validation to ensure the Name field isn't empty when creating new categories.

+// NewCategory creates a new Category with validation
+func NewCategory(name string, hidden bool) (*Category, error) {
+    if name == "" {
+        return nil, fmt.Errorf("category name cannot be empty")
+    }
+    return &Category{
+        Name: name,
+        Hidden: hidden,
+    }, nil
+}
cli/flags/prefix.go (1)

11-12: Add deprecation notice details.

The comment marks TerragruntPrefix as deprecated but doesn't explain why or what to use instead.

-	// TerragruntPrefix is an environment variable deprecated prefix.
+	// TerragruntPrefix is a deprecated environment variable prefix.
+	// Deprecated: Use TgPrefix instead for consistency with the new naming convention.
pkg/log/format/formatter.go (2)

19-20: Consider using a more descriptive field name for output control.

The field name disabledOutput is a bit ambiguous. Consider a more specific name that better describes its purpose.

-	disabledOutput bool
+	suppressOutput bool

34-36: Add logging for disabled output state.

When output is disabled, it might be helpful to log this state change at debug level for troubleshooting.

 if formatter.placeholders == nil || formatter.disabledOutput {
+	if formatter.disabledOutput {
+		// Log at debug level that output is disabled
+		entry.Debug("Log output is currently disabled")
+	}
 	return nil, nil
 }
cli/commands/run/command.go (1)

94-113: Enhance command validation error messages.

The error messages could be more helpful by suggesting similar commands when an invalid command is provided.

+func findSimilarCommands(cmd string, validCmds []string) []string {
+	var similar []string
+	for _, valid := range validCmds {
+		if strings.HasPrefix(valid, cmd) || 
+		   levenshtein.Distance(cmd, valid) <= 2 {
+			similar = append(similar, valid)
+		}
+	}
+	return similar
+}

 func validateCommand(opts *options.TerragruntOptions) error {
 	if opts.DisableCommandValidation || collections.ListContainsElement(tf.CommandNames, opts.TerraformCommand) {
 		return nil
 	}

+	similar := findSimilarCommands(opts.TerraformCommand, tf.CommandNames)
 	var errMsg string
+	var suggestions string
+	if len(similar) > 0 {
+		suggestions = fmt.Sprintf("\n\nDid you mean one of these?\n  %s", 
+			strings.Join(similar, "\n  "))
+	}
internal/cli/args.go (1)

84-94: Consider pre-allocating the result slice.

While the Remove method works correctly, we could optimize it by pre-allocating the result slice with a capacity of len(args)-1 since we know we're removing exactly one element.

-	result := make([]string, 0, len(args))
+	result := make([]string, 0, len(args)-1)
🧰 Tools
🪛 golangci-lint (1.62.2)

87-87: ranges should only be cuddled with assignments used in the iteration

(wsl)

cli/app.go (1)

327-339: Consider adding a comment explaining the flag propagation.

While the code is correct, it would be helpful to add a comment explaining why we need to propagate the -no-color flag to Terraform.

 	// Since Terragrunt and Terraform have the same `-no-color` flag,
 	// if a user specifies `-no-color` for Terragrunt, we should propagate it to Terraform as well.
+	// This ensures consistent color output behavior between Terragrunt and Terraform.
 	if opts.Logger.Formatter().DisabledColors() {
 		args = append(args, tf.FlagNameNoColor)
 	}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6f40746 and b6bb130.

📒 Files selected for processing (35)
  • cli/app.go (9 hunks)
  • cli/commands/exec/action.go (1 hunks)
  • cli/commands/exec/options.go (1 hunks)
  • cli/commands/hclfmt/action.go (3 hunks)
  • cli/commands/info/strict/command.go (1 hunks)
  • cli/commands/run/command.go (1 hunks)
  • cli/commands/run/file_copy_getter.go (2 hunks)
  • cli/commands/run/target.go (4 hunks)
  • cli/commands/shortcuts.go (1 hunks)
  • cli/commands/stack/action.go (3 hunks)
  • cli/flags/flag_opts.go (1 hunks)
  • cli/flags/global/flags.go (1 hunks)
  • cli/flags/prefix.go (1 hunks)
  • config/config.go (6 hunks)
  • internal/cache/context.go (1 hunks)
  • internal/cli/args.go (4 hunks)
  • internal/cli/context.go (1 hunks)
  • internal/cli/exit_code.go (1 hunks)
  • internal/cli/flag.go (1 hunks)
  • internal/cli/funcs.go (1 hunks)
  • internal/cli/map_flag.go (7 hunks)
  • internal/experiment/errors.go (1 hunks)
  • internal/strict/category.go (1 hunks)
  • internal/strict/controls/control.go (1 hunks)
  • internal/strict/controls/deprecated_command.go (1 hunks)
  • internal/strict/controls/deprecated_env_var.go (1 hunks)
  • internal/strict/status.go (1 hunks)
  • internal/strict/view/plaintext/render.go (1 hunks)
  • internal/strict/view/writer.go (1 hunks)
  • main.go (3 hunks)
  • pkg/log/format/formatter.go (5 hunks)
  • pkg/log/formatter.go (1 hunks)
  • test/fixtures/exec-cmd/script.sh (1 hunks)
  • test/integration_windows_test.go (1 hunks)
  • tf/detailed_exitcode.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (14)
  • cli/commands/exec/options.go
  • internal/cli/funcs.go
  • cli/commands/run/target.go
  • tf/detailed_exitcode.go
  • internal/cache/context.go
  • test/integration_windows_test.go
  • cli/commands/hclfmt/action.go
  • cli/commands/run/file_copy_getter.go
  • cli/commands/exec/action.go
  • internal/strict/view/writer.go
  • cli/commands/shortcuts.go
  • cli/commands/stack/action.go
  • cli/flags/flag_opts.go
  • cli/commands/info/strict/command.go
🧰 Additional context used
🪛 golangci-lint (1.62.2)
main.go

72-72: return statements should not be cuddled if block has more than two lines

(wsl)

internal/cli/args.go

87-87: ranges should only be cuddled with assignments used in the iteration

(wsl)

internal/strict/category.go

59-59: return statements should not be cuddled if block has more than two lines

(wsl)

🪛 Shellcheck (0.10.0)
test/fixtures/exec-cmd/script.sh

[warning] 9-9: TF_VAR_foo is referenced but not assigned.

(SC2154)


[warning] 15-15: TF_VAR_bar is referenced but not assigned.

(SC2154)

⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: unessential
  • GitHub Check: build-and-test
  • GitHub Check: Pull Request has non-contributor approval
🔇 Additional comments (34)
main.go (2)

4-15: Clean package organization! 👍

The switch from shell.DetailedExitCode to tf.DetailedExitCode and the new imports align nicely with the CLI redesign goals.

Also applies to: 20-20


48-50: Smart error handling! 🎯

Great improvement on the error stack trace handling:

  • Only logs stack traces when they exist
  • Uses the appropriate TRACE level for detailed error information
internal/strict/view/plaintext/render.go (4)

19-19: Great use of interface compliance check.

Verifying Render implements view.Render at compile time is a neat way to catch mismatches early.


23-25: Constructor looks good.

Straightforward constructor returning a pointer to Render. Nicely done.


66-79: Formatting approach is sound.

Using tabwriter for alignment is a simple, effective choice. No issues here.


81-89: Good encapsulation of template building & execution.

It's clean to separate the template creation from its final execution—the structure boosts readability.

internal/cli/context.go (1)

17-23: Looks neat!
The constructor approach is straightforward and keeps everything readable. Thumbs up, no issues spotted here.

pkg/log/formatter.go (1)

51-63: Well-structured error handling.
The early check for a nil log entry and returning an error prevents unexpected panics or downstream issues. This is a good defensive step.

internal/strict/controls/deprecated_env_var.go (2)

44-51: Clear error and warning messages.
The approach of returning compelling guidance when environment variables are deprecated is carefully done. Users can better understand what changed and how to proceed.


57-95: Confirm behavior for multiple environment variables.
If newFlag can have multiple environment variables, this code references just the first one. That might be fine, but it could be worth verifying that the remaining vars aren't needed or used differently.

✅ Verification successful

🏁 Script executed:

#!/bin/bash
# Searching references to newFlag.GetEnvVars() to confirm usage patterns and ensure consistent handling across the codebase.
rg -A 5 'newFlag\.GetEnvVars\(\)'

Length of output: 1210


Verification of Multiple Environment Variables Handling

The search output shows that the code consistently accesses only the first environment variable returned by newFlag.GetEnvVars(). For instance, it uses util.FirstElement(util.RemoveEmptyElements(newFlag.GetEnvVars())) when assigning newName and similarly indexes the slice in the conditional block. There isn't any evidence in the codebase suggesting that the remaining environment variables are handled differently or need separate processing. This indicates that the current behavior, while only referencing the first element, is intentional and consistent.

internal/strict/controls/control.go (1)

42-44: Consider concurrency safety for shared fields.

OnceWarn nicely prevents repeated warnings, but other fields (like Enabled or Error) could also be accessed from multiple goroutines. If you anticipate concurrent evaluations, consider wrapping updates to shared fields in thread-safe operations or clarifying single-thread usage in doc comments.

internal/cli/flag.go (2)

32-33: Review usage of MapFlagSetterFunc.

MapFlagSetterFunc[K any, V any] is defined but isn't visibly used in this file. If it’s still needed, consider adding references or usage examples. Otherwise, removing it may keep the code lean and focused.


251-270: Validate multiple environment variables usage.

The ApplyFlag function loops over all environment variables for a flag. When MultipleSet() is true, setting the same env var multiple times could be unexpected. Confirm that’s the intended behavior, or warn users to unify multiple environment values into one.

cli/flags/global/flags.go (2)

49-60: Deprecated flags mirror current flags.

The deprecated flag names here match the new flags (e.g., DeprecatedLogLevelFlagName = "log-level"). This provides no real migration path and may confuse users. Consider renaming them to distinguish from active flags.


304-318: Add a safeguard against infinite loop in nested commands.

As noted previously, this loop breaks only when cmd == nil. If there’s a cyclic subcommand reference, it might never fail and keep iterating. Add a counter break to match maxIterations.

internal/cli/exit_code.go (2)

4-10: Nice work on the exit code constants! 👍

The exit codes follow standard Unix conventions, and the use of iota for automatic enumeration is a clean approach. The comments clearly explain each code's purpose.


15-20: Well-designed error interface extension

The ExitCoder interface nicely extends the standard error interface while maintaining compatibility with Go's error wrapping conventions through Unwrap().

internal/experiment/errors.go (1)

38-40: Excellent user guidance in error message! 🌟

Great job including both the error context and a link to documentation. This helps users understand and resolve the issue quickly.

internal/strict/status.go (1)

47-59: Nice color-coded status output! 🎨

The colored output improves readability and user experience. Good job handling the default case too!

cli/commands/run/command.go (1)

27-28: Remove shortcut examples until fully implemented.

Based on the past review comments, we should be careful about advertising shortcuts that aren't fully supported yet.

-			"# Run a plan\nterragrunt run -- plan\n# Shortcut:\n# terragrunt plan",
-			"# Run output with -json flag\nterragrunt run -- output -json\n# Shortcut:\n# terragrunt output -json",
+			"# Run a plan\nterragrunt run -- plan",
+			"# Run output with -json flag\nterragrunt run -- output -json",
internal/cli/args.go (4)

9-12: LGTM! Good choice for the constant name and value.

The BuiltinCmdSep constant with value "--" follows the standard Unix/Linux convention for separating command options from positional arguments.


29-38: LGTM! Clean and efficient implementation of Split.

The function efficiently splits the slice at the first occurrence of the separator, returning both parts. The early return with nil for the second part when no separator is found is a nice touch.


40-44: LGTM! Smart use of slices.Clone for safe copying.

The WithoutBuiltinCmdSep method safely handles the command separator by using slices.Clone to avoid modifying the original slice.


142-157: LGTM! Well-structured command name extraction.

The CommandNameN method is a clean implementation that finds the nth non-flag argument. This refactoring improves code reuse as it's used by both CommandName and SubCommandName.

internal/cli/map_flag.go (2)

85-93: LGTM! Clean environment variable lookup implementation.

The environment variable lookup is properly encapsulated in a function that handles both the lookup and splitting of values.


95-99: LGTM! Good use of generics for type safety.

The code properly uses generic type variables for key and value types, making the implementation type-safe while remaining flexible.

cli/app.go (2)

145-177: LGTM! Well-structured global flags handling.

The GlobalFlags function provides a clean way to handle backward compatibility for moved flags while preventing duplicates.


179-201: LGTM! Smart handling of duplicate no-color flags.

The removeNoColorFlagDuplicates function efficiently handles the edge case where both --no-color and --terragrunt-no-color are present, preventing the "setting the flag multiple times" error.

config/config.go (2)

42-44: LGTM! Good addition of recommended config name constant.

The RecommendedParentConfigName constant with value "root.hcl" helps standardize the naming convention for parent config files.


792-794: LGTM! Improved error message for file info retrieval.

The error message now clearly indicates that the failure occurred during file info retrieval, making debugging easier.

test/fixtures/exec-cmd/script.sh (4)

1-1: Shebang line is correctly set.

The script properly uses #!/usr/bin/env bash, which dynamically determines the path to the Bash interpreter. This follows the previous suggestion by yhakbar.


7-7: Echo statement outputs command arguments and working directory.

The echo command clearly prints the first two arguments along with the present working directory ($PWD), which aids in debugging and verification.


9-13: Verification of TF_VAR_foo value works as intended.

The conditional check confirms that TF_VAR_foo is set to "FOO" and, if not, provides an informative error message including the current value (or "not set" if undefined) before exiting. Note that Shellcheck warnings (SC2154: TF_VAR_foo is referenced but not assigned) can be safely ignored here since this variable is expected to be set externally.

🧰 Tools
🪛 Shellcheck (0.10.0)

[warning] 9-9: TF_VAR_foo is referenced but not assigned.

(SC2154)


15-19: Verification of TF_VAR_bar value is clear and effective.

Similar to the previous block, this segment checks that TF_VAR_bar equals "BAR". The error message and exit mechanism ensure users understand what variable is misconfigured. The static analysis warnings regarding unassigned variables are acceptable in this context.

🧰 Tools
🪛 Shellcheck (0.10.0)

[warning] 15-15: TF_VAR_bar is referenced but not assigned.

(SC2154)

Comment on lines +47 to +57
// EnvVar returns a string that is the concatenation of the slice values with the given `name`,
// using underscores as separators, replacing dashes with underscores, converting to uppercase.
func (prefix Prefix) EnvVar(name string) string {
if name == "" {
return ""
}

name = strings.Join(append(prefix, name), "_")

return strings.ToUpper(strings.ReplaceAll(name, "-", "_"))
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add input validation for environment variable names.

The EnvVar method should validate that the input name follows environment variable naming conventions.

 func (prefix Prefix) EnvVar(name string) string {
 	if name == "" {
 		return ""
 	}
+
+	// Validate environment variable name
+	if strings.ContainsAny(name, "$.{}\\ ") {
+		return ""
+	}
 
 	name = strings.Join(append(prefix, name), "_")
 	return strings.ToUpper(strings.ReplaceAll(name, "-", "_"))
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// EnvVar returns a string that is the concatenation of the slice values with the given `name`,
// using underscores as separators, replacing dashes with underscores, converting to uppercase.
func (prefix Prefix) EnvVar(name string) string {
if name == "" {
return ""
}
name = strings.Join(append(prefix, name), "_")
return strings.ToUpper(strings.ReplaceAll(name, "-", "_"))
}
// EnvVar returns a string that is the concatenation of the slice values with the given `name`,
// using underscores as separators, replacing dashes with underscores, converting to uppercase.
func (prefix Prefix) EnvVar(name string) string {
if name == "" {
return ""
}
// Validate environment variable name
if strings.ContainsAny(name, "$.{}\\ ") {
return ""
}
name = strings.Join(append(prefix, name), "_")
return strings.ToUpper(strings.ReplaceAll(name, "-", "_"))
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (8)
cli/commands/stack/action.go (1)

63-75: Consider consistent error handling in switch cases

While we've improved the experiment check error handling, the error handling in the switch cases could be more consistent.

Consider updating the error handling in the switch cases to match the new pattern:

-			return errors.New(err)
+			return cli.NewExitError(err, cli.ExitCodeGeneralError)

This would make the error handling consistent throughout the function.

internal/cli/args.go (1)

84-95: Consider using a more efficient filter pattern.

While the current implementation works well, we could make it more idiomatic and potentially more efficient.

-func (args Args) Remove(name string) Args {
-	result := make([]string, 0, len(args))
-
-	for _, arg := range args {
-		if arg != name {
-			result = append(result, arg)
-		}
-	}
-
-	return result
-}
+func (args Args) Remove(name string) Args {
+	return Args(slices.DeleteFunc(slices.Clone(args), func(s string) bool {
+		return s == name
+	}))
+}
main.go (2)

24-28: Consider enhancing the error message for log level setup.

While the new flag-based approach is great, we could make the error message more helpful by including the valid log levels.

Here's a suggested improvement:

-		opts.Logger.Error(err.Error())
+		opts.Logger.Errorf("Failed to set log level: %v. Valid levels are: TRACE, DEBUG, INFO, WARN, ERROR", err)

69-74: Add documentation for context enhancements.

The setupContext function is well-structured, but let's make it even clearer with some documentation.

Consider adding comments like this:

 func setupContext(opts *options.TerragruntOptions, exitCode *tf.DetailedExitCode) context.Context {
+	// Start with a base context
 	ctx := context.Background()
+	// Add support for detailed exit codes
 	ctx = tf.ContextWithDetailedExitCode(ctx, exitCode)
 
+	// Add logger support for consistent logging across the application
 	return log.ContextWithLogger(ctx, opts.Logger)
 }
test/integration_strict_test.go (1)

31-31: Great improvements to error messaging! 🎯

The changes look good! The error messages are clearer, and moving to a generic "deprecated-commands" control is more maintainable than having individual controls for each deprecated command.

Consider adding a comment explaining the "deprecated-commands" control to help future maintainers understand its purpose and scope. Something like:

 			name:           "plan-all with plan-all strict control",
+			// Using deprecated-commands control to handle all deprecated command validations
 			controls:       []string{"deprecated-commands"},

Also applies to: 36-37, 39-39

docs/_docs/04_reference/06-experiments.md (1)

140-158: Stabilization Criteria for CLI Redesign
The checklist under the "Criteria for stabilization" for cli-redesign is comprehensive and covers various new commands and flag updates. As development progresses, consider updating this list to mirror actual improvements. In the long term, grouping related items could enhance readability if the list expands further.

cli/commands/stack/command.go (2)

10-23: Consider grouping related constants

The constants could be more organized by grouping them into related categories:

 const (
 	// CommandName stack command name.
 	CommandName          = "stack"
+
+	// Flag names
 	OutputFormatFlagName = "format"
 	JSONFormatFlagName   = "json"
 	RawFormatFlagName    = "raw"
 
+	// Subcommands
 	generate = "generate"
 	run      = "run"
 	output   = "output"
 
+	// Output formats
 	rawOutputFormat  = "raw"
 	jsonOutputFormat = "json"
 )

26-28: Document the prefix parameter's purpose

The new prefix parameter could use some documentation to explain its role in flag handling.

 // NewFlags builds the flags for stack.
+// prefix: Used to prepend environment variable names with a consistent prefix.
+// The prefix helps avoid naming conflicts and maintains consistency across the CLI.
 func NewFlags(opts *options.TerragruntOptions, prefix flags.Prefix) cli.Flags {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b6bb130 and 1e0a679.

📒 Files selected for processing (11)
  • cli/commands/run/command.go (1 hunks)
  • cli/commands/stack/action.go (2 hunks)
  • cli/commands/stack/command.go (3 hunks)
  • docs/_docs/04_reference/06-experiments.md (3 hunks)
  • internal/cli/args.go (4 hunks)
  • internal/strict/category.go (1 hunks)
  • internal/strict/controls/deprecated_flag_name.go (1 hunks)
  • internal/strict/view/plaintext/render.go (1 hunks)
  • main.go (3 hunks)
  • options/options.go (12 hunks)
  • test/integration_strict_test.go (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • internal/strict/controls/deprecated_flag_name.go
  • cli/commands/run/command.go
  • internal/strict/view/plaintext/render.go
  • internal/strict/category.go
  • options/options.go
🧰 Additional context used
🧠 Learnings (1)
cli/commands/stack/action.go (1)
Learnt from: levkohimins
PR: gruntwork-io/terragrunt#3723
File: cli/commands/stack/action.go:160-160
Timestamp: 2025-02-10T13:36:19.542Z
Learning: The project uses a custom error package `github.com/gruntwork-io/terragrunt/internal/errors` which provides similar functionality to `fmt.Errorf` but includes stack traces. Prefer using this package's error functions (e.g., `errors.Errorf`, `errors.New`) over the standard library's error handling.
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: unessential
  • GitHub Check: build-and-test
  • GitHub Check: Pull Request has non-contributor approval
🔇 Additional comments (23)
cli/commands/stack/action.go (4)

8-8: LGTM! Clean import addition.

The new CLI package import supports the improved error handling pattern.


24-25: Nice improvement in experiment checking and error handling! 👍

The changes make two solid improvements:

  1. Using Evaluate() instead of direct map access is more maintainable
  2. Consistent error handling with cli.NewExitError provides better error reporting

33-34: Maintaining consistency with RunGenerate - good job!

The same improvements in experiment checking and error handling are applied here, keeping the codebase consistent.


48-49: Experiment check updated consistently 👍

The same improvements in experiment checking and error handling are applied here.

internal/cli/args.go (4)

9-12: LGTM! Clear and purposeful constants.

The constants are well-named and their values are appropriate for their intended use.


29-38: Improve method documentation.

The documentation should be more descriptive about what the method does and its return values.

-// Split splits `args` into two slices separated by `sep`.
+// Split splits `args` into two slices at the first occurrence of `sep`.
+// Returns (args before sep, args after sep). If sep is not found,
+// returns (all args, nil).

40-44: LGTM! Safe handling of slice operations.

Good use of slices.Clone to prevent modifying the original slice when appending.


143-170: LGTM! Great refactoring to reduce duplication.

The introduction of CommandNameN as a helper method is a nice improvement that:

  • Reduces code duplication
  • Makes the code more maintainable
  • Keeps the logic clear and focused
main.go (2)

4-15: Nice package organization! 👍

The move to tf.DetailedExitCode and the reorganization of imports aligns well with the PR's goal of better CLI structure.

Also applies to: 20-20


47-50: Smart error handling improvement! 🎯

Great job on making the error output cleaner by only showing the stack trace when it's not empty. This makes the output much more user-friendly.

test/integration_strict_test.go (4)

6-6: Nice package reorganization! 👍

Moving from the strict package to the centralized errors package makes error handling more consistent across the codebase.


46-46: Smart move with the error handling! 🧠

Using ErrorContains and a simpler error message makes the test more resilient to minor message changes. This should reduce the maintenance burden that was mentioned in the previous reviews.

Also applies to: 68-68


95-95: Clear and helpful error messages! 📝

The anti-pattern messaging is much more informative now, and the comment explaining why we can't test strict mode is super helpful - it directly addresses the confusion from previous reviews.

Also applies to: 101-101, 103-103


111-111: Document the CLI redesign experiment 📚

The CLI flag simplification looks good, but the --experiment cli-redesign flag needs some context.

Could you add a comment explaining:

  1. What the CLI redesign experiment does?
  2. When it will become the default behavior?
  3. If there are any known limitations?

This will help other developers understand the transition period and what to expect.

Also applies to: 120-120

docs/_docs/04_reference/06-experiments.md (5)

68-71: Consistent Addition of New Experimental Feature
The new bullet point for [cli-redesign](#cli-redesign) fits nicely with the existing list. Just verify that the anchor link directs readers correctly to the section below.


72-92: Symlinks Experiment Section Formatting
The updated headers for the symlinks section—including "What it does", "How to provide feedback", and "Criteria for stabilization"—are well structured and clear. This formatting will help users quickly locate the details they seek.

🧰 Tools
🪛 LanguageTool

[grammar] ~90-~90: The operating system from Apple is written “macOS”.
Context: ...cannot be supported). - [ ] Ensure that MacOS integration tests still work. See [#361...

(MAC_OS)


[grammar] ~91-~91: The operating system from Apple is written “macOS”.
Context: ...616). - [ ] Add integration tests for MacOS in CI. ### stacks Support for Terra...

(MAC_OS)


93-118: Updated Stacks Section
The revisions to the stacks experiment section, especially the headers for its subsections, are consistent with the other experiments. Please double-check that the checkboxes accurately represent the current progress and intended stabilization criteria.


119-131: Introduction of CLI Redesign Section
The new cli-redesign section is a great addition that outlines important new functionalities like the run and exec commands. Ensure that the description and provided details align with the actual implementation and related RFC (issue #3445).


132-139: Feedback Guidelines for CLI Redesign
The "How to provide feedback" section for cli-redesign is clear and directs users to the appropriate discussion forum. It might be beneficial to briefly mention how user feedback will be used to iterate on these features in future releases.

cli/commands/stack/command.go (4)

4-8: Clean import organization! 👍

The imports are well-organized and the new flags package is properly integrated.


90-90: Nice simplification! 👍

The direct use of cli.ShowCommandHelp is cleaner than the previous implementation.


29-52: Verify environment variable generation

The environment variable generation looks good, but let's verify the naming convention is consistent across commands.

✅ Verification successful

🏁 Script executed:

#!/bin/bash
# Search for other NewFlags implementations to verify prefix usage consistency
ast-grep --pattern 'func NewFlags(opts *options.TerragruntOptions, prefix flags.Prefix) cli.Flags {
  $$$
  tgPrefix := prefix.Prepend($$$)
  $$$
}'

Length of output: 62086


Environment Variable Naming Consistency Confirmed

The consistency check shows that all commands (including the stack command) use the same logic for generating environment variables. Each implementation creates a tgPrefix by calling prefix.Prepend(flags.TgPrefix) and then uses tgPrefix.EnvVars(FlagName) to set the environment variables. No discrepancies were found across the commands.


58-61: Verify the impact of ErrorOnUndefinedFlag

The change from DisallowUndefinedFlags to ErrorOnUndefinedFlag looks good, but let's verify the error messaging is user-friendly.

✅ Verification successful

🏁 Script executed:

#!/bin/bash
# Search for other commands using ErrorOnUndefinedFlag to verify consistency
rg "ErrorOnUndefinedFlag:\s*true" -A 2 -B 2

Length of output: 4800


Error messaging for undefined flags is consistent and user-friendly.

The use of ErrorOnUndefinedFlag: true appears uniformly across multiple commands (as seen in files like cli/help_test.go, cli/commands/catalog/command.go, etc.), which indicates that when an undefined flag is provided, users will receive a consistent and clear error message. Given that this flag leverages the underlying CLI library's error handling, it should deliver a user-friendly notification. No anomalies or user confusion were evident from our repository search.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

terragrunt providers lock … passes arguments in reverse (wrong) order
3 participants