使用clap编写自己的命令行

rust_snippets

clap::_features

https://docs.rs/clap/latest/clap/_features/index.html

clap::_derive::_tutorial ( Derive Tutorial )

https://docs.rs/clap/latest/clap/_derive/_tutorial/index.html

添加derive feature


cargo add clap --features derive

./target/debug/clap_basic.exe --help

配置Parser

#[derive(Parser)]
#[command(name = "MyApp")]
#[command(author = "Kevin K. <kbknapp@gmail.com>")]
#[command(version = "1.0")]
#[command(about = "Does awesome things", long_about = None)]
struct Cli {
    #[arg(long)]
    two: String,
    #[arg(long)]
    one: String,
}
Usage: 02_apps_derive[EXE] --two <TWO> --one <ONE>

Options:
      --two <TWO>  
      --one <ONE>  
  -h, --help       Print help information
  -V, --version    Print version information
#[derive(Parser)]
#[command(author, version, about, long_about = None)] // Read from `Cargo.toml`
struct Cli {
    #[arg(long)]
    two: String,
    #[arg(long)]
    one: String,
}
Usage: 02_crate_derive[EXE] --two <TWO> --one <ONE>

Options:
      --two <TWO>  
      --one <ONE>  
  -h, --help       Print help information
  -V, --version    Print version information

添加参数

上一节给出的方法,需要用户键入’–one –two’来指定某个参数的值

传统的命令行也有通过键入参数的顺序来获取参数的方式,即是省略了’–one –two’

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    // 注意: 这里没有 '#[arg(long)]'
    name: Option<String>,
}
Usage: 03_03_positional_derive[EXE] [NAME]

Arguments:
  [NAME]  

参数的默认动作ArgAction是Set,要接受多值操作,使用Append

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    name: Vec<String>,
}
Usage: 03_03_positional_mult_derive[EXE] [NAME]...

Arguments:
  [NAME]...  

Options:
  -h, --help     Print help information
  -V, --version  Print version information

$ 03_03_positional_mult_derive
name: []

$ 03_03_positional_mult_derive bob
name: ["bob"]

Options

可以指定简写模式

 #[arg(short = 'n')] and #[arg(long = "name")]
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    #[arg(short, long)]
    name: Option<String>,
}
$ 03_02_option_derive
name: None

$ 03_02_option_derive --name bob
name: Some("bob")

$ 03_02_option_derive --name=bob
name: Some("bob")

$ 03_02_option_derive -n bob
name: Some("bob")

$ 03_02_option_derive -n=bob
name: Some("bob")

$ 03_02_option_derive -nbob
name: Some("bob")

Flags

标志也可以是可以开/关的开关。这是通过 #[arg(action = ArgAction::SetTrue)] 属性启用的 ArgAction默认是SetTrue。要接受多个标志,请使用 Count

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    #[arg(short, long, action = clap::ArgAction::Count)]
    verbose: u8,
}
Usage: 03_01_flag_count_derive[EXE] [OPTIONS]

Options:
  -v, --verbose...  
  -h, --help        Print help information
  -V, --version     Print version information

$ 03_01_flag_count_derive
verbose: 0

$ 03_01_flag_count_derive --verbose
verbose: 1

$ 03_01_flag_count_derive --verbose --verbose
verbose: 2

子命令

子命令通过#[derive(Subcommand)] 派生并通过#[command(subcommand)] 属性添加。

use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Adds files to myapp
    Add { name: Option<String> },
}

use clap::{Args, Parser, Subcommand};

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Adds files to myapp
    Add(Add),
}

#[derive(Args)]
struct Add {
    name: Option<String>,
}

fn main() {
    let cli = Cli::parse();

    // You can check for the existence of subcommands, and if found use their
    // matches just as you would the top level cmd
    match &cli.command {
        Commands::Add(name) => {
            println!("'myapp add' was used, name is: {:?}", name.name)
        }
    }
}

参数的默认值

use clap::Parser;

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    #[arg(default_value_t = 2020)]
    port: u16,
}

