cols 0.2.0

Smart adaptive formatting of columnar data
Documentation

Smart adaptive formatting of tabular data.

This crate provides an idiomatic Rust library for building and printing tables with automatic column width calculation, tree rendering, multiple output formats, and a filter expression language.

Quick start

use cols::{Table, Column, print_table_to_string};

let mut table = Table::new();
table.add_column(Column::new("NAME").width_fixed(10));
table.add_column(Column::new("SIZE").width_fixed(6).right(true));

let l1 = table.new_line(None);
table.line_mut(l1).data_set(0, "sda").data_set(1, "100G");

let l2 = table.new_line(None);
table.line_mut(l2).data_set(0, "sdb").data_set(1, "50G");

let output = print_table_to_string(&table).unwrap();
assert!(output.contains("sda"));
assert!(output.contains("100G"));

Output modes

Set via [Table::output_mode_set]:

use cols::{OutputMode, Table, print_table_to_string};

let mut table = Table::new();
table.name_set("devices");
table.new_column("NAME");
table.new_column("SIZE");

let row = table.new_line(None);
table.line_mut(row).data_set(0, "sda").data_set(1, "100G");

// JSON output
table.output_mode_set(OutputMode::Json);
let json = print_table_to_string(&table).unwrap();
assert!(json.contains("\"NAME\": \"sda\""));

// Export (shell-friendly KEY="value" pairs)
table.output_mode_set(OutputMode::Export);
let export = print_table_to_string(&table).unwrap();
assert!(export.contains("NAME=\"sda\""));
Mode Description
[OutputMode::Normal] Human-readable with column alignment and padding
[OutputMode::Raw] Compact, separator-delimited (no padding)
[OutputMode::Export] NAME="value" pairs for shell consumption
[OutputMode::Json] JSON array of objects
[OutputMode::ShellVar] Like Export but with shell-safe column names
[OutputMode::Csv] RFC 4180 CSV with automatic quoting
[OutputMode::Markdown] Pipe-delimited markdown table

Column width hints

Width hints guide the print engine's layout algorithm. Set them via the builder methods on [Column]:

Method Effect
(default) Auto — sized to fit content
[Column::width_fixed] Absolute minimum character width
[Column::width_fraction] Fraction of terminal width (only when output is a terminal)
[Column::width] Set a [WidthHint] enum value directly

Column flags

Per-column rendering behavior is controlled via builder methods on [Column]:

Method Effect
[Column::tree] Draws tree connectors (use with [Table::new_line] parent)
[Column::right] Right-aligns cell data
[Column::truncate] Truncates data that exceeds the column width
[Column::wrap] Wraps long data across multiple output lines
[Column::hidden] Stores data in memory but omits it from output
[Column::strict_width] Never shrink the column below its hint width
[Column::no_extremes] Ignore outlier values when calculating widths

Tree rendering

Mark a column as a tree column with [Column::tree] and pass a parent [LineId] when creating child lines. The print engine draws Unicode (or ASCII) tree connectors automatically.

use cols::{Table, Column, print_table_to_string};

let mut table = Table::new();
table.headings_set(false);
table.ascii_set(true);
table.add_column(Column::new("NAME").tree(true));

let root = table.new_line(None);
table.line_mut(root).data_set(0, "parent");

let child1 = table.new_line(Some(root));
table.line_mut(child1).data_set(0, "child-a");

let child2 = table.new_line(Some(root));
table.line_mut(child2).data_set(0, "child-b");

let output = print_table_to_string(&table).unwrap();
assert!(output.contains("|-child-a"));
assert!(output.contains("`-child-b"));

Sorting

Sort child lines under a parent using [Table::sort]. A custom comparator can be set per-column with [Column::order_function]. Sorting reorders children within each parent node; for flat tables, add rows as children of a single root line.

use cols::{Table, Column, print_table_to_string};

let mut table = Table::new();
table.ascii_set(true);
table.headings_set(false);
table.add_column(Column::new("NAME").tree(true));

let root = table.new_line(None);
table.line_mut(root).data_set(0, "root");

for name in ["zebra", "apple", "mango"] {
    let row = table.new_line(Some(root));
    table.line_mut(row).data_set(0, name);
}

table.sort(0); // sort children of root by column 0

let output = print_table_to_string(&table).unwrap();
let lines: Vec<&str> = output.lines().collect();
// lines[0] = root, children appear sorted after it
assert!(lines[1].contains("apple"));
assert!(lines[2].contains("mango"));
assert!(lines[3].contains("zebra"));

Groups

Groups express M:N relationships between lines — multiple lines can be "members" of a group, and other lines can be "children" of that group. The print engine draws bracket connectors on the left margin.

use cols::{Table, TermForce, print_table_to_string};

let mut table = Table::new();
table.headings_set(false);
table.ascii_set(true);
table.termwidth_set(40);
table.termforce_set(TermForce::Always);
table.new_column("NAME");

let group = table.new_group();

let m1 = table.new_line(None);
table.line_mut(m1).data_set(0, "server");
let m2 = table.new_line(None);
table.line_mut(m2).data_set(0, "worker");

let child = table.new_line(None);
table.line_mut(child).data_set(0, "logger");

table.group_add_member(group, m1);
table.group_add_member(group, m2);
table.group_add_child(group, child);

let output = print_table_to_string(&table).unwrap();
assert!(output.contains(",-server"));
assert!(output.contains("|-worker"));
assert!(output.contains("`-logger"));

Cloning

Both [Table] and [Column] implement [Clone]. Cloning a table produces an independent copy including all lines and column definitions. If a column has a custom comparator set via [Column::order_function], the clone shares the same comparator function (via Arc).

use cols::Table;

let mut table = Table::new();
table.new_column("X");
let row = table.new_line(None);
table.line_mut(row).data_set(0, "hello");

let clone = table.clone();
assert_eq!(clone.nlines(), table.nlines());

Filtering

Parse filter expressions with [Filter::parse] and evaluate them against line data. Supports comparisons, boolean logic, regex matching, and integer suffixes (K, M, G, ...).

use cols::Filter;

let f = Filter::parse(r#"SIZE > 1G && TYPE == "disk""#).unwrap();
let matches = f.evaluate(|col| match col {
    "SIZE" => Some("4294967296".into()), // 4G
    "TYPE" => Some("disk".into()),
    _ => None,
}).unwrap();
assert!(matches);