Liking cljdoc? Tell your friends :D

Command Line Interface

clj-string-layout ships a command-line formatter that reads CSV, TSV, or whitespace-separated data from stdin or a file and writes a formatted table to stdout. The same entry point is exposed three ways: through the Clojure CLI, through a Babashka task that shells out to the JVM, and through a Babashka task that runs natively under SCI with no JVM startup at all.

Synopsis

clojure -M:cli -- [options] [file]
bb format     -- [options] [file]
bb bb-format     [options] [file]

  Reads input from FILE, "-", or stdin. Writes a formatted table to stdout.
  Exits 0 on success, 2 on argument or input error.

Quick examples

# CSV with a header row, rendered as Markdown
clojure -M:cli -- --from csv --to markdown --headers data.csv

# Tab-separated input, ASCII-grid output, via Babashka (no JVM)
bb bb-format --from tsv --to ascii-grid < data.tsv

# Whitespace-separated input, box-drawing output, expanded to 60 columns
bb bb-format --from whitespace --to box --headers --width 60 --fill < data.txt

# Pipe through a Unix tool, then back through the formatter
printf 'item,qty\npear,4\napple,12\nkiwi,8\n' \
  | bb bb-format --from csv --to csv --headers \
  | sort \
  | bb bb-format --from csv --to box

Options

OptionMeaning
--input FORMAT, --from FORMATInput format. One of csv, tsv, whitespace. Defaults to csv.
--format FORMAT, --to FORMATOutput format. Any value returned by clj-string-layout.table/formats. Defaults to plain.
--headersTreat the first input row as headers.
--no-headersTreat every input row as data (the default).
--no-escapeDisable output-format escaping. Use this if the input is already escaped for the target format.
--width NTarget total width. Only takes effect when paired with --fill on a generated format (plain, the markdown variants, the box variants, ascii-grid).
--fillExpand cell padding to consume --width. Has no effect without --width.
-h, --helpPrint built-in help and exit.

--help is generated from the live cli/formats registry, so it always reflects the formats currently available.

Inputs

Source

FormBehaviour
clojure -M:cli -- data.csvRead data.csv.
clojure -M:cli -- -Read stdin (the - is explicit).
clojure -M:cli --No file given — read stdin.
clojure -M:cli -- a.csv b.csvError — only one input file is supported.

The same rules apply to bb format and bb bb-format.

CSV (--from csv)

RFC 4180-style parser. Handles quoted fields (including separators and doubled quotes inside quoted fields) and CR / LF / CRLF row separators. The parser is intentionally lenient about text after a closing quote, so imperfect CSV exports usually still load.

$ printf 'name,note\nalice,"a, b"\nbob,plain text\n' \
    | bb bb-format --from csv --to box --headers
┌───────┬────────────┐
│ name  │ note       │
├───────┼────────────┤
│ alice │ a, b       │
├───────┼────────────┤
│ bob   │ plain text │
└───────┴────────────┘

Doubled quotes inside a quoted field unescape to a single ":

$ printf 'name,quote\nalice,"she said ""hi"""\nbob,plain\n' \
    | bb bb-format --from csv --to box --headers
┌───────┬───────────────┐
│ name  │ quote         │
├───────┼───────────────┤
│ alice │ she said "hi" │
├───────┼───────────────┤
│ bob   │ plain         │
└───────┴───────────────┘

Embedded newlines inside quoted fields are preserved literally. That matches the RFC 4180 spec for the parsing side, but visual formats like :box and :ascii-grid have no way to lay out a multi-line cell — the output will break mid-row when it hits the embedded \n. If your data has newlines in cells, either preprocess them out (tr '\n' ' ') or target a format whose escaper handles them: :markdown converts them to <br>, :tsv and :org to visible escape sequences, :html keeps them inside <td>.

TSV (--from tsv)

Tab-separated values. One row per line. No quoting — every tab is a separator. Use --no-escape if your data already contains backslash escape sequences that match the TSV escaper's output.

$ printf 'name\tqty\tprice\napple\t12\t1.50\npear\t4\t2.00\n' \
    | bb bb-format --from tsv --to markdown --headers