fn main() {
    let cli = Cli::parse();

    println!("port: {:?}", cli.port);
}
Usage: 03_05_default_values_derive[EXE] [PORT]

Arguments:
  [PORT]  [default: 2020]

Options:
  -h, --help     Print help information
  -V, --version  Print version information

$ 03_05_default_values_derive
port: 2020

参数的Validation

https://docs.rs/clap/latest/clap/macro.value_parser.html

枚举值

如果您有要测试的特定值的参数,则可以派生 ValueEnum。 允许您指定该参数的有效值

use clap::{Parser, ValueEnum};

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// What mode to run the program in
    #[arg(value_enum)]
    mode: Mode,
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum Mode {
    /// Run swiftly
    Fast,
    /// Crawl slowly but steadily
    ///
    /// This paragraph is ignored because there is no long help text for possible values.
    Slow,
}
$ 04_01_enum_derive fast
Hare

$ 04_01_enum_derive slow
Tortoise

$ 04_01_enum_derive medium
? failed
error: 'medium' isn't a valid value for '<MODE>'
  [possible values: fast, slow]

验证值的有效性

验证并解析为任何数据类型

use clap::Parser;

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// Network port to use
    #[arg(value_parser = clap::value_parser!(u16).range(1..))]
    port: u16,
}
$ 04_02_parse_derive 22
PORT = 22

$ 04_02_parse_derive foobar
? failed
error: Invalid value 'foobar' for '<PORT>': invalid digit found in string

For more information try '--help'

$ 04_02_parse_derive 0
? failed
error: Invalid value '0' for '<PORT>': 0 is not in 1..=65535

自定义Parse验证有效性函数

use std::ops::RangeInclusive;

use clap::Parser;

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// Network port to use
    #[arg(value_parser = port_in_range)] // port_in_range 就是自定义验证函数
    port: u16,
}

fn main() {
    let cli = Cli::parse();

    println!("PORT = {}", cli.port);
}

const PORT_RANGE: RangeInclusive<usize> = 1..=65535;

// 自定义验证函数
fn port_in_range(s: &str) -> Result<u16, String> {
    let port: usize = s
        .parse()
        .map_err(|_| format!("`{}` isn't a port number", s))?;
    if PORT_RANGE.contains(&port) {
        Ok(port as u16)
    } else {
        Err(format!(
            "Port not in range {}-{}",
            PORT_RANGE.start(),
            PORT_RANGE.end()
        ))
    }
}

验证参数间的关系

可以声明 Arg 甚至 ArgGroup 之间的依赖关系或冲突 假设有多个参数,并且您希望其中一个是必需的,但是将所有参数都设为必需,是不可行的,因为它们可能相互冲突

ArgGroups : https://docs.rs/clap/latest/clap/builder/struct.ArgGroup.html 最常见的用途是要求一个且只有一个参数出现在给定的参数集合之中。

use clap::{ArgGroup, Parser};

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(group(
ArgGroup::new("vers") // derive种设置group
.required(true)
.args(["set_ver", "major", "minor", "patch"]), // Option: 四个参数只能出现一个
))]
struct Cli {
    /// set version manually
    #[arg(long, value_name = "VER")]
    set_ver: Option<String>,

    /// auto inc major
    #[arg(long)]
    major: bool,

    /// auto inc minor
    #[arg(long)]
    minor: bool,

    /// auto inc patch
    #[arg(long)]
    patch: bool,

    /// some regular input
    #[arg(group = "input")] //'input' group 参数
    input_file: Option<String>,

    /// some special input argument
    #[arg(long, group = "input")] //'input' group 参数
    spec_in: Option<String>,

    #[arg(short, requires = "input")]  // 'requires' 指定当该组存在时必须存在的参数或组
    config: Option<String>,
}
$ ./target/debug/clap_basic.exe --set-ver 2.1.1
Version: 2.1.1

$ ./target/debug/clap_basic.exe --set-ver 2.1.1 --major 5
error: The argument '--set-ver <VER>' cannot be used with '--major'

$ ./target/debug/clap_basic.exe --major
Version: 2.2.3

