Version 1 of tabulate

Updated 2015-08-31 13:57:05 by dbohdan

Convert standard input into pretty-printed tables. Inspired by https://github.com/joepvd/table . Works in Tcl 8.5+ and Jim Tcl.

Use example

$ ps | jimsh ./table.tcl   
┌─────┬─────┬────────┬─────┐
│ PID │ TTY │  TIME  │ CMD │
├─────┼─────┼────────┼─────┤
│20583│pts/3│00:00:01│ zsh │
├─────┼─────┼────────┼─────┤
│23301│pts/3│00:00:00│  ne │
├─────┼─────┼────────┼─────┤
│28007│pts/3│00:00:00│  ps │
├─────┼─────┼────────┼─────┤
│28008│pts/3│00:00:00│jimsh│
└─────┴─────┴────────┴─────┘

Code

#! /usr/bin/env tclsh
# Tabulate v0.1.1 -- turn standard input into a table.
# Copyright (C) 2015 Danyil Bohdan
# License: MIT
namespace eval ::tabulate {
    variable defaultStyle {
        top {
            left ┌
            padding ─
            separator ┬
            right ┐
        }
        separator {
            left ├
            padding ─
            separator ┼
            right ┤
        }
        row {
            left │
            padding { }
            separator │
            right │
        }
        bottom {
            left └
            padding ─
            separator ┴
            right ┘
        }
    }
}

# Return a value from dictionary like dict get would if it is there.
# Otherwise return the default value.
proc ::tabulate::dict-get-default {dictionary default args} {
    if {[dict exists $dictionary {*}$args]} {
        dict get $dictionary {*}$args
    } else {
        return $default
    }
}

proc ::tabulate::formatRow {row columnWidths substyle} {
    set result {}
    append result [dict get $substyle left]
    set fieldCount [expr { [llength $columnWidths] / 2 }]
    for {set i 0} {$i < $fieldCount} {incr i} {
        set field [lindex $row $i]
        set padding [expr {
            [dict get $columnWidths $i] - [string length $field]
        }]
        set rightPadding [expr { $padding / 2 }]
        set leftPadding [expr { $padding - $rightPadding }]
        append result [string repeat [dict get $substyle padding] $leftPadding]
        append result $field
        append result [string repeat [dict get $substyle padding] $rightPadding]
        if {$i < $fieldCount - 1} {
            append result [dict get $substyle separator]
        }
    }
    append result [dict get $substyle right]
    return $result
}

proc ::tabulate::tabulate args {
    set data [dict get $args -data]
    set style [dict-get-default $args $::tabulate::defaultStyle -style]

    set columnWidths {}
    foreach row $data {
        for {set i 0} {$i < [llength $row]} {incr i} {
            set field [lindex $row $i]
            set currentLength [string length $field]
            set width [dict-get-default $columnWidths 0 $i]
            if {$currentLength > $width} {
                dict set columnWidths $i $currentLength
            }
        }
    }

    set emptyRow {}
    for {set i 0} {$i < ([llength $columnWidths] / 2)} {incr i} {
        lappend emptyRow {}
    }

    set result {}
    lappend result [::tabulate::formatRow $emptyRow $columnWidths \
            [dict get $style top]]
    foreach row $data {
        # Row.
        lappend result [::tabulate::formatRow $row $columnWidths \
                [dict get $style row]]
        # Separator.
        lappend result [::tabulate::formatRow $emptyRow $columnWidths \
                [dict get $style separator]]
    }
    set result [lrange $result 0 end-1]
    lappend result [::tabulate::formatRow $emptyRow $columnWidths \
                [dict get $style bottom]]

    return [join $result \n]
}

proc ::tabulate::main {argv0 argv} {
    puts [::tabulate::tabulate -data [lrange [split [read stdin] \n] 0 end-1]]
}

# If this is the main script...
if {[info exists argv0] && ([file tail [info script]] eq [file tail $argv0])} {
    ::tabulate::main $argv0 $argv
}

See also