| name  | qty | price |
|:----- |:--- |:----- |
| apple | 12  | 1.50  |
| pear  | 4   | 2.00  |

Whitespace (--from whitespace)

Splits each non-blank line on runs of whitespace. Trims leading and trailing whitespace. Blank lines are skipped. Useful for reformatting output from ps, df, ls -l, or similar.

$ printf '  name   qty   price\n  apple  12    1.50\n  pear    4   2.00\n' \
    | bb bb-format --from whitespace --to ascii-grid --headers
+-------+-----+-------+
| name  | qty | price |
+-------+-----+-------+
| apple | 12  | 1.50  |
+-------+-----+-------+
| pear  | 4   | 2.00  |
+-------+-----+-------+

Outputs

The CLI accepts every keyword in clj-string-layout.table/formats. Same input, four representative outputs:

$ INPUT='item,qty,price\napple,12,1.50\npear,4,2.00\n'

$ printf "$INPUT" | bb bb-format --headers       # --to plain (default)
item   qty  price
apple  12   1.50
pear   4    2.00

$ printf "$INPUT" | bb bb-format --to markdown --headers
| item  | qty | price |
|:----- |:--- |:----- |
| apple | 12  | 1.50  |
| pear  | 4   | 2.00  |

$ printf "$INPUT" | bb bb-format --to psql --headers
 item   | qty | price
--------+-----+------
 apple  | 12  | 1.50
 pear   | 4   | 2.00

$ printf "$INPUT" | bb bb-format --to box --headers
┌───────┬─────┬───────┐
│ item  │ qty │ price │
├───────┼─────┼───────┤
│ apple │ 12  │ 1.50  │
├───────┼─────┼───────┤
│ pear  │ 4   │ 2.00  │
└───────┴─────┴───────┘

The other available output formats are markdown-left, markdown-center, markdown-right, double-box, unicode-box, unicode-double-box, ascii-box, ascii-double-box, ascii-grid, csv, tsv, pipe, org, rst, and html. See the examples gallery for the same data rendered through every named format.

Width and fill

By default, every named format auto-sizes to its content. To produce fixed-width output, pair --width with --fill:

$ printf 'name,qty\napple,12\n' | bb bb-format --to box --headers --width 40 --fill
┌───────────────────┬──────────────────┐
│ name              │ qty              │
├───────────────────┼──────────────────┤
│ apple             │ 12               │
└───────────────────┴──────────────────┘

--fill is what makes the columns expand. Without it --width is silently ignored for the box/markdown/ascii-grid/plain formats (they have no fill markers to expand into). For formats that have no width semantics at all (csv, tsv, pipe, html), --width and --fill are both ignored.

Piping

The CLI is a well-behaved Unix filter: stdin in, stdout out, exit 0 on success. That makes composition straightforward.

# pipe from curl
curl -s https://example.com/data.csv | bb bb-format --to box --headers

# reformat then sort by the first column, then re-render as a box
printf 'item,qty\npear,4\napple,12\nkiwi,8\n' \
  | bb bb-format --to csv --headers \
  | sort \
  | bb bb-format --to box

# read CSV, emit Markdown, copy to the clipboard (macOS)
bb bb-format --to markdown --headers data.csv | pbcopy

The --to csv round-trip is especially handy: it normalises the input, escapes CSV-unsafe characters, and gives you a known-good intermediate representation for downstream Unix tools.

Exit codes

CodeMeaning
0Success. Output written to stdout.
2Argument error or input error. Diagnostic on stderr.

Examples of 2:

$ bb bb-format --bogus
Unsupported CLI option
$ echo $?
2

$ echo 'a,b' | bb bb-format --to notarealformat
Unsupported --to value
$ echo $?
2

The CLI never throws an uncaught exception in normal operation — any ex-info with a known :type is rendered as a one-line diagnostic and the process exits 2.

Babashka vs JVM startup

PathCold startSetup
clojure -M:cli --~700 msrequires JVM and Clojure deps cache
bb format --~700 msidentical to above; just shells through bb
bb bb-format~50 msruns natively under SCI, no JVM

For one-off pipes the difference is the cost of inhaling a JVM. For repeated runs (e.g. tight shell loops) it dominates everything else, so prefer bb bb-format. The output is identical.

