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.
- Features at a glance
- Installation
- Quick start
- CLI reference
- Exclude files
- Group-by-type target files
- Configuration file
- Automation examples
- Contributing & support
- Deterministic sorting – blocks are sorted by logical type priority (
terraform→variable→locals→data→resource→module→import→moved→removed→check→output), then alphabetically by label within each type group. Use--no-sort-by-typeto revert to plain alphabetical type ordering. - Terraform-aware meta args –
count,for_each,providers,moved,removed,check, and friends are placed exactly where Terraform expects them. - Group-by-type output –
tforganize sort -grewrites 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.yamlor supply--config. - CI friendly – published as a Go binary and as
ghcr.io/dthagard/tforganize:latestfor Docker/GitLab/GitHub runners.
brew tap dthagard/tap
brew install tforganizeRequires Go 1.23+
go install github.com/dthagard/tforganize@latestdocker run --rm -v "$(pwd)":/tforganize -w /tforganize ghcr.io/dthagard/tforganize:latest sort -i .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 ./sortedSort all nested directories recursively:
tforganize sort --recursive --inline .Sort multiple files at once:
tforganize sort main.tf variables.tf outputs.tfSort 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-commentsPreserve 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 \
.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.
| 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 |
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 .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'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.
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.
Add tforganize as a pre-commit hook so it runs automatically before every commit.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/dthagard/tforganize
rev: v0.0.0 # replace with the desired tag
hooks:
- id: tforganizeThis 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.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/dthagard/tforganize
rev: v0.0.0 # replace with the desired tag
hooks:
- id: tforganize-dockerThis pulls ghcr.io/dthagard/tforganize:latest and runs the sort inside the container. Ideal for teams where not everyone has Go installed.
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: infrastructurestages: [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: infrastructureversion: 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:
- linttrigger:
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 tforganizeTF_DIRS := $(shell find . -type d -not -path '*/.terraform/*')
tforganize-all:
@for dir in $(TF_DIRS); do echo "Organizing $$dir"; tforganize sort --inline $$dir; donedocker run --rm -v "$(pwd)":/tforganize -w /tforganize ghcr.io/dthagard/tforganize:latest sort -i .- Issues / ideas → GitHub Issues
- PRs welcome — please run
go test ./...and include a short description of the behavior change. - Licensed under MIT.
Happy organizing!
