Skip to content

Instantly share code, notes, and snippets.

@plar
Last active August 1, 2018 16:26
Show Gist options
  • Save plar/9a3fd4f36e18240daf27d8145f1b74cf to your computer and use it in GitHub Desktop.
Save plar/9a3fd4f36e18240daf27d8145f1b74cf to your computer and use it in GitHub Desktop.
tf_diff: Humanizing the output of `terraform plan` using python `difflib` module
#!/usr/bin/env python3
#
# `tf_diff` is a CLI tool for humanizing the output of `terraform plan` using python `difflib` module
# Requirements:
# 1. Python 3.x
# 2. If you want color output then you need to install a python module `colorama` (pip install colorama)
#
# Installation:
# Copy the file into your `bin` directory and make it executable (chmod u+x ~/bin/tf_diff)
#
# Usage:
# terraform plan ... | tf_plan
import sys, os, time, difflib, optparse, re
try:
from colorama import Fore, Back, Style, init
init()
except ImportError: # fallback so that the imported classes always exist
class ColorFallback():
__getattr__ = lambda self, name: ''
Fore = Back = Style = ColorFallback()
def color_diff(diff):
for line in diff:
if line.startswith('---') or line.startswith('+++'):
yield Fore.WHITE + escape_ansi(line).strip('\n') + Fore.RESET
elif line.startswith('@@'):
yield Fore.CYAN + escape_ansi(line).strip('\n') + Fore.RESET
elif line.startswith('+'):
yield Fore.GREEN + line + Fore.RESET
elif line.startswith('-'):
yield Fore.RED + line + Fore.RESET
elif line.startswith('^'):
yield Fore.BLUE + line + Fore.RESET
else:
yield line
START_MARKER = "Terraform will perform the following actions:"
# Remove the ANSI escape sequences from a string in python
def escape_ansi(line):
ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]')
return ansi_escape.sub('', line)
def print_line(line):
print(line, end='', flush=True)
def main():
usage = "usage: %prog [options]"
parser = optparse.OptionParser(usage)
parser.add_option("-c", action="store_true", default=False,
help='Produce a context format diff')
parser.add_option("-u", action="store_true", default=False,
help='Produce a unified format diff (default)')
parser.add_option("-n", action="store_true", default=False,
help='Produce a ndiff format diff')
parser.add_option("-l", "--lines", type="int", default=3,
help='Set number of context lines (default 3)')
(options, args) = parser.parse_args()
n = options.lines
state = "init"
content_name = ""
content_line = ""
rx = re.compile(r"\\$", flags=re.MULTILINE)
for line in sys.stdin:
if line is None or line == "" or line.strip() == "":
print_line(line)
if state == "init":
if line.startswith(START_MARKER):
state = "analyze"
print_line(line)
elif state == "analyze":
if escape_ansi(line).strip().startswith("~"):
state = "expect_content"
content_name = line.strip()[2:]
print_line(line)
elif state == "expect_content":
res = escape_ansi(line).strip().split(':', 1)
if len(res) != 2:
continue
line = res[1]
content_line = line.strip()
content_line = content_line.replace("\\n", "\n")
content_line = content_line.replace("\\\"", "\"")
content_line = rx.sub("", content_line)
left_content, right_content = content_line.split(" => ")
left_content = left_content.strip("\" ")
right_content = right_content.strip("\" ")
left_lines = left_content.split("\n")
right_lines = right_content.split("\n")
if options.c:
diff = difflib.context_diff(left_lines,
right_lines,
"%s old" % content_name,
"%s new" % content_name,
n=n)
elif options.n:
diff = difflib.ndiff(left_lines, right_lines)
else:
diff = difflib.unified_diff(left_lines,
right_lines,
"%s old" % content_name,
"%s new" % content_name,
n=n)
# reset color
print(Fore.RESET, end='')
diff = color_diff(diff)
for line in diff:
print(" ", line.strip("\n"))
# switch back to analyze phase
state = "analyze"
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment