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 WindowsKey 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: exitThe 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 byterraform login.RunningInAutomation bool— set whenTF_IN_AUTOMATIONis non-empty.AllowExperimentalFeatures bool— gates experimental subcommands likecloudandstate 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:
applyanddestroysharecommand.ApplyCommand(the destroy variant setsDestroy: true).env list/new/select/deleteare aliases for the correspondingworkspace ...commands, with aLegacyName: trueflag that emits a deprecation note.rpcapibypasses the entirecommandpackage — its factory is built directly byrpcapi.CLICommandFactory. This is intentional:rpcapiis 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
Commandsmap incommands.goand implement the command ininternal/command/. If it should appear in the workflow help, add it toPrimaryCommands. - Hiding an obsolete command: add it to
HiddenCommands. - Changing how flags are spliced: edit
mergeEnvArgsinmain.go. - Changing what the
command.Metacarries: edit theMetastruct ininternal/command/meta.goand themeta := command.Meta{...}literal incommands.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.