Skip to content

Instantly share code, notes, and snippets.

@yermulnik
Last active June 24, 2024 22:26
Show Gist options
  • Save yermulnik/7e0cf991962680d406692e1db1b551e6 to your computer and use it in GitHub Desktop.
Save yermulnik/7e0cf991962680d406692e1db1b551e6 to your computer and use it in GitHub Desktop.
Sort Terraform (HCL) file by Resource Block Names using GNU `awk`
#!/usr/bin/env -S awk -f
# https://gist.github.com/yermulnik/7e0cf991962680d406692e1db1b551e6
# Tested with GNU Awk 5.1.0, API: 3.0 (GNU MPFR 4.1.0, GNU MP 6.2.1)
# Usage: /path/to/tf_vars_sort.awk < variables.tf | tee sorted_variables.tf
# Note: "chmod +x /path/to/tf_vars_sort.awk" before use
# No licensing; yermulnik@gmail.com, 2021-2024
{
# skip blank lines at the beginning of file
if (!resource_type && length($0) == 0) next
# pick only known Terraform resource definition block types of the 1st level
# https://github.com/hashicorp/terraform/blob/main/internal/configs/parser_config.go#L92-L230
switch ($0) {
# ex: block_type {
case /^[[:space:]]*(import|locals|moved|removed|terraform)[[:space:]]+{/:
resource_type = $1
resource_ident = resource_type "|" block_counter++
break
# ex: block_type type_label name_label {
case /^[[:space:]]*(data|resource)[[:space:]]+("?[[:alnum:]_-]+"?[[:space:]]+){2}{/:
resource_type = $1
resource_subtype = $2
resource_name = $3
resource_ident = resource_type "|" resource_subtype "|" resource_name
break
# ex: block_type name_label {
case /^[[:space:]]*(check|module|output|provider|variable)[[:space:]]+"?[[:alnum:]_-]+"?[[:space:]]+{/:
resource_type = $1
resource_name = $2
resource_ident = resource_type "|" resource_name
break
}
arr[resource_ident] = arr[resource_ident] ? arr[resource_ident] RS $0 : $0
} END {
# exit if there was solely empty input
# (input consisting of multiple empty lines only, counts in as empty input too)
if (length(arr) == 0) exit
# declare empty array (the one to hold final result)
split("", res)
# case-insensitive string operations in this block
# (primarily for the `asort()` call below)
IGNORECASE = 1
# sort by `resource_ident` which is a key in our case
asort(arr)
# blank-lines-fix each block
for (item in arr) {
split(arr[item],new_arr,RS)
# remove multiple blank lines at the end of resource definition block
while (length(new_arr[length(new_arr)]) == 0) delete new_arr[length(new_arr)]
# add one single blank line at the end of the resource definition block
# so that blocks are delimited with a blank like to align with TF code style
new_arr[length(new_arr)+1] = RS
# fill resulting array with data from each resource definition block
for (line in new_arr) {
# trim whitespaces at the end of each line in resource definition block
gsub(/[[:space:]]+$/, "", new_arr[line])
res[length(res)+1] = new_arr[line]
}
}
# ensure there are no extra blank lines at the beginning and end of data
while (length(res[1]) == 0) delete res[1]
while (length(res[length(res)]) == 0) delete res[length(res)]
# print resulting data to stdout
for (line in res) {
print res[line]
}
}
@wilson
Copy link

wilson commented Feb 14, 2023

Just wanted to express my thanks for this. Saves me trouble all the time.

@CGAndrej
Copy link

CGAndrej commented Mar 3, 2023

Is it possible to add a -recursive feature?

Do it for every file in my subdirectories as well. Looks like great work and a big value add but can't find the repo in chocolatey or homebrew!
image
image

@yermulnik
Copy link
Author

@CGAndrej This script isn't mean to be run on its own, but rather to read from stdin and print result to stdout — this is why a feature to process directories recursively isn't doable.
To achieve your goal you may leverage e.g. find like this:

#!/usr/bin/env bash
set -e # abort on errors

chmod +x /path/to/tf_vars_sort.awk

find path/to/dir/with/tf/files/ -name "*.tf" -print0 | while read -d $'\0' FILE; do
    echo "Processing: $FILE"
    cp "$FILE" "$FILE.bak" # preserve backup of original file
    /path/to/tf_vars_sort.awk < "$FILE" > "${FILE}.sorted"
    mv "${FILE}.sorted" "$FILE" # overwrite original file
done

Hope this helps.

@yermulnik
Copy link
Author

JFYI: recently there was tfsort utility released, which provides the alike functionality, but is written in Golang — https://github.com/AlexNabokikh/tfsort

@camilosantana
Copy link

tfsort utility released, which provides the alike functionality

@yermulnik This awk script supports more than variables.tf and output.tf. I've been unable to use tfsort for regular resource files whereas this works for a greater context.

@yermulnik
Copy link
Author

@yermulnik This awk script supports more than variables.tf and output.tf. I've been unable to use tfsort for regular resource files whereas this works for a greater context.

@camilosantana Glad to hear this script makes value 👍

@inkstom
Copy link

inkstom commented Jan 3, 2024

Still works, macos Sonoma 14.2.1, GNU Awk 5.3.0, API 4.0, (GNU MPFR 4.2.1, GNU MP 6.3.0), just needed to change awk to 'gawk' in the first line of the script. https://formulae.brew.sh/formula/gawk

#!/usr/bin/env -S gawk -f
...

@perkerk
Copy link

perkerk commented Jun 24, 2024

Wasn't working for "resource" blocks. Until I added break statements to the end of each case! https://www.gnu.org/software/gawk/manual/gawk.html#Break-Statement

@yermulnik
Copy link
Author

Wasn't working for "resource" blocks. Until I added break statements to the end of each case!

@perkerk Thanks for pointing this out.
I've just updated this gist with break in each case statement.
And also added more 1st-level block types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment