Open-Source Wikis

/

Terraform

/

Systems

/

CLI dispatch

hashicorp/terraform

CLI dispatch

Active contributors: terraform-core team, James Bardin, Martin Atkins, Liam Cervante.

Purpose

Everything before the first subcommand. This subsystem turns terraform <args...> into a call into the matching command.<...>Command.Run. It includes the binary entry point (main.go), the subcommand factory map (commands.go), the -chdir option, the TF_CLI_ARGS[_subcommand] env-var splicing, and the autocomplete plumbing.

Directory layout

.
├── main.go              # realMain() + helpers
├── commands.go          # subcommand factory map (Commands), PrimaryCommands, HiddenCommands
├── help.go              # custom helpFunc, top-level usage
├── checkpoint.go        # opt-in update check
├── telemetry.go         # OpenTelemetry init
├── provider_source.go   # producing the providerSrc passed into commands
├── working_dir.go       # WorkingDir constructor for command.Meta
├── version.go           # build-time version constants
├── experiments.go       # experimentsAllowed flag
├── signal_unix.go       # forwardSignals for *nix
└── signal_windows.go    # forwardSignals for Windows

Key types

Symbol File Description
realMain() main.go The actual entry point invoked from main(). Returns the process exit code.
Commands commands.go map[string]cli.CommandFactory. Indexed by subcommand name; values are factories that close over command.Meta.
PrimaryCommands commands.go Order in which the workflow commands (init, validate, plan, apply, destroy) appear in terraform -help.
HiddenCommands commands.go Subcommands omitted from help. Currently env, internal-plugin, push, rpcapi.
Ui commands.go cli.Ui used by every command for input/output. Wrapped so Warn calls become Output (warnings go to stdout).
runCheckpoint(ctx, config) checkpoint.go Background goroutine that pings the HashiCorp Checkpoint service for new versions, controlled by CLI config.

How it works

sequenceDiagram
    autonumber
    participant Shell
    participant Main as main.go
    participant Cfg as cliconfig.LoadConfig
    participant Disco as svchost/disco
    participant ProvSrc as providerSource()
    participant Init as initCommands()
    participant CLI as cli.CLI
    participant Cmd as Commands[<name>].Run

    Shell->>Main: terraform <args>
    Main->>Main: openTelemetryInit + tracer.Start
    Main->>Cfg: load CLI config (~/.terraformrc, env)
    Main->>Disco: NewWithCredentialsSource
    Main->>ProvSrc: build providerSrc from config.ProviderInstallation
    Main->>Main: extractChdirOption(args) → maybe os.Chdir
    Main->>Init: initCommands(ctx, wd, streams, config, services, providerSrc, …)
    Init-->>Main: populates package-level Commands map
    Main->>CLI: new cli.CLI with Commands + helpFunc
    Main->>Main: mergeEnvArgs(TF_CLI_ARGS / TF_CLI_ARGS_<sub>)
    Main->>CLI: cliRunner.Run()
    CLI->>Cmd: dispatch
    Cmd-->>CLI: exitCode
    CLI-->>Main: exitCode
    Main->>Main: if exit != 0, drain logging.PluginPanics()
    Main-->>Shell: exit

The dispatch loop is short. Most of the work in realMain() is setup: the CLI configuration, the Service Discovery client, the provider source, the streams, the working directory, the OpenTelemetry span, and the unmanaged-providers map (TF_REATTACH_PROVIDERS). These are bundled into a command.Meta struct that every command receives.

command.Meta — the shared dependency bag

Defined in internal/command/meta.go. Every command embeds it. Notable fields:

  • WorkingDir *workdir.Dir — the working directory abstraction that knows about .terraform/, the data dir override (TF_DATA_DIR), and the lock file path.
  • Streams *terminal.Streams — stdout, stderr, stdin, plus terminal feature detection.
  • View *views.View — the entry point to the rendering layer.
  • Services *disco.Disco — service discovery client used to resolve provider registries and HCP Terraform endpoints.
  • ProviderSource getproviders.Source — provider installer source.
  • BrowserLauncher webbrowser.Launcher — used by terraform login.
  • RunningInAutomation bool — set when TF_IN_AUTOMATION is non-empty.
  • AllowExperimentalFeatures bool — gates experimental subcommands like cloud and state migrate.
  • ShutdownCh <-chan struct{} — signals SIGINT/SIGTERM into commands so they can stop gracefully.

commands.go factory map

The factory map is a long, alphabetized list. Most entries are trivial:

"plan": func() (cli.Command, error) {
    return &command.PlanCommand{Meta: meta}, nil
},

A handful of subcommands are reused with different flags:

  • apply and destroy share command.ApplyCommand (the destroy variant sets Destroy: true).
  • env list/new/select/delete are aliases for the corresponding workspace ... commands, with a LegacyName: true flag that emits a deprecation note.
  • rpcapi bypasses the entire command package — its factory is built directly by rpcapi.CLICommandFactory. This is intentional: rpcapi is meant to give automation as direct access to Terraform Core as possible, without CLI ergonomics.

When meta.AllowExperimentalFeatures is true, three more entries are appended: cloud, test cleanup, and state migrate.

-chdir extraction

Terraform supports -chdir=<dir> as a pre-subcommand option, parsed by extractChdirOption(args) in main.go. The function returns the new args list with -chdir=... removed and the directory the process should os.Chdir into. Because it runs before initCommands, every command sees the new working directory through command.Meta.WorkingDir.

The original (pre-chdir) directory is also retained — initCommands receives it as originalWorkingDir and stores it on the meta in case a command needs it (e.g. for reading the CLI config from a relative TERRAFORM_CONFIG_FILE).

TF_CLI_ARGS env splicing

mergeEnvArgs(envName, cmd, args) reads TF_CLI_ARGS (or TF_CLI_ARGS_<subcommand>), parses it with mattn/go-shellwords, and inserts the parsed tokens after the first non-flag argument. This is how operators inject extra flags into every invocation, e.g.:

export TF_CLI_ARGS_plan="-parallelism=20"

It's called twice: once for TF_CLI_ARGS (applied to all subcommands), once for TF_CLI_ARGS_<suffix> (applied only when the suffix matches the current subcommand).

"Did you mean...?" for unknown commands

Before the cli.CLI runner kicks in, realMain() checks whether the resolved subcommand exists in Commands. If not, it computes a Levenshtein-based suggestion via internal/didyoumean.NameSuggestion and prints:

Terraform has no command named "ploan". Did you mean "plan"?

This bypasses the default cli.CLI error to give a more focused message.

Plugin lifecycle

Provider plugins are spawned as child processes. defer plugin.CleanupClients() in realMain() ensures every plugin process is terminated when the CLI exits, including in the panic path. Plugin panics are captured separately via logging.PluginPanics() and re-emitted as errors after the dispatch returns, so users see plugin crashes even when they happen during a graceful shutdown.

Entry points for modification

  • Adding a new top-level subcommand: add an entry to the Commands map in commands.go and implement the command in internal/command/. If it should appear in the workflow help, add it to PrimaryCommands.
  • Hiding an obsolete command: add it to HiddenCommands.
  • Changing how flags are spliced: edit mergeEnvArgs in main.go.
  • Changing what the command.Meta carries: edit the Meta struct in internal/command/meta.go and the meta := command.Meta{...} literal in commands.go.

See command package for what happens inside each subcommand handler.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

CLI dispatch – Terraform wiki | Factory