[pve-devel] [PATCH installer 3/5] assistant: replace clap with pico-args for command argument parsing

Christoph Heiss c.heiss at proxmox.com
Fri May 9 14:09:15 CEST 2025


Drops clap for pico-args, as the former is pretty heavy-weight and
completely overhauls its API with ~every major release.

While it results in a bit more boilerplate code, it makes (especially
long-term) maintenance simpler.

As for the resulting binary, there is a ~13.5% decrease in binary size,
from 4M to 3.4MiB (after stripping).

   text    data     bss     dec     hex filename
3793413  299000     616 4093029  3e7465 proxmox-auto-install-assistant-clap
3245990  286136     472 3532598  35e736 proxmox-auto-install-assistant-pico

Further, the dependency tree goes from 114 to 103 dependencies, as well
as the package compile time on my machine (on hot caches), roughly
measured with

`cargo clean && time cargo build --package proxmox-chroot --release`

from ~31 to ~28s.

A big chunk of the added lines are simply the help menus for the
different subcommands of the tool, the actual code changes are rather
small.

No functional changes intended.

Signed-off-by: Christoph Heiss <c.heiss at proxmox.com>
---
 debian/control                             |   1 -
 proxmox-auto-install-assistant/Cargo.toml  |   6 +-
 proxmox-auto-install-assistant/src/main.rs | 536 +++++++++++++++------
 proxmox-auto-installer/Cargo.toml          |   1 -
 proxmox-auto-installer/src/answer.rs       |   6 +-
 proxmox-auto-installer/src/utils.rs        |   7 +-
 6 files changed, 387 insertions(+), 170 deletions(-)

diff --git a/debian/control b/debian/control
index 51c45a4..a2005a2 100644
--- a/debian/control
+++ b/debian/control
@@ -10,7 +10,6 @@ Build-Depends: cargo:native,
                libpve-common-perl,
                librsvg2-bin,
                librust-anyhow-1-dev,
-               librust-clap-4+derive-dev,
                librust-cursive-0.21+crossterm-backend-dev,
                librust-glob-0.3-dev,
                librust-hex-0.4-dev,
diff --git a/proxmox-auto-install-assistant/Cargo.toml b/proxmox-auto-install-assistant/Cargo.toml
index 43a968f..9b4a9c4 100644
--- a/proxmox-auto-install-assistant/Cargo.toml
+++ b/proxmox-auto-install-assistant/Cargo.toml
@@ -12,13 +12,9 @@ homepage = "https://www.proxmox.com"
 
 [dependencies]
 anyhow.workspace = true
-log.workspace = true
-proxmox-installer-common.workspace = true
 proxmox-auto-installer.workspace = true
-serde = { workspace = true, features = ["derive"] }
+proxmox-installer-common = { workspace = true, features = [ "cli" ] }
 serde_json.workspace = true
 toml.workspace = true
-regex.workspace = true
 
-clap = { version = "4.0", features = ["derive"] }
 glob = "0.3"
diff --git a/proxmox-auto-install-assistant/src/main.rs b/proxmox-auto-install-assistant/src/main.rs
index b64623b..6ba6617 100644
--- a/proxmox-auto-install-assistant/src/main.rs
+++ b/proxmox-auto-install-assistant/src/main.rs
@@ -1,25 +1,31 @@
-use anyhow::{Result, bail, format_err};
-use clap::{Args, Parser, Subcommand, ValueEnum};
+//! This tool can be used to prepare a Proxmox installation ISO for automated installations.
+//! Additional uses are to validate the format of an answer file or to test match filters and print
+//! information on the properties to match against for the current hardware.
+
+#![forbid(unsafe_code)]
+
+use anyhow::{Context, Result, bail, format_err};
 use glob::Pattern;
-use serde::Serialize;
 use std::{
     collections::BTreeMap,
     fmt, fs,
     io::{self, Read},
     path::{Path, PathBuf},
-    process::{Command, Stdio},
+    process::{self, Command, Stdio},
+    str::FromStr,
 };
 
 use proxmox_auto_installer::{
     answer::{Answer, FilterMatch},
     sysinfo::SysInfo,
     utils::{
-        AutoInstSettings, FetchAnswerFrom, HttpOptions, get_matched_udev_indexes, get_nic_list,
-        get_single_udev_index, verify_email_and_root_password_settings, verify_first_boot_settings,
+        AutoInstSettings, FetchAnswerFrom, HttpOptions, default_partition_label,
+        get_matched_udev_indexes, get_nic_list, get_single_udev_index,
+        verify_email_and_root_password_settings, verify_first_boot_settings,
         verify_locale_settings,
     },
 };
-use proxmox_installer_common::{FIRST_BOOT_EXEC_MAX_SIZE, FIRST_BOOT_EXEC_NAME};
+use proxmox_installer_common::{FIRST_BOOT_EXEC_MAX_SIZE, FIRST_BOOT_EXEC_NAME, cli};
 
 static PROXMOX_ISO_FLAG: &str = "/auto-installer-capable";
 
@@ -27,225 +33,439 @@ static PROXMOX_ISO_FLAG: &str = "/auto-installer-capable";
 /// [LocaleInfo](`proxmox_installer_common::setup::LocaleInfo`) struct.
 const LOCALE_INFO: &str = include_str!("../../locale-info.json");
 