$ ./target/debug/clap_basic.exe --major input
Version: 2.2.3

$ ./target/debug/clap_basic.exe input
error: The following required arguments were not provided:
  <--set-ver <VER>|--major|--minor|--patch>

$ ./target/debug/clap_basic.exe --major input -c config.toml --spec-in input.txt
error: The argument '[INPUT_FILE]' cannot be used with '--spec-in <SPEC_IN>'

$ ./target/debug/clap_basic.exe --major input -c config.toml
Version: 2.2.3
Doing work using input input and config config.toml

$ ./target/debug/clap_basic.exe --major input --spec-in input.txt
error: The argument '[INPUT_FILE]' cannot be used with '--spec-in <SPEC_IN>'

$ ./target/debug/clap_basic.exe --major -c config.toml --spec-in input.txt
Version: 2.2.3
Doing work using input input.txt and config config.toml

$ ./target/debug/clap_basic.exe --major --spec-in input.txt
Version: 2.2.3

$ ./target/debug/clap_basic.exe --major -c config.toml
error: The following required arguments were not provided:
  <INPUT_FILE|--spec-in <SPEC_IN>>

$ ./target/debug/clap_basic.exe --major input -c config.toml
Version: 2.2.3
Doing work using input input and config config.toml

用户自定义验证

https://docs.rs/clap/latest/clap/_derive/_tutorial/index.html#custom-validation

测试

clap 将大多数开发错误报告为 debug_assert!s。您应该有一个调用 Command::debug_assert 的测试,而不是检查每个子命令

use clap::Parser;

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// Network port to use
    port: u16,
}

fn main() {
    let cli = Cli::parse();

    println!("PORT = {}", cli.port);
}

#[test]
fn verify_cli() {
    use clap::CommandFactory;
    Cli::command().debug_assert()
}

clap::_tutorial ( Builder Tutorial )

https://docs.rs/clap/latest/clap/_tutorial/index.html

可以使用使用字符串创建具有多个参数的应用程序。

添加cargo feature

cargo add clap --features derive

cargo build

快速开始

use std::path::PathBuf;

use clap::{arg, command, value_parser, ArgAction, Command};

pub fn builder_clap_test() {
    let matches = command!() // requires `cargo` feature
        // 位置参数
        .arg(arg!([name] "Optional name to operate on"))
        // --option参数
        .arg(
            arg!(
                -c --config <FILE> "Sets a custom config file"
            )
                // We don't have syntax yet for optional options, so manually calling `required`
                // https://docs.rs/clap/latest/clap/builder/struct.Arg.html#method.required
                // 指定参数是否必须存, 默认是必须的
                .required(false)
                .value_parser(value_parser!(PathBuf)),
        )
        .arg(arg!(
            -d --debug ... "Turn debugging information on"
        ))
        .subcommand(
            Command::new("test")
                .about("does testing things")
                .arg(arg!(-l --list "lists test values").action(ArgAction::SetTrue)),
                // https://docs.rs/clap/latest/clap/builder/enum.ArgAction.html
                // 解析时遇到参数时的行为
                // Set,
                // Append,
                // SetTrue,
                // SetFalse,
                // Count,
                // Help,
                // Version,
        )
        .get_matches();

    // You can check the value provided by positional arguments, or option arguments
    if let Some(name) = matches.get_one::<String>("name") { // get_one : Gets the value of a specific option or positional argument.
        println!("Value for name: {}", name);
    }

    if let Some(config_path) = matches.get_one::<PathBuf>("config") {
        println!("Value for config: {}", config_path.display());
    }

    // You can see how many times a particular flag or argument occurred
    // Note, only flags can have multiple occurrences
    match matches
        .get_one::<u8>("debug")
        .expect("Count's are defaulted")
    {
        0 => println!("Debug mode is off"),
        1 => println!("Debug mode is kind of on"),
        2 => println!("Debug mode is on"),
        _ => println!("Don't be crazy"),
    }

    // You can check for the existence of subcommands, and if found use their
    // matches just as you would the top level cmd
    if let Some(matches) = matches.subcommand_matches("test") {
        // "$ myapp test" was run
        if *matches.get_one::<bool>("list").expect("defaulted by clap") {
            // "$ myapp test -l" was run
            println!("Printing testing lists...");
        } else {
            println!("Not printing testing lists...");
        }
    }

    // Continued program logic goes here...
}

