Clap (derive) - Add custom section for documentation

I have a use case where I want to group keyword arguments into groups in order to display them in separate section when running my_program --help.

I read about ArgGroups at clap::_derive::_tutorial - Rust but could not find what I was looking for. I don't need any relationships or dependencies between the keyword arguments. I just want to group them into sections for documentation purposes when printing --help to have a better overview - they don't exclude one another and can be used together in whatever order.

In my concrete example I want:

Command line arguments

Usage: my_program [OPTIONS] <TTY>

Arguments:
  <TTY>  Path to the serial TTY device

Options:
  -h, --help
          Print help

Zigbee options:
  -r, --reinitialize
          Whether to reinitialize the device
  -L, --link-key <LINK_KEY>
          The link key for the new network [default: foo]
  -N, --network-key <NETWORK_KEY>
          The network key for the new network [default: bar]
  -P, --pan-id <PAN_ID>
          The PAN ID for the new network [default: 1234]
  -E, --extended-pan-id <EXTENDED_PAN_ID>
          The Extended PAN ID for the new network [default: 00:00:00:00:00:00:00:00]
      --protocol-version <PROTOCOL_VERSION>
          The protocol version to use [default: 8]
  -s, --channel-size <CHANNEL_SIZE>
          The channel size for communication with the device [default: 64]

Radio options:
  -c, --channel <CHANNEL>
          The radio channel for the new network [default: 12]
  -p, --tx-power <TX_POWER>
          The radio transmit power for the device [default: 8]
  -b, --tx-power-mode-boost
          Whether to use the TX power boost mode

Concentrator options:
      --min-time <MIN_TIME>
          Minimum time (in seconds) for concentrator [default: 1m]
      --max-time <MAX_TIME>
          Maximum time (in seconds) for concentrator [default: 1h]
      --route-error-threshold <ROUTE_ERROR_THRESHOLD>
          Route error threshold for concentrator [default: 8]
      --delivery-failure-threshold <DELIVERY_FAILURE_THRESHOLD>
          Delivery failure threshold for concentrator [default: 8]
      --max-hops <MAX_HOPS>
          Maximum hops for concentrator [default: 0]

Web server options:
  -a, --listen-address <LISTEN_ADDRESS>
          The IP address to listen on [default: 0.0.0.0:9000]

Instead of

Command line arguments

Usage: my_program [OPTIONS] <TTY>

Arguments:
  <TTY>  Path to the serial TTY device

Options:
  -r, --reinitialize
          Whether to reinitialize the device
  -L, --link-key <LINK_KEY>
          The link key for the new network [default: foo]
  -N, --network-key <NETWORK_KEY>
          The network key for the new network [default: bar]
  -P, --pan-id <PAN_ID>
          The PAN ID for the new network [default: 1234]
  -E, --extended-pan-id <EXTENDED_PAN_ID>
          The Extended PAN ID for the new network [default: 00:00:00:00:00:00:00:00]
      --protocol-version <PROTOCOL_VERSION>
          The protocol version to use [default: 8]
  -s, --channel-size <CHANNEL_SIZE>
          The channel size for communication with the device [default: 64]
  -c, --channel <CHANNEL>
          The radio channel for the new network [default: 12]
  -p, --tx-power <TX_POWER>
          The radio transmit power for the device [default: 8]
  -b, --tx-power-mode-boost
          Whether to use the TX power boost mode
      --min-time <MIN_TIME>
          Minimum time (in seconds) for concentrator [default: 1m]
      --max-time <MAX_TIME>
          Maximum time (in seconds) for concentrator [default: 1h]
      --route-error-threshold <ROUTE_ERROR_THRESHOLD>
          Route error threshold for concentrator [default: 8]
      --delivery-failure-threshold <DELIVERY_FAILURE_THRESHOLD>
          Delivery failure threshold for concentrator [default: 8]
      --max-hops <MAX_HOPS>
          Maximum hops for concentrator [default: 0]
  -a, --listen-address <LISTEN_ADDRESS>
          The IP address to listen on [default: 0.0.0.0:9000]
  -h, --help
          Print help

Got it using next_help_heading:


/// Zigbee network parameters.
#[derive(Debug, Args)]
#[clap(next_help_heading = "Zigbee parameters")]
pub struct ZigbeeParameters {
    #[clap(index = 1, help = "Path to the serial TTY device")]
    pub(crate) tty: String,
    #[clap(long, short = 'r', help = "Whether to reinitialize the device")]
    pub(crate) reinitialize: bool,
    #[clap(long, short = 'L', help = "The link key for the new network", default_value_t = LINK_KEY)]
    pub(crate) link_key: Key,
    #[clap(long, short = 'N', help = "The network key for the new network", default_value_t = NETWORK_KEY)]
    pub(crate) network_key: Key,
    #[clap(long, short = 'P', help = "The PAN ID for the new network", default_value_t = PAN_ID)]
    pub(crate) pan_id: u16,
    #[clap(long, short = 'E', help = "The Extended PAN ID for the new network", default_value_t = EXTENDED_PAN_ID)]
    pub(crate) extended_pan_id: MacAddr8,
    #[clap(long, help = "The protocol version to use", default_value_t = PROTOCOL_VERSION)]
    pub(crate) protocol_version: u8,
    #[clap(long, short = 's', help = "The channel size for communication with the device", default_value_t = CHANNEL_SIZE)]
    pub(crate) channel_size: usize,
    #[clap(flatten)]
    pub(crate) radio: RadioParameters,
    #[clap(flatten)]
    pub(crate) concentrator: ConcentratorParameters,
}

/// Radio parameters.
#[derive(Debug, Args)]
#[clap(next_help_heading = "Radio parameters")]
pub struct RadioParameters {
    #[clap(long, short = 'c', help = "The radio channel for the new network", default_value_t = RADIO_CHANNEL)]
    pub(crate) channel: u8,
    #[clap(long, short = 'p', help = "The radio transmit power for the device", default_value_t = RADIO_TX_POWER)]
    pub(crate) tx_power: i8,
    #[clap(long, short = 'b', help = "Whether to use the TX power boost mode")]
    pub(crate) tx_power_mode_boost: bool,
}

/// Concentrator parameters.
#[derive(Debug, Args)]
#[clap(next_help_heading = "Concentrator parameters")]
pub struct ConcentratorParameters {
    #[clap(long, help = "Minimum time (in seconds) for concentrator", default_value_t = MIN_TIME.into())]
    pub(crate) min_time: humantime::Duration,
    #[clap(long, help = "Maximum time (in seconds) for concentrator", default_value_t = MAX_TIME.into())]
    pub(crate) max_time: humantime::Duration,
    #[clap(long, help = "Route error threshold for concentrator", default_value_t = ROUTE_ERROR_THRESHOLD)]
    pub(crate) route_error_threshold: u8,
    #[clap(long, help = "Delivery failure threshold for concentrator", default_value_t = DELIVERY_FAILURE_THRESHOLD)]
    pub(crate) delivery_failure_threshold: u8,
    #[clap(long, help = "Maximum hops for concentrator", default_value_t = MAX_HOPS)]
    pub(crate) max_hops: u8,
}