Skip to content
This repository has been archived by the owner on Jan 1, 2022. It is now read-only.

Handle parsing vector of custom structures #140

Open
epage opened this issue Dec 6, 2021 · 14 comments
Open

Handle parsing vector of custom structures #140

epage opened this issue Dec 6, 2021 · 14 comments

Comments

@epage
Copy link
Owner

epage commented Dec 6, 2021

Issue by krystian-wojtas
Saturday Feb 22, 2020 at 19:26 GMT
Originally opened as clap-rs/clap#1704


What are you trying to do?

I have group of options which could be used multiple times. All such groups should be kept without overwriting previous one.

$ prometheus_sensors_exporter \
    \
    `# 2 physical sensors located on physycial different i2c bus or address` \
    --sensor \
        --sensor-device=tmp102 \
        --sensor-name="temperature_tmp102_outdoor" \
        --sensor-i2c-bus=0 \
        --sensor-i2c-address=0x48 \
    --sensor \
        --sensor-device=tmp102 \
        --sensor-name="temperature_tmp102_indoor" \
        --sensor-i2c-bus=1 \
        --sensor-i2c-address=0x49 \

--sensor specifies instance of new group. Then suboptions are only for current group.

Ideally parsing would generate instance of Vector<Sensor> defined something like

pub enum Device {
    Tmp102,
    Si7021,
}

pub struct Sensor {
    sensor_device: Device,
    sensor_name: String,
    sensor_i2c_bus: Option<usize>,
    sensor_i2c_address: Option<u8>,
}

Could it be possible?

Or maybe would you recommend to model it in some another way which could be easier to implement?

What have you tried already?

I tired few examples, but I didn't find anything similar so far.

@epage
Copy link
Owner Author

epage commented Dec 6, 2021

Comment by krystian-wojtas
Sunday Feb 23, 2020 at 23:22 GMT


I tried some prove of concept. Actually it is implemented with structopt, not clap. Just to give some idea.

I found that structopt has method from_iter which I can use instead of from_args. Then I can provide some subgroup of all parameters and invoke it few times as many sensors are defined by provided user command line parameters.

To split all parameters in such subgroups I used Vector::split function with custom delimiter --sensor.

Code

Here is implementation with only 2 such sensor parameters

// Standard modules paths
use std::env;
use structopt::StructOpt;

#[derive(StructOpt, Debug)]
#[structopt()]
pub struct OptGeneric {

    // Socket

    /// Address of interface to bind
    #[structopt(long, default_value = "0.0.0.0")]
    socket_interface: String,

    /// Number in decimal
    #[structopt(long, default_value = "9898")]
    socket_port: u16,

}


#[derive(StructOpt, Debug)]
#[structopt()]
pub struct OptSensor {

    /// Number in decimal
    #[structopt(long, default_value = "1")]
    sensor_i2c_bus: usize,

    /// Number in hexadecimal
    #[structopt(long)]
    sensor_i2c_address: Option<u8>,

}


#[derive(Debug)]
pub struct Config {
    generic: OptGeneric,
    sensors: Vec<OptSensor>,
}

impl Config {

    pub fn parse(args: std::env::Args) -> Config {

        // Split command line arguments into groups separated by '--sensor' delimiter
        let arguments: Vec<String> = args.collect();
        let mut sensor_parameters_groups = arguments.split(|argument| argument == "--sensor");

         // First group of parameters is generic without any information for specific sensor
         let generic_parameters = sensor_parameters_groups.next()
             .unwrap();

         // Parse generic parameters
         let generic = OptGeneric::from_iter(generic_parameters);

         // Create vector for sensors to be parsed
         let mut sensors = Vec::new();

         // Iterate over rest of groups each containing complete sensor information
         for sensor_parameters in sensor_parameters_groups {

             // OptSensor::from_iter is going to skip first item, so it have to be put anything to be skipped
             // TODO maybe StructOpt::from_iter should not skip first item to not have to workaround it?
             let array_with_empty_item = [String::from("")];
             let iter_with_extra_first_item = array_with_empty_item.iter().chain(sensor_parameters);

             // Parse each sensor information
             let sensor = OptSensor::from_iter(iter_with_extra_first_item);

             // Fill sensors vector
             sensors.push(sensor);
         }

        // Return result
        Config{generic, sensors}
    }

}


fn main() -> Result<(), Box<dyn std::error::Error>> {

    // Parse configuration
    let config = Config::parse(env::args());

    // Debug purpose only
    println!("{:#?}", config);

    // Return success
    Ok(())
}

Run

Invocations are like that:

  • without any arguments
% cargo run --
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/prometheus_sensors_exporter`
Config {
    generic: OptGeneric {
        socket_interface: "0.0.0.0",
        socket_port: 9898,
    },
    sensors: [],
}
  • some sensors
% cargo run -- \
    \
    `# generic configuration` \
    --socket-interface=1.1.1.1 \
    --socket-port=10000 \
    \
    `# 3 physical sensors located on physycial different i2c bus or address` \
    --sensor \
        --sensor-i2c-bus=0 \
        --sensor-i2c-address=1 \
    --sensor \
        --sensor-i2c-bus=0 \
        --sensor-i2c-address=2 \
    --sensor \
        --sensor-i2c-bus=1 \
        --sensor-i2c-address=1 \
>
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/prometheus_sensors_exporter --socket-interface=1.1.1.1 --socket-port=10000 --sensor --sensor-i2c-bus=0 --sensor-i2c-address=1 --sensor --sensor-i2c-bus=0 --sensor-i2c-address=2 --sensor --sensor-i2c-bus=1 --sensor-i2c-address=1`
Config {
    generic: OptGeneric {
        socket_interface: "1.1.1.1",
        socket_port: 10000,
    },
    sensors: [
        OptSensor {
            sensor_i2c_bus: 0,
            sensor_i2c_address: Some(
                1,
            ),
        },
        OptSensor {
            sensor_i2c_bus: 0,
            sensor_i2c_address: Some(
                2,
            ),
        },
        OptSensor {
            sensor_i2c_bus: 1,
            sensor_i2c_address: Some(
                1,
            ),
        },
    ],
}
  • help message
% cargo run -- --help
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/prometheus_sensors_exporter --help`
prometheus_sensors_exporter 0.1.2

USAGE:
    prometheus_sensors_exporter [OPTIONS]

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

OPTIONS:
        --socket-interface <socket-interface>    Address of interface to bind [default: 0.0.0.0]
        --socket-port <socket-port>              Number in decimal [default: 9898]

Questions

  • Is it some valid approach?
  • Could clap be used to implement something similar?
  • How to include sensors parameters into help message?
  • There is some workaround in array_with_empty_item. Library is going to skip first element, so I have to provide anything to be skipped. Does clap also requires such workaround?

@epage
Copy link
Owner Author

epage commented Dec 6, 2021

Comment by krystian-wojtas
Monday Feb 24, 2020 at 20:45 GMT


I have some another tries

subcommand enum

Code

// Standard modules paths
use structopt::StructOpt;

#[derive(StructOpt, Debug)]
pub struct OptGeneric {

    // Socket

    /// Address of interface to bind
    #[structopt(long, default_value = "0.0.0.0")]
    socket_interface: String,

    /// Number in decimal
    #[structopt(long, default_value = "9898")]
    socket_port: u16,

    #[structopt(subcommand)]
    sensor: OptSensor
}

#[derive(StructOpt, Debug)]
pub enum OptSensor {

    Sensor {
        /// Number in decimal
        #[structopt(long, default_value = "1")]
        sensor_i2c_bus: usize,

        /// Number in hexadecimal
        #[structopt(long)]
        sensor_i2c_address: Option<u8>,
    }
}


fn main() -> Result<(), Box<dyn std::error::Error>> {

    // Parse configuration
    let config = OptGeneric::from_args();

    // Debug purpose only
    println!("{:#?}", config);

    // Return success
    Ok(())
}

Run with one sensor

% cargo run -- \
    \
    `# generic configuration` \
    --socket-interface=1.1.1.1 \
    --socket-port=10000 \
    \
    sensor \
        --sensor-i2c-bus=0 \
        --sensor-i2c-address=1 \


    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/prometheus_sensors_exporter --socket-interface=1.1.1.1 --socket-port=10000 sensor --sensor-i2c-bus=0 --sensor-i2c-address=1`
OptGeneric {
    socket_interface: "1.1.1.1",
    socket_port: 10000,
    sensor: Sensor {
        sensor_i2c_bus: 0,
        sensor_i2c_address: Some(
            1,
        ),
    },
}

Run with 2 sensors

% cargo run -- \
    \
    `# generic configuration` \
    --socket-interface=1.1.1.1 \
    --socket-port=10000 \
    \
    sensor \
        --sensor-i2c-bus=0 \
        --sensor-i2c-address=1 \
    sensor \
      --sensor-i2c-bus=1 \
      --sensor-i2c-address=1 \
>
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/prometheus_sensors_exporter --socket-interface=1.1.1.1 --socket-port=10000 sensor --sensor-i2c-bus=0 --sensor-i2c-address=1 sensor --sensor-i2c-bus=1 --sensor-i2c-address=1`
error: Found argument 'sensor' which wasn't expected, or isn't valid in this context

USAGE:
    prometheus_sensors_exporter sensor --sensor-i2c-address <sensor-i2c-address> --sensor-i2c-bus <sensor-i2c-bus>

For more information try --help

subcommand vec enum

Code

// Standard modules paths
use structopt::StructOpt;

#[derive(StructOpt, Debug)]
pub struct OptGeneric {

    // Socket

    /// Address of interface to bind
    #[structopt(long, default_value = "0.0.0.0")]
    socket_interface: String,

    /// Number in decimal
    #[structopt(long, default_value = "9898")]
    socket_port: u16,

    #[structopt(subcommand)]
    sensors: Vec<OptSensor>
}

#[derive(StructOpt, Debug)]
pub enum OptSensor {

    Sensor {
        /// Number in decimal
        #[structopt(long, default_value = "1")]
        sensor_i2c_bus: usize,

        /// Number in hexadecimal
        #[structopt(long)]
        sensor_i2c_address: Option<u8>,
    }
}


fn main() -> Result<(), Box<dyn std::error::Error>> {

    // Parse configuration
    let config = OptGeneric::from_args();

    // Debug purpose only
    println!("{:#?}", config);

    // Return success
    Ok(())
}

Cannot compile

% cargo build
   Compiling prometheus_sensors_exporter v0.1.2 (/home/k/project/prometheus_sensors_exporter)
error[E0277]: the trait bound `std::vec::Vec<OptSensor>: structopt::StructOptInternal` is not satisfied
  --> src/main.rs:17:17
   |
17 |     #[structopt(subcommand)]
   |                 ^^^^^^^^^^ the trait `structopt::StructOptInternal` is not implemented for `std::vec::Vec<OptSensor>`
   |
   = note: required by `structopt::StructOptInternal::from_subcommand`

error[E0277]: the trait bound `std::vec::Vec<OptSensor>: structopt::StructOptInternal` is not satisfied
 --> src/main.rs:4:10
  |
4 | #[derive(StructOpt, Debug)]
  |          ^^^^^^^^^ the trait `structopt::StructOptInternal` is not implemented for `std::vec::Vec<OptSensor>`
  |
  = note: required by `structopt::StructOptInternal::augment_clap`

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0277`.
error: could not compile `prometheus_sensors_exporter`.