配置Parser

使用 Command 开始构建解析器

use clap::{arg, Command};

fn main() {
    let matches = Command::new("MyApp")
        .version("1.0")
        .author("Kevin K. <kbknapp@gmail.com>")
        .about("Does awesome things")
        .arg(arg!(--two <VALUE>).required(true))
        .arg(arg!(--one <VALUE>).required(true))
        .get_matches();
    // or 从 Cargo.toml 获取这些信息
    // let matches = command!()
    //     .arg(arg!(--two <VALUE>).required(true))
    //     .arg(arg!(--one <VALUE>).required(true))
    //     .get_matches();

    println!(
        "two: {:?}",
        matches.get_one::<String>("two").expect("required")
    );
    println!(
        "one: {:?}",
        matches.get_one::<String>("one").expect("required")
    );
}

添加参数

顺序位置类参数

use clap::{command, Arg};

fn main() {
    let matches = command!() // requires `cargo` feature
        .arg(Arg::new("name"))
        .get_matches();

    println!("name: {:?}", matches.get_one::<String>("name"));
}

注意: 默认的 ArgAction 是 [Set][crate::ArgAction::Set] 如果需要接受多个参数 使用 ArgAction::Append

se clap::{command, Arg, ArgAction};

fn main() {
    let matches = command!() // requires `cargo` feature
        .arg(Arg::new("name").action(ArgAction::Append))
        .get_matches();

    println!("name: {:?}", matches.get_one::<String>("name"));
}

Options类参数

use clap::{command, Arg};

fn main() {
    let matches = command!() // requires `cargo` feature
        .arg(Arg::new("name").short('n').long("name"))
        .get_matches();

    println!("name: {:?}", matches.get_one::<String>("name"));
}
// 也可以使用 宏 arg!
// arg!(-c --config <FILE> "Sets a custom config file")

Flags

标志也可以是可以打开/关闭的开关

use clap::{command, Arg, ArgAction};

fn main() {
    let matches = command!() // requires `cargo` feature
        .arg(
            Arg::new("verbose")
                .short('v')
                .long("verbose")
                .action(ArgAction::SetTrue), // Flags
        )
        .get_matches();

    println!("verbose: {:?}", matches.get_flag("verbose"));
}
$ 03_01_flag_bool
verbose: false

$ 03_01_flag_bool --verbose
verbose: true

$ 03_01_flag_bool --verbose --verbose
? failed
error: The argument '--verbose' was provided more than once, but cannot be used multiple times

要接受多个标志,请使用 Count

let matches = command!() // requires `cargo` feature
        .arg(
            Arg::new("verbose")
                .short('v')
                .long("verbose")
                .action(ArgAction::Count),
        )
        .get_matches();
$ 03_01_flag_count
verbose: 0

$ 03_01_flag_count --verbose
verbose: 1

$ 03_01_flag_count --verbose --verbose
verbose: 2

子命令

通过 Command::subcommand 添加的命令

use clap::{arg, command, Command};

fn main() {
    let matches = command!() // requires `cargo` feature
        .propagate_version(true) // 指定对所有子命令使用当前命令的版本。
        .subcommand_required(true) // 如果运行时不存在子命令,则出错并正常退出。
        .arg_required_else_help(true) // 如果不存在参数,则退出
        .subcommand(
            Command::new("add")
                .about("Adds files to myapp")
                .arg(arg!([NAME])),
        )
        .get_matches();

    match matches.subcommand() {
        Some(("add", sub_matches)) => println!(
            "'myapp add' was used, name is: {:?}",
            sub_matches.get_one::<String>("NAME")
        ),
        _ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"),
    }
}
$ 03_04_subcommands add bob
'myapp add' was used, name is: Some("bob")

默认值

use clap::{arg, command, value_parser};

fn main() {
    let matches = command!() // requires `cargo` feature
        .arg(
            arg!([PORT])
                .value_parser(value_parser!(u16))
                .default_value("2020"),
        )
        .get_matches();

    println!(
        "port: {:?}",
        matches
            .get_one::<u16>("PORT")
            .expect("default ensures there is always a value")
    );
}
$ 03_05_default_values
port: 2020

$ 03_05_default_values 22
port: 22

Validation

如果想要限定参数的内容,可以使用枚举参数值

use clap::{arg, command};

fn main() {
    let matches = command!() // requires `cargo` feature
        .arg(
            arg!(<MODE>)
                .help("What mode to run the program in")
                .value_parser(["fast", "slow"]), // 枚举参数值
        )
        .get_matches();

    // Note, it's safe to call unwrap() because the arg is required
    match matches
        .get_one::<String>("MODE")
        .expect("'MODE' is required and parsing will fail if its missing")
        .as_str()
    {
        "fast" => {
            println!("Hare");
        }
        "slow" => {
            println!("Tortoise");
        }
        _ => unreachable!(),
    }
}
$ 04_01_possible fast
Hare

$ 04_01_possible slow
Tortoise

$ 04_01_possible medium
? failed
error: 'medium' isn't a valid value for '<MODE>'
  [possible values: fast, slow]

如果启动deriver feature可以使用ValueEnum实现同样的功能

use clap::{arg, builder::PossibleValue, command, value_parser, ValueEnum};

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum Mode {
    Fast,
    Slow,
}

// Can also be derived with feature flag `derive`
impl ValueEnum for Mode {
    fn value_variants<'a>() -> &'a [Self] {
        &[Mode::Fast, Mode::Slow]
    }

    fn to_possible_value<'a>(&self) -> Option<PossibleValue> {
        Some(match self {
            Mode::Fast => PossibleValue::new("fast").help("Run swiftly"),
            Mode::Slow => PossibleValue::new("slow").help("Crawl slowly but steadily"),
        })
    }
}

impl std::fmt::Display for Mode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.to_possible_value()
            .expect("no values are skipped")
            .get_name()
            .fmt(f)
    }
}

impl std::str::FromStr for Mode {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        for variant in Self::value_variants() {
            if variant.to_possible_value().unwrap().matches(s, false) {
                return Ok(*variant);
            }
        }
        Err(format!("Invalid variant: {}", s))
    }
}

fn main() {
    let matches = command!() // requires `cargo` feature
        .arg(
            arg!(<MODE>)
                .help("What mode to run the program in")
                .value_parser(value_parser!(Mode)),
        )
        .get_matches();

    // Note, it's safe to call unwrap() because the arg is required
    match matches
        .get_one::<Mode>("MODE")
        .expect("'MODE' is required and parsing will fail if its missing")
    {
        Mode::Fast => {
            println!("Hare");
        }
        Mode::Slow => {
            println!("Tortoise");
        }
    }
}

验证参数值范围

use clap::{arg, command, value_parser};

fn main() {
    let matches = command!() // requires `cargo` feature
        .arg(
            arg!(<PORT>)
                .help("Network port to use")
                .value_parser(value_parser!(u16).range(1..)),
        )
        .get_matches();

    // Note, it's safe to call unwrap() because the arg is required
    let port: u16 = *matches
        .get_one::<u16>("PORT")
        .expect("'PORT' is required and parsing will fail if its missing");
    println!("PORT = {}", port);
}

自定义验证函数

fn main() {
    let matches = command!() // requires `cargo` feature
        .arg(
            arg!(<PORT>)
                .help("Network port to use")
                .value_parser(port_in_range),
        )
        .get_matches();

    // Note, it's safe to call unwrap() because the arg is required
    let port: u16 = *matches
        .get_one::<u16>("PORT")
        .expect("'PORT' is required and parsing will fail if its missing");
    println!("PORT = {}", port);
}

const PORT_RANGE: RangeInclusive<usize> = 1..=65535;

