tforg

command module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 8, 2026 License: MIT Imports: 2 Imported by: 0

README

tforg — fast Terraform formatter + file organizer

CI

tforg does two things to a Terraform codebase, very fast:

  1. Formats every .tf file with output byte-identical to terraform fmt (it uses HashiCorp's own hclwrite library — the same code that powers terraform fmt — so there is no external binary dependency and no drift).

  2. Organizes top-level blocks into their conventional files within each directory, creating the files when they don't exist:

    Block type File
    resource, module main.tf
    data data.tf
    variable variables.tf
    output outputs.tf
    locals locals.tf
    provider providers.tf
    terraform versions.tf
    moved moved.tf
    import imports.tf
    removed removed.tf
    check checks.tf
    ephemeral ephemeral.tf

Blocks are moved as verbatim text (leading comments travel with their block), then formatted. Blocks only ever move between files in the same directory, since a directory is the module boundary. Source files left empty by the moves are deleted. On a benchmark of 1,000 files across 100 module directories, a full run takes ~50ms and a no-op verification ~25ms.

Install

With Go:

go install github.com/FutureFrenzy96/tforg@latest   # or pin @v0.1.0

go install puts the binary in $(go env GOPATH)/bin — make sure that's on your PATH (export PATH="$PATH:$(go env GOPATH)/bin" in your shell profile).

Without Go: download a prebuilt binary for macOS/Linux/Windows from the releases page, unpack it, and put tforg somewhere on your PATH.

From a checkout: go build -ldflags="-s -w" -o tforg .

Releasing (maintainers)

Releases are automated: push a tag and GitHub Actions runs GoReleaser, which builds binaries for all platforms, generates checksums and a changelog, and publishes the release.

git tag v0.1.0 && git push origin main --tags

Usage

tforg                    # format + organize the current directory, recursively
tforg path/to/repo       # ... a specific directory
tforg modules/vpc/x.tf   # ... a single file (blocks move to siblings in its dir)
tforg -staged            # ... the .tf files currently staged in git
tforg -check .           # report what would change, write nothing (CI-friendly)
tforg -diff .            # unified diff of pending changes (implies -check)
tforg -sort .            # also alphabetize variable/output blocks
tforg -fmt-only .        # formatting only, no block moves
tforg -quiet .           # errors only
tforg -exclude '**/generated/**' .   # skip matching files (or `ignore` in .tforg.hcl)
tforg -no-color .        # plain output (NO_COLOR / CLICOLOR_FORCE also honored)
tforg -map terraform=terraform.tf,module=modules.tf .   # override destinations
tforg -version           # print version
tforg install-hook       # write .git/hooks/pre-commit for the current repo

Output is grouped per directory and color-coded — each conventional file has its own color (green main.tf, yellow variables.tf, blue data.tf, ...), +/-/~ mark created, deleted, and reformatted files, and a summary line shows the total and elapsed time:

example
  everything.tf → versions.tf   terraform
  everything.tf → variables.tf  variable "region"
  + variables.tf  created
  - everything.tf  deleted (empty)

✓ fixed 12 files in 2 directories · 6ms

Colors turn off automatically when output is piped (CI logs stay clean) and under NO_COLOR; set CLICOLOR_FORCE=1 to keep them when a hook runner captures output.

Exit codes: 0 nothing to do · 1 changes were made (or are needed, with -check) · 2 error (e.g. a file that does not parse).

Configuration: .tforg.hcl

Some blocks legitimately belong outside the default convention — say a module that only loads data sources, which you want in data.tf rather than main.tf. Rather than annotating your Terraform with comments, declare the exception once in a .tforg.hcl at the repo root:

place "module" "network_data" {
  file = "data.tf"
}

Rules are checked before the type mapping and the first match wins; anything unmatched follows the normal convention. The second label is the block's name (labels joined with . for two-label blocks, so aws_instance.web addresses a specific resource) and may be a glob (data_*) if you later adopt a naming convention. Two other forms are supported:

place "module" "legacy" {
  keep = true            # leave it wherever it currently lives
}

map {
  terraform = "terraform.tf"   # change a type's default destination
}

ignore = ["**/generated/**", "*.gen.tf"]   # never touch generated Terraform

ignore patterns are gitignore-style globs, relative to the .tforg.hcl location; bare patterns (no /) match any path component or file name. Matched files are neither reorganized nor formatted. The -exclude flag is the CLI equivalent (relative to the working directory).

The nearest .tforg.hcl — in the target directory or any parent — applies, so one file at the repo root covers every nested module. -config path points at an explicit file, -no-config ignores config files entirely, and CLI flags always win (-map 'module:network_data=data.tf' is the one-off equivalent of a place rule). Placement stays fully deterministic and idempotent: it depends only on your code and config, never on run history.

Git pre-commit hook

From inside any repo, run:

tforg install-hook

This writes a .git/hooks/pre-commit (respecting core.hooksPath; add -force to overwrite an existing hook) that runs tforg -staged: only the staged .tf files are checked, and if anything is rewritten the commit is aborted so you can review and re-stage. The same script lives at hooks/pre-commit if you prefer to copy it manually.

Note for partial staging (git add -p): like any formatter hook, tforg rewrites the working-tree file, so staged and unstaged hunks of the same file are formatted together.

If you use the pre-commit framework, this repo ships a .pre-commit-hooks.yaml:

repos:
  - repo: https://github.com/FutureFrenzy96/tforg
    rev: v0.1.0
    hooks:
      - id: tforg

Behavior details

  • Duplicate detection: two blocks with the same address in one module (variable "region" defined twice, identical resource addresses, ...) and the same key defined in two locals blocks are reported as errors with file:line locations (exit 2) and nothing is moved — catching at commit time what Terraform would only surface at plan time. Provider aliases, repeated terraform/locals blocks themselves, and override files are exempt, as Terraform allows those.
  • Sorting (-sort, opt-in): variable and output blocks are alphabetized within their files. A file is skipped when reordering would be unsafe (mixed block types, or standalone comments between blocks). Resources are never reordered.
  • Atomic writes: files are written via temp-file-and-rename, so an interrupted run can never leave a truncated .tf behind.
  • Comments: comment lines directly above a block move with it; comments separated from a block by a blank line stay where they are. A comment on the closing-brace line travels with the block.
  • Heredocs and strings are never confused for block boundaries — the real HCL lexer decides what is code and what is a comment.
  • Override files (override.tf, *_override.tf) are formatted but their blocks are never moved, preserving Terraform's override merge semantics.
  • Unknown block types stay where they are.
  • Files that fail to parse are reported (exit 2) and left untouched, and no blocks are moved into an unparseable destination.
  • .tf.json, .terraform/, .git/, and hidden directories are skipped.
  • Idempotent: running twice always yields "nothing to do".

License

MIT

Documentation

Overview

Command tforg formats Terraform files (terraform fmt equivalent) and organizes top-level blocks into their conventional files. See internal/cli for the command implementation and internal/engine for the core logic.

Directories

Path Synopsis
internal
cli
Package cli parses flags, walks the target tree, and reports results; the actual file rewriting lives in internal/engine.
Package cli parses flags, walks the target tree, and reports results; the actual file rewriting lives in internal/engine.
engine
Package engine implements tforg's core: parsing Terraform files, moving top-level blocks into their conventional files, duplicate detection, and terraform-fmt-identical formatting via hclwrite.
Package engine implements tforg's core: parsing Terraform files, moving top-level blocks into their conventional files, duplicate detection, and terraform-fmt-identical formatting via hclwrite.
gitx
Package gitx holds tforg's git integration: staged-file discovery and the pre-commit hook installer.
Package gitx holds tforg's git integration: staged-file discovery and the pre-commit hook installer.
ui
Package ui renders tforg's terminal output: the color palette and unified diffs.
Package ui renders tforg's terminal output: the color palette and unified diffs.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL