Repository: bytecodealliance/wasm-tools (rust)

Issue:
[wasm-metadata] redesign the human-readable terminal output
I've been having some trouble parsing the textual output of the `wasm-tools metadata show` command. Since the command supports a `--json` flag to make the output machine-interpretable, I wanted to float the idea of changing the default textual rendering.

## Inspiration

I'm not an expert on CLI interfaces, so I was asking around for good references of CLI tool output, and I was pointed towards the output of [muesli/duf](https://github.com/muesli/duf). This creates a table for each device category, being relatively easy to parse on a glance.

![ascii tables in a terminal](https://github.com/muesli/duf/blob/master/duf.png?raw=true)

## Current Output

Here is an example of what the current output looks like for a `wasi:http` component, written in Rust. If you don't understand the underlying component/provider data model, making sense of the output can be pretty difficult. In preparing to file this issue I've read the `@provider` spec in `webassembly/tool-conventions`, so I understand the hierarchy now - but I wish this would have been easier.

```text
component:
    processed-by:
        wit-component: 0.216.0
    module rust_wasi_hello.wasm:
        language:
            Rust
            C11
        processed-by:
            rustc: 1.82.0 (f6e511eec 2024-10-15)
            clang: 18.1.2-wasi-sdk (https://github.com/llvm/llvm-project 26a1d6601d727a96f4301d0d8647b5a42760ae0c)
            wit-component: 0.20.1
            wit-bindgen-rust: 0.33.0
            wit-bindgen-c: 0.17.0
    module wit-component:adapter:wasi_snapshot_preview1:
        language:
            Rust
        processed-by:
            rustc: 1.80.0 (051478957 2024-07-21)
    module wit-component:shim:
        processed-by:
            wit-component: 0.216.0
    module wit-component:fixups:
        processed-by:
            wit-component: 0.216.0
    component:
```

## Proposed Output

Here is a sketch I made using Monodraw of what `wasm-tools metadata show` output could look like if we took inspiration from `duf`. This creates metadata per-component and module, making it easier to visually track which parts of the binary carry which metadata.

The idea would be to also add some basic colors to make the output easier to scan too. But as I mentioned earlier on: we wouldn't be changing the `--json` output at all. That way this only affects the human-readable output, and not the machine-readable one.

```text
┌──────────────────────────────────────┐                                            
│ component sample-wasi-http-rust      │                                            
├──────────┬────────────────┬──────────┤                                            
│ KIND     │ PRODUCER       │ VERSION  │                                            
├──────────┼────────────────┼──────────┤                                            
│ tool     │ wit-component  │ 0.216.0  │                                            
└──────────┴────────────────┴──────────┘                                            
┌──────────────────────────────────────────────────────────────────────────────────┐
│ module sample_wasi_http_rust.wasm                                                │
├──────────┬──────────────────┬────────────────────────────────────────────────────┤
│ KIND     │ PRODUCER         │ VERSION                                            │
├──────────┼──────────────────┼────────────────────────────────────────────────────┤
│ language │ Rust             │ -                                                  │
├──────────┼──────────────────┼────────────────────────────────────────────────────┤
│ language │ C11              │ -                                                  │
├──────────┼──────────────────┼────────────────────────────────────────────────────┤
│ tool     │ rustc            │ 1.82.0 (f6e511eec 2024-10-15)                      │
├──────────┼──────────────────┼────────────────────────────────────────────────────┤
│ tool     │ clang            │ 18.1.2-wasi-sdk                                    │
│          │                  │ (https://github.com/llvm/llvm-project              │
├──────────┼──────────────────┼────────────────────────────────────────────────────┤
│ tool     │ wit-component    │ 0.20.1                                             │
├──────────┼──────────────────┼────────────────────────────────────────────────────┤
│ tool     │ wit-bindgen-rust │ 0.20.1                                             │
├──────────┼──────────────────┼────────────────────────────────────────────────────┤
│ tool     │ wit-bindgen-c    │ 0.20.1                                             │
└──────────┴──────────────────┴────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────┐                            
│ module wit-component:adapter:wasi_snapshot_preview1: │                            
├──────────┬──────────┬────────────────────────────────┤                            
│ KIND     │ PRODUCER │ VERSION                        │                            
├──────────┼──────────┼────────────────────────────────┤                            
│ language │ Rust     │ -                              │                            
├──────────┼──────────┼────────────────────────────────┤                            
│ tool     │ rustc    │ 1.82.0 (f6e511eec 2024-10-15)  │                            
└──────────┴──────────┴────────────────────────────────┘                            
┌────────────────────────────────────┐                                              
│ module wit-component-shim          │                                              
├──────────┬───────────────┬─────────┤                                              
│ KIND     │ PRODUCER      │ VERSION │                                              
├──────────┼───────────────┼─────────┤                                              
│ language │ Rust          │ -       │                                              
├──────────┼───────────────┼─────────┤                                              
│ tool     │ wit-component │ 0.216.0 │                                              
└──────────┴───────────────┴─────────┘                                              
┌────────────────────────────────────┐                                              
│ module wit-component:fixups        │                                              
├──────────┬───────────────┬─────────┤                                              
│ KIND     │ PRODUCER      │ VERSION │                                              
├──────────┼───────────────┼─────────┤                                              
│ language │ Rust          │ -       │                                              
├──────────┼───────────────┼─────────┤                                              
│ tool     │ wit-component │ 0.216.0 │                                              
└──────────┴───────────────┴─────────┘                                              

Pull request:
[wasm-metadata] pretty-print cli output
Closes #1923. While this doesn't quite deliver on the vision I sketched in the issue, I believe it's a meaningful step up from the status quo. I've also filed https://github.com/Nukesor/comfy-table/issues/160, which if accepted will hopefully allow us to more closely match `duf`'s output.

I'm not quite sure what the process for introducing new dependencies here is. To pretty-print tables, we're going to need *some* printing library - and `comfy-table` seemed like a reasonable choice. I looked at the deps, and nothing particularly stood out to me as a potential problem?

Thanks!

## Screenshot
This is what the output currently looks like. I might do another pass to add colors, etc - but this seemed like a good starting point.

<img width="1044" alt="Screenshot 2024-12-10 at 03 08 38" src="https://github.com/user-attachments/assets/a51337e7-3141-4f2b-aec1-cad326598a0a">



Patch:
diff --git a/Cargo.lock b/Cargo.lock
index c78924b3..4dc9a09f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -291,6 +291,17 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
 
+[[package]]
+name = "comfy-table"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24f165e7b643266ea80cb858aed492ad9280e3e05ce24d4a99d7d7b889b6a4d9"
+dependencies = [
+ "strum",
+ "strum_macros",
+ "unicode-width",
+]
+
 [[package]]
 name = "component"
 version = "0.0.0"
@@ -1458,6 +1469,12 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
+[[package]]
+name = "rustversion"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
+
 [[package]]
 name = "ruzstd"
 version = "0.7.2"
@@ -1600,6 +1617,25 @@ version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
 
+[[package]]
+name = "strum"
+version = "0.26.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
+
+[[package]]
+name = "strum_macros"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn",
+]
+
 [[package]]
 name = "symbolic_expressions"
 version = "5.0.3"
@@ -1979,6 +2015,7 @@ dependencies = [
  "bitflags",
  "clap",
  "clap_complete",
+ "comfy-table",
  "cpp_demangle",
  "env_logger",
  "gimli 0.30.0",
diff --git a/Cargo.toml b/Cargo.toml
index 239bf9ad..0a906a5d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -74,6 +74,7 @@ anyhow = "1.0.58"
 arbitrary = "1.1.0"
 clap = { version = "4.0.0", features = ["derive"] }
 clap_complete = "4.4.7"
+comfy-table = { version = "7.1.3", default-features = false }
 criterion = { version = "0.5.1", default-features = false }
 env_logger = "0.11"
 indexmap = { version = "2.0.0", default-features = false }
@@ -122,6 +123,7 @@ env_logger = { workspace = true }
 log = { workspace = true }
 clap = { workspace = true, features = ['wrap_help'] }
 clap_complete = { workspace = true, optional = true }
+comfy-table = { workspace = true }
 tempfile = "3.2.0"
 wat = { workspace = true, features = ['dwarf', 'component-model'] }
 termcolor = { workspace = true }
diff --git a/crates/wasm-metadata/src/payload.rs b/crates/wasm-metadata/src/payload.rs
index 6acb22aa..7aa57417 100644
--- a/crates/wasm-metadata/src/payload.rs
+++ b/crates/wasm-metadata/src/payload.rs
@@ -1,4 +1,3 @@
-use std::fmt::{self, Display};
 use std::ops::Range;
 
 use anyhow::Result;
@@ -181,48 +180,4 @@ impl Payload {
             Self::Component { children, .. } => children.push(child),
         }
     }
-
-    fn display(&self, f: &mut fmt::Formatter, indent: usize) -> fmt::Result {
-        let spaces = std::iter::repeat(" ").take(indent).collect::<String>();
-        match self {
-            Self::Module(Metadata {
-                name, producers, ..
-            }) => {
-                if let Some(name) = name {
-                    writeln!(f, "{spaces}module {name}:")?;
-                } else {
-                    writeln!(f, "{spaces}module:")?;
-                }
-                if let Some(producers) = producers {
-                    producers.display(f, indent + 4)?;
-                }
-                Ok(())
-            }
-            Self::Component {
-                children,
-                metadata: Metadata {
-                    name, producers, ..
-                },
-            } => {
-                if let Some(name) = name {
-                    writeln!(f, "{spaces}component {name}:")?;
-                } else {
-                    writeln!(f, "{spaces}component:")?;
-                }
-                if let Some(producers) = producers {
-                    producers.display(f, indent + 4)?;
-                }
-                for c in children {
-                    c.display(f, indent + 4)?;
-                }
-                Ok(())
-            }
-        }
-    }
-}
-
-impl Display for Payload {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        self.display(f, 0)
-    }
 }
diff --git a/crates/wasm-metadata/src/producers.rs b/crates/wasm-metadata/src/producers.rs
index 104e7498..180f3adb 100644
--- a/crates/wasm-metadata/src/producers.rs
+++ b/crates/wasm-metadata/src/producers.rs
@@ -1,7 +1,6 @@
 use anyhow::Result;
 use indexmap::{map::Entry, IndexMap};
 use serde_derive::Serialize;
-use std::fmt;
 use wasm_encoder::Encode;
 use wasmparser::{BinaryReader, KnownCustom, Parser, ProducersSectionReader};
 
@@ -150,27 +149,6 @@ impl Producers {
     pub fn add_to_wasm(&self, input: &[u8]) -> Result<Vec<u8>> {
         rewrite_wasm(&None, self, &None, &None, &None, &None, &None, input)
     }
-
-    pub(crate) fn display(&self, f: &mut fmt::Formatter, indent: usize) -> fmt::Result {
-        let indent = std::iter::repeat(" ").take(indent).collect::<String>();
-        for (fieldname, fieldvalues) in self.0.iter() {
-            writeln!(f, "{indent}{fieldname}:")?;
-            for (name, version) in fieldvalues {
-                if version.is_empty() {
-                    writeln!(f, "{indent}    {name}")?;
-                } else {
-                    writeln!(f, "{indent}    {name}: {version}")?;
-                }
-            }
-        }
-        Ok(())
-    }
-}
-
-impl fmt::Display for Producers {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        self.display(f, 0)
-    }
 }
 
 /// Contents of a producers field
diff --git a/src/bin/wasm-tools/metadata.rs b/src/bin/wasm-tools/metadata.rs
index 3e0ff6e1..48d787de 100644
--- a/src/bin/wasm-tools/metadata.rs
+++ b/src/bin/wasm-tools/metadata.rs
@@ -1,6 +1,12 @@
-use anyhow::Result;
 use std::io::Write;
 
+use anyhow::Result;
+use comfy_table::modifiers::UTF8_ROUND_CORNERS;
+use comfy_table::presets::UTF8_FULL;
+use comfy_table::{ContentArrangement, Table};
+use termcolor::WriteColor;
+use wasm_metadata::{Metadata, Payload};
+
 /// Manipulate metadata (module name, producers) to a WebAssembly file.
 #[derive(clap::Parser)]
 pub enum Opts {
@@ -44,11 +50,11 @@ impl ShowOpts {
         let input = self.io.parse_input_wasm()?;
         let mut output = self.io.output_writer()?;
 
-        let metadata = wasm_metadata::Payload::from_binary(&input)?;
+        let payload = wasm_metadata::Payload::from_binary(&input)?;
         if self.json {
-            write!(output, "{}", serde_json::to_string(&metadata)?)?;
+            write!(output, "{}", serde_json::to_string(&payload)?)?;
         } else {
-            write!(output, "{metadata}")?;
+            fmt_payload(&payload, &mut output)?;
         }
         Ok(())
     }
@@ -82,3 +88,75 @@ impl AddOpts {
         Ok(())
     }
 }
+
+fn fmt_payload(payload: &Payload, f: &mut Box<dyn WriteColor>) -> Result<()> {
+    let mut table = Table::new();
+    table
+        .load_preset(UTF8_FULL)
+        .apply_modifier(UTF8_ROUND_CORNERS)
+        .set_content_arrangement(ContentArrangement::Dynamic)
+        .set_width(80)
+        .set_header(vec!["KIND", "VALUE"]);
+    let Metadata {
+        name,
+        author,
+        description,
+        producers,
+        licenses,
+        source,
+        homepage,
+        range,
+    } = payload.metadata();
+
+    // Print the basic information
+    let kind = match payload {
+        Payload::Component { .. } => "component",
+        Payload::Module(_) => "module",
+    };
+    table.add_row(vec!["kind", &kind]);
+    let name = name.as_deref().unwrap_or("<unknown>");
+    table.add_row(vec!["name", &name]);
+    table.add_row(vec![
+        "range",
+        &format!("0x{:x}..0x{:x}", range.start, range.end),
+    ]);
+
+    // Print the OCI annotations
+    if let Some(description) = description {
+        table.add_row(vec!["description", &description.to_string()]);
+    }
+    if let Some(licenses) = licenses {
+        table.add_row(vec!["licenses", &licenses.to_string()]);
+    }
+    if let Some(author) = author {
+        table.add_row(vec!["author", &author.to_string()]);
+    }
+    if let Some(source) = source {
+        table.add_row(vec!["source", &source.to_string()]);
+    }
+    if let Some(homepage) = homepage {
+        table.add_row(vec!["homepage", &homepage.to_string()]);
+    }
+
+    if let Some(producers) = producers {
+        for (name, pairs) in producers.iter() {
+            for (field, version) in pairs.iter() {
+                match version.len() {
+                    0 => table.add_row(vec![name, &format!("{field}")]),
+                    _ => table.add_row(vec![name, &format!("{field} [{version}]")]),
+                };
+            }
+        }
+    }
+
+    // Write the table to the writer
+    writeln!(f, "{table}")?;
+
+    if let Payload::Component { children, .. } = payload {
+        for payload in children {
+            fmt_payload(payload, f)?;
+        }
+    }
+
+    Ok(())
+}


Test patch:
diff --git a/tests/cli/add-metadata-merge-sections.wat.stdout b/tests/cli/add-metadata-merge-sections.wat.stdout
index 7ce7a97b..accb94fc 100644
--- a/tests/cli/add-metadata-merge-sections.wat.stdout
+++ b/tests/cli/add-metadata-merge-sections.wat.stdout
@@ -1,6 +1,15 @@
-module:
-    language:
-        foo: 3
-        bar: 1
-    sdk:
-        foo: 2
+╭──────────┬───────────╮
+│ KIND     ┆ VALUE     │
+╞══════════╪═══════════╡
+│ kind     ┆ module    │
+├╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
+│ name     ┆ <unknown> │
+├╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
+│ range    ┆ 0x0..0x36 │
+├╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
+│ language ┆ foo [3]   │
+├╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
+│ language ┆ bar [1]   │
+├╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
+│ sdk      ┆ foo [2]   │
+╰──────────┴───────────╯
\ No newline at end of file
diff --git a/tests/cli/add-metadata-overwrite-name.wat.stdout b/tests/cli/add-metadata-overwrite-name.wat.stdout
index 085a5600..e02f74d9 100644
--- a/tests/cli/add-metadata-overwrite-name.wat.stdout
+++ b/tests/cli/add-metadata-overwrite-name.wat.stdout
@@ -1,1 +1,9 @@
-module foo:
+╭───────┬───────────╮
+│ KIND  ┆ VALUE     │
+╞═══════╪═══════════╡
+│ kind  ┆ module    │
+├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
+│ name  ┆ foo       │
+├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
+│ range ┆ 0x0..0x15 │
+╰───────┴───────────╯
\ No newline at end of file
diff --git a/tests/cli/add-metadata.wat.stdout b/tests/cli/add-metadata.wat.stdout
index 775d0001..3fa15ddc 100644
--- a/tests/cli/add-metadata.wat.stdout
+++ b/tests/cli/add-metadata.wat.stdout
@@ -1,7 +1,15 @@
-module foo:
-    language:
-        bar: 1
-    processed-by:
-        baz: 1
-    sdk:
-        my-sdk: 2
+╭──────────────┬────────────╮
+│ KIND         ┆ VALUE      │
+╞══════════════╪════════════╡
+│ kind         ┆ module     │
+├╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ name         ┆ foo        │
+├╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ range        ┆ 0x0..0x54  │
+├╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ language     ┆ bar [1]    │
+├╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ processed-by ┆ baz [1]    │
+├╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ sdk          ┆ my-sdk [2] │
+╰──────────────┴────────────╯
\ No newline at end of file
diff --git a/tests/cli/metadata-add-component.wat.stdout b/tests/cli/metadata-add-component.wat.stdout
index d099bd0b..bc4c6875 100644
--- a/tests/cli/metadata-add-component.wat.stdout
+++ b/tests/cli/metadata-add-component.wat.stdout
@@ -1,4 +1,20 @@
-component foo:
-    language:
-        foo: 1
-    module:
+╭──────────┬───────────╮
+│ KIND     ┆ VALUE     │
+╞══════════╪═══════════╡
+│ kind     ┆ component │
+├╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
+│ name     ┆ foo       │
+├╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
+│ range    ┆ 0x0..0x65 │
+├╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
+│ language ┆ foo [1]   │
+╰──────────┴───────────╯
+╭───────┬───────────╮
+│ KIND  ┆ VALUE     │
+╞═══════╪═══════════╡
+│ kind  ┆ module    │
+├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
+│ name  ┆ <unknown> │
+├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
+│ range ┆ 0xa..0x31 │
+╰───────┴───────────╯
\ No newline at end of file
diff --git a/tests/cli/metadata-component.wat.stdout b/tests/cli/metadata-component.wat.stdout
index b96562a4..dc109a55 100644
--- a/tests/cli/metadata-component.wat.stdout
+++ b/tests/cli/metadata-component.wat.stdout
@@ -1,3 +1,27 @@
-component my-name:
-    module submodule:
-    module another submodule:
+╭───────┬───────────╮
+│ KIND  ┆ VALUE     │
+╞═══════╪═══════════╡
+│ kind  ┆ component │
+├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
+│ name  ┆ my-name   │
+├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
+│ range ┆ 0x0..0x88 │
+╰───────┴───────────╯
+╭───────┬───────────╮
+│ KIND  ┆ VALUE     │
+╞═══════╪═══════════╡
+│ kind  ┆ module    │
+├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
+│ name  ┆ submodule │
+├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
+│ range ┆ 0xa..0x25 │
+╰───────┴───────────╯
+╭───────┬───────────────────╮
+│ KIND  ┆ VALUE             │
+╞═══════╪═══════════════════╡
+│ kind  ┆ module            │
+├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ name  ┆ another submodule │
+├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ range ┆ 0x27..0x4a        │
+╰───────┴───────────────────╯
\ No newline at end of file
diff --git a/tests/cli/metadata.wat.stdout b/tests/cli/metadata.wat.stdout
index 71d91c13..b255f9ff 100644
--- a/tests/cli/metadata.wat.stdout
+++ b/tests/cli/metadata.wat.stdout
@@ -1,1 +1,9 @@
-module:
+╭───────┬───────────╮
+│ KIND  ┆ VALUE     │
+╞═══════╪═══════════╡
+│ kind  ┆ module    │
+├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
+│ name  ┆ <unknown> │
+├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
+│ range ┆ 0x0..0x8  │
+╰───────┴───────────╯
\ No newline at end of file