· go golang

Aligning Text in Go with text/tabwriter

I recently wrote a small tool in Go, called Trawl, that prints out various information about the network interfaces on your machine. The output is in a columnar fashion, much like a csv file, as so:

trawl-output

Getting the output into that format has gone through a few iterations; firstly static, then more dynamic and then I stumbled across Go’s tabwriter package, which made everything better.

Initially all the column widths were static. An IP address will be at most 15 characters wide, 192.168.100.200, a MAC address will be 17, 10:02:b5:e4:de:8c, etc. etc.

More problematic were the interface names. I built the tool to work on Windows as well as Unix based systems, and Windows interfaces have ridiculously long, but obvious, names like Wireless Network Connection, rather than something short and less obvious like wlp1s0, the wireless interface on my Ubuntu laptop. So I had to consider this variance in length, and did so by just counting the longest interface names I encountered and setting the width to that, which didn’t take into account interfaces I hadn’t, or wouldn’t, encounter. I know, I know, this is awful design/code, and did lead to issues. Anyway, it looked something like this;

The String method used for printing:

func (iface *Interface) String() string {
	ifaceString := osString()
	return fmt.Sprintf(
		ifaceString,
		iface.Name,
		iface.IPv4Addr,
		iface.IPv4Mask,
		iface.IPv4Network,
		strconv.Itoa(iface.MTU),
		iface.HardwareAddr,
		iface.IPv6Addr,
	)
}

The osString function to determine what operating system the tool is being used on:

func osString() (s string) {
	switch os {
	case win:
		s = windowsString
	case linux:
		s = linuxString
	case darwin:
		s = darwinString
	}
	return
}

The static strings:

windowsString     = "%-35s  %-15s  %-15s  %-18s  %-5s  %-17s  %s\n"
linuxString       = "%-10s  %-15s  %-15s  %-18s  %-5s  %-17s  %s\n"
darwinString      = "%-10s  %-15s  %-15s  %-18s  %-5s  %-17s  %s\n"

And looping through each interface, printing as we go:

ifaces := getIfaces(loopback)

for _, iface := range ifaces {
    i, err := New(iface)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf(i.String())
}

Horrible I know, but it got the tool built. Thankfully, refactoring happened.

I knew when writing this that it just would not do, so next iteration I got dynamic. I realised that it was the interface names that were the main problem, the element I didn’t know in advance, and so couldn’t predict, and that differed across operating systems. So I left the other columns with static numbers and spent more time than I would have liked working out how to format the string to use a width not known in advance. This was kinda buried in the fmt package documentation and took a bit of messing around on the Go Playground to get it right, maybe I’m just slow on the uptake (person shrugging emoji).

Either or both of the flags may be replaced with the character ‘*‘, causing their values to be obtained from the next operand, which must be of type int.

Let’s have an example:

name := "rob"

// here we're saying we want the width to be 10 characters
// and we're going to pad with zeroes just so it's visually clearer
s1 := "%010s\n"
fmt.Printf(s1, name)

// now we're going to replace the 10 with an asterisk and provide the
// width we want in the Printf statement
s2 := "%0*s\n"
// Note the width comes first, to build the verb before it can accept
// the string
fmt.Printf(s2, 15, name)

// output
// 0000000rob
// 000000000000rob

Okay, easy enough, when you know how. So, armed with this knowledge I added an extra for loop to find the maximum width of all the interface names and committed. My code now looked something like so:

The new dynamic string, the only one, no matter what your OS:

outputString := "%-*s  %-15s  %-15s  %-18s  %-5s  %-17s  %s\n"

I replaced the String method with this function (still getting to understand when to use a function and when to use a method):

func ifaceString(l int, i *Iface) string {
	return fmt.Sprintf(
		outputString,
		l,
		setMissingValue(i.Name),
		setMissingValue(i.IPv4Addr),
		setMissingValue(i.IPv4Mask),
		setMissingValue(i.IPv4Network),
		setMissingValue(i.MTU),
		setMissingValue(i.HardwareAddr),
		setMissingValue(i.IPv6Addr),
	)
}

setMissingValue just replaces an empty string with a -, and l is the maximum string length (column width), passed in here:

var maxLen int
var ifs []*Iface

// build up our slice of interfaces,
// working out the longest interface name as we go
for _, iface := range getIfaces(loopback, filter) {
    i, err := New(iface)
    if err != nil {
        log.Fatal(err)
    }

    ifs = append(ifs, i)

    if nameLen := len(i.Name); nameLen > maxLen {
        maxLen = nameLen
    }
}

// now we know how long the interface name column needs to be
// we can print all the interfaces out.
for _, i := range ifs {
    fmt.Printf(ifaceString(maxLen, i))
}

Super, I don’t have to have sleepless nights now wondering if someone somewhere is cursing me because they’re output is ugly because they have a network interface with a very very long name. Yay, win for software.

And then I somehow, don’t remember how, found tabwriter, and all was good.

Package tabwriter implements a write filter (tabwriter.Writer) that translates tabbed columns in input into properly aligned text.

This basically means it takes care of the boring formatting stuff for you. With tabwriter you create a new writer, fill it with all your columns of data and then flush it out, all nicely formatted. Sweet, shoulders of giants and all that.

So, what does this mean for the code? Well, I reverted back to having a String method, but replaced my static/semi-dymanic string with a simple tab separated one, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t", like so:

func (i *Iface) String() string {
	return fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%s\t%s\t",
		setMissingValue(i.Name),
		setMissingValue(i.IPv4Addr),
		setMissingValue(i.IPv4Mask),
		setMissingValue(i.IPv4Network),
		setMissingValue(i.MTU),
		setMissingValue(i.HardwareAddr),
		setMissingValue(i.IPv6Addr),
	)
}

I then create a tabwriter.NewWriter, load it up with all the interfaces, and column headers (names) if needs be, and then flush it all out:

w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)

if names {
    // add headers to the tabwriter
    fmt.Fprintln(w, tabbedNames())
}

for _, iface := range validInterfaces(loopback, filter) {
    i, err := New(iface)
    if err != nil {
        log.Fatal(err)
        return
    }
    // add each interface to the tabwriter.
    // I realised here as well that I don't need to specify
    // the String method, the print statement knows to use it
    fmt.Fprintln(w, i)
}
// flush out all the interfaces, printing them to stdout
w.Flush()

How nice is that, tabwriter I mean, heaven knows how amateurish my code is! Needless to say it’s a huge improvement on my original. I’m not sure if there’s a performance hit, when there’s just a few interfaces there’s no noticeable difference in loading them all into the writer before flushing them out rather than printing them as they are created. Maybe if someone was using it with a substantial amount of interfaces then it might make a difference, but then if that’s the case, probably better using a standard GNU/Linux tool, yeah.

So what is it we actually pass in to create that new tabwriter? Here’s the NewWriter function signature:

func NewWriter(output io.Writer, minwidth, tabwidth, padding int, padchar byte, flags uint) *Writer

The first thing we pass in is a writer, to specify where we want our output to go, any type of writer interface will do. In my code above I pass in os.Stdout, but we could just as easily output to a file, or anything that implements the io.Writer interface.

Then we can pass in some formatting options; minwidth is the minimum column width, tabwidth is the number of spaces you want tab to be if you specify tab as the padchar, padding is the number of cells you want to add to the contents of the cell before it’s width is computed, with this you can prevent data that might take up the entire cell from butting up against the data in an adjacent cell, padchar is the ASCII character to fill out the cell with, this can be any byte character, whether that’s a space, ' ', a symbol, '*' or whatever. If you specify a tab character here, '\t', then the tabwidth will be used to determine it’s width.

Lastly there are some flags we can pass in. These flags are represented by uint numbers, so if we don’t want any flags we can just pass in 0. There’s six flags, dealing with HTML, stripping escape characters, overriding the default left alignment to align right (no centre alignment that I can see), discarding empty columns, overriding padchar to always indent columns with tabs and a handy Debug flag that sticks in a vertical bar, |, to distinguish where your columns are.

Let’s have a play with this then.

func main() {
    // create a new tabwriter
    w := new(tabwriter.Writer)
    // we can initialise the tabwriter with
    // our preferred settings independently
    // of it's creation
    //
    // here we're outputting to stdout, with
    // right-aligned, space-separated columns,
    // with no padding, and we're going to add
    // the debug flag for a clearer visual output.
    w.Init(os.Stdout, 0, 0, 0, ' ', tabwriter.Debug|tabwriter.AlignRight)
    fmt.Fprintln(w, "A\tB\tC\tD\tE\tF\tG\t")
    fmt.Fprintln(w, "my\tvoice\tis\tmy\tpassport\tverify\tme\t")
    fmt.Fprintln(w)
    w.Flush()

    // we can re-initialise the tabwriter with
    // different settings if we like, let's just
    // add some padding so things aren't so cramped
    w.Init(os.Stdout, 0, 0, 2, ' ', tabwriter.Debug|tabwriter.AlignRight)
    fmt.Fprintln(w, "A\tB\tC\tD\tE\tF\tG\t")
    fmt.Fprintln(w, "my\tvoice\tis\tmy\tpassport\tverify\tme\t")
    fmt.Fprintln(w)
    w.Flush()

    // And for some real fun let's pad with a symbol
    w.Init(os.Stdout, 0, 0, 2, '*', tabwriter.Debug|tabwriter.AlignRight)
    fmt.Fprintln(w, "A\tB\tC\tD\tE\tF\tG\t")
    fmt.Fprintln(w, "my\tvoice\tis\tmy\tpassport\tverify\tme\t")
    fmt.Fprintln(w)
    w.Flush()

    // And finally let's have a tab-separated column,
    // with a tab-stop of 8
    w.Init(os.Stdout, 0, 8, 2, '\t', tabwriter.Debug|tabwriter.AlignRight)
    fmt.Fprintln(w, "A\tB\tC\tD\tE\tF\tG\t")
    fmt.Fprintln(w, "my\tvoice\tis\tmy\tpassport\tverify\tme\t")
    fmt.Fprintln(w)
    w.Flush()
}

// Output
//
//  A|    B| C| D|       E|     F| G|
// my|voice|is|my|passport|verify|me|
//
//    A|      B|   C|   D|         E|       F|   G|
//   my|  voice|  is|  my|  passport|  verify|  me|
//
// ***A|******B|***C|***D|*********E|*******F|***G|
// **my|**voice|**is|**my|**passport|**verify|**me|
//
// A	    |B		|C	    |D	    |E              |F      |G      |
// my	    |voice	|is	    |my	    |passport       |verify |me     |

Note that the AlignRight flag doesn’t appear to make a difference in a tab-separated column. Also note the \t at the end of the strings, making the columns tab-terminated rather than tab-separated:

Column cells must be tab-terminated, not tab-separated: non-tab terminated trailing text at the end of a line forms a cell but that cell is not part of an aligned column.

There you have it. text/tabwriter is a small, but useful package. A nice little addition to my toolbox.

  • LinkedIn
  • Tumblr
  • Reddit
  • Google+
  • Pinterest
  • Pocket