-/// This tool can be used to prepare a Proxmox installation ISO for automated installations.
-/// Additional uses are to validate the format of an answer file or to test match filters and
-/// print information on the properties to match against for the current hardware.
-#[derive(Parser, Debug)]
-#[command(author, version, about, long_about = None)]
-struct Cli {
-    #[command(subcommand)]
-    command: Commands,
+/// Arguments for the `device-info` command.
+struct CommandDeviceInfoArgs {
+    /// Device type for which information should be shown.
+    device_type: AllDeviceTypes,
 }
 
-#[derive(Subcommand, Debug)]
-enum Commands {
-    PrepareIso(CommandPrepareISO),
-    ValidateAnswer(CommandValidateAnswer),
-    DeviceMatch(CommandDeviceMatch),
-    DeviceInfo(CommandDeviceInfo),
-    SystemInfo(CommandSystemInfo),
+impl cli::Subcommand for CommandDeviceInfoArgs {
+    fn parse(args: &mut cli::Arguments) -> Result<Self> {
+        Ok(Self {
+            device_type: args
+                .opt_value_from_str(["-t", "--type"])?
+                .unwrap_or(AllDeviceTypes::All),
+        })
+    }
+
+    fn print_usage() {
+        eprintln!(
+            r#"Show device information that can be used for filters.
+
+USAGE:
+  {} device-info [OPTIONS]
+
+OPTIONS:
+  -t, --type <type>  For which device type information should be shown [default: all] [possible values: all, network, disk]
+  -h, --help         Print this help
+  -V, --version      Print version
+    "#,
+            env!("CARGO_PKG_NAME")
+        );
+    }
+
+    fn run(&self) -> Result<()> {
+        info(self)
+    }
 }
 
-/// Show device information that can be used for filters
-#[derive(Args, Debug)]
-struct CommandDeviceInfo {
-    /// For which device type information should be shown
-    #[arg(name="type", short, long, value_enum, default_value_t=AllDeviceTypes::All)]
-    device: AllDeviceTypes,
-}
-
-/// Test which devices the given filter matches against
-///
-/// Filters support the following syntax:
-/// - `?`               Match a single character
-/// - `*`               Match any number of characters
-/// - `[a]`, `[0-9]`  Specific character or range of characters
-/// - `[!a]`          Negate a specific character of range
-///
-/// To avoid globbing characters being interpreted by the shell, use single quotes.
-/// Multiple filters can be defined.
-///
-/// Examples:
-/// Match disks against the serial number and device name, both must match:
-///
-/// ```sh
-/// proxmox-auto-install-assistant match --filter-match all disk 'ID_SERIAL_SHORT=*2222*' 'DEVNAME=*nvme*'
-/// ```
-#[derive(Args, Debug)]
-#[command(verbatim_doc_comment)]
-struct CommandDeviceMatch {
-    /// Device type to match the filter against
-    r#type: Devicetype,
+/// Arguments for the `device-match` command.
+struct CommandDeviceMatchArgs {
+    /// Device type to match the filter against.
+    device_type: DeviceType,
 
     /// Filter in the format KEY=VALUE where the key is the UDEV key and VALUE the filter string.
     /// Multiple filters are possible, separated by a space.
     filter: Vec<String>,
 
     /// Defines if any filter or all filters must match.
-    #[arg(long, value_enum, default_value_t=FilterMatch::Any)]
     filter_match: FilterMatch,
 }
 
-/// Validate if an answer file is formatted correctly.
-#[derive(Args, Debug)]
-struct CommandValidateAnswer {
-    /// Path to the answer file
+impl cli::Subcommand for CommandDeviceMatchArgs {
+    fn parse(args: &mut cli::Arguments) -> Result<Self> {
+        let filter_match = args
+            .opt_value_from_str("--filter-match")?
+            .unwrap_or(FilterMatch::Any);
+
+        let device_type = args.free_from_str().context("parsing device type")?;
+        let mut filter = vec![];
+        while let Some(s) = args.opt_free_from_str()? {
+            filter.push(s);
+        }
+
+        Ok(Self {
+            device_type,
+            filter,
+            filter_match,
+        })
+    }
+
+    fn print_usage() {
+        eprintln!(
+            r#"Test which devices the given filter matches against.
+
+Filters support the following syntax:
+- `?`               Match a single character
+- `*`               Match any number of characters
+- `[a]`, `[0-9]`  Specific character or range of characters
+- `[!a]`          Negate a specific character of range
+
+To avoid globbing characters being interpreted by the shell, use single quotes.
+Multiple filters can be defined.
+
+Examples:
+Match disks against the serial number and device name, both must match:
+
+$ proxmox-auto-install-assistant match --filter-match all disk 'ID_SERIAL_SHORT=*2222*' 'DEVNAME=*nvme*'
+
+USAGE:
+  {} device-match [OPTIONS] <TYPE> [FILTER]...
+
+ARGUMENTS:
+  <TYPE>
+          Device type to match the filter against
+
+          [possible values: network, disk]
+
+  [FILTER]...
+          Filter in the format KEY=VALUE where the key is the UDEV key and VALUE the filter string. Multiple filters are possible, separated by a space.
+
+OPTIONS:
+      --filter-match <FILTER_MATCH>
+          Defines if any filter or all filters must match [default: any] [possible values: any, all]
+
+  -h, --help         Print this help
+  -V, --version      Print version
+    "#,
+            env!("CARGO_PKG_NAME")
+        );
+    }
+
+    fn run(&self) -> Result<()> {
+        match_filter(self)
+    }
+}
+
+/// Arguments for the `validate-answer` command.
+struct CommandValidateAnswerArgs {
+    /// Path to the answer file.
     path: PathBuf,
-    #[arg(short, long, default_value_t = false)]
+    /// Whether to also show the full answer as parsed.
     debug: bool,
 }
 
-/// Prepare an ISO for automated installation.
-///
-/// The behavior of how to fetch an answer file must be set with the '--fetch-from' parameter. The
-/// answer file can be:{n}
-/// * integrated into the ISO itself ('iso'){n}
-/// * present on a partition / file-system, matched by its label ('partition'){n}
-/// * requested via an HTTP Post request ('http').
-///
-/// The URL for the HTTP mode can be defined for the ISO with the '--url' argument. If not present,
-/// it will try to get a URL from a DHCP option (250, TXT) or by querying a DNS TXT record for the
-/// domain 'proxmox-auto-installer.{search domain}'.
-///
-/// The TLS certificate fingerprint can either be defined via the '--cert-fingerprint' argument or
-/// alternatively via the custom DHCP option (251, TXT) or in a DNS TXT record located at
-/// 'proxmox-auto-installer-cert-fingerprint.{search domain}'.
-///
-/// The latter options to provide the TLS fingerprint will only be used if the same method was used
-/// to retrieve the URL. For example, the DNS TXT record for the fingerprint will only be used, if
-/// no one was configured with the '--cert-fingerprint' parameter and if the URL was retrieved via
-/// the DNS TXT record.
-///
-/// If the 'partition' mode is used, the '--partition-label' parameter can be used to set the
-/// partition label the auto-installer should search for. This defaults to 'proxmox-ais'.
-#[derive(Args, Debug)]
-struct CommandPrepareISO {
-    /// Path to the source ISO to prepare
+impl cli::Subcommand for CommandValidateAnswerArgs {
+    fn parse(args: &mut cli::Arguments) -> Result<Self> {
+        Ok(Self {
+            debug: args.contains(["-d", "--debug"]),
+            // Needs to be last
+            path: args.free_from_str()?,
+        })
+    }
+
+    fn print_usage() {
+        eprintln!(
+            r#"Validate if an answer file is formatted correctly.
+
+USAGE:
+  {} validate-answer [OPTIONS] <PATH>
+
+ARGUMENTS:
+  <PATH>  Path to the answer file.
+
+OPTIONS:
+  -d, --debug        Also show the full answer as parsed.
+  -h, --help         Print this help
+  -V, --version      Print version
+    "#,
+            env!("CARGO_PKG_NAME")
+        );
+    }
+
+    fn run(&self) -> Result<()> {
+        validate_answer(self)
+    }
+}
+
+/// Arguments for the `prepare-iso` command.
+struct CommandPrepareISOArgs {
+    /// Path to the source ISO to prepare.
     input: PathBuf,
 
     /// Path to store the final ISO to, defaults to an auto-generated file name depending on mode
     /// and the same directory as the source file is located in.
-    #[arg(long)]
     output: Option<PathBuf>,
 
     /// Where the automatic installer should fetch the answer file from.
-    #[arg(long, value_enum)]
     fetch_from: FetchAnswerFrom,
 
     /// Include the specified answer file in the ISO. Requires the '--fetch-from'  parameter
     /// to be set to 'iso'.
-    #[arg(long)]
     answer_file: Option<PathBuf>,
 
-    /// Specify URL for fetching the answer file via HTTP
-    #[arg(long)]
+    /// Specify URL for fetching the answer file via HTTP.
     url: Option<String>,
 
     /// Pin the ISO to the specified SHA256 TLS certificate fingerprint.
-    #[arg(long)]
     cert_fingerprint: Option<String>,
 
     /// Staging directory to use for preparing the new ISO file. Defaults to the directory of the
     /// input ISO file.
-    #[arg(long)]
     tmp: Option<String>,
 
     /// Can be used in combination with `--fetch-from partition` to set the partition label
     /// the auto-installer will search for.
     // FAT can only handle 11 characters (per specification at least, drivers might allow more),
     // so shorten "Automated Installer Source" to "AIS" to be safe.
-    #[arg(long, default_value_t = { "proxmox-ais".to_owned() } )]
     partition_label: String,
 
     /// Executable file to include, which should be run on the first system boot after the
     /// installation. Can be used for further bootstrapping the new system.
     ///
     /// Must be appropriately enabled in the answer file.
-    #[arg(long)]
     on_first_boot: Option<PathBuf>,
 }
 
-/// Show the system information that can be used to identify a host.
-///
-/// The shown information is sent as POST HTTP request when fetching the answer file for the
-/// automatic installation through HTTP, You can, for example, use this to return a dynamically
-/// assembled answer file.
-#[derive(Args, Debug)]
-struct CommandSystemInfo {}
+impl cli::Subcommand for CommandPrepareISOArgs {
+    fn parse(args: &mut cli::Arguments) -> Result<Self> {
+        Ok(Self {
+            output: args.opt_value_from_str("--output")?,
+            fetch_from: args.value_from_str("--fetch-from")?,
+            answer_file: args.opt_value_from_str("--answer-file")?,
+            url: args.opt_value_from_str("--url")?,
+            cert_fingerprint: args.opt_value_from_str("--cert-fingerprint")?,
+            tmp: args.opt_value_from_str("--tmp")?,
+            partition_label: args
+                .opt_value_from_str("--partition-label")?
+                .unwrap_or_else(default_partition_label),
+            on_first_boot: args.opt_value_from_str("--on-first-boot")?,
+            // Needs to be last
+            input: args.free_from_str()?,
+        })
+    }
 
-#[derive(Args, Debug)]
-struct GlobalOpts {
-    /// Output format
-    #[arg(long, short, value_enum)]
-    format: OutputFormat,
+    fn print_usage() {
+        eprintln!(
+            r#"Prepare an ISO for automated installation.
+
+The behavior of how to fetch an answer file must be set with the '--fetch-from' parameter.
+The answer file can be:
+ * integrated into the ISO itself ('iso')
+ * present on a partition / file-system, matched by its label ('partition')
+ * requested via an HTTP Post request ('http').
+
+The URL for the HTTP mode can be defined for the ISO with the '--url' argument. If not present, it
+will try to get a URL from a DHCP option (250, TXT) or by querying a DNS TXT record for the domain
+'proxmox-auto-installer.{{search domain}}'.
+
+The TLS certificate fingerprint can either be defined via the '--cert-fingerprint' argument or
+alternatively via the custom DHCP option (251, TXT) or in a DNS TXT record located at
+'proxmox-auto-installer-cert-fingerprint.{{search domain}}'.
+
+The latter options to provide the TLS fingerprint will only be used if the same method was used to
+retrieve the URL. For example, the DNS TXT record for the fingerprint will only be used, if no one
+was configured with the '--cert-fingerprint' parameter and if the URL was retrieved via the DNS TXT
+record.
+
+If the 'partition' mode is used, the '--partition-label' parameter can be used to set the partition
+label the auto-installer should search for. This defaults to 'proxmox-ais'.
+
+USAGE:
+  {} prepare-iso [OPTIONS] --fetch-from <FETCH_FROM> <INPUT>
+
+ARGUMENTS:
+  <INPUT>
+          Path to the source ISO to prepare
+
+OPTIONS:
+      --output <OUTPUT>
+          Path to store the final ISO to, defaults to an auto-generated file name depending on mode
+          and the same directory as the source file is located in.
+
+      --fetch-from <FETCH_FROM>
+          Where the automatic installer should fetch the answer file from.
+
+          [possible values: iso, http, partition]
+
+      --answer-file <ANSWER_FILE>
+          Include the specified answer file in the ISO. Requires the '--fetch-from' parameter to
+          be set to 'iso'.
+
+      --url <URL>
+          Specify URL for fetching the answer file via HTTP.
+
+      --cert-fingerprint <CERT_FINGERPRINT>
+          Pin the ISO to the specified SHA256 TLS certificate fingerprint.
+
+      --tmp <TMP>
+          Staging directory to use for preparing the new ISO file. Defaults to the directory of the
+          input ISO file.
+
+      --partition-label <PARTITION_LABEL>
+          Can be used in combination with `--fetch-from partition` to set the partition label the
+          auto-installer will search for.
+
+          [default: proxmox-ais]
+
+      --on-first-boot <ON_FIRST_BOOT>
+          Executable file to include, which should be run on the first system boot after the
+          installation. Can be used for further bootstrapping the new system.
+
+          Must be appropriately enabled in the answer file.
+
+  -h, --help         Print this help
+  -V, --version      Print version
+    "#,
+            env!("CARGO_PKG_NAME")
+        );
+    }
+
+    fn run(&self) -> Result<()> {
+        prepare_iso(self)
+    }
 }
 
-#[derive(Clone, Debug, ValueEnum, PartialEq)]
+/// Arguments for the `system-info` command.
+struct CommandSystemInfoArgs;
+
+impl cli::Subcommand for CommandSystemInfoArgs {
+    fn parse(_: &mut cli::Arguments) -> Result<Self> {
+        Ok(Self)
+    }
+
+    fn print_usage() {
+        eprintln!(
+            r#"Show the system information that can be used to identify a host.
+
+The shown information is sent as POST HTTP request when fetching the answer file for the
+automatic installation through HTTP, You can, for example, use this to return a dynamically
+assembled answer file.
+
+USAGE:
+  {} system-info [OPTIONS]
+
+OPTIONS:
+  -h, --help         Print this help
+  -V, --version      Print version
+    "#,
+            env!("CARGO_PKG_NAME")
+        );
+    }
+
+    fn run(&self) -> Result<()> {
+        show_system_info(self)
+    }
+}
+
+#[derive(PartialEq)]
 enum AllDeviceTypes {
     All,
     Network,
     Disk,
 }
 
-#[derive(Clone, Debug, ValueEnum)]
-enum Devicetype {
+impl FromStr for AllDeviceTypes {
+    type Err = anyhow::Error;
+
+    fn from_str(s: &str) -> Result<Self> {
+        match s.to_lowercase().as_ref() {
+            "all" => Ok(AllDeviceTypes::All),
+            "network" => Ok(AllDeviceTypes::Network),
+            "disk" => Ok(AllDeviceTypes::Disk),
+            _ => bail!("unknown device type '{s}'"),
+        }
+    }
+}
+
+enum DeviceType {
     Network,
     Disk,
 }
 
-#[derive(Clone, Debug, ValueEnum)]
-enum OutputFormat {
-    Pretty,
-    Json,
-}
+impl FromStr for DeviceType {
+    type Err = anyhow::Error;
 
-#[derive(Serialize)]
-struct Devs {
-    disks: Option<BTreeMap<String, BTreeMap<String, String>>>,
-    nics: Option<BTreeMap<String, BTreeMap<String, String>>>,
-}
-
-fn main() {
-    let args = Cli::parse();
-    let res = match &args.command {
-        Commands::PrepareIso(args) => prepare_iso(args),
-        Commands::ValidateAnswer(args) => validate_answer(args),
-        Commands::DeviceInfo(args) => info(args),
-        Commands::DeviceMatch(args) => match_filter(args),
-        Commands::SystemInfo(args) => show_system_info(args),
-    };
-    if let Err(err) = res {
-        eprintln!("Error: {err:?}");
-        std::process::exit(1);
+    fn from_str(s: &str) -> Result<Self> {
+        match s.to_lowercase().as_ref() {
+            "network" => Ok(DeviceType::Network),
+            "disk" => Ok(DeviceType::Disk),
+            _ => bail!("unknown device type '{s}'"),
+        }
     }
 }
 
-fn info(args: &CommandDeviceInfo) -> Result<()> {
-    let mut devs = Devs {
-        disks: None,
-        nics: None,
-    };
+fn main() -> process::ExitCode {
+    cli::run(cli::AppInfo {
+        global_help: &format!(
+            r#"This tool can be used to prepare a Proxmox installation ISO for automated installations.
+Additional uses are to validate the format of an answer file or to test match filters and
+print information on the properties to match against for the current hardware
 
-    if args.device == AllDeviceTypes::Network || args.device == AllDeviceTypes::All {
+USAGE:
+  {} <COMMAND>
+
+COMMANDS:
+  prepare-iso      Prepare an ISO for automated installation
+  validate-answer  Validate if an answer file is formatted correctly
+  device-match     Test which devices the given filter matches against
+  device-info      Show device information that can be used for filters
+  system-info      Show the system information that can be used to identify a host
+
+GLOBAL OPTIONS:
+  -h, --help       Print help
+  -V, --version    Print version
+"#,
+            env!("CARGO_PKG_NAME")
+        ),
+        on_command: |s, args| match s {
+            Some("prepare-iso") => cli::handle_command::<CommandPrepareISOArgs>(args),
+            Some("validate-answer") => cli::handle_command::<CommandValidateAnswerArgs>(args),
+            Some("device-match") => cli::handle_command::<CommandDeviceMatchArgs>(args),
+            Some("device-info") => cli::handle_command::<CommandDeviceInfoArgs>(args),
+            Some("system-info") => cli::handle_command::<CommandSystemInfoArgs>(args),
+            Some(s) => bail!("unknown subcommand '{s}'"),
+            None => bail!("subcommand required"),
+        },
+    })
+}
+
+fn info(args: &CommandDeviceInfoArgs) -> Result<()> {
+    let nics = if matches!(
+        args.device_type,
+        AllDeviceTypes::All | AllDeviceTypes::Network
+    ) {
         match get_nics() {
-            Ok(res) => devs.nics = Some(res),
+            Ok(res) => Some(res),
             Err(err) => bail!("Error getting NIC data: {err}"),
         }
-    }
-    if args.device == AllDeviceTypes::Disk || args.device == AllDeviceTypes::All {
+    } else {
+        None
+    };
+
+    let disks = if matches!(args.device_type, AllDeviceTypes::All | AllDeviceTypes::Disk) {
         match get_disks() {
-            Ok(res) => devs.disks = Some(res),
+            Ok(res) => Some(res),
             Err(err) => bail!("Error getting disk data: {err}"),
         }
-    }
-    println!("{}", serde_json::to_string_pretty(&devs).unwrap());
+    } else {
+        None
+    };
+
+    serde_json::to_writer_pretty(
+        std::io::stdout(),
+        &serde_json::json!({
+            "disks": disks,
+            "nics": nics,
+        }),
+    )?;
     Ok(())
 }
 
-fn match_filter(args: &CommandDeviceMatch) -> Result<()> {
-    let devs: BTreeMap<String, BTreeMap<String, String>> = match args.r#type {
-        Devicetype::Disk => get_disks().unwrap(),
-        Devicetype::Network => get_nics().unwrap(),
+fn match_filter(args: &CommandDeviceMatchArgs) -> Result<()> {
+    let devs: BTreeMap<String, BTreeMap<String, String>> = match args.device_type {
+        DeviceType::Disk => get_disks().unwrap(),
+        DeviceType::Network => get_nics().unwrap(),
     };
     // parse filters
 
@@ -266,21 +486,21 @@ fn match_filter(args: &CommandDeviceMatch) -> Result<()> {
     }
 
     // align return values
-    let result = match args.r#type {
-        Devicetype::Disk => {
+    let result = match args.device_type {
+        DeviceType::Disk => {
             get_matched_udev_indexes(&filters, &devs, args.filter_match == FilterMatch::All)
         }
-        Devicetype::Network => get_single_udev_index(&filters, &devs).map(|r| vec![r]),
+        DeviceType::Network => get_single_udev_index(&filters, &devs).map(|r| vec![r]),
     };
 
     match result {
-        Ok(result) => println!("{}", serde_json::to_string_pretty(&result).unwrap()),
+        Ok(result) => serde_json::to_writer_pretty(std::io::stdout(), &result)?,
         Err(err) => bail!("Error matching filters: {err}"),
     }
     Ok(())
 }
 
-fn validate_answer(args: &CommandValidateAnswer) -> Result<()> {
+fn validate_answer(args: &CommandValidateAnswerArgs) -> Result<()> {
     let answer = parse_answer(&args.path)?;
     if args.debug {
         println!("Parsed data from answer file:\n{:#?}", answer);
@@ -288,7 +508,7 @@ fn validate_answer(args: &CommandValidateAnswer) -> Result<()> {
     Ok(())
 }
 
-fn show_system_info(_args: &CommandSystemInfo) -> Result<()> {
+fn show_system_info(_args: &CommandSystemInfoArgs) -> Result<()> {
     match SysInfo::as_json_pretty() {
         Ok(res) => println!("{res}"),
         Err(err) => eprintln!("Error fetching system info: {err}"),
@@ -296,7 +516,7 @@ fn show_system_info(_args: &CommandSystemInfo) -> Result<()> {
     Ok(())
 }
 
-fn prepare_iso(args: &CommandPrepareISO) -> Result<()> {
+fn prepare_iso(args: &CommandPrepareISOArgs) -> Result<()> {
     check_prepare_requirements(args)?;
     let uuid = get_iso_uuid(&args.input)?;
 
@@ -393,7 +613,7 @@ fn prepare_iso(args: &CommandPrepareISO) -> Result<()> {
     Ok(())
 }
 
-fn final_iso_location(args: &CommandPrepareISO) -> PathBuf {
+fn final_iso_location(args: &CommandPrepareISOArgs) -> PathBuf {
     if let Some(specified) = args.output.clone() {
         return specified;
     }
@@ -606,11 +826,11 @@ fn parse_answer(path: impl AsRef<Path> + fmt::Debug) -> Result<Answer> {
     }
 }
 
-fn check_prepare_requirements(args: &CommandPrepareISO) -> Result<()> {
+fn check_prepare_requirements(args: &CommandPrepareISOArgs) -> Result<()> {
     match Path::try_exists(&args.input) {
         Ok(true) => (),
-        Ok(false) => bail!("Source file does not exist."),
-        Err(_) => bail!("Source file does not exist."),
+        Ok(false) => bail!("Source file {:?} does not exist.", args.input),
+        Err(err) => bail!("Failed to stat source file {:?}: {err:#}", args.input),
     }
 
     match Command::new("xorriso")
diff --git a/proxmox-auto-installer/Cargo.toml b/proxmox-auto-installer/Cargo.toml
index 221d5d2..8a5283e 100644
--- a/proxmox-auto-installer/Cargo.toml
+++ b/proxmox-auto-installer/Cargo.toml
@@ -19,7 +19,6 @@ serde_json.workspace = true
 serde_plain.workspace = true
 toml.workspace = true
 
-clap = { version = "4.0", features = ["derive"] }
 glob = "0.3"
 
 [dev-dependencies]
diff --git a/proxmox-auto-installer/src/answer.rs b/proxmox-auto-installer/src/answer.rs
index 34065e8..eb1d829 100644
--- a/proxmox-auto-installer/src/answer.rs
+++ b/proxmox-auto-installer/src/answer.rs
@@ -1,5 +1,4 @@
 use anyhow::{Result, format_err};
-use clap::ValueEnum;
 use proxmox_installer_common::{
     options::{
         BtrfsCompressOption, BtrfsRaidLevel, FsType, ZfsChecksumOption, ZfsCompressOption,
@@ -364,13 +363,16 @@ pub enum DiskSelection {
     Selection(Vec<String>),
     Filter(BTreeMap<String, String>),
 }
-#[derive(Clone, Deserialize, Debug, PartialEq, ValueEnum)]
+
+#[derive(Clone, Deserialize, Debug, PartialEq)]
 #[serde(rename_all = "lowercase", deny_unknown_fields)]
 pub enum FilterMatch {
     Any,
     All,
 }
 
+serde_plain::derive_fromstr_from_deserialize!(FilterMatch);
+
 #[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
 #[serde(rename_all = "lowercase", deny_unknown_fields)]
 pub enum Filesystem {
diff --git a/proxmox-auto-installer/src/utils.rs b/proxmox-auto-installer/src/utils.rs
index 365e01a..e049748 100644
--- a/proxmox-auto-installer/src/utils.rs
+++ b/proxmox-auto-installer/src/utils.rs
@@ -1,5 +1,4 @@
 use anyhow::{Context, Result, bail};
-use clap::ValueEnum;
 use glob::Pattern;
 use log::info;
 use std::{collections::BTreeMap, process::Command};
@@ -95,7 +94,7 @@ pub fn get_single_udev_index(
     Ok(dev_index.unwrap())
 }
 
-#[derive(Deserialize, Serialize, Debug, Clone, ValueEnum, PartialEq)]
+#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
 #[serde(rename_all = "lowercase", deny_unknown_fields)]
 pub enum FetchAnswerFrom {
     Iso,
@@ -103,6 +102,8 @@ pub enum FetchAnswerFrom {
     Partition,
 }
 
+serde_plain::derive_fromstr_from_deserialize!(FetchAnswerFrom);
+
 #[derive(Deserialize, Serialize, Clone, Default, PartialEq, Debug)]
 pub struct HttpOptions {
     #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -121,7 +122,7 @@ pub struct AutoInstSettings {
     pub http: HttpOptions,
 }
 
-fn default_partition_label() -> String {
+pub fn default_partition_label() -> String {
     "proxmox-ais".to_owned()
 }
 
-- 
2.49.0





More information about the pve-devel mailing list