To learn more, run the command again with --verbose.

Should I implement StructOptInternal trait? Is it something recommended in this case?

@epage
Copy link
Owner Author

epage commented Dec 6, 2021

Comment by pksunkara
Monday Feb 24, 2020 at 20:45 GMT


Can you try with the master branch of clap? We have clap_derive

@epage
Copy link
Owner Author

epage commented Dec 6, 2021

Comment by krystian-wojtas
Monday Feb 24, 2020 at 20:58 GMT


@pksunkara, sure I would try it.

@epage
Copy link
Owner Author

epage commented Dec 6, 2021

Comment by krystian-wojtas
Monday Feb 24, 2020 at 21:12 GMT


Using master clap

subcommand enum

Code

// Standard modules paths
use clap::Clap;

#[derive(Clap, Debug)]
pub struct OptGeneric {

    // Socket

    /// Address of interface to bind
    #[clap(long, default_value = "0.0.0.0")]
    socket_interface: String,

    /// Number in decimal
    #[clap(long, default_value = "9898")]
    socket_port: u16,

    #[clap(subcommand)]
    sensor: OptSensor
}

#[derive(Clap, Debug)]
pub enum OptSensor {

    Sensor {
        /// Number in decimal
        #[clap(long, default_value = "1")]
        sensor_i2c_bus: usize,

        /// Number in hexadecimal
        #[clap(long)]
        sensor_i2c_address: Option<u8>,
    }
}


fn main() -> Result<(), Box<dyn std::error::Error>> {

    // Parse configuration
    let config = OptGeneric::parse();

    // Debug purpose only
    println!("{:#?}", config);

    // Return success
    Ok(())
}

Run with one sensor is ok

% cargo run -- \
    \
    `# generic configuration` \
    --socket-interface=1.1.1.1 \
    --socket-port=10000 \
    \
    sensor \
        --sensor-i2c-bus=0 \
        --sensor-i2c-address=1 \

   Compiling prometheus_sensors_exporter v0.1.2 (/home/k/project/prometheus_sensors_exporter)
    Finished dev [unoptimized + debuginfo] target(s) in 0.52s
     Running `target/debug/prometheus_sensors_exporter --socket-interface=1.1.1.1 --socket-port=10000 sensor --sensor-i2c-bus=0 --sensor-i2c-address=1`
OptGeneric {
    socket_interface: "1.1.1.1",
    socket_port: 10000,
    sensor: Sensor {
        sensor_i2c_bus: 0,
        sensor_i2c_address: Some(
            1,
        ),
    },
}

Run with 2 sensors is not allowed

% cargo run -- \
    \
    `# generic configuration` \
    --socket-interface=1.1.1.1 \
    --socket-port=10000 \
    \
    sensor \
        --sensor-i2c-bus=0 \
        --sensor-i2c-address=1 \
    sensor \
      --sensor-i2c-bus=1 \
      --sensor-i2c-address=1 \

    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/prometheus_sensors_exporter --socket-interface=1.1.1.1 --socket-port=10000 sensor --sensor-i2c-bus=0 --sensor-i2c-address=1 sensor --sensor-i2c-bus=1 --sensor-i2c-address=1`
error: Found argument 'sensor' which wasn't expected, or isn't valid in this context
If you tried to supply `sensor` as a PATTERN use `-- sensor`

USAGE:
    prometheus_sensors_exporter sensor [OPTIONS]

For more information try --help

subcommand vec enum

Code

// Standard modules paths
use clap::Clap;

#[derive(Clap, Debug)]
pub struct OptGeneric {

    // Socket

    /// Address of interface to bind
    #[clap(long, default_value = "0.0.0.0")]
    socket_interface: String,

    /// Number in decimal
    #[clap(long, default_value = "9898")]
    socket_port: u16,

    #[clap(subcommand)]
    sensors: Vec<OptSensor>
}

#[derive(Clap, Debug)]
pub enum OptSensor {

    Sensor {
        /// Number in decimal
        #[clap(long, default_value = "1")]
        sensor_i2c_bus: usize,

        /// Number in hexadecimal
        #[clap(long)]
        sensor_i2c_address: Option<u8>,
    }
}


fn main() -> Result<(), Box<dyn std::error::Error>> {

    // Parse configuration
    let config = OptGeneric::parse();

    // Debug purpose only
    println!("{:#?}", config);

    // Return success
    Ok(())
}

Cannot compile. Error message is much better than structopt

[Running 'cargo check']
    Checking prometheus_sensors_exporter v0.1.2 (/home/k/project/prometheus_sensors_exporter)
error[E0277]: the trait bound `std::vec::Vec<OptSensor>: clap::derive::Subcommand` is not satisfied
 --> src/main.rs:4:10
  |
4 | #[derive(Clap, Debug)]
  |          ^^^^ the trait `clap::derive::Subcommand` is not implemented for `std::vec::Vec<OptSensor>`
  |
  = note: required by `clap::derive::Subcommand::augment_subcommands`

error[E0277]: the trait bound `std::vec::Vec<OptSensor>: clap::derive::Subcommand` is not satisfied
  --> src/main.rs:17:12
   |
17 |     #[clap(subcommand)]
   |            ^^^^^^^^^^ the trait `clap::derive::Subcommand` is not implemented for `std::vec::Vec<OptSensor>`
   |
   = note: required by `clap::derive::Subcommand::from_subcommand`

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0277`.
error: could not compile `prometheus_sensors_exporter`.

To learn more, run the command again with --verbose.
[Finished running. Exit status: 101]

@epage
Copy link
Owner Author

epage commented Dec 6, 2021

Comment by krystian-wojtas
Monday Feb 24, 2020 at 21:27 GMT


Clap has

Support their own sub-arguments, and sub-sub-commands independent of the parent

Maybe this is some solution? Could it be sub-sub-sub-...-commands also? Could it be wrapped into Option (with Box?) to brake infinite recursion?

This try is interesting as it compiles ok, but overflow stack in runtime

Code

// Standard modules paths
use clap::Clap;

#[derive(Clap, Debug)]
pub struct OptGeneric {

    // Socket

    /// Address of interface to bind
    #[clap(long, default_value = "0.0.0.0")]
    socket_interface: String,

    /// Number in decimal
    #[clap(long, default_value = "9898")]
    socket_port: u16,

    #[clap(subcommand)]
    sensors: OptSensor
}

#[derive(Clap, Debug)]
pub enum OptSensor {

    Sensor {
        /// Number in decimal
        #[clap(long, default_value = "1")]
        sensor_i2c_bus: usize,

        /// Number in hexadecimal
        #[clap(long)]
        sensor_i2c_address: Option<u8>,

        #[clap(subcommand)]
        sensors: Option<Box<OptSensor>>
    }
}


fn main() -> Result<(), Box<dyn std::error::Error>> {

    // Parse configuration
    let config = OptGeneric::parse();

    // Debug purpose only
    println!("{:#?}", config);

    // Return success
    Ok(())
}

Run

% cargo run -- \
    \
    `# generic configuration` \
    --socket-interface=1.1.1.1 \
    --socket-port=10000 \
    \
    sensor \
        --sensor-i2c-bus=0 \
        --sensor-i2c-address=1 \


   Compiling prometheus_sensors_exporter v0.1.2 (/home/k/project/prometheus_sensors_exporter)
    Finished dev [unoptimized + debuginfo] target(s) in 0.62s
     Running `target/debug/prometheus_sensors_exporter --socket-interface=1.1.1.1 --socket-port=10000 sensor --sensor-i2c-bus=0 --sensor-i2c-address=1`

thread 'main' has overflowed its stack
fatal runtime error: stack overflow
[1]    9558 abort (core dumped)  cargo run -- `# generic configuration` --socket-interface=1.1.1.1  sensor

@epage
Copy link
Owner Author

epage commented Dec 6, 2021

Comment by krystian-wojtas
Monday Feb 24, 2020 at 21:28 GMT


I see that there is some proper example of subcommands on some nested levels
https://github.com/clap-rs/clap/blob/master/examples/20_subcommands.rs

@epage
Copy link
Owner Author

epage commented Dec 6, 2021

Comment by krystian-wojtas
Monday Feb 24, 2020 at 22:07 GMT


I see that example from documentation have specified tree of subcommands known ahead at compile time.

Here is some another attempt

not multiple

Code

// Standard modules paths
use clap::Clap;

#[derive(Clap, Debug)]
pub struct OptGeneric {

    // Socket

    /// Address of interface to bind
    #[clap(long, default_value = "0.0.0.0")]
    socket_interface: String,

    /// Number in decimal
    #[clap(long, default_value = "9898")]
    socket_port: u16,

    #[clap(subcommand)]
    sensor: OptSensor,
}

#[derive(Clap, Debug)]
pub enum OptSensor {

    Sensor {

        /// Number in decimal
        #[clap(long, default_value = "1")]
        sensor_i2c_bus: usize,

        /// Number in hexadecimal
        #[clap(long)]
        sensor_i2c_address: Option<u8>,

        #[clap()]
        more_optional_sensors: Vec<String>,
    }
}


fn main() -> Result<(), Box<dyn std::error::Error>> {

    // Parse configuration
    let config = OptGeneric::parse();

    // Debug purpose only
    println!("{:#?}", config);

    // Return success
    Ok(())
}

Run help

% cargo run -- help
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/prometheus_sensors_exporter help`
prometheus_sensors_exporter

USAGE:
    prometheus_sensors_exporter [OPTIONS] <SUBCOMMAND>

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

OPTIONS:
        --socket-interface <socket-interface>    Address of interface to bind [default: 0.0.0.0]
        --socket-port <socket-port>              Number in decimal [default: 9898]

SUBCOMMANDS:
    help      Prints this message or the help of the given subcommand(s)
    sensor

Run help for subcommand

% cargo run -- help sensor
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/prometheus_sensors_exporter help sensor`
prometheus_sensors_exporter-sensor

USAGE:
    prometheus_sensors_exporter sensor [OPTIONS] [more-optional-sensors]...

ARGS:
    <more-optional-sensors>...

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

OPTIONS:
        --sensor-i2c-address <sensor-i2c-address>    Number in hexadecimal
        --sensor-i2c-bus <sensor-i2c-bus>            Number in decimal [default: 1]

Run with one sensor

% cargo run -- \
    \
    `# generic configuration` \
    --socket-interface=1.1.1.1 \
    --socket-port=10000 \
    \
    sensor \
        --sensor-i2c-bus=0 \
        --sensor-i2c-address=1 \
>
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/prometheus_sensors_exporter --socket-interface=1.1.1.1 --socket-port=10000 sensor --sensor-i2c-bus=0 --sensor-i2c-address=1`
OptGeneric {
    socket_interface: "1.1.1.1",
    socket_port: 10000,
    sensor: Sensor {
        sensor_i2c_bus: 0,
        sensor_i2c_address: Some(
            1,
        ),
        more_optional_sensors: [],
    },
}

Run with 2 sensors

% cargo run -- \
    \
    `# generic configuration` \
    --socket-interface=1.1.1.1 \
    --socket-port=10000 \
    \
    sensor \
        --sensor-i2c-bus=0 \
        --sensor-i2c-address=1 \
    \
    sensor \
        --sensor-i2c-bus=1 \
        --sensor-i2c-address=1 \
>
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/prometheus_sensors_exporter --socket-interface=1.1.1.1 --socket-port=10000 sensor --sensor-i2c-bus=0 --sensor-i2c-address=1 sensor --sensor-i2c-bus=1 --sensor-i2c-address=1`
error: The argument '--sensor-i2c-bus <sensor-i2c-bus>' was provided more than once, but cannot be used multiple times

USAGE:
    prometheus_sensors_exporter sensor [OPTIONS] [more-optional-sensors]...

For more information try --help

multiple

// Standard modules paths
use clap::Clap;

#[derive(Clap, Debug)]
pub struct OptGeneric {

    // Socket

    /// Address of interface to bind
    #[clap(long, default_value = "0.0.0.0")]
    socket_interface: String,

    /// Number in decimal
    #[clap(long, default_value = "9898")]
    socket_port: u16,

    #[clap(subcommand)]
    sensor: OptSensor,
}

#[derive(Clap, Debug)]
pub enum OptSensor {

    Sensor {

        /// Number in decimal
        #[clap(long, default_value = "1", multiple = true)]
        sensor_i2c_bus: usize,

        /// Number in hexadecimal
        #[clap(long, multiple = true)]
        sensor_i2c_address: Option<u8>,

        #[clap()]
        more_optional_sensors: Vec<String>,
    }
}


fn main() -> Result<(), Box<dyn std::error::Error>> {

    // Parse configuration
    let config = OptGeneric::parse();

    // Debug purpose only
    println!("{:#?}", config);

    // Return success
    Ok(())
}

Run with 2 sensors

% cargo run -- \
    \
    `# generic configuration` \
    --socket-interface=1.1.1.1 \
    --socket-port=10000 \
    \
    sensor \
        --sensor-i2c-bus=0 \
        --sensor-i2c-address=1 \
    \
    sensor \
        --sensor-i2c-bus=1 \
        --sensor-i2c-address=1 \

    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/prometheus_sensors_exporter --socket-interface=1.1.1.1 --socket-port=10000 sensor --sensor-i2c-bus=0 --sensor-i2c-address=1 sensor --sensor-i2c-bus=1 --sensor-i2c-address=1`
OptGeneric {
    socket_interface: "1.1.1.1",
    socket_port: 10000,
    sensor: Sensor {
        sensor_i2c_bus: 0,
        sensor_i2c_address: Some(
            1,
        ),
        more_optional_sensors: [
            "sensor",
        ],
    },
}

Parsing consumes all parameters overwriting previous ones and they are not available in more_optional_sensors strings vector.

@epage
Copy link
Owner Author

epage commented Dec 6, 2021

Comment by krystian-wojtas
Monday Feb 24, 2020 at 23:14 GMT


Here is best result I have so far

structopt

Code

// Standard modules paths
use std::env;
use clap::Clap;

#[derive(Clap, Debug)]
pub struct OptBasic {

    // Socket

    /// Address of interface to bind
    #[clap(long, default_value = "0.0.0.0")]
    socket_interface: String,

    /// Number in decimal
    #[clap(long, default_value = "9898")]
    socket_port: u16,

    #[clap(flatten)]
    sensor: OptSensor,

    // Field is declared only for help message
    // Vector is never filled
    /// Each sensor definition should start with <sensor> keyword followed by its options
    #[clap()]
    more_optional_sensors: Vec<String>,
}


#[derive(Clap, Debug)]
pub struct OptSensor {

    /// Number in decimal
    #[clap(long, default_value = "1")]
    sensor_i2c_bus: usize,

    /// Number in hexadecimal
    #[clap(long)]
    sensor_i2c_address: Option<u8>,
}


#[derive(Debug)]
pub struct Config {

    // Address of interface to bind
    socket_interface: String,

    // Number in decimal
    socket_port: u16,

    // All sensors
    sensors: Vec<OptSensor>,
}

impl Config {

    pub fn parse(args: std::env::Args) -> Self {

        // Split command line arguments into groups separated by '--sensor' delimiter
        let arguments: Vec<String> = args.collect();
        let mut sensor_parameters_groups = arguments.split(|argument| argument == "sensor");

         // First group of parameters is basic without any information for specific sensor
         let basic_parameters = sensor_parameters_groups.next()
             .unwrap();

         // Parse basic parameters
         let basic = OptBasic::from_iter(basic_parameters);

         // Create vector to be filled with all parsed sensors
         let mut sensors = Vec::new();

         // Add already parsed basic sensor to list of all sensors
         sensors.push(basic.sensor);

         // Iterate over rest of groups each containing complete sensor information
         for sensor_parameters in sensor_parameters_groups {

             // OptSensor::from_iter is going to skip first item, so it have to be put anything to be skipped
             // TODO maybe StructOpt::from_iter should not skip first item to not have to workaround it?
             let array_with_empty_item = [String::from("")];
             let iter_with_extra_first_item = array_with_empty_item.iter().chain(sensor_parameters);

             // Parse each sensor information
             let sensor = OptSensor::from_iter(iter_with_extra_first_item);

             // Fill sensors vector
             sensors.push(sensor);
         }

        // Return result
        Config {
            socket_interface: basic.socket_interface,
            socket_port: basic.socket_port,
            sensors,
        }
    }

}


fn main() -> Result<(), Box<dyn std::error::Error>> {

    // Parse configuration
    let config = Config::parse(env::args());

    // Debug purpose only
    println!("{:#?}", config);

    // Return success
    Ok(())
}

Run help

% cargo run -- --help
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/prometheus_sensors_exporter --help`
prometheus_sensors_exporter 0.1.2

USAGE:
    prometheus_sensors_exporter [OPTIONS] [more-optional-sensors]...

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

OPTIONS:
        --sensor-i2c-address <sensor-i2c-address>    Number in hexadecimal
        --sensor-i2c-bus <sensor-i2c-bus>            Number in decimal [default: 1]
        --socket-interface <socket-interface>        Address of interface to bind [default: 0.0.0.0]
        --socket-port <socket-port>                  Number in decimal [default: 9898]

ARGS:
    <more-optional-sensors>...    Each sensor definition should start with <sensor> keyword followed by its options

Run with one sensor

% cargo run -- \
    \
    `# generic configuration` \
    --socket-interface=1.1.1.1 \
    --socket-port=10000 \
    \
    `# Basic sensor` \
    --sensor-i2c-bus=4 \

    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/prometheus_sensors_exporter --socket-interface=1.1.1.1 --socket-port=10000 --sensor-i2c-bus=4`
Config {
    socket_interface: "1.1.1.1",
    socket_port: 10000,
    sensors: [
        OptSensor {
            sensor_i2c_bus: 4,
            sensor_i2c_address: None,
        },
    ],
}

Run with 3 sensors

% cargo run -- \
    \
    `# generic configuration` \
    --socket-interface=1.1.1.1 \
    --socket-port=10000 \
    \
    `# Basic sensor` \
    --sensor-i2c-bus=4 \
    \
    `# More optional sensors` \
    sensor \
        --sensor-i2c-bus=0 \
        --sensor-i2c-address=1 \
    \
    sensor \
        --sensor-i2c-bus=1 \
        --sensor-i2c-address=1 \

    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/prometheus_sensors_exporter --socket-interface=1.1.1.1 --socket-port=10000 --sensor-i2c-bus=4 sensor --sensor-i2c-bus=0 --sensor-i2c-address=1 sensor --sensor-i2c-bus=1 --sensor-i2c-address=1`
Config {
    socket_interface: "1.1.1.1",
    socket_port: 10000,
    sensors: [
        OptSensor {
            sensor_i2c_bus: 4,
            sensor_i2c_address: None,
        },
        OptSensor {
            sensor_i2c_bus: 0,
            sensor_i2c_address: Some(
                1,
            ),
        },
        OptSensor {
            sensor_i2c_bus: 1,
            sensor_i2c_address: Some(
                1,
            ),
        },
    ],
}

clap

Code

// Standard modules paths
use std::env;
use clap::Clap;

#[derive(Clap, Debug)]
pub struct OptBasic {

    // Socket

    /// Address of interface to bind
    #[clap(long, default_value = "0.0.0.0")]
    socket_interface: String,

    /// Number in decimal
    #[clap(long, default_value = "9898")]
    socket_port: u16,

    #[clap(flatten)]
    sensor: OptSensor,

    // Field is declared only for help message
    // Vector is never filled
    /// Each sensor definition should start with <sensor> keyword followed by its options
    #[clap()]
    more_optional_sensors: Vec<String>,
}


#[derive(Clap, Debug)]
pub struct OptSensor {

    /// Number in decimal
    #[clap(long, default_value = "1")]
    sensor_i2c_bus: usize,

    /// Number in hexadecimal
    #[clap(long)]
    sensor_i2c_address: Option<u8>,
}


#[derive(Debug)]
pub struct Config {

    // Address of interface to bind
    socket_interface: String,

    // Number in decimal
    socket_port: u16,

    // All sensors
    sensors: Vec<OptSensor>,
}

impl Config {

    pub fn parse(args: std::env::Args) -> Self {

        // Split command line arguments into groups separated by '--sensor' delimiter
        let arguments: Vec<String> = args.collect();
        let mut sensor_parameters_groups = arguments.split(|argument| argument == "sensor");

         // First group of parameters is basic without any information for specific sensor
         let basic_parameters = sensor_parameters_groups.next()
             .unwrap();

         // Parse basic parameters
         let basic = OptBasic::from_iter(basic_parameters);

         // Create vector to be filled with all parsed sensors
         let mut sensors = Vec::new();

         // Add already parsed basic sensor to list of all sensors
         sensors.push(basic.sensor);

         // Iterate over rest of groups each containing complete sensor information
         for sensor_parameters in sensor_parameters_groups {

             // OptSensor::from_iter is going to skip first item, so it have to be put anything to be skipped
             // TODO maybe StructOpt::from_iter should not skip first item to not have to workaround it?
             let array_with_empty_item = [String::from("")];
             let iter_with_extra_first_item = array_with_empty_item.iter().chain(sensor_parameters);

             // Parse each sensor information
             let sensor = OptSensor::from_iter(iter_with_extra_first_item);

             // Fill sensors vector
             sensors.push(sensor);
         }

        // Return result
        Config {
            socket_interface: basic.socket_interface,
            socket_port: basic.socket_port,
            sensors,
        }
    }

}


fn main() -> Result<(), Box<dyn std::error::Error>> {

    // Parse configuration
    let config = Config::parse(env::args());

    // Debug purpose only
    println!("{:#?}", config);

    // Return success
    Ok(())
}

Cannot compile

[Running 'cargo check']
    Checking prometheus_sensors_exporter v0.1.2 (/home/k/project/prometheus_sensors_exporter)
error[E0599]: no function or associated item named `from_iter` found for type `OptBasic` in the current scope
  --> src/main.rs:68:32
   |
6  | pub struct OptBasic {
   | ------------------- function or associated item `from_iter` not found for this
...
68 |          let basic = OptBasic::from_iter(basic_parameters);
   |                                ^^^^^^^^^ function or associated item not found in `OptBasic`
   |
   = help: items from traits can only be used if the trait is implemented and in scope
   = note: the following trait defines an item `from_iter`, perhaps you need to implement it:
           candidate #1: `std::iter::FromIterator`

error[E0599]: no function or associated item named `from_iter` found for type `OptSensor` in the current scope
  --> src/main.rs:85:38
   |
30 | pub struct OptSensor {
   | -------------------- function or associated item `from_iter` not found for this
...
85 |              let sensor = OptSensor::from_iter(iter_with_extra_first_item);
   |                                      ^^^^^^^^^ function or associated item not found in `OptSensor`
   |
   = help: items from traits can only be used if the trait is implemented and in scope
   = note: the following trait defines an item `from_iter`, perhaps you need to implement it:
           candidate #1: `std::iter::FromIterator`

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0599`.
error: could not compile `prometheus_sensors_exporter`.

To learn more, run the command again with --verbose.
[Finished running. Exit status: 101]

Is there any similar method to pass custom iterator for clap parser?

@epage
Copy link
Owner Author

epage commented Dec 6, 2021

Comment by krystian-wojtas
Tuesday Feb 25, 2020 at 20:22 GMT


structopt::from_iter is called clap::parse_from.

Here is same thing as previous implemented with clap.

clap

Code

// Standard modules paths
use std::env;
use clap::Clap;

#[derive(Clap, Debug)]
pub struct OptBasic {

    // Socket

    /// Address of interface to bind
    #[clap(long, default_value = "0.0.0.0")]
    socket_interface: String,

    /// Number in decimal
    #[clap(long, default_value = "9898")]
    socket_port: u16,

    #[clap(flatten)]
    sensor: OptSensor,

    // Field is declared only for help message
    // Vector is never filled
    /// Each sensor definition should start with <sensor> keyword followed by its options
    #[clap()]
    more_optional_sensors: Vec<String>,
}


#[derive(Clap, Debug)]
pub struct OptSensor {

    /// Number in decimal
    #[clap(long, default_value = "1")]
    sensor_i2c_bus: usize,

    /// Number in hexadecimal
    #[clap(long)]
    sensor_i2c_address: Option<u8>,
}


#[derive(Debug)]
pub struct Config {

    // Address of interface to bind
    socket_interface: String,

    // Number in decimal
    socket_port: u16,

    // All sensors
    sensors: Vec<OptSensor>,
}

impl Config {

    pub fn parse(args: std::env::Args) -> Self {

        // Split command line arguments into groups separated by '--sensor' delimiter
        let arguments: Vec<String> = args.collect();
        let mut sensor_parameters_groups = arguments.split(|argument| argument == "sensor");

         // First group of parameters is basic without any information for specific sensor
         let basic_parameters = sensor_parameters_groups.next()
             .unwrap();

         // Parse basic parameters
         let basic = OptBasic::parse_from(basic_parameters);

         // Create vector to be filled with all parsed sensors
         let mut sensors = Vec::new();

         // Add already parsed basic sensor to list of all sensors
         sensors.push(basic.sensor);

         // Iterate over rest of groups each containing complete sensor information
         for sensor_parameters in sensor_parameters_groups {

             // Parse each sensor information
             let sensor = OptSensor::parse_from(sensor_parameters);

             // Fill sensors vector
             sensors.push(sensor);
         }

        // Return result
        Config {
            socket_interface: basic.socket_interface,
            socket_port: basic.socket_port,
            sensors,
        }
    }

}


fn main() -> Result<(), Box<dyn std::error::Error>> {

    // Parse configuration
    let config = Config::parse(env::args());

    // Debug purpose only
    println!("{:#?}", config);

    // Return success
    Ok(())
}

Run with some sensors

% cargo run -- \
    \
    `# generic configuration` \
    --socket-interface=1.1.1.1 \
    --socket-port=10000 \
    \
    `# Basic sensor` \
    --sensor-i2c-bus=4 \
    \
    `# More optional sensors` \
    sensor \
        --sensor-i2c-bus=0 \
        --sensor-i2c-address=1 \
    \
    sensor \
        --sensor-i2c-bus=1 \
        --sensor-i2c-address=1 \

   Compiling prometheus_sensors_exporter v0.1.2 (/home/k/project/prometheus_sensors_exporter)
    Finished dev [unoptimized + debuginfo] target(s) in 0.65s
     Running `target/debug/prometheus_sensors_exporter --socket-interface=1.1.1.1 --socket-port=10000 --sensor-i2c-bus=4 sensor --sensor-i2c-bus=0 --sensor-i2c-address=1 sensor --sensor-i2c-bus=1 --sensor-i2c-address=1`
Config {
    socket_interface: "1.1.1.1",
    socket_port: 10000,
    sensors: [
        OptSensor {
            sensor_i2c_bus: 4,
            sensor_i2c_address: None,
        },
        OptSensor {
            sensor_i2c_bus: 1,
            sensor_i2c_address: Some(
                1,
            ),
        },
        OptSensor {
            sensor_i2c_bus: 1,
            sensor_i2c_address: Some(
                1,
            ),
        },
    ],
}

Help

% cargo run -- --help
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/prometheus_sensors_exporter --help`
prometheus_sensors_exporter

USAGE:
    prometheus_sensors_exporter [OPTIONS] [more-optional-sensors]...

ARGS:
    <more-optional-sensors>...    Each sensor definition should start with <sensor> keyword followed by its options

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

OPTIONS:
        --sensor-i2c-address <sensor-i2c-address>    Number in hexadecimal
        --sensor-i2c-bus <sensor-i2c-bus>            Number in decimal [default: 1]
        --socket-interface <socket-interface>        Address of interface to bind [default: 0.0.0.0]
        --socket-port <socket-port>                  Number in decimal [default: 9898]

It works fine. Clap and structopt are great :) Clap looks a little better.

@epage
Copy link
Owner Author

epage commented Dec 6, 2021

Comment by krystian-wojtas
Tuesday Feb 25, 2020 at 20:31 GMT


To summarize:

I have parsing of command line arguments using this great library. For my case I had to split all commands to some smaller groups and run individual parsing for them.

I'm wondering if it could be possible to express this logic by clap itself? Is such case worth to implement? Would it be more users interested in it? Or there are a low chance that anyone else would like to use clap in similar way?

Is it useful to implement passing vector of enums? Something like

#[clap(subcommand)] 
// or #[clap(subcommands)]  ?
field: Vec<UserEnum>

Maybe there is some better way to model such command line arguments to fit into current clap implementation?

@epage
Copy link
Owner Author

epage commented Dec 6, 2021

Comment by pksunkara
Wednesday Feb 26, 2020 at 01:40 GMT


@CreepySkeleton What do you think about this whole issue? It's a different way of doing things for sure.

@epage
Copy link
Owner Author

epage commented Dec 6, 2021

Comment by CreepySkeleton
Thursday Feb 27, 2020 at 16:01 GMT


I'm afraid there's no way to implement this exact design with clap, but you can get pretty close.

You see, --sensor takes arbitrary number of values up to the next occurrence of --sensor. There's no way to express this condition via clap API, unfortunately, so it couldn't work, not out of box.

You can overcome this limitation by applying some sort of external preprocessing, splitting the args into groups manually, as you do in the structopt example.

I can think about the alternative:

$ prometheus_sensors_exporter \
    \
    `# 2 physical sensors located on physycial different i2c bus or address` \
    --sensor \
        sensor-device=tmp102 \
        sensor-name="temperature_tmp102_outdoor" \
        sensor-i2c-bus=0 \
        sensor-i2c-address=0x48 \
    --sensor \
        sensor-device=tmp102 \
        sensor-name="temperature_tmp102_indoor" \
        sensor-i2c-bus=1 \
        sensor-i2c-address=0x49 \

The idea is to threat each group as one option with multiple arguments, each parameter is a key-value pair. Unfortunately, parsing these values is up to you, and the auto generated help message leaves much to be desired.

I'm wondering if it could be possible to express this logic by clap itself? Is such case worth to implement? Would it be more users interested in it? Or there are a low chance that anyone else would like to use clap in similar way?

Those are good questions, and whether we will consider adding support for this use case or not depends on the answers. This is the first such request, and it is also the first time I personally encounter this design.

Maybe there is some better way to model such command line arguments to fit into current clap implementation?

Ideas are welcome!

@epage
Copy link
Owner Author

epage commented Dec 6, 2021

Comment by leshow
Tuesday Mar 03, 2020 at 22:03 GMT


I found this issue trying to do the exact same thing. The case is that I want to pass multiple --something where each Something has a set of values.

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

No branches or pull requests

1 participant