fn port_in_range(s: &str) -> Result<u16, String> {
    let port: usize = s
        .parse()
        .map_err(|_| format!("`{}` isn't a port number", s))?;
    if PORT_RANGE.contains(&port) {
        Ok(port as u16)
    } else {
        Err(format!(
            "Port not in range {}-{}",
            PORT_RANGE.start(),
            PORT_RANGE.end()
        ))
    }
}

参数的相互关系

ArgGroups 使声明关系变得更容易,而不必单独列出每个关系,或者当您希望规则应用“任何但不是全部”参数时

use std::path::PathBuf;

use clap::{arg, command, value_parser, ArgAction, ArgGroup};

fn main() {
    // Create application like normal
    let matches = command!() // requires `cargo` feature
        // Add the version arguments
        .arg(arg!(--"set-ver" <VER> "set version manually"))
        .arg(arg!(--major         "auto inc major").action(ArgAction::SetTrue))
        .arg(arg!(--minor         "auto inc minor").action(ArgAction::SetTrue))
        .arg(arg!(--patch         "auto inc patch").action(ArgAction::SetTrue))
        // Create a group, make it required, and add the above arguments
        .group(
            ArgGroup::new("vers")
                .required(true)
                .args(["set-ver", "major", "minor", "patch"]),
        )
        // Arguments can also be added to a group individually, these two arguments
        // are part of the "input" group which is not required
        .arg(
            arg!([INPUT_FILE] "some regular input")
                .value_parser(value_parser!(PathBuf))
                .group("input"),
        )
        .arg(
            arg!(--"spec-in" <SPEC_IN> "some special input argument")
                .value_parser(value_parser!(PathBuf))
                .group("input"),
        )
        // Now let's assume we have a -c [config] argument which requires one of
        // (but **not** both) the "input" arguments
        .arg(
            arg!(config: -c <CONFIG>)
                .value_parser(value_parser!(PathBuf))
                .requires("input"),
        )
        .get_matches();

    // Let's assume the old version 1.2.3
    let mut major = 1;
    let mut minor = 2;
    let mut patch = 3;

    // See if --set-ver was used to set the version manually
    let version = if let Some(ver) = matches.get_one::<String>("set-ver") {
        ver.to_owned()
    } else {
        // Increment the one requested (in a real program, we'd reset the lower numbers)
        let (maj, min, pat) = (
            matches.get_flag("major"),
            matches.get_flag("minor"),
            matches.get_flag("patch"),
        );
        match (maj, min, pat) {
            (true, _, _) => major += 1,
            (_, true, _) => minor += 1,
            (_, _, true) => patch += 1,
            _ => unreachable!(),
        };
        format!("{}.{}.{}", major, minor, patch)
    };

    println!("Version: {}", version);

    // Check for usage of -c
    if matches.contains_id("config") {
        let input = matches
            .get_one::<PathBuf>("INPUT_FILE")
            .unwrap_or_else(|| matches.get_one::<PathBuf>("spec-in").unwrap())
            .display();
        println!(
            "Doing work using input {} and config {}",
            input,
            matches.get_one::<PathBuf>("config").unwrap().display()
        );
    }
}

自定义验证

https://docs.rs/clap/latest/clap/_tutorial/index.html#custom-validation

测试

clap 将大多数开发错误报告为 debug_assert!s。 应该有一个调用 Command::debug_assert 的测试,而不是检查每个子命令

use clap::{arg, command, value_parser};

fn main() {
    let matches = cmd().get_matches();

    // Note, it's safe to call unwrap() because the arg is required
    let port: usize = *matches
        .get_one::<usize>("PORT")
        .expect("'PORT' is required and parsing will fail if its missing");
    println!("PORT = {}", port);
}

fn cmd() -> clap::Command {
    command!() // requires `cargo` feature
        .arg(
            arg!(<PORT>)
                .help("Network port to use")
                .value_parser(value_parser!(usize)),
        )
}

#[test]
fn verify_cmd() {
    cmd().debug_assert();
}

𝓞𝓷 𝔂𝓸𝓾𝓻 𝓶𝓪𝓻𝓴