Programmatic use

cli/render is the same entry point invoked by -main, but returns a vector of output lines instead of printing or exiting. It accepts every key the command-line flags produce, plus optional :width (integer), :fill? (boolean), and :display-width (a function from string to display width, useful when the caller passes ANSI- or wide-glyph data that the layout engine needs to measure correctly):

(require '[clj-string-layout.cli :as cli])

(cli/render {:input :csv :format :markdown :headers? true}
            "Name,Note\nalice,\"a,b\"\n")
;; => ["| Name  | Note |"
;;     "|:----- |:---- |"
;;     "| alice | a,b  |"]

(cli/render {:input :csv :format :box :headers? true
             :width 40 :fill? true}
            "item,qty\napple,12\n")
;; => ["┌───────────────────┬──────────────────┐"
;;     "│ item              │ qty              │"
;;     "├───────────────────┼──────────────────┤"
;;     "│ apple             │ 12               │"
;;     "└───────────────────┴──────────────────┘"]

cli/parse-args parses a string sequence into the same options map and is useful when you want CLI-flavoured argument parsing inside your own program:

(cli/parse-args ["--from" "tsv" "--to" "box" "--headers" "--width" "60" "--fill"])
;; => {:input :tsv :format :box :headers? true :escape? true
;;     :width 60 :fill? true}

cli/parse-input exposes the CSV/TSV/whitespace parsers on their own so you can reuse them outside the rendering path:

(cli/parse-input :csv "name,note\nalice,\"a,b\"\n")
;; => [["name" "note"] ["alice" "a,b"]]

Large data

The CLI is not safe for huge inputs. It slurps the entire input via slurp, parses it to a vector, then renders eagerly through table/table. Empirically this means the CLI tops out somewhere between a few hundred thousand and a million rows depending on the JVM heap and the output format. On a 1 M-row CSV (~37 MB), clojure -M:cli -- needs several gigabytes of heap and runs ~6 s; the same input OOMs at -Xmx 512m.

For inputs that won't fit in heap, drop the CLI and use the streaming primitives from a small Clojure script:

(require '[clj-string-layout.core :as core])

(defn split-lines [^java.io.BufferedReader r]
  (lazy-seq
    (when-let [line (.readLine r)]
      (cons (clojure.string/split line #",") (split-lines r)))))

(with-open [r (java.io.BufferedReader. (java.io.FileReader. "big.csv"))
            w (clojure.java.io/writer "out.txt")]
  (let [rows (drop 1 (split-lines r))]      ; skip the header line
    (core/layout-into! w rows
                       {:col-widths [10 20 8]   ; required for streaming
                        :layout {:cols ["[L]  [L]  [R]"]}})))

That runs 1 M rows in ~2 s with -Xmx 256m and constant memory. The recipe book has the full Large Data section with measured numbers, the row-layout case, and the two-pass / sample-then-stream patterns for when widths aren't known up front.

A few caveats specific to this approach:

  • The line-splitter above does not handle quoted CSV fields. For RFC 4180-compliant streaming you'd need a streaming CSV parser (clojure.data.csv works well and reads lazily).
  • The lower-level engine needs :col-widths to stream. Without them, it does a full input scan to compute widths and you're back to eager memory use.

Troubleshooting

SymptomLikely cause
Input contains no rows (exit 2)File or stdin produced zero rows. Check the input format actually matches the data.
Unsupported --to value (exit 2)Typo in the output format name. Run --help for the live list.
Unsupported --from value (exit 2)Same, for input format.
--width must be a non-negative integer (exit 2)Pass a positive integer, e.g. --width 60.
Only one input file may be supplied (exit 2)Pass exactly one file path, or - for stdin.
Numeric columns end up left-alignedCSV/TSV/whitespace input is always read as strings; the CLI has no per-column alignment knob. For aligned numerics, use the table API programmatically with :columns [{:from :qty :align :right}].
Embedded newlines split a cell across linesQuoted-CSV newlines stay literal in the output. Preprocess to strip or replace them if your renderer can't handle a multi-line cell.
--width 30 does nothingPair it with --fill. Without --fill, only the explicitly fill-aware presets respect :width, and the CLI's default formats are not those presets.

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close