Last active
August 1, 2018 16:26
-
-
Save plar/9a3fd4f36e18240daf27d8145f1b74cf to your computer and use it in GitHub Desktop.
tf_diff: Humanizing the output of `terraform plan` using python `difflib` module
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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