aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml12
-rw-r--r--README.md67
-rw-r--r--src/main.rs250
3 files changed, 329 insertions, 0 deletions
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..d1346c1
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "rust-clap-output-formats"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+clap = {version = "4", features = ["derive", "env", "wrap_help"]}
+comfy-table = "6.1.4"
+serde = {version = "1", features = ["derive"]}
+serde_json = "1"
+serde_yaml = "0.9"
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..10619d3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,67 @@
+How to make CLI tools composing different output formats using [clap](https://crates.io/crates/clap).
+
+Demo:
+
+ $ ./target/debug/rust-clap-output-formats --help
+ Usage: rust-clap-output-formats <COMMAND>
+
+ Commands:
+ debug Debug output by default
+ text Text output by default
+ api Unformatted JSON output by default
+ json Pretty formatted JSON output by default
+ yaml YAML output by default
+ table Table output by default
+ help Print this message or the help of the given subcommand(s)
+
+ Options:
+ -h, --help Print help
+
+ $ ./target/debug/rust-clap-output-formats debug --help
+ Debug output by default
+
+ Usage: rust-clap-output-formats debug [OPTIONS]
+
+ Options:
+ --debug Display as internal debug representation
+ --text Display as text
+ --api Display as unformatted JSON
+ -h, --help Print help
+
+ $ ./target/debug/rust-clap-output-formats table --help
+ Table output by default
+
+ Usage: rust-clap-output-formats table [OPTIONS]
+
+ Options:
+ --table Display as table
+ --yaml Display as YAML
+ --json Display as pretty formatted JSON
+ --api Display as unformatted JSON
+ --text Display as text
+ --debug Display as internal debug representation
+ -h, --help Print help
+
+ $ ./target/debug/rust-clap-output-formats json
+ {
+ "name": "Hello",
+ "value": "world"
+ }
+
+ $ ./target/debug/rust-clap-output-formats table
+ +-------+-------+
+ | Name | Value |
+ +===============+
+ | Hello | world |
+ +-------+-------+
+
+ $ ./target/debug/rust-clap-output-formats table --json
+ {
+ "name": "Hello",
+ "value": "world"
+ }
+
+ $ ./target/debug/rust-clap-output-formats table --yaml
+ name: Hello
+ value: world
+
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..68894bf
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,250 @@
+use clap::Parser;
+use comfy_table::{ContentArrangement, Row, Table};
+use std::fmt;
+
+trait DisplayType<X> {
+ fn next_fmt(&self, x: &X) -> Option<String>;
+ fn fmt(&self, x: &X) -> String;
+ fn display(&self, x: &X) -> String {
+ self.next_fmt(x).unwrap_or_else(|| self.fmt(x))
+ }
+}
+
+trait ToTable {
+ fn to_table(&self) -> Table;
+}
+
+#[derive(Parser)]
+struct NoDisplay {}
+
+impl<X> DisplayType<X> for NoDisplay {
+ fn next_fmt(&self, _: &X) -> Option<String> {
+ None
+ }
+ fn fmt(&self, _: &X) -> String {
+ String::new()
+ }
+}
+
+#[derive(Parser)]
+struct TextDisplay<T: clap::Args> {
+ /// Display as text
+ #[clap(long, group = "fmt_type")]
+ text: bool,
+ #[clap(flatten)]
+ next: T,
+}
+
+impl<X, T> DisplayType<X> for TextDisplay<T>
+where
+ X: fmt::Display,
+ T: DisplayType<X> + clap::Args,
+{
+ fn next_fmt(&self, x: &X) -> Option<String> {
+ self.text
+ .then(|| self.fmt(x))
+ .or_else(|| self.next.next_fmt(x))
+ }
+ fn fmt(&self, x: &X) -> String {
+ format!("{x}")
+ }
+}
+
+#[derive(Parser)]
+struct DebugDisplay<T: clap::Args> {
+ /// Display as internal debug representation
+ #[clap(long, group = "fmt_type")]
+ debug: bool,
+ #[clap(flatten)]
+ next: T,
+}
+
+impl<X, T> DisplayType<X> for DebugDisplay<T>
+where
+ X: fmt::Debug,
+ T: DisplayType<X> + clap::Args,
+{
+ fn next_fmt(&self, x: &X) -> Option<String> {
+ self.debug
+ .then(|| self.fmt(x))
+ .or_else(|| self.next.next_fmt(x))
+ }
+ fn fmt(&self, x: &X) -> String {
+ format!("{x:?}")
+ }
+}
+
+#[derive(Parser)]
+struct ApiDisplay<T: clap::Args> {
+ /// Display as unformatted JSON
+ #[clap(long, group = "fmt_type")]
+ api: bool,
+ #[clap(flatten)]
+ next: T,
+}
+
+impl<X, T> DisplayType<X> for ApiDisplay<T>
+where
+ X: serde::Serialize,
+ T: DisplayType<X> + clap::Args,
+{
+ fn next_fmt(&self, x: &X) -> Option<String> {
+ self.api
+ .then(|| self.fmt(x))
+ .or_else(|| self.next.next_fmt(x))
+ }
+ fn fmt(&self, x: &X) -> String {
+ serde_json::to_string(x).expect("Cannot serialize item to JSON")
+ }
+}
+
+#[derive(Parser)]
+struct JsonDisplay<T: clap::Args> {
+ /// Display as pretty formatted JSON
+ #[clap(long, group = "fmt_type")]
+ json: bool,
+ #[clap(flatten)]
+ next: T,
+}
+
+impl<X, T> DisplayType<X> for JsonDisplay<T>
+where
+ X: serde::Serialize,
+ T: DisplayType<X> + clap::Args,
+{
+ fn next_fmt(&self, x: &X) -> Option<String> {
+ self.json
+ .then(|| self.fmt(x))
+ .or_else(|| self.next.next_fmt(x))
+ }
+ fn fmt(&self, x: &X) -> String {
+ serde_json::to_string_pretty(x).expect("Cannot serialize item to JSON")
+ }
+}
+
+#[derive(Parser)]
+struct YamlDisplay<T: clap::Args> {
+ /// Display as YAML
+ #[clap(long, group = "fmt_type")]
+ yaml: bool,
+ #[clap(flatten)]
+ next: T,
+}
+
+impl<X, T> DisplayType<X> for YamlDisplay<T>
+where
+ X: serde::Serialize,
+ T: DisplayType<X> + clap::Args,
+{
+ fn next_fmt(&self, x: &X) -> Option<String> {
+ self.yaml
+ .then(|| self.fmt(x))
+ .or_else(|| self.next.next_fmt(x))
+ }
+ fn fmt(&self, x: &X) -> String {
+ serde_yaml::to_string(x).expect("Cannot serialize item to YAML")
+ }
+}
+
+#[derive(Parser)]
+struct TableDisplay<T: clap::Args> {
+ /// Display as table
+ #[clap(long, group = "fmt_type", alias = "tabular")]
+ table: bool,
+ #[clap(flatten)]
+ next: T,
+}
+
+impl<X, T> DisplayType<X> for TableDisplay<T>
+where
+ X: ToTable,
+ T: DisplayType<X> + clap::Args,
+{
+ fn next_fmt(&self, x: &X) -> Option<String> {
+ self.table
+ .then(|| self.fmt(x))
+ .or_else(|| self.next.next_fmt(x))
+ }
+ fn fmt(&self, x: &X) -> String {
+ x.to_table().to_string()
+ }
+}
+
+#[derive(Parser)]
+enum App {
+ /// Debug output by default
+ Debug {
+ #[clap(flatten)]
+ output: DebugDisplay<TextDisplay<ApiDisplay<NoDisplay>>>,
+ },
+ /// Text output by default
+ Text {
+ #[clap(flatten)]
+ output: TextDisplay<DebugDisplay<ApiDisplay<NoDisplay>>>,
+ },
+ /// Unformatted JSON output by default
+ Api {
+ #[clap(flatten)]
+ output: ApiDisplay<TextDisplay<DebugDisplay<NoDisplay>>>,
+ },
+ /// Pretty formatted JSON output by default
+ Json {
+ #[clap(flatten)]
+ output: JsonDisplay<ApiDisplay<TextDisplay<DebugDisplay<NoDisplay>>>>,
+ },
+ /// YAML output by default
+ Yaml {
+ #[clap(flatten)]
+ output: YamlDisplay<JsonDisplay<ApiDisplay<TextDisplay<DebugDisplay<NoDisplay>>>>>,
+ },
+ /// Table output by default
+ Table {
+ #[clap(flatten)]
+ output: TableDisplay<
+ YamlDisplay<JsonDisplay<ApiDisplay<TextDisplay<DebugDisplay<NoDisplay>>>>>,
+ >,
+ },
+}
+
+#[derive(Debug, serde::Serialize)]
+struct Foo {
+ name: String,
+ value: String,
+}
+
+impl fmt::Display for Foo {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}={}", self.name, self.value)
+ }
+}
+
+impl ToTable for Foo {
+ fn to_table(&self) -> Table {
+ let mut table = Table::new();
+
+ table
+ .set_content_arrangement(ContentArrangement::Dynamic)
+ .set_header(Row::from(vec!["Name", "Value"]))
+ .add_row(Row::from(vec![&self.name, &self.value]));
+
+ table
+ }
+}
+
+fn main() {
+ let app = App::parse();
+
+ let foo = Foo {
+ name: "Hello".to_string(),
+ value: "world".to_string(),
+ };
+
+ match app {
+ App::Debug { output } => println!("{}", output.display(&foo)),
+ App::Text { output } => println!("{}", output.display(&foo)),
+ App::Api { output } => println!("{}", output.display(&foo)),
+ App::Json { output } => println!("{}", output.display(&foo)),
+ App::Yaml { output } => println!("{}", output.display(&foo)),
+ App::Table { output } => println!("{}", output.display(&foo)),
+ }
+}