Version 5 of tabulate

Updated 2015-08-31 14:38:46 by dbohdan

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

Download with wiki-reaper: wiki-reaper -x 41682 1 > tabulate.tcl

Use example

$ ps | jimsh ./tabulate.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.2.0 -- 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
    }
}

# Format a list as a table row. Does *not* append a newline after the row.
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
}

# Convert a list of lists into a string representing a table in pseudographics.
proc ::tabulate::tabulate args {
    set data [dict get $args -data]
    set style [dict-get-default $args $::tabulate::defaultStyle -style]

    # Find out the maximum width of each column.
    set columnWidths {} ;# Dictionary.
    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
            }
        }
    }

    # A dummy row for formatting the table's decorative elements with
    # [formatRow].
    set emptyRow {}
    for {set i 0} {$i < ([llength $columnWidths] / 2)} {incr i} {
        lappend emptyRow {}
    }

    set result {}
    set rowCount [llength $data]
    # Top of the table.
    lappend result [formatRow $emptyRow $columnWidths [dict get $style top]]
    # For each row...
    for {set i 0} {$i < $rowCount} {incr i} {
        set row [lindex $data $i]
        # Row.
        lappend result [formatRow $row $columnWidths [dict get $style row]]
        # Separator.
        if {$i < $rowCount - 1} {
            lappend result [formatRow $emptyRow $columnWidths \
                    [dict get $style separator]]
        }
    }
    # Bottom of the table.
    lappend result [::tabulate::formatRow $emptyRow $columnWidths \
                [dict get $style bottom]]

    return [join $result \n]
}

# Read the input, process the command line options and output the result.
proc ::tabulate::main {argv0 argv} {
    set data [lrange [split [read stdin] \n] 0 end-1]

    # Input field separator. If none is given treat each line of input as a Tcl
    # list.
    set FS [dict-get-default $argv {} -FS]
    if {$FS ne {}} {
        set updateData {}
        foreach line $data {
            lappend updateData [split $line $FS]
        }
        set data $updateData
    }

    puts [::tabulate::tabulate -data $data]
}

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

See also