CLI WG: Declarative structured/human output

Prior art and thoughts

LambdaMoo-based interface
Common utilities: Columns, Lists, Paragraphs
Common markup language abstract over output using document models
LISP machine style: Present object to an output (depending on output device)
CLIM: ~vdom with tree of objects. “present an object” — interact with object itself, using commands
machine output: JSON, JSON-LD, SHACLE
web-independent w3c specs like pointer events, flexbox
human output styles, cf. chrono-english, chrono-humanize, humansize, translations (cf. Project Fluent)
libxo, generating human strings and xml/json/etc. from soemthing like xo_emit(" {:lines/%7ju} {:words/%7ju} {:characters/%7ju} {d:filename/%s}\n", linect, wordct, charct, file);
JSX-style API (cf. rsx), or elm-like (cf. elm-lang/html)
teaching serde to output json-ld (adding context annotations)
one root message type → generate JSON schema as CLI API doc for other CLI apps to consume
composing types means composing their output. should work for human representation but will probably require the ability overwrite based on context

Pascal’s ideas for a CLI-focused MVP

Pitch

We should make a library that makes it easy/convenient to output stuff for humans on a console (so people will want to use this instead of println!), which also gives you common formatting options, and that gives you JSON output for free.

What to do

  1. Define structs/enums
  1. derive Serialize
  1. implement a trait (let’s call it O for simplicity) that turns the fields’ data into an element tree
  1. Provide a way to write that O impl super easy
  1. Provide a bunch of built-in “tags” (to print stuff in bold, colored) and a macro for JSX-style syntax?
  1. Provide a macro to make this whole thing a one-liner? Strawman proposal: define_message! { Message { code: i32, message: String, } => <bold>{code}</bold>: {message} }
  1. Make sure these compose: Assume we have a define_message! { ErrorCode { code: i32 } => if code < 0 { <span fs="red">Error ({code}):</span> } else { <span>Info ({code})</span> } } then the above Message definition should still work with just replacing i32 with ErrorCode
  1. Output stuff like output!(Message { code: 42, message: "Foobar".into() });
  1. we should evaluate how valuable single-line define-and-output might be, especially if people are used to println! and log
  1. Init a drain for the outputs at the start of your fn main (like cli_output_thing::init()?;) that sets up coloring, human vs. json output, etc.
  1. (optional) also provide a replacement for the log macros ( info!, warn!, err!, etc.) and an adapter for the std log crate to work with our drain (maybe build on slog)

Further steps

We should try to keep in mind that most of the suggestions here can be implement in a more generic way — that element tree thing could also work for other markup formats (like roff or, obviously, html).

A next increment could add support for getting more information out of a root Message type: Similar to a library that exposes a single Error type, a CLI could expose a Message type that defines all (machine-readable) output (e.g. by rendering Rust types with serde annotation to a JSON schema file). This is something that will probably not “sell” our library to most users, but if it turns out to be easy to get, would benefit us in the long run.
There is a trade-off between “meticulously defining types and organizing them in a hierarchy” and "just use single-line define-and-output (a.k.a. ad-hoc formatting) everywhere” that we should discuss and present to users.

Can this enable localized output?

Interesting test cases

  • cargo
  • not easy: weird multi-process printing rustfix does to sync output because Windows’ stdout isn’t line buffered (we spawn a TCP server that accepts diagnostic messages and prints them)