Skip to content

Conversation

wfchandler
Copy link
Collaborator

Add tabular output with --format flag

Tabular output is much easier to quickly scan for value, as well as
being more amenable to traditional UNIX scripting. For example,
compare finding the block_size for an image in the following output:

  $ oxide image list --format=json
  [
    {
      "block_size": 512,
      "description": "Debian 13 generic cloud image",
      "id": "3672f476-51ff-49ab-bd2c-723151a921c6",
      "name": "debian-13",
      "os": "Debian",
      "size": 3221225472,
      "time_created": "2025-08-11T16:48:17.827861Z",
      "time_modified": "2025-08-11T16:52:23.411189Z",
      "version": "13"
    }, {
      "block_size": 4096,
      "description": "Silo image for Packer acceptance testing.",
      "id": "f13b140e-4d34-4060-b898-6316cdcc2f1e",
      "name": "packer-acc-test-silo-image",
      "os": "",
      "size": 1073741824,
      "time_created": "2025-05-15T23:11:14.015948Z",
      "time_modified": "2025-05-15T23:12:02.098119Z",
      "version": ""
    }
  ]

Versus:

  $ oxide image list --format=table:name,block_size
   NAME                        BLOCK_SIZE
   debian-13                   512
   packer-acc-test-silo-image  4096

For response types that are amenable to tabular formatting, we add a
--format flag to the subcommand. This takes an optional
comma-separated list of field names to print, as larger response items
can easily overflow typical terminal widths. The available field names
are listed in the --help output for the subcommand.

Not all API return values can be reasonably formatted as a table.
Endpoints returning (), byte streams, and unstructured
serde_json::Value objects are deliberately excluded.

Internally, this is implemented using the newly added ResponseFields
trait, which xtask creates as part of the generated CLI. This enables
getting the field names of a type and accessing them as a
serde_json::Value via their name.

@wfchandler wfchandler force-pushed the wc/generated-tabular-output branch from cc28eeb to 7a9f271 Compare September 16, 2025 19:15
@wfchandler
Copy link
Collaborator Author

Dependent on oxidecomputer/progenitor#1195

@wfchandler
Copy link
Collaborator Author

wfchandler commented Sep 16, 2025

How this exposes the available fields to users.

Long help shows fields for the subcommand:

$ ./target/debug/oxide image list --help
List images which are global or scoped to the specified project. The images are returned sorted by creation date, with the most recent images appearing first.

Usage: oxide image list [OPTIONS]

Options:
      --format <format>
          Format in which to print output
          
          Possible values:
            - json                    Output as pretty-printed JSON
            - table                   Output as table with all columns displayed
            - table:field1,field2,... Output as table, specifying which columns to display
          
          Examples:
            --format json
            --format table
            --format table:name,id,description
          
          Available fields:
            - block_size
            - description
            - digest
            - id
            - name
            - os
            - project_id
            - size
            - time_created
            - time_modified
            - version

      --limit <limit>
          Maximum number of items returned by a single call

      --project <project>
          Name or ID of the project

      --sort-by <sort-by>
          [possible values: name_ascending, name_descending, id_ascending]

  -h, --help
          Print help (see a summary with '-h')

Global Options:
      --profile <PROFILE>
          Configuration profile to use for commands

Failing to provide any valid field names will show the field list:

$ ./target/debug/oxide image list --format=table:whatever
WARNING: 'whatever' is not a valid field
ERROR: None of the requested '--format' fields are present in this command's output

Available fields:
 block_size
 description
 digest
 id
 name
 os
 project_id
 size
 time_created
 time_modified
 version

We're about to expand the logic around `OxideOverride`, move it into its
own module now for a more understandable diff.
@wfchandler wfchandler force-pushed the wc/generated-tabular-output branch from 90dabb2 to 23d088e Compare September 25, 2025 15:45
Tabular output is much easier to quickly scan for value, as well as
being more amenable to traditional UNIX scripting. For example,
compare finding the block_size for an image in the following output:

  $ oxide image list --format=json
  [
    {
      "block_size": 512,
      "description": "Debian 13 generic cloud image",
      "id": "3672f476-51ff-49ab-bd2c-723151a921c6",
      "name": "debian-13",
      "os": "Debian",
      "size": 3221225472,
      "time_created": "2025-08-11T16:48:17.827861Z",
      "time_modified": "2025-08-11T16:52:23.411189Z",
      "version": "13"
    }, {
      "block_size": 4096,
      "description": "Silo image for Packer acceptance testing.",
      "id": "f13b140e-4d34-4060-b898-6316cdcc2f1e",
      "name": "packer-acc-test-silo-image",
      "os": "",
      "size": 1073741824,
      "time_created": "2025-05-15T23:11:14.015948Z",
      "time_modified": "2025-05-15T23:12:02.098119Z",
      "version": ""
    }
  ]

Versus:

  $ oxide image list --format=table:name,block_size
   NAME                        BLOCK_SIZE
   debian-13                   512
   packer-acc-test-silo-image  4096

For query operations with a response type that is amenable to tabular
formatting, we add a `--format` flag to the subcommand. This takes an
optional comma-separated list of field names to print, as larger
response items can easily overflow typical terminal widths. The
available field names are listed in the `--help` output for the
subcommand.

The existing JSON format remains the default. Future changes may add the
ability to set a default format on a per-command basis.

Not all API return values can be reasonably formatted as a table.
Endpoints returning `()`, byte streams, and unstructured
`serde_json::Value` objects are deliberately excluded.

Internally, this is implemented using the newly added `ResponseFields`
trait, which xtask creates as part of the generated CLI. This enables
getting the field names of a type and accessing them as a
`serde_json::Value` via their name.
@wfchandler wfchandler force-pushed the wc/generated-tabular-output branch from 23d088e to 2fda6da Compare September 29, 2025 19:42
@wfchandler wfchandler marked this pull request as ready for review September 29, 2025 20:23
Comment on lines +619 to +621
let action_words = HashSet::from([
"Add", "Attach", "Create", "Demote", "Detach", "Probe", "Promote", "Reboot", "Resend",
"Start", "Stop", "Update",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I go back and forth on whether this is a good idea. As the comment says, I think having --format for write actions will just add noise to the help test.

However, this list of words to filter on is inevitably going to fall out of date, so we'll end up with most write operations not having the flag.

&[#(#field_strings),*]
}

#[allow(clippy::borrow_deref_ref)]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clippy complains about deref for types::SwitchLinkState.

&[#(#field_strings),*]
}

#[allow(clippy::needless_borrows_for_generic_args)]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not borrowing the field would require a clone for non-copy types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant