From a68fff8ea021c3867139d82e858be1aa6343f3a3 Mon Sep 17 00:00:00 2001 From: Igor Pashev Date: Fri, 21 Jul 2023 10:37:36 +0200 Subject: Initial commit --- Cargo.toml | 12 +++ README.md | 67 ++++++++++++++++ src/main.rs | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 329 insertions(+) create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/main.rs 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 + + 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 { + fn next_fmt(&self, x: &X) -> Option; + 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 DisplayType for NoDisplay { + fn next_fmt(&self, _: &X) -> Option { + None + } + fn fmt(&self, _: &X) -> String { + String::new() + } +} + +#[derive(Parser)] +struct TextDisplay { + /// Display as text + #[clap(long, group = "fmt_type")] + text: bool, + #[clap(flatten)] + next: T, +} + +impl DisplayType for TextDisplay +where + X: fmt::Display, + T: DisplayType + clap::Args, +{ + fn next_fmt(&self, x: &X) -> Option { + 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 { + /// Display as internal debug representation + #[clap(long, group = "fmt_type")] + debug: bool, + #[clap(flatten)] + next: T, +} + +impl DisplayType for DebugDisplay +where + X: fmt::Debug, + T: DisplayType + clap::Args, +{ + fn next_fmt(&self, x: &X) -> Option { + 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 { + /// Display as unformatted JSON + #[clap(long, group = "fmt_type")] + api: bool, + #[clap(flatten)] + next: T, +} + +impl DisplayType for ApiDisplay +where + X: serde::Serialize, + T: DisplayType + clap::Args, +{ + fn next_fmt(&self, x: &X) -> Option { + 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 { + /// Display as pretty formatted JSON + #[clap(long, group = "fmt_type")] + json: bool, + #[clap(flatten)] + next: T, +} + +impl DisplayType for JsonDisplay +where + X: serde::Serialize, + T: DisplayType + clap::Args, +{ + fn next_fmt(&self, x: &X) -> Option { + 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 { + /// Display as YAML + #[clap(long, group = "fmt_type")] + yaml: bool, + #[clap(flatten)] + next: T, +} + +impl DisplayType for YamlDisplay +where + X: serde::Serialize, + T: DisplayType + clap::Args, +{ + fn next_fmt(&self, x: &X) -> Option { + 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 { + /// Display as table + #[clap(long, group = "fmt_type", alias = "tabular")] + table: bool, + #[clap(flatten)] + next: T, +} + +impl DisplayType for TableDisplay +where + X: ToTable, + T: DisplayType + clap::Args, +{ + fn next_fmt(&self, x: &X) -> Option { + 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>>, + }, + /// Text output by default + Text { + #[clap(flatten)] + output: TextDisplay>>, + }, + /// Unformatted JSON output by default + Api { + #[clap(flatten)] + output: ApiDisplay>>, + }, + /// Pretty formatted JSON output by default + Json { + #[clap(flatten)] + output: JsonDisplay>>>, + }, + /// YAML output by default + Yaml { + #[clap(flatten)] + output: YamlDisplay>>>>, + }, + /// Table output by default + Table { + #[clap(flatten)] + output: TableDisplay< + YamlDisplay>>>>, + >, + }, +} + +#[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)), + } +} -- cgit v1.2.3