Created
April 18, 2018 14:57
-
-
Save tvinke/1cfebf5e81f5ac635457d77764997f62 to your computer and use it in GitHub Desktop.
Groovy script to output JSON from standard input as a table to the terminal (examples of CliBuilder, JsonSlurper and ANSI coloring)
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
// Small JSON printer | |
// | |
// Just feed some JSON to this script to display it in tabular form. | |
// Usage e.g. | |
// http api.example.com/users/1 Authorization:"..." | groovy JsonTable.groovy | |
// | |
// Limit amount of rows | |
// http api.example.com/users Authorization:"..." | groovy JsonTable.groovy -l 10 | |
// | |
// Only show certain keys of the JSON and limit amount of rows to 1 | |
// http api.example.com/users Authorization:"..." | groovy JsonTable.groovy -sk id,name -l 1 | |
// | |
// Output is something like: | |
// ╔═════════╦═══════════════╗ | |
// ║ id ║ name ║ | |
// ╠═════════╬═══════════════╣ | |
// ║ 1 ║ Bob ║ | |
// ╚═════════╩═══════════════╝ | |
// 1 of 138 items | |
import groovy.json.* | |
import groovy.transform.builder.Builder | |
import static Ansi.* | |
// parse command-line args | |
def cli = new CliBuilder(usage: "${this.class.name}.groovy [options]", header: "Options", width: 200) | |
cli.h longOpt: "help", "Show usage information" | |
cli.sk longOpt: "show-keys", args: 1, argName: "keys", "Comma-separated list of keys to show e.g. 'id,date,whatever'. By default everything is shown." | |
cli.hk longOpt: "hide-keys", args: 1, argName: "keys", "Comma-separated list of keys to hide e.g. 'id,date,whatever'. By default nothing is hidden." | |
cli.n longOpt: "nulls", "Show null values textually as 'null'. By default nothing is displayed." | |
cli.l longOpt: "limit", args: 1, argName: 'number', "Limit the output to this number of results e.g. 10. By default, no limits." | |
def options = cli.parse(args) | |
if (!options) { | |
return | |
} | |
if (options.h) { | |
cli.usage() | |
return | |
} | |
// set defaults | |
Set showKeys = options.hasOption('show-keys') ? options.sk.split(',') : [] | |
Set hideKeys = options.hasOption('hide-keys') ? options.hk.split(',') : [] | |
boolean showNulls = options.hasOption('nulls') | |
int limit = options.hasOption('l') ? Integer.parseInt(options.l) : 0 | |
// parse JSON | |
def result | |
try { | |
result = new JsonSlurper().parse System.in.newReader() | |
} catch (JsonException e) { | |
System.err.println "Unable to parse JSON: $e" | |
System.exit(-1) | |
} | |
// output | |
def table = new Table(result, showKeys, hideKeys, showNulls) | |
table.printTable(limit) | |
class Table { | |
static final List<String> COLOR_OUTLINE = [Ansi.DARK_GRAY] | |
static final List<String> COLOR_KEY = [Ansi.GREEN, Ansi.BOLD] | |
static final List<String> COLOR_VALUE = [Ansi.CYAN] | |
final def json | |
final Set keys | |
final Map<String, Integer> columnWidths = [:] | |
final boolean showNulls | |
Table(def jsonResult, Set showKeys = [], Set hideKeys = [], boolean showNulls = false) { | |
this.json = jsonResult | |
// determine keys (labels) to show | |
this.keys = determineKeys(showKeys, hideKeys) | |
// determine column widths for padding purposes | |
this.columnWidths = determineColumnWidths() | |
this.showNulls = showNulls | |
} | |
/** | |
* Determine the widths for each column per key - usable to pad each column | |
* to a fixed width based on the longest value in the JSON for each key. | |
* | |
* <p>The whole JSON is traversed to determine this. | |
* | |
* @return mapping of a key to its width | |
*/ | |
Map<String, Integer> determineColumnWidths() { | |
Map<String, Integer> widths = [:].withDefault { 10 } | |
List jsonList = (json instanceof Map ? [json] : json) | |
jsonList.each { row -> | |
keys.each { key -> | |
String value = row[key] | |
int valueSize = value?.size() ?: 0 | |
widths[key] = Math.max(widths[key], Math.max(key.size(), valueSize)) | |
} | |
} | |
return widths | |
} | |
/** | |
* Return the keys from the JSON, taking into account | |
* <ol> | |
* <li>the available keys - by traversing the resource (or first resource in case of a collection) | |
* <li>of the available keys, only leave the explicitly specified keys to show | |
* <li>of the resulting keys, hide the explicitly specified keys to hide | |
* </ol> | |
* | |
* If at the end no keys are left, all available keys are returned. | |
* | |
* @param showKeys Explicitly specified keys to show | |
* @param hideKeys Explicitly specified keys to hide | |
* @return resulting keys | |
*/ | |
Set<String> determineKeys(Set showKeys, Set hideKeys) { | |
Set availableKeys | |
if (json instanceof List) { | |
availableKeys = json ? json.first().keySet() : [] | |
} else { | |
availableKeys = json.keySet() | |
} | |
(availableKeys.intersect(showKeys) - hideKeys) ?: (availableKeys - hideKeys) | |
} | |
/** | |
* Print the JSON in tabular format. | |
* | |
* @param limit Optionally, the max amount of resources to display, if limit > 0. | |
*/ | |
void printTable(int limit = 0) { | |
if (!json) { | |
return | |
} | |
def printHeader = { | |
printRow keys, new Row(outlineStart: "╔", outlinePadding: "═", outlineDelim: "╦", outlineEnd: "╗") | |
printRow keys, new Row(outlineStart: "║", outlinePadding: " ", outlineDelim: "║", outlineEnd: "║", valueColor: COLOR_KEY, valueWriter: { key -> key }) | |
printRow keys, new Row(outlineStart: "╠", outlinePadding: "═", outlineDelim: "╬", outlineEnd: "╣") | |
} | |
def printBody = { resource -> | |
printRow keys, new Row(outlineStart: "║", outlineDelim: "║", outlineEnd: "║", valueWriter: { key -> | |
def value = resource[key] | |
value != null ? value : (showNulls ? 'null' : '') | |
}) | |
} | |
def printFooter = { | |
printRow keys, new Row(outlineStart: "╚", outlineDelim: "╩", outlineEnd: "╝") | |
} | |
printHeader() | |
if (json instanceof List) { | |
// collection | |
int fromIndex = 0 | |
int toIndex = Math.min(json.size(), limit ?: Integer.MAX_VALUE) | |
json.subList(fromIndex, toIndex).each { printBody(it) } | |
printFooter() | |
printlnColor "${toIndex-fromIndex} of ${json.size()} items", COLOR_OUTLINE | |
} else { | |
// single resource | |
printBody(json) | |
printFooter() | |
} | |
} | |
/** | |
* Represents a row in the table with its formatting options. | |
*/ | |
@Builder | |
class Row { | |
String outlineStart = "╔" | |
String outlinePadding = "═" | |
String outlineDelim = "╦" | |
String outlineEnd = "╗" | |
List<String> outlineColor = COLOR_OUTLINE | |
List<String> valueColor = COLOR_VALUE | |
Closure<String> valueWriter | |
} | |
void printRow(Set keys, Row row) { | |
printColor row.outlineStart, row.outlineColor | |
keys.eachWithIndex { key, i -> | |
int paddingRight = columnWidths[key] + 3 | |
def endChar = (i < keys.size() - 1) ? row.outlineDelim : row.outlineEnd + "\n" | |
def value = "" | |
def valueColor = row.outlineColor | |
def outlinePadding = row.outlinePadding | |
if (row.valueWriter) { | |
value = row.valueWriter(key) | |
if (value != null) { | |
value = " $value" | |
valueColor = row.valueColor | |
outlinePadding = " " | |
} | |
} | |
printColor "$value".padRight(paddingRight, outlinePadding), valueColor | |
printColor endChar, row.outlineColor | |
} | |
} | |
} | |
/** | |
* Small ANSI coloring utility. | |
* | |
* @see http://www.bluesock.org/~willg/dev/ansi.html | |
* @see https://gist.github.com/dainkaplan/4651352 | |
*/ | |
class Ansi { | |
static final String NORMAL = "\u001B[0m" | |
static final String BOLD = "\u001B[1m" | |
static final String ITALIC = "\u001B[3m" | |
static final String UNDERLINE = "\u001B[4m" | |
static final String BLINK = "\u001B[5m" | |
static final String RAPID_BLINK = "\u001B[6m" | |
static final String REVERSE_VIDEO = "\u001B[7m" | |
static final String INVISIBLE_TEXT = "\u001B[8m" | |
static final String BLACK = "\u001B[30m" | |
static final String RED = "\u001B[31m" | |
static final String GREEN = "\u001B[32m" | |
static final String YELLOW = "\u001B[33m" | |
static final String BLUE = "\u001B[34m" | |
static final String MAGENTA = "\u001B[35m" | |
static final String CYAN = "\u001B[36m" | |
static final String WHITE = "\u001B[37m" | |
static final String DARK_GRAY = "\u001B[1;30m" | |
static final String LIGHT_RED = "\u001B[1;31m" | |
static final String LIGHT_GREEN = "\u001B[1;32m" | |
static final String LIGHT_YELLOW = "\u001B[1;33m" | |
static final String LIGHT_BLUE = "\u001B[1;34m" | |
static final String LIGHT_PURPLE = "\u001B[1;35m" | |
static final String LIGHT_CYAN = "\u001B[1;36m" | |
static String color(String text, String ansiValue) { | |
ansiValue + text + NORMAL | |
} | |
static String color(String text, List<String> ansiValues) { | |
ansiValues.inject(text, { coloredText, ansiValue -> | |
color(coloredText, ansiValue) | |
}) | |
} | |
static printColor(String text, String ansiValue) { | |
print color(text, ansiValue) | |
} | |
static printColor(String text, List<String> ansiValues) { | |
print color(text, ansiValues) | |
} | |
static printlnColor(String text, String ansiValue) { | |
println color(text, ansiValue) | |
} | |
static printlnColor(String text, List<String> ansiValues) { | |
println color(text, ansiValues) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment