Skip to content

feat(cli): generic output formatter to eliminate --output flag duplication #1750

@jeffmaury

Description

@jeffmaury

Problem Statement

The OpenShell CLI has grown to support --output flags on many commands (e.g., sandbox list, gateway list, provider list-profiles), allowing users to specify output format as JSON, YAML, or table. However, each command duplicates the same 15-20 line match statement to handle output formatting, leading to significant code duplication and maintenance burden.

Current duplication:

  • gateway_list() - line 1292 in run.rs
  • sandbox_list() - line 3185 in run.rs
  • provider_list_profiles() - line 4700 in run.rs
  • provider_profile_export() - line 4751 in run.rs
  • Policy commands - lines 6622, 6729, 6809 in run.rs

Each command repeats ~15-20 lines of boilerplate for the same logic, totaling ~91 lines of duplicated code initially (and growing with each new command that supports --output).

Proposed Design

Create a new output.rs module with generic helper functions that handle the repetitive output formatting logic using an early-return pattern. This preserves the existing code structure while eliminating duplication.

Key design decisions:

  1. Helper functions over traits/macros - Simpler, easier to understand, works with existing code
  2. Early-return pattern - Returns bool to signal if output was handled, caller returns immediately or continues to table rendering
  3. Three helper variants - Handles different use cases (collections, single items, pre-formatted output)
  4. Incremental migration - Can be adopted command-by-command without breaking changes

Core API:

// For collections of items
pub fn print_output_collection<T, F>(
    format: &str,
    items: &[T],
    to_json: F,
) -> Result<bool>
where
    F: Fn(&T) -> serde_json::Value

// For single items
pub fn print_output_single<T>(
    format: &str,
    item: &T,
    to_json: impl Fn(&T) -> serde_json::Value,
) -> Result<bool>

// For pre-formatted output
pub fn print_output_direct(
    format: &str,
    json_fn: impl FnOnce() -> Result<String>,
    yaml_fn: impl FnOnce() -> Result<String>,
) -> Result<bool>

Usage example:

Before (18 lines):

match output {
    "json" => {
        let items: Vec<serde_json::Value> = sandboxes.iter().map(sandbox_to_json).collect();
        println!("{}", serde_json::to_string_pretty(&items).into_diagnostic()?);
        return Ok(());
    }
    "yaml" => {
        let items: Vec<serde_json::Value> = sandboxes.iter().map(sandbox_to_json).collect();
        print!("{}", serde_yml::to_string(&items).into_diagnostic()?);
        return Ok(());
    }
    "table" => {}
    _ => return Err(miette!("unsupported output format: {output}")),
}

After (3 lines):

if print_output_collection(output, &sandboxes, sandbox_to_json)? {
    return Ok(());
}

Files to modify:

  1. Create: crates/openshell-cli/src/output.rs (~80 lines)
  2. Update: crates/openshell-cli/src/lib.rs - add module declaration
  3. Update: crates/openshell-cli/src/run.rs - migrate commands to use helpers

Alternatives Considered

  1. Traits - More idiomatic Rust but requires implementing traits on types we don't own, adding complexity
  2. Macros - Would reduce boilerplate but makes code harder to debug and understand
  3. Do nothing - Continue duplicating code, but this makes adding new output formats harder and increases maintenance burden

The helper function approach was chosen for simplicity, type safety, and ease of adoption.

Agent Investigation

Analyzed crates/openshell-cli/src/main.rs (2745 lines) and crates/openshell-cli/src/run.rs (8768 lines):

  • Identified 7+ commands with duplicated output formatting logic
  • Confirmed OutputFormat enum exists at line 652 of main.rs
  • Verified dependencies (serde, serde_json, serde_yml) are already available
  • Each command has its own converter function (e.g., sandbox_to_json, gateway_to_json)
  • Pattern is consistent across all commands, making a generic solution feasible

Benefits:

  • Reduces duplication by ~91 lines initially
  • Single source of truth for output formatting
  • Type-safe, compiler-verified
  • Easy to extend (e.g., adding CSV format requires updating one module)
  • Backward compatible
  • Incremental adoption (migrate commands one at a time)

Metadata

Metadata

Assignees

No one assigned

    Labels

    gator:validatedGator validated this issue as ready for workstate:triage-neededOpened without agent diagnostics and needs triage

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions