Skip to content

Instantly share code, notes, and snippets.

@tvinke
Created April 18, 2018 14:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tvinke/1cfebf5e81f5ac635457d77764997f62 to your computer and use it in GitHub Desktop.
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)
// 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