Skip to content

dthagard/tforganize

Repository files navigation

tforganize

Opinionated Terraform file organizer — keep modules, variables, resources, and meta-arguments in a predictable order.

tforganize is a CLI that rewrites .tf files so they match a consistent layout. It sorts blocks, enforces Terraform's canonical meta-argument order, optionally splits output by block type, and protects custom headers/comments when you want to keep them.

tforganize demo

Table of contents

Features at a glance

  • Deterministic sorting – blocks are sorted by logical type priority (terraformvariablelocalsdataresourcemoduleimportmovedremovedcheckoutput), then alphabetically by label within each type group. Use --no-sort-by-type to revert to plain alphabetical type ordering.
  • Terraform-aware meta argscount, for_each, providers, moved, removed, check, and friends are placed exactly where Terraform expects them.
  • Group-by-type outputtforganize sort -g rewrites files into logical targets (variables.tf, outputs.tf, checks.tf, imports.tf, main.tf, …).
  • Header/comment control – strip comments entirely, preserve them, or keep/apply a custom header banner.
  • Stdin support – pipe HCL content via stdin (cat main.tf | tforganize sort -) for easy integration with other tools.
  • Inline or out-of-place – update files in place (--inline) or emit to an output directory for review/CI.
  • Configurable – every flag has a YAML counterpart so you can save defaults in .tforganize.yaml or supply --config.
  • CI friendly – published as a Go binary and as ghcr.io/dthagard/tforganize:latest for Docker/GitLab/GitHub runners.

Installation

Homebrew

brew tap dthagard/tap
brew install tforganize

Go

Requires Go 1.23+

go install github.com/dthagard/tforganize@latest

Docker

docker run --rm -v "$(pwd)":/tforganize -w /tforganize ghcr.io/dthagard/tforganize:latest sort -i .

Quick start

Sort everything in the current directory in-place:

tforganize sort -i .

Split blocks by type (creates variables.tf, outputs.tf, checks.tf, imports.tf, etc.):

tforganize sort --group-by-type --output-dir ./sorted

Sort all nested directories recursively:

tforganize sort --recursive --inline .

Sort multiple files at once:

tforganize sort main.tf variables.tf outputs.tf

Sort from stdin:

cat main.tf | tforganize sort -

Preview changes without writing anything:

tforganize sort --diff .

Collapse empty blocks (e.g. data "aws_region" "current" {}) to one line:

tforganize sort --compact-empty-blocks --inline .

Strip section-divider comments (e.g. # === Section ===, # ---) while sorting:

tforganize sort --strip-section-comments --inline .

Check for drift in CI (exits non-zero and shows what changed):

tforganize sort --diff --check .

Keep a copyright header while stripping other comments:

tforganize sort \
  --inline \
  --has-header \
  --header-pattern "$(cat header.txt)" \
  --keep-header \
  --remove-comments

Preserve a multi-line /** **/ header using a partial pattern:

tforganize sort \
  --inline \
  --has-header \
  --header-pattern 'Copyright' \
  --keep-header \
  .

Use --header-end-pattern for precise control over where the header ends:

tforganize sort \
  --inline \
  --has-header \
  --header-pattern '/**' \
  --header-end-pattern '**/' \
  --keep-header \
  .

CLI reference

Usage: tforganize sort [file | folder | -] ... [flags]

Flags:
  -c, --check                   exit non-zero if any file would change (dry-run mode)
      --compact-empty-blocks    collapse empty blocks to a single line (e.g. data "aws_region" "current" {})
      --config string           YAML config path (default $HOME/.tforganize.yaml)
  -d, --debug                   enable verbose logging
      --diff                    show a unified diff of changes instead of writing files
  -x, --exclude stringArray     glob pattern to exclude from sorting (repeatable; supports **)
  -g, --group-by-type           write each block type to its default file (see table below)
  -e, --has-header              treat files as having a header matched by --header-pattern
      --header-end-pattern string  pattern marking the end of a multi-line header block (e.g. '**/' or '*/')
  -p, --header-pattern string   string that identifies the header block (can be a substring like 'Copyright')
  -i, --inline                  rewrite files in place (otherwise write to --output-dir)
  -k, --keep-header             preserve the matched header in the output (requires --has-header and pattern)
      --no-sort-by-type         sort blocks alphabetically by type instead of using logical type ordering
  -o, --output-dir string       directory for sorted files (required unless --inline)
  -R, --recursive               sort all nested directories (each directory independently)
  -r, --remove-comments         drop all comments except headers kept via --keep-header
      --strip-section-comments  remove section-divider comments (e.g. # === Section ===, # ---)

--diff and --check can be combined: --diff --check prints the unified diff and exits non-zero if any file would change.

Exit codes

Code Meaning
0 Success (or no changes in --check mode)
1 Runtime error (invalid flags, parse failure, I/O error, etc.)
2 --check detected files that would change

Environment variables

All flags can also be set via environment variables prefixed with TFORGANIZE_. Dashes become underscores:

TFORGANIZE_INLINE=true tforganize sort .
TFORGANIZE_GROUP_BY_TYPE=true tforganize sort --output-dir ./sorted .
TFORGANIZE_NO_SORT_BY_TYPE=true tforganize sort .
TFORGANIZE_EXCLUDE='.terraform/**' tforganize sort .

Exclude files

You can exclude specific files or directories from sorting using glob patterns. The pattern is matched against the file path relative to the target directory.

  • Supports standard wildcards (*, ?)
  • Supports recursive matching (**)
# Skip .terraform directory and generated files
tforganize sort . --exclude '.terraform/**' --exclude '*.generated.tf'

Group-by-type target files

When --group-by-type (or group-by-type: true in config) is enabled, blocks are emitted to the following defaults:

Block type File name
data data.tf
locals locals.tf
output outputs.tf
terraform versions.tf
variable variables.tf
check checks.tf
import imports.tf
moved main.tf
removed main.tf
everything else main.tf

You can feed multiple files and directories; tforganize builds the combined AST, sorts it, and then writes these grouped files to the chosen output.

Configuration file

All flags can be set via YAML (default $HOME/.tforganize.yaml or pass --config). Example:

# ~/.tforganize.yaml
group-by-type: true
inline: true
remove-comments: false
has-header: true
keep-header: true
header-pattern: |
  /**
   * Company Confidential
   */
# Optional: use header-end-pattern for partial header-pattern values
# header-pattern: "Copyright"
# header-end-pattern: "**/"
exclude:
  - .terraform/**
  - terraform.tfstate.d/**
  - "*.generated.tf"

Key fields:

Key Description
check Same as --check
compact-empty-blocks Same as --compact-empty-blocks
diff Same as --diff
exclude List of glob patterns to exclude
group-by-type Same as --group-by-type
has-header Indicates a header block exists
header-end-pattern Pattern marking the end of a multi-line header (e.g. **/)
header-pattern String that identifies the header block (can be a substring)
inline Same as --inline
keep-header Re-emit the matched header (requires the two options above)
no-sort-by-type Same as --no-sort-by-type
output-dir Same as --output-dir
recursive Same as --recursive
remove-comments Same as --remove-comments
strip-section-comments Same as --strip-section-comments

tforganize refuses to run with keep-header: true unless has-header is true and header-pattern is non-empty — the same validation applies to CLI flags.

Automation examples

pre-commit

Add tforganize as a pre-commit hook so it runs automatically before every commit.

Script hook (requires tforganize on PATH)

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/dthagard/tforganize
    rev: v0.0.0  # replace with the desired tag
    hooks:
      - id: tforganize

This runs scripts/pre-commit-tforganize.sh, which calls tforganize sort --inline on the staged .tf files. If tforganize is not installed you will see a clear error with installation instructions.

Docker hook (no Go installation required)

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/dthagard/tforganize
    rev: v0.0.0  # replace with the desired tag
    hooks:
      - id: tforganize-docker

This pulls ghcr.io/dthagard/tforganize:latest and runs the sort inside the container. Ideal for teams where not everyone has Go installed.

GitHub Actions

name: Terraform hygiene

on:
  pull_request:

jobs:
  tforganize:
    runs-on: ubuntu-latest
    container: ghcr.io/dthagard/tforganize:latest
    steps:
      - uses: actions/checkout@v4
      - run: tforganize sort --diff --check "$TF_ROOT"
    env:
      TF_ROOT: infrastructure

GitLab CI

stages: [lint]

terraform:lint:
  stage: lint
  image:
    name: ghcr.io/dthagard/tforganize:latest
    entrypoint: [""]
  script:
    - tforganize sort --diff --check "$TF_ROOT"
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  variables:
    TF_ROOT: infrastructure

CircleCI

version: 2.1

executors:
  tforganize:
    docker:
      - image: ghcr.io/dthagard/tforganize:latest

jobs:
  lint:
    executor: tforganize
    steps:
      - checkout
      - run: tforganize sort --diff --check "$TF_ROOT"

workflows:
  version: 2
  terraform:
    jobs:
      - lint

Azure Pipelines

trigger:
  branches:
    include: [ main ]

pool:
  vmImage: ubuntu-latest

container: ghcr.io/dthagard/tforganize:latest

steps:
  - checkout: self
  - script: tforganize sort --diff --check $(TF_ROOT)
    displayName: Run tforganize

Makefile loop

TF_DIRS := $(shell find . -type d -not -path '*/.terraform/*')

tforganize-all:
	@for dir in $(TF_DIRS); do 	  echo "Organizing $$dir"; 	  tforganize sort --inline $$dir; 	done

Docker one-liner

docker run --rm -v "$(pwd)":/tforganize -w /tforganize   ghcr.io/dthagard/tforganize:latest sort -i .

Contributing & support

  • Issues / ideas → GitHub Issues
  • PRs welcome — please run go test ./... and include a short description of the behavior change.
  • Licensed under MIT.

Happy organizing!

About

CLI tool to organize your Terraform code

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages