Formatting tables in the terminal

Widths from the data

A table is three problems: how wide each column is, how to pad a cell, and where the borders go. The first one is a pass over the rows, so the widths do not depend on a literal nobody updates when a name gets longer:

rows = [["Name", "Count"], ["Ada", 3], ["Bob", 12]] widths = [0, 0] rows.each { |row| row.each_with_index { |cell, i| widths[i] = [widths[i], cell.to_s.length].max } }

ljust, rjust and center take that width and an optional fill character, so the padding is one call per cell. A number column is right aligned and a text column is left aligned, which is the whole visual grammar of a table.

The bordered table

Build the border from the widths, print it, the rows, and the border again. map plus join is the line:

border = "+" + widths.map { |w| "-" * (w + 2) }.join("+") + "+" puts border rows.each do |row| puts "| " + row.each_with_index.map { |cell, i| cell.to_s.ljust(widths[i]) }.join(" | ") + " |" end puts border

ruby table.rb

+------+-------+ | Name | Count | | Ada | 3 | | Bob | 12 | +------+-------+

The two spaces around a cell are the "| " and " | " in the join, which is why the border adds two to each width. The table is plain text on purpose: it survives a terminal, a log file and a diff without a dependency.

The same rows as CSV

The same array is the input for a machine-readable version. CSV escapes the separators and the quotes, which string concatenation does not. The csv library is installed later, in lesson 132; the command below assumes it.

ruby -e 'require "csv"; rows = [["Name", "Count"], ["Ada", 3]]; print CSV.generate { |c| rows.each { |r| c << r } }'

Name,Count Ada,3

One script can print both: the table to the terminal and the CSV to a file, chosen by an option. Printing the table and calling it data is the mistake; a caller that parses columns by position breaks on the first cell with a space in it.

Only columns and a format

For two columns, a format string is shorter than a width calculation. it is the implicit block parameter, and the splat passes the two cells to the format:

ruby -e 'rows = [["Ada", 3], ["Bob", 12]]; rows.each { puts format("%-10s %3d", *it) }'

Ada 3 Bob 12

Fixed widths are fine when the values are known to be short: a name under ten characters, a count under a thousand. A name that grows past the width pushes the column instead of breaking, which is the failure mode to accept knowingly.

What to watch for

length counts characters and a terminal measures display columns. A wide character or an emoji makes the border shorter than the text; the fix is a width library, not a wider guess.

A table written to a file and then read back is data only by accident. If another program reads it, write CSV or JSON; keep the bordered table for a person.

Next: Encodings

Article author: Arthur Isaev