Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ populate middleware:
| Flag | Middleware field | Default | Purpose |
| --- | --- | --- | --- |
| `--output`, `-o` | `output_format` | `json` | Output format: `json`, `human`, or `toon`. |
| `--json` | `output_format` | — | Shorthand for `--output json`. |
| `--toon` | `output_format` | — | Shorthand for `--output toon`. |
Comment thread
jpage-godaddy marked this conversation as resolved.
| `--human` | `output_format` | — | Shorthand for `--output human`. |
| `--verbose` | `verbose` | empty | Includes metadata; no value means `all`. |
| `--dry-run` | `dry_run` | `false` | Short-circuits mutating/destructive commands. |
| `--fields` | `fields` | empty | Selects comma-separated output fields. |
Expand Down Expand Up @@ -337,8 +340,9 @@ my-cli project list --fields name,status
my-cli project list --output human
```

JSON output is the default. Human output is optimized for terminal reading. TOON can be requested
explicitly with `--output toon`.
JSON output is the default. Human output is optimized for terminal reading. Each format has a
shorthand flag: `--json`, `--human`, and `--toon` are equivalent to `--output json`,
`--output human`, and `--output toon` respectively.

Errors are rendered through the same envelope path as successful data. Framework errors are mapped
to process exit codes by category. Callers that need a specific process status can use
Expand Down
3 changes: 3 additions & 0 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ Framework global flags populate middleware and apply consistently to every comma
| Flag | Purpose |
| --- | --- |
| `--output`, `-o` | Output format: `json`, `human`, or `toon`. |
| `--json` | Shorthand for `--output json`. |
| `--toon` | Shorthand for `--output toon`. |
| `--human` | Shorthand for `--output human`. |
| `--verbose` | Includes metadata; no value means all metadata. |
| `--dry-run` | Short-circuits mutating/destructive commands. |
| `--fields` | Selects comma-separated output fields. |
Expand Down
49 changes: 45 additions & 4 deletions src/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,16 +170,45 @@ pub fn register_global_flags(command: Command) -> Command {
.value_name("KEYWORD")
.help("Search commands and guides by keyword"),
)
.arg(
Arg::new("json")
.long("json")
.global(true)
.action(ArgAction::SetTrue)
.help("Shorthand for --output json"),
)
.arg(
Arg::new("toon")
.long("toon")
.global(true)
.action(ArgAction::SetTrue)
.help("Shorthand for --output toon"),
)
.arg(
Arg::new("human")
.long("human")
.global(true)
.action(ArgAction::SetTrue)
.help("Shorthand for --output human"),
)
}

#[must_use]
/// Extracts framework-global flags from parsed `clap` matches.
pub fn global_flags_from_matches(matches: &ArgMatches) -> GlobalFlags {
GlobalFlags {
output_format: matches
let output_format = if matches.get_flag("toon") {
"toon".to_owned()
} else if matches.get_flag("human") {
"human".to_owned()
} else {
matches
.get_one::<String>("output")
.cloned()
.unwrap_or_else(|| "json".to_owned()),
.unwrap_or_else(|| "json".to_owned())
};

GlobalFlags {
output_format,
verbose: matches
.get_one::<String>("verbose")
.cloned()
Expand Down Expand Up @@ -237,7 +266,10 @@ pub fn extract_search_query(args: &[impl AsRef<str>]) -> String {
}

#[must_use]
/// Extracts `--output`/`-o` from raw args before normal parsing.
/// Extracts output format from raw args.
///
/// Recognizes `--output <format>` / `-o <format>` / `--output=<format>`,
/// plus `--json`, `--toon`, and `--human` as shorthand for their respective formats.
pub fn extract_output_format(args: &[impl AsRef<str>]) -> String {
for index in 0..args.len() {
let arg = args[index].as_ref();
Expand All @@ -249,6 +281,15 @@ pub fn extract_output_format(args: &[impl AsRef<str>]) -> String {
if let Some(value) = arg.strip_prefix("--output=") {
return value.to_owned();
}
if arg == "--json" {
return "json".to_owned();
}
if arg == "--toon" {
return "toon".to_owned();
}
if arg == "--human" {
return "human".to_owned();
}
}
"json".to_owned()
}
Expand Down
51 changes: 51 additions & 0 deletions tests/derive_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,54 @@ async fn derive_bridge_handles_flattened_structs() {
assert_eq!(json["data"]["page_size"], 50);
assert_eq!(json["data"]["page"], 0);
}

// --- output shorthand aliases ---

#[tokio::test]
async fn json_shorthand_flag_produces_json_output() {
let cli = derive_cli();

let result = cli
.run(["derive-test", "greet", "hello", "--name", "World", "--json"])
.await;
assert_eq!(result.exit_code, 0, "stderr: {}", result.rendered);
let json: Value = serde_json::from_str(&result.rendered).expect("valid json");
assert_eq!(json["data"]["messages"], json!(["Hello, World!"]));
}

#[tokio::test]
async fn toon_shorthand_flag_produces_toon_output() {
let cli = derive_cli();

let result = cli
.run(["derive-test", "greet", "hello", "--name", "World", "--toon"])
.await;
assert_eq!(result.exit_code, 0, "output: {}", result.rendered);
assert!(
!result.rendered.starts_with('{'),
"toon output should not be raw JSON: {}",
result.rendered
);
}

#[tokio::test]
async fn human_shorthand_flag_produces_human_output() {
let cli = derive_cli();

let result = cli
.run([
"derive-test",
"greet",
"hello",
"--name",
"World",
"--human",
])
.await;
assert_eq!(result.exit_code, 0, "output: {}", result.rendered);
assert!(
!result.rendered.starts_with('{'),
"human output should not be raw JSON: {}",
result.rendered
);
}
16 changes: 16 additions & 0 deletions tests/foundation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3435,6 +3435,22 @@ fn raw_search_and_output_extraction_matches_legacy_bypass_helpers() {
extract_output_format(&["my-cli", "--search", "foo"]),
"json"
);
assert_eq!(
extract_output_format(&["my-cli", "project", "list", "--json"]),
"json"
);
assert_eq!(
extract_output_format(&["my-cli", "project", "list", "--toon"]),
"toon"
);
assert_eq!(
extract_output_format(&["my-cli", "--toon", "project", "list"]),
"toon"
);
assert_eq!(
extract_output_format(&["my-cli", "project", "list", "--human"]),
"human"
);

assert!(has_true_schema_flag(&["my-cli", "release", "--schema"]));
assert!(has_true_schema_flag(&[
Expand Down
Loading