diff --git a/internal/command/arguments/console.go b/internal/command/arguments/console.go new file mode 100644 index 0000000000..b666d01039 --- /dev/null +++ b/internal/command/arguments/console.go @@ -0,0 +1,82 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Console represents the command-line arguments for the console command. +type Console struct { + // Vars are the variable-related flags (-var, -var-file). + Vars *Vars + + // StatePath is the path to the state file. + StatePath string + + // EvalFromPlan controls whether to evaluate expressions against a plan + // instead of the current state. + EvalFromPlan bool + + // InputEnabled is used to disable interactive input for unspecified + // variable and backend config values. Default is true. + InputEnabled bool + + // CompactWarnings enables compact warning output. + CompactWarnings bool + + // TargetFlags are the raw -target flag values. + TargetFlags []string + + // ConfigPath is the path to a directory of Terraform configuration files. + ConfigPath string +} + +// ParseConsole processes CLI arguments, returning a Console value and +// diagnostics. If errors are encountered, a Console value is still returned +// representing the best effort interpretation of the arguments. +func ParseConsole(args []string) (*Console, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + con := &Console{ + Vars: &Vars{}, + } + + pwd, err := getwd() + if err != nil { + return nil, diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Error getting pwd", + err.Error(), + )) + } + + cmdFlags := extendedFlagSet("console", nil, nil, con.Vars) + cmdFlags.StringVar(&con.StatePath, "state", "", "path") + cmdFlags.BoolVar(&con.EvalFromPlan, "plan", false, "evaluate from plan") + cmdFlags.BoolVar(&con.InputEnabled, "input", true, "input") + cmdFlags.BoolVar(&con.CompactWarnings, "compact-warnings", false, "compact-warnings") + cmdFlags.Var((*FlagStringSlice)(&con.TargetFlags), "target", "target") + + if err := cmdFlags.Parse(args); err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + err.Error(), + )) + } + + args = cmdFlags.Args() + + if len(args) != 0 { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Too many command line arguments", + "The console command does not expect any positional arguments. Did you mean to use -chdir?", + )) + } + + con.ConfigPath = pwd + + return con, diags +} diff --git a/internal/command/arguments/console_test.go b/internal/command/arguments/console_test.go new file mode 100644 index 0000000000..c22d560cd1 --- /dev/null +++ b/internal/command/arguments/console_test.go @@ -0,0 +1,179 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestParseConsole_valid(t *testing.T) { + testCases := map[string]struct { + args []string + want *Console + }{ + "defaults": { + nil, + &Console{ + Vars: &Vars{}, + InputEnabled: true, + }, + }, + "state flag": { + []string{"-state", "mystate.tfstate"}, + &Console{ + Vars: &Vars{}, + StatePath: "mystate.tfstate", + InputEnabled: true, + }, + }, + "plan flag": { + []string{"-plan"}, + &Console{ + Vars: &Vars{}, + EvalFromPlan: true, + InputEnabled: true, + }, + }, + "input disabled": { + []string{"-input=false"}, + &Console{ + Vars: &Vars{}, + InputEnabled: false, + }, + }, + "compact warnings": { + []string{"-compact-warnings"}, + &Console{ + Vars: &Vars{}, + InputEnabled: true, + CompactWarnings: true, + }, + }, + "all flags": { + []string{"-state", "mystate.tfstate", "-plan", "-input=false", "-compact-warnings"}, + &Console{ + Vars: &Vars{}, + StatePath: "mystate.tfstate", + EvalFromPlan: true, + InputEnabled: false, + CompactWarnings: true, + }, + }, + } + + cmpOpts := cmpopts.IgnoreUnexported(Vars{}) + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseConsole(tc.args) + if len(diags) > 0 { + t.Fatalf("unexpected diags: %v", diags) + } + if diff := cmp.Diff(tc.want, got, cmpOpts); diff != "" { + t.Errorf("unexpected result\n%s", diff) + } + }) + } +} + +func TestParseConsole_invalid(t *testing.T) { + testCases := map[string]struct { + args []string + want *Console + wantDiags tfdiags.Diagnostics + }{ + "unknown flag": { + []string{"-boop"}, + &Console{ + Vars: &Vars{}, + InputEnabled: true, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + "flag provided but not defined: -boop", + ), + }, + }, + "positional argument": { + []string{"./mydir"}, + &Console{ + Vars: &Vars{}, + InputEnabled: true, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Too many command line arguments", + "The console command does not expect any positional arguments. Did you mean to use -chdir?", + ), + }, + }, + } + + cmpOpts := cmpopts.IgnoreUnexported(Vars{}) + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, gotDiags := ParseConsole(tc.args) + if diff := cmp.Diff(tc.want, got, cmpOpts); diff != "" { + t.Errorf("unexpected result\n%s", diff) + } + tfdiags.AssertDiagnosticsMatch(t, gotDiags, tc.wantDiags) + }) + } +} + +func TestParseConsole_vars(t *testing.T) { + testCases := map[string]struct { + args []string + want []FlagNameValue + }{ + "no var flags by default": { + args: nil, + want: nil, + }, + "one var": { + args: []string{"-var", "foo=bar"}, + want: []FlagNameValue{ + {Name: "-var", Value: "foo=bar"}, + }, + }, + "one var-file": { + args: []string{"-var-file", "cool.tfvars"}, + want: []FlagNameValue{ + {Name: "-var-file", Value: "cool.tfvars"}, + }, + }, + "ordering preserved": { + args: []string{ + "-var", "foo=bar", + "-var-file", "cool.tfvars", + "-var", "boop=beep", + }, + want: []FlagNameValue{ + {Name: "-var", Value: "foo=bar"}, + {Name: "-var-file", Value: "cool.tfvars"}, + {Name: "-var", Value: "boop=beep"}, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseConsole(tc.args) + if len(diags) > 0 { + t.Fatalf("unexpected diags: %v", diags) + } + if vars := got.Vars.All(); !cmp.Equal(vars, tc.want) { + t.Fatalf("unexpected result\n%s", cmp.Diff(vars, tc.want)) + } + }) + } +} diff --git a/internal/command/console.go b/internal/command/console.go index 1515a2b9fd..28d12dc0c3 100644 --- a/internal/command/console.go +++ b/internal/command/console.go @@ -27,32 +27,34 @@ type ConsoleCommand struct { } func (c *ConsoleCommand) Run(args []string) int { - args = c.Meta.process(args) - var evalFromPlan bool - cmdFlags := c.Meta.extendedFlagSet("console") - cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") - cmdFlags.BoolVar(&evalFromPlan, "plan", false, "evaluate from plan") - cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } - if err := cmdFlags.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing command line flags: %s\n", err.Error())) + parsedArgs, diags := arguments.ParseConsole(c.Meta.process(args)) + + // Copy parsed flags back to Meta + c.Meta.statePath = parsedArgs.StatePath + c.Meta.input = parsedArgs.InputEnabled + c.Meta.compactWarnings = parsedArgs.CompactWarnings + c.Meta.targetFlags = parsedArgs.TargetFlags + + varItems := parsedArgs.Vars.All() + c.Meta.variableArgs = arguments.FlagNameValueSlice{ + FlagName: "-var", + Items: &varItems, + } + + if diags.HasErrors() { + c.showDiagnostics(diags) return 1 } - configPath, err := ModulePath(cmdFlags.Args()) - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - configPath = c.Meta.normalizePath(configPath) + configPath := c.Meta.normalizePath(parsedArgs.ConfigPath) // Check for user-supplied plugin path + var err error if c.pluginPath, err = c.loadPluginPath(); err != nil { c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err)) return 1 } - var diags tfdiags.Diagnostics - // Load the backend b, backendDiags := c.backend(configPath, arguments.ViewHuman) diags = diags.Append(backendDiags) @@ -116,7 +118,7 @@ func (c *ConsoleCommand) Run(args []string) int { } var scope *lang.Scope - if evalFromPlan { + if parsedArgs.EvalFromPlan { var planDiags tfdiags.Diagnostics _, scope, planDiags = lr.Core.PlanAndEval(lr.Config, lr.InputState, lr.PlanOpts) diags = diags.Append(planDiags)