Command Line Unminifier

by Gearbox

JavaScript and Cascading Style Sheets (CSS) files are usually posted online in minified format (minimum number of spaces and everything on a single line) in order to save transfer bandwidth.  This makes it hard to analyze.  Even though there are online tools to unminify such files, using a command line utility to do this work has the advantage of integration with already existing CLI tools (via pipes or command calls) and the potential to work in bulk.

Such a CLI tool can take the minified content of a JavaScript or CSS file using the standard input and output the unminified content using the standard output.  The default space indentation can be set to two spaces, but can be overridden via an input parameter (indent).  For example, if we name the tool script webballoon.py to unminify the content of a file, we can call it on Linux or Mac like:

$ cat script.min.js | ./webballoon.py

or to use a custom indent of four spaces:

$ cat script.min.js | ./webballoon.py --indent 4

For implementation, Python 3 is a great choice, as it is installed by default on most Linux distros and has a huge number of packages for pretty much everything.

We start with the Python "shebang" line (#!/usr/bin/python3), which tells Bash to use the Python 3 interpreter if the file is called directly without passing it as a parameter to Python 3 (e.g. ./webballoon.py instead of python3 ./webballoon.py).

Next, we import the packages we need:

Although it might seem overkill to use a parameter parsing package for a single input parameter, the application might be further extended in the future, plus there's the advantage that you can call the script with a -h or --help parameter and it displays automatically generated help.

The _get_indent_size function is used to retrieve the value of the indent input parameter (defaults to 2 if the parameter is not specified).  It uses the _check_larger_than_zero helper function to validate that the indent parameter has a specified value that's numeric and greater than zero.

We then use the _get_indent_spaces helper function to get a white space string of variable size that can be use to display the indent and the _print_to_stream helper function to print text to standard output stream without adding a new line at the end.

The core functionality resides in the main function.  It is the module entry point and contains the input processing logic.  It parses the input character-by-character and, based on the encountered characters, generates the output as follows:

1.)  In case the { character is encountered, it means that what will follow is in an inner scope, so we output {, move to the next line, and output an increased indent.

2.)  In case the } character is encountered, it means that what will follow is in an outer scope, so we move to the next line, output a decreased indent, }, move to the next line, and output the current indent.

3.)  In case the ; character is encountered, it means that what will follow is on a separate line at the same indent, so we output ;, move to the next line, and output the indent.

4.)  In case the , character is encountered, it means that multiple elements are separated, so we output a space followed by ,.

5.)  Any other character is outputted as is.  The full code listing below is available under Boost Software License 1.0 and is also available online at: github.com/gearbx/webballoon.

#!/usr/bin/python3

import argparse
import sys

_BAD_INPUT_ERROR_CODE = 1

def _check_larger_than_zero(value) -> int:
    try:
        v = int(value)
        if v <= 0:
            print(f"Invalid parameter value {value}", file=sys.stderr)
            sys.exit(_BAD_INPUT_ERROR_CODE)
        return v
    except Exception:
        print(f"Invalid parameter value {value}", file=sys.stderr)
        sys.exit(_BAD_INPUT_ERROR_CODE)

def _get_indent_size() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--indent", type=_check_larger_than_zero, default=2, help="the indent size in spaces.(default 2)")
    args = parser.parse_args()
    return args.indent

def _get_indent_spaces(indent: int) -> str:
    return " " * indent

def _print_to_stream(text: str):
    print(text, end="", file=sys.stdout)

def main():
    indent_increase = _get_indent_size()
    indent = 0
    for line in sys.stdin:
        for character in line:
            if character == "{":
                indent += indent_increase
                _print_to_stream(" {\n" + _get_indent_spaces(indent))
            elif character == "}":
                indent = max(0, indent - indent_increase)
                _print_to_stream("\n" + _get_indent_spaces(indent) + "}\n" + _get_indent_spaces(indent))
            elif character == ";":
                _print_to_stream(";\n" + _get_indent_spaces(indent))
            elif character == ",":
                _print_to_stream(", ")
            else:
                _print_to_stream(character)

if __name__ == "__main__":
    main()

Code: webballoon.py

Return to $2600 Index