深入探索 Rust 的 clap 库:命令行解析的艺术

news2024/11/13 11:14:56

版本声明

  • Rust: 1.76
  • clap: 4.5.1
  • clap_complete 4.5.1
  • rpassword: 7.3.1

结论先行

本文将从 CLI(Command Line Interface)命令行工具的概述讲起,介绍一个优秀的命令行工具应该具备的功能和特性。然后介绍 Rust 中一个非常优秀的命令行解析工具 clap 经典使用方法,并利用 clap 实现一个类似于 curl 的工具 httpie。文章最后还将 clap 于 Go 语言中同样优秀的命令行解析工具 cobra 进行一个简单对比,便于读者进一步体会 clap 的简洁和优秀。

本文将包含以下几个部分:

  1. CLI 概述:从 CLI 的基本概念出发,介绍优秀命令行工具应该具备的功能特性,并以 curl 作为经典范例进行说明。
  2. 详细介绍 clap:基于 clap 官方文档,分别详细介绍 clap 以 derive 和 builder 两个方式构建 cli 的常用方法。
  3. 实战 httpie:参考陈天老师的《Rust 编程第一课》,用最新的 clap 版本(1.7.6)实现 httpie 工具。
  4. 对比 cobra:从设计理念和目标、功能特点、使用场景等方面简要对比 clap 和 Go 流行的命令行解析库 cobra。

特此声明,本文包含 AI 辅助生成内容,如有错误遗漏之处,敬请指出。

CLI 概述

CLI(Command Line Interface,命令行界面)是一种允许用户通过文本命令与计算机程序或操作系统进行交互的接口。与图形用户界面(GUI,Graphical User Interface)相比,CLI 不提供图形元素,如按钮或图标,而是依赖于文本输入。用户通过键盘输入特定的命令行指令,命令行界面解释这些指令并执行相应的操作。

一款优秀的 CLI 工具应该具备以下的功能和特性,以提升用户体验和效率:

一个优秀的命令行工具(CLI, Command Line Interface)应该具备以下功能和特性,以提升用户体验和效率:

  1. 直观易用
    • 简洁的命令语法:命令和参数的设计应直观易懂,方便用户记忆和使用。
    • 自动补全:支持命令和参数的自动补全功能,提高用户输入效率。
    • 命令别名:提供常用命令的简短别名,减少输入的工作量。
  2. 强大的帮助系统
    • 详细的帮助文档:每个命令和参数都应有清晰的说明文档。
    • 示例使用方式:提供常见的使用示例,帮助用户快速理解和应用。
    • 内置帮助命令:通过如--help-h参数轻松访问帮助信息。
  3. 错误处理与反馈
    • 清晰的错误信息:出现错误时,提供明确、具体的错误信息,帮助用户快速定位问题。
    • 建议和解决方案:在可能的情况下,给出错误解决的建议或自动修复选项。
  4. 高效的执行和输出
    • 快速响应:命令执行应迅速,减少用户等待时间。
    • 格式化的输出:提供易于阅读和解析的输出格式,如表格、JSON 或 XML 等。
    • 输出过滤和排序:允许用户根据需要过滤和排序输出结果,提高信息的查找效率。
  5. 跨平台兼容
    • 多平台支持:能够在不同的操作系统上运行,如 Windows、macOS、Linux 等。
    • 环境适应性:自动适应不同的终端环境和字符编码,确保输出显示正确。
  6. 安全性
    • 安全的默认设置:默认配置应强调安全,避免暴露敏感信息。
    • 数据加密:在处理敏感信息(如密码)时,应使用加密手段保护数据安全。
  7. 版本管理
    • 版本控制:提供命令查看工具版本,支持多版本共存或升级。
    • 向后兼容:新版本应尽量保持与旧版本的兼容性,避免破坏用户现有的工作流程。

这些特性不仅能够提高用户的工作效率,还能增强用户体验,使命令行工具更加强大和易用。

下面我们以 curl 为例,看看优秀的 CLI 工具大概长什么样子。

curl 是一种命令行工具和库,用于传输数据。它支持多种协议,包括 HTTP、HTTPS、FTP、FTPS、SCP、SFTP、TFTP、TELNET、DICT、LDAP、LDAPS、IMAP、POP3、SMTP 和 RTSP 等。curl 是一个非常强大和灵活的工具,广泛应用于自动化脚本、系统测试、数据收集和许多其他用途。

进入终端,我们可以用下面命令查看 curl 的说明文档:

➜  ~ curl --help
Usage: curl [options...] <url>
 -d, --data <data>          HTTP POST data
 -f, --fail                 Fail fast with no output on HTTP errors
 -h, --help <category>      Get help for commands
 -i, --include              Include protocol response headers in the output
 -o, --output <file>        Write to file instead of stdout
 -O, --remote-name          Write output to a file named as the remote file
 -s, --silent               Silent mode
 -T, --upload-file <file>   Transfer local FILE to destination
 -u, --user <user:password> Server user and password
 -A, --user-agent <name>    Send User-Agent <name> to server
 -v, --verbose              Make the operation more talkative
 -V, --version              Show version number and quit

This is not the full help, this menu is stripped into categories.
Use "--help category" to get an overview of all categories.
For all options use the manual or "--help all".

使用示例:

  • 下载文件:
    curl -O http://example.com/file.txt
    
  • 发送 POST 请求:
    curl -d "param1=value1&param2=value2" http://example.com/resource
    
  • 使用 HTTPS 并忽略证书验证:
    curl -k https://example.com
    
  • 使用基本认证:
    curl -u username:password http://example.com
    

curl 的这些特性使其成为开发者、系统管理员和自动化脚本中广泛使用的工具之一。

clap

概述

clap,代表 Command Line Argument Parser,是一个旨在创建直观、易用且功能强大的命令行界面的 Rust 库。截至目前(2024.2),clap 已经发展到了 4.5.1 版本,它通过简化命令行参数的处理,让开发者能更专注于应用逻辑的构建。

clap 之所以在 Rust 社区如此流行,得益于以下几个优点:

1. 易于使用

clap 的设计理念是让命令行参数的解析变得简单而直观。即使是没有经验的开发者也能快速上手,通过几行代码就能实现复杂的命令行参数解析。

2. 功能丰富

clap 提供了广泛的功能来满足各种命令行解析需求,包括但不限于:

  • 自动生成的帮助信息clap 能根据定义的参数自动生成帮助信息,包括参数的说明、类型、默认值等。
  • 强大的错误提示:当用户输入无效的命令行参数时,clap 会提供清晰且有用的错误提示,帮助用户快速定位问题。
  • 参数验证:开发者可以为参数设定验证规则,确保输入的参数符合预期。
  • 复杂的命令结构:支持子命令的嵌套,允许构建复杂的命令行应用结构。
  • 自定义派生:通过 clap 的派生宏,可以简化命令行解析器的定义,使代码更加清晰。

3. 高度可定制

clap 允许开发者高度定制命令行解析的行为和外观,包括自定义帮助信息的格式、控制错误消息的显示方式等。这种灵活性意味着你可以根据应用程序的需求调整 clap 的行为。

4. 性能优异

尽管 clap 功能强大,但它仍然非常注重性能。clap 经过优化,以尽可能少的性能开销处理命令行参数。

5. 活跃的社区支持

clap 有一个非常活跃的社区,在 GitHub 上不断有新的贡献者加入。这意味着 clap 不断地得到改进和更新,同时也有大量的社区资源可供参考。

Derive vs Builder (1) 初探

clap 提供了 2 种构建命令行的方式,分别为 DeriveBuilder。顾名思义,Derive 就是利用宏强大的功能来构建命令行,而 Builder 则采用构建者模式链式构建命令行工具。

在这里我们先给出示例来直观感受这 2 种构建方式的不同:

Derive:

#[derive(Parser)]
#[command(version, author, about, long_about = None)]
struct Cli {
    /// Specify your name
    name: String,

    /// Specify your age optionally
    #[arg(short, long)]
    age: Option<i8>,
}

fn main() {
    let cli = Cli::parse();
    println!("name: {}", cli.name);
    println!("age: {:?}", cli.age);
}

Builder:

fn main() {
    let matches = Command::new("myapp")
  			.version("1.0.0")
  			.author("hedon")
  			.about("this is the short about")
  			.long_about("this is the long about")
        .arg(arg!([NAME]).required(true).help("Specify your name"))
        .arg(arg!(-a --age <AGE>)
            .value_parser(clap::value_parser!(u8))
            .help("Specify your age optionally"))
        .get_matches();

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

这 2 个程序都实现了相同的功能,使用 --help ,输出的内容大致都如下:

Usage: derive [OPTIONS] <NAME>

Arguments:
  <NAME>  Specify your name

Options:
  -a, --age <AGE>  Specify your age optionally
  -h, --help       Print help

通过观察,可以发现 Derive 模式下,宏中的每一个属性,如 versionauthor 等,都对应到 Builder 模式下一个同名的函数。

下面我们将从**「应用配置」「参数类型」「参数校验」**三个方面,分别介绍 clap 中 Derive 和 Builder 两种模式构建 CLI 的常用方法。

特别说明:后续的例子均在 examples 目录下实现,故编译和执行命令都包含 example。

目录结构大概如下:

➜  learn-clap git:(master) ✗ tree         
.
├── Cargo.lock
├── Cargo.toml
├── examples
│   ├── optional.rs
├── src
│   └── main.rs
└── target
    └── release
        └── examples
        		└── optional

Derive

要使用 clap 的 Derive 模式,需要:

cargo add clap --features derive

1. 应用配置

我们需要定义一个 strut 来表示我们的 application,利用它来承载应用的参数:

/// The example of clap derive
#[derive(Parser)]
#[command(version, author, about, long_about = None)]
struct Cli {
    /// Specify your name
    name: String,

    /// Specify your age optionally
    #[arg(short, long)]
    age: Option<i8>,
}

fn main() {
    let cli = Cli::parse();
    println!("name: {}", cli.name);
    println!("age: {:?}", cli.age);
}

#[derive(Parser)] 是一个过程宏(procedural macro),用于自动为结构体实现 clap::Parser trait。这使得该结构体可以用来解析命令行参数。

  • 使用 #[derive(Parser)],你可以简化命令行解析的代码,因为 clap 会根据结构体的字段自动生成命令行解析的逻辑。
  • 每个字段都对应一个命令行参数,字段的类型和属性用来决定参数的解析方式和验证规则。

#[command(version, about, long_about = None)] 属性用于为整个命令行程序提供元信息,它支持以下几个元素:

#[command] 支持的元素

#[arg(short, long)] 属性用于配置命令参数的元信息,它支持以下几个属性:

属性方法默认值/行为备注
idArg::idfield’s name当属性不存在时,使用字段名
value_parserArg::value_parserauto-select based on field type当属性不存在时,会基于字段类型自动选择实现
actionArg::actionauto-select based on field type当属性不存在时,会基于字段类型自动选择动作
helpArg::helpDoc comment summary当属性不存在时,使用文档注释摘要
long_helpArg::long_helpDoc comment with blank line, else nothing当属性不存在时,使用文档注释,如果有空行
verbatim_doc_commentMinimizes preprocessing-将文档注释转换为 help/long_help 时最小化预处理
shortArg::shortno short set当属性不存在时,没有短名称设置
longArg::longno long set当属性不存在时,没有长名称设置
envArg::envno env set当属性不存在时,没有环境变量设置
from_globalRead Arg::global-无论在哪个子命令中,都读取 Arg::global 参数
value_enumParse with ValueEnum-使用 ValueEnum 解析值
skipIgnore this fieldfills the field with Default::default()忽略此字段,用 <expr> 或 Default::default() 填充
default_valueArg::default_valueArg::required(false)设置默认值,并将 Arg 设置为非必须
default_value_tArg::default_valueArg::required(false)要求 std::fmt::Display 与 Arg::value_parser 相匹配
default_values_tArg::default_valuesArg::required(false)要求字段类型为 Vec<T>,T 实现 std::fmt::Display
default_value_os_tArg::default_value_osArg::required(false)要求 std::convert::Into<OsString>
default_values_os_tArg::default_values_osArg::required(false)要求字段类型为 Vec<T>,T 实现std::convert::Into<OsString>

2. 参数类型

2.1 Arguments & Options

从上面这个输出样例中:

the example of clap derive

Usage: derive [OPTIONS] <NAME>

Arguments:
  <NAME>  Specify your name

Options:
  -a, --age <AGE>  Specify your age optionally
  -h, --help       Print help

可以看到跟在命令后面有 2 中参数类型:

  • Arguments: 直接在命令后面指定值,如 cmd hedon,有严格的顺序要求。
  • Options: 需要用 -{short}--{long} 来指定是哪个参数,无严格的顺序要求。

它们的定义区别就是是否使用了 #[arg]

  • Options: 指定了 short 或 long。
  • Arguments: 没有 short 和 long。
#[derive(Parser)]
struct Cli {
  /// 会被解析成 [NAME]
  name: String,
  
  /// 会被解析成 -a <AGE>
  #[arg(short, long)]
  age: u8,
}
2.2 可选参数

可以使用 Option 来实现可选参数:

use clap::Parser;

#[derive(Parser)]
#[command(version, author, about, long_about = None)]
struct Cli {
    name: Option<String>,

    #[arg(short, long)]
    age: Option<u8>,
}

fn main() {
    let cli = Cli::parse();
    println!("name: {:?}", cli.name);
    println!("age: {:?}", cli.age);
}

编译:

cargo build --example optional --release

执行:

/target/release/examples/optional --help 

输出:

this is the about from Cargo.toml

Usage: optional [OPTIONS] [NAME]

Arguments:
  [NAME]  

Options:
  -a, --age <AGE>  
  -h, --help       Print help
  -V, --version    Print version

测试:

➜  learn-clap git:(master) ✗ ./target/release/examples/optional       
name: None
age: None
➜  learn-clap git:(master) ✗ ./target/release/examples/optional -a 1  
name: None
age: Some(1)
➜  learn-clap git:(master) ✗ ./target/release/examples/optional hedon  
name: Some("hedon")
age: None
➜  learn-clap git:(master) ✗ ./target/release/examples/optional hedon -a 18
name: Some("hedon")
age: Some(18)
2.3 枚举参数

可以使用 enum 搭配 value_enum 来实现多选一参数,并限制可选参数的取值。

use clap::{Parser, ValueEnum};

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

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum Mode {
    /// run in fast mode
    Fast,
    /// run in slow mode
    Slow,
}

fn main() {
    let cli = Cli::parse();
    match cli.mode {
        Mode::Fast => println!("fast!!!!!"),
        Mode::Slow => println!("slow......"),
    }
}

输出:

Usage: enum <MODE>

Arguments:
  <MODE>
          Choose the program mode run in

          Possible values:
          - fast: run in fast mode
          - slow: run in slow mode

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

  -V, --version
          Print version
2.4 累计参数

累积参数允许用户通过重复指定同一个标志(例如 -d)来累加值或效果,通常用于控制命令行应用的详细级别(verbosity level)或其他需要根据次数变化的行为。

在很多命令行工具中,累积参数常见于控制日志输出的详细程度。例如,一个 -v(verbose)标志可能每被指定一次,就增加一层详细级别。所以,-vvv(等价于 -v -v -v) 会比单个 -v 提供更多的详细信息。

clap 中可以通过 clap::ArgAction::Count 来实现这种累积参数。

use clap::Parser;

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

fn main() {
    let cli = Cli::parse();
    println!("verbose: {}", cli.verbose);
}

输出:

➜  learn-clap git:(master) ✗ ./target/release/examples/accurate --help
this is the about from Cargo.toml

Usage: accurate [OPTIONS]

Options:
  -v, --verbose...  
  -h, --help        Print help
  -V, --version     Print version
➜  learn-clap git:(master) ✗ ./target/release/examples/accurate -v    
verbose: 1
➜  learn-clap git:(master) ✗ ./target/release/examples/accurate -vvvv
verbose: 4
2.5 变长参数

有时候我们希望接收变长参数,比如说:

del file1 file2 file3

这个时候可以使用 Vec<> 来实现。

use clap::Parser;

#[derive(Parser)]
#[command(version, author, about, long_about = None)]
struct Cli {
    files: Vec<String>,
}

fn main() {
    let cli = Cli::parse();
    println!("files: {:?}", cli.files);
}

输出:

➜  learn-clap git:(master) ✗ ./target/release/examples/var_length --help
this is the about from Cargo.toml

Usage: var_length [FILES]...

Arguments:
  [FILES]...  

Options:
  -h, --help     Print help
  -V, --version  Print version
➜  learn-clap git:(master) ✗ ./target/release/examples/var_length       
files: []
➜  learn-clap git:(master) ✗ ./target/release/examples/var_length file1 
files: ["file1"]
➜  learn-clap git:(master) ✗ ./target/release/examples/var_length file1 file2
files: ["file1", "file2"]
➜  learn-clap git:(master) ✗ ./target/release/examples/var_length file1 file2 file3
files: ["file1", "file2", "file3"]
2.6 标志参数

对于标志参数,只要指定类型为 bool,就可以自动实现了。

use clap::Parser;

#[derive(Parser)]
#[command(version, author, about, long_about = None)]
struct Cli {
    #[arg(short, long)]
    verbose: bool,
}

fn main() {
    let cli = Cli::parse();
    println!("verbose: {}", cli.verbose);
}

输出:

➜  learn-clap git:(master) ✗ ./target/release/examples/flag --help
Usage: flag [OPTIONS]

Options:
  -v, --verbose  
  -h, --help     Print help
  -V, --version  Print version
➜  learn-clap git:(master) ✗ ./target/release/examples/flag       
verbose: false
➜  learn-clap git:(master) ✗ ./target/release/examples/flag -v    
verbose: true
2.7 子命令

在更复杂的命令行工具中,除了主命令,还有子命令,甚至子命令下面还有子命令,其实就是一颗命令树。

command tree

clap 中可以使用 #[command(subcommand)] 搭配 #[derive(Subcommand)] 实现子命令功能。

use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(version, author, about, long_about = None)]
struct Cli {
    #[command(subcommand)]
    test: Option<Test>,
}

#[derive(Subcommand)]
enum Test {
    /// Add a number
    Add {
        #[arg(short, long)]
        num: u16,
    },
    /// Sub a number
    Sub {
        #[arg(short, long)]
        num: u16,
    }
}

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

    if let Some(test) = cli.test {
        match test {
            Test::Add {num} => println!("test add num: {:?}", num),
            Test::Sub {num} => println!("test sub num: {:?}", num),
        }
    }
}

输出:

➜  learn-clap git:(master) ✗ ./target/release/examples/subcommand --help      
this is the about from Cargo.toml

Usage: subcommand [COMMAND]

Commands:
  add   Add a number
  sub   Sub a number
  help  Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help
  -V, --version  Print version
➜  learn-clap git:(master) ✗ ./target/release/examples/subcommand add --help
Add a number

Usage: subcommand add --num <NUM>

Options:
  -n, --num <NUM>  
  -h, --help       Print help
➜  learn-clap git:(master) ✗ ./target/release/examples/subcommand add -n 1  
test add num: 1
➜  learn-clap git:(master) ✗ ./target/release/examples/subcommand sub -n 2
test sub num: 2

3. 参数校验

3.1 类型校验

可以发现,使用 Derive 模式的时候,我们在参数后面指定参数类型的时候,clap 就会对我们输入参数进行类型检查,不匹配的时候会输出丰富的报错信息和指导建议。

error: invalid value 'xxxx' for '--num <NUM>': invalid digit found in string

For more information, try '--help'.

默认支持:

  • 原生类型:bool, String, OsString, PathBufusizeisize
  • 范围数据:u8, i8, u16, i16, u32, i32, u64, i64
  • 实现了 ValueEnum 的 enum 类型
  • 实现了 From<OsString>From<&OsStr>FromStr 的类型

这是因为他们都实现了 TypedValueParser trait,你自定义的类型也可以实现这个 triat,这样就可以自动进行类型校验了。

clap 还提供了一些更加严格的参数校验功能。👇🏻

3.2 枚举校验

对于实现 ValueEnum 的枚举类型,如果输入的值不是枚举中定义的,则 clap 会报错并提示可选值。

我们复用上面介绍「多选一参数」的代码:

use clap::{Parser, ValueEnum};

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

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum Mode {
    /// run in fast mode
    Fast,
    /// run in slow mode
    Slow,
}

fn main() {
    let cli = Cli::parse();
    match cli.mode {
        Mode::Fast => println!("fast!!!!!"),
        Mode::Slow => println!("slow......"),
    }
}

使用错误的值进行尝试:

➜  learn-clap git:(master) ✗ ./target/release/examples/enum xxxx               
error: invalid value 'xxxx' for '<MODE>'
  [possible values: fast, slow]

For more information, try '--help'.
3.3 范围校验

如果你想要实现数字类型范围限制的话,比如端口号参数的范围应该是 [1, 65535],那可以使用 value_parser! = clap::value_parser!(u16).range(1..) 来实现这个功能:

use clap::Parser;

#[derive(Parser)]
#[command(version, author, about, long_about = None)]
struct Cli {
    #[arg(short, long, value_parser = clap::value_parser!(u16).range(1..))]
    port: u16,
}

fn main() {
    let cli = Cli::parse();
    println!("port: {:?}", cli.port);
}

输出:

➜  learn-clap git:(master) ✗ ./target/release/examples/range --help             
this is the about from Cargo.toml

Usage: range --port <PORT>

Options:
  -p, --port <PORT>  
  -h, --help         Print help
  -V, --version      Print version
  
➜  learn-clap git:(master) ✗ ./target/release/examples/range -p 0  
error: invalid value '0' for '--port <PORT>': 0 is not in 1..=65535

For more information, try '--help'.

➜  learn-clap git:(master) ✗ ./target/release/examples/range -p 11111111
error: invalid value '11111111' for '--port <PORT>': 11111111 is not in 1..=65535

For more information, try '--help'.

➜  learn-clap git:(master) ✗ ./target/release/examples/range -p 1111    
port: 1111

在这个例子中,value_parser = clap::value_parser!(u16).range(1..) 的含义可以分为两部分解释:

1. clap::value_parser!(u16)

这部分使用 value_parser! 宏为命令行参数指定了 u16 类型的解析器。这意味着输入的参数值会被尝试解析为无符号 16 位整数(u16)。如果输入不能被成功解析为 u16 类型(例如,输入是非数字字符串或者数字过大/过小而不符合 u16 的范围),clap 会报错并提示用户输入有效的参数值。

2. .range(1…)

这部分进一步限制了参数值的有效范围。.range(1..) 指定了参数值必须大于或等于 1(包含 1),但没有上限。换句话说,任何小于 1 的值都将被认为是无效的,clap 会因此报错并要求用户输入一个符合范围要求的值。这在需要限定参数值为正数时非常有用。

结合起来,value_parser = clap::value_parser!(u16).range(1..) 创建了一个规则,要求命令行参数必须是一个大于或等于 1 的 u16 类型的数值。这在很多场景下都非常有用,比如当你需要用户指定一个正数端口号时。

在 RustRover 中,你可以在 Builder 模式,通过在 clap::value_parser!() 中指定其他的类型,然后输入 . 获得其他类型的内置校验规则。

3.4 自定义校验

对于更复杂的规则,clap 还支持自定义校验规则。比如上面对 port 的校验,也可以自己实现。

use std::ops::RangeInclusive;
use clap::Parser;

#[derive(Parser)]
#[command(version, author, about, long_about = None)]
struct Cli {
    #[arg(short, long, value_parser = parse_port)]
    port: u16,
}

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

fn parse_port(s: &str) -> Result<u16, String> {
    let port: usize = s
        .parse()
        .map_err(|_| format!("`{s}` isn't a port number`"))?;

    if PORT_RANGE.contains(&port) {
        Ok(port as u16)
    } else {
        Err(format!(
            "port not in range {}-{}",
            PORT_RANGE.start(),
            PORT_RANGE.end(),
        ))
    }
}

fn main() {
    let cli = Cli::parse();
    println!("port: {:?}", cli.port);
}

在代码中,我们直接使用 value_parser = parse_port 来指定自定义的校验规则。

我们自定义的校验规则为:

fn parse_port(s: &str) -> Result<u16, String> {}

它需要满足:

  • 入参是 &str
  • 出参是 Result<参数类型, String>

可以测试输出:

➜  learn-clap git:(master) ✗ ./target/release/examples/custom --help
this is the about from Cargo.toml

Usage: custom --port <PORT>

Options:
  -p, --port <PORT>  
  -h, --help         Print help
  -V, --version      Print version
  
➜  learn-clap git:(master) ✗ ./target/release/examples/custom -p xxx
error: invalid value 'xxx' for '--port <PORT>': `xxx` isn't a port number`

For more information, try '--help'.

➜  learn-clap git:(master) ✗ ./target/release/examples/custom -p 0  
error: invalid value '0' for '--port <PORT>': port not in range 1-65535

For more information, try '--help'.

➜  learn-clap git:(master) ✗ ./target/release/examples/custom -p 9527
port: 9527
3.5 关联参数

有时候参数直接还有关联关系,比如说:

  • 依赖:必须存在 -a 参数,-b 参数才有意义,即要使用 -b 参数时,必须指定 -a 参数。
  • 互斥:-a-b 只能同时存在一个。

可以使用 requires 实现依赖关系:

use clap::Parser;

#[derive(Parser)]
#[command(version, author, about, long_about = None)]
struct Cli {
    #[arg(short, long)]
    a: Option<String>,

    #[arg(short, long,requires = "a")]
    b: Option<String>,
}

fn main() {
    let cli = Cli::parse();
    println!("a: {:?}", cli.a);
    println!("b: {:?}", cli.b);
}

上述代码中,我们在参数 b 中加入了 requires = "a",表示要使用 b 参数必须要有 a 参数。

输出:

➜  learn-clap git:(master) ✗ ./target/release/examples/relation      
a: None
b: None
➜  learn-clap git:(master) ✗ ./target/release/examples/relation -b 1 
error: the following required arguments were not provided:
  --a <A>

Usage: relation --a <A> --b <B>

For more information, try '--help'.

➜  learn-clap git:(master) ✗ ./target/release/examples/relation -a 1
a: Some("1")
b: None

➜  learn-clap git:(master) ✗ ./target/release/examples/relation -a 1 -b 2
a: Some("1")
b: Some("2")

可以使用 #[group(required = true, mutiple = false)] 来实现互斥关系:

use clap::{Args, Parser};

#[derive(Parser)]
#[command(version, author, about, long_about = None)]
struct Cli {
    #[command(flatten)]
    args: Only,
}

#[derive(Args, Debug)]
#[group(required = true, multiple = false)]
struct Only {
    #[arg(long)]
    a: Option<String>,
    #[arg(long)]
    b: Option<String>,
    #[arg(long)]
    c: Option<String>,
    #[arg(long)]
    d: Option<String>,
}

fn main() {
    let cli = Cli::parse();
    println!("only: {:?}", cli.args)
}

#[command(flattern)] 直接将结构体里面的参数平铺。

#[group] 用于将一组参数归为一个组,required = true 表示必须提供该 group 中的参数,multiple = false 表示只能有一个参数被提供。

测试输出如下:

➜  learn-clap git:(master) ✗ ./target/release/examples/only_one --help
this is the about from Cargo.toml

Usage: only_one <--a <A>|--b <B>|--c <C>|--d <D>>

Options:
      --a <A>    
      --b <B>    
      --c <C>    
      --d <D>    
  -h, --help     Print help
  -V, --version  Print version
  
➜  learn-clap git:(master) ✗ ./target/release/examples/only_one       
error: the following required arguments were not provided:
  <--a <A>|--b <B>|--c <C>|--d <D>>

Usage: only_one <--a <A>|--b <B>|--c <C>|--d <D>>

For more information, try '--help'.

➜  learn-clap git:(master) ✗ ./target/release/examples/only_one --a 1      
only: Only { a: Some("1"), b: None, c: None, d: None }

➜  learn-clap git:(master) ✗ ./target/release/examples/only_one --a 1 --b 2
error: the argument '--a <A>' cannot be used with '--b <B>'

Usage: only_one <--a <A>|--b <B>|--c <C>|--d <D>>

For more information, try '--help'.

➜  learn-clap git:(master) ✗ ./target/release/examples/only_one --b 2      
only: Only { a: None, b: Some("2"), c: None, d: None }

Builder

使用 clap 的 Builder 模式,一般情况下不需要额外引入其他的 features:

cargo add clap

但是如果要使用 command! 来构建应用的话,需要引入 cargo 这个 features:

cargo add clap --features cargo

1. 应用配置

在 Builder 模式下,你可以使用 command!() Command::new("appname") 来构建一个命令行应用,其中 command!() 默认将 appname 设置应用名称,而 Command::new() 必须显示指定 appname。

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

fn main() {
    // let matches = command!()
    let matches = Command::new("MyApp")
        // Application configuration
        .version("1.0.0")
        .author("hedon")
        .about("This the intro of the cli application")
        // Application args
        .arg(arg!([NAME]).help("Specify your name"))
        .arg(
            Arg::new("age").short('a').long("age").value_parser(value_parser!(u8))
        )
    .get_matches();

    // Read and parse command args
    if let Some(name) = matches.get_one::<String>("NAME") {
        println!("Value for name: {name}");
    }
    if let Some(age) = matches.get_one::<u8>("age") {
        println!("Value for age: {age}");
    }
}

这段代码分为以下几个部分:

1. 创建命令行应用实例

let matches = Command::new("MyApp")

这里使用 Command::new 方法创建了一个新的命令行应用实例,命名为 "MyApp"

2. 配置应用

.version("1.0.0")
.author("hedon")
.about("This the intro of the cli application")

接下来,使用链式调用方法配置应用的版本号为 "1.0.0",作者为 "hedon",并添加了一个简短的描述。

这里等价于 Builder 模式下的:

#[command(version, author, about)]

3. 添加命令行参数

.arg(arg!([NAME]).help("Specify your name"))
.arg(
    Arg::new("age").short('a').long("age").value_parser(value_parser!(u8))
)

这部分代码添加了两个命令行参数:

  • .arg(arg!([NAME]).required(true).help("Specify your name")) 使用 arg! 宏添加了一个名为 NAME 的必需参数,并提供了一些帮助信息。
  • .arg(Arg::new("age").short('a').long("age").value_parser(value_parser!(u8))) 创建了另一个参数 age,可以通过 -a--age 来指定。这个参数使用了 value_parser 宏来指明它的值应被解析为 u8 类型的数字。

4. 解析命令行参数

.get_matches();

使用 .get_matches() 方法来解析命令行参数并将结果存储在 matches 变量中。

5. 读取并打印参数值

if let Some(name) = matches.get_one::<String>("NAME") {
    println!("Value for name: {name}");
}
if let Some(age) = matches.get_one::<u8>("age") {
    println!("Value for age: {age}");
}

最后,使用 matches.get_one::<T>("arg_name") 方法尝试获取指定名称的参数值。如果成功找到,则将其打印出来。这里分别尝试获取 "NAME""age" 参数的值,并使用 println! 宏将它们打印到控制台。

使用 -- help 测试输出如下:

This the intro of the cli application

Usage: app2 [OPTIONS] [NAME]

Arguments:
  [NAME]  Specify your name

Options:
  -a, --age <AGE>  
  -h, --help       Print help
  -V, --version    Print version

你可以将其与「Derive - 应用配置」进行比较,应该很容易找到它们之间的对应关系。

在 Derive 中 #[command]#[arg] 支持的属性,都可以在 Builder 中找到对应的同名的函数,这里就不赘述了。

2. 参数类型

在 Builder 模式中,配置参数有两种方式:

  • arg!([-short] [–long] id)
  • Args::new(“id”).short(‘s’).long(“long”)
2.1 Arguments & Options
Arguments:
  [NAME]  Specify your name

Options:
  -a, --age <AGE>  
  • Argument: 不包含 -{short}--{long}
  • Options: 包含 -{short}--{long}
.arg(arg!([NAME]).help("Specify your name"))
.arg(arg!(-a --age <AGE>).value_parser(value_parser!(u16)))
2.2 可选参数

根据约定,<> 表示必须,而 [] 表示可选:

.arg(arg!(<NAME>)   // 必须
.arg(arg!([ADDRESS]))  // 可选

你也可以使用 .required(bool) 函数明确指出是否必须:

.arg(arg!(<NAME>).required(true))

.required() 的优先级高于 <>[],但是建议你在构建的时候还是遵循约定。

2.3 枚举参数

第 1 种:在 value_parser() 中直接指定可选的枚举参数

.arg(arg!(<MODE>).value_parser(["fast, slow"]))

第 2 种:使用枚举,但是枚举需要实现 ValueEnum trait

这里又有 2 种方式,你可以向 Derive 一样引入 derive features,然后直接 #[derive(ValueElem)] 使用默认实现,也可以手动实现。我更倾向于前者。

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

fn main() {
    let matches = command!()
        // .arg(arg!(<MODE>).value_parser(["fast, slow"]))
        .arg(
            arg!(<MODE>).value_parser(value_parser!(Mode)).required(true)
        )
        .get_matches();

    match matches.get_one::<Mode>("MODE")
        .expect("'Mode' is required and parsing will fail if its missing"){
        Mode::Fast => println!("fast"),
        Mode::Slow => println!("slow"),
    }
}

#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, ValueEnum)]
enum Mode {
    /// Run in fast mode
    Fast,
    /// Run in slow mode
    Slow,
}
2.4 累计参数

使用 clap::ArgAction::Count 设置参数为累计参数,然后使用 get_count(id) 获取参数的值:

use clap::{arg, command};

fn main() {
    let matches = command!()
        .arg(arg!(-v --verbose...).action(clap::ArgAction::Count))
        .get_matches();
    println!("v count: {:?}", matches.get_count("verbose"));
}

这里要注意,arg!() 中参数的定义,也要符合累计参数的格式 -{short} --{long}...

2.5 变长参数

使用 clap::ArgAction::Append 设置参数为变长参数,然后使用 get_many::<类型>("id") 获取参数的值:

use clap::{arg, Command};

fn main() {
    let matches = Command::new("append-application")
        .arg(arg!([FILES]...).action(clap::ArgAction::Append))
        .get_matches();

    let files = matches
        .get_many::<String>("FILES")
        .unwrap_or_default()
        .map(|v|v.as_str())
        .collect::<Vec<_>>();

    println!("files: {:?}", files);
}

这里要注意,arg!() 中参数的定义,也要符合变长参数的格式 [arg]|<arg>...

2.6 标志参数

使用 clap::ArgAction::SetTrueclap::ArgAction::SetFalse 设置参数为标志参数,然后使用 get_flag() 获取参数的值:

use clap::{arg, command};

fn main() {
    let matches = command!()
        .arg(arg!(-d --debug).action(clap::ArgAction::SetTrue))
        .arg(arg!(-v --verbose).action(clap::ArgAction::SetFalse))
        .get_matches();
    println!("debug: {:?}", matches.get_flag("debug"));
    println!("verbose: {:?}", matches.get_flag("verbose"))
}

其中:

  • clap::ArgAction::SetTrue : 设置参数的话,则为 true,否则 false(默认)。
  • clap::ArgAction::SetFalse : 设置参数的话,则为 false,否则 true(默认)。

测试:

➜  learn-clap-builder git:(master) ✗ ./target/release/examples/flag      
debug: false
verbose: true
➜  learn-clap-builder git:(master) ✗ ./target/release/examples/flag -d   
debug: true
verbose: true
➜  learn-clap-builder git:(master) ✗ ./target/release/examples/flag -v   
debug: false
verbose: false
2.7 子命令

可以使用 subcommand(sub_cmd)subcommand([sub_cmd1, sub_cmd2]) 来添加子命令,解析的时候使用 matches.subcommand() 匹配子命令,再按照之前的规则解析子命令中对应的参数即可。

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

fn main() {
    let matches = Command::new("myapp")
        .subcommands([
            Command::new("add")
                .arg(arg!(<NUM>).value_parser(value_parser!(i16))),
            Command::new("sub")
                .arg(arg!(<NUM>).value_parser(value_parser!(i16))),
        ])
        .get_matches();

    match matches.subcommand() {
        Some(("add", add_cmd)) => println!(
            "'myapp add' was used, num is: {:?}",
            add_cmd.get_one::<i16>("NUM"),
        ),
        Some(("sub", sub_cmd)) => println!(
            "'myapp sub' was used, num is: {:?}",
            sub_cmd.get_one::<i16>("NUM"),
        ),
        _ => unreachable!()
    }
}

3. 参数校验

3.1 类型校验

使用 value_parser!() 在括号中指定类型,clap 就会自动帮我们对参数进行类型校验,当然你在获取参数值 get_one::<类型>() 的时候,类型要对上,否则会 panic。

默认支持:

  • 原生类型:bool, String, OsString, PathBufusizeisize
  • 范围数据:u8, i8, u16, i16, u32, i32, u64, i64
  • 实现了 ValueEnum 的 enum 类型
  • 实现了 From<OsString>From<&OsStr>FromStr 的类型
3.2 枚举校验

2.3 中枚举参数的说明中,已经体现了枚举校验的功能了,这里不赘述。

3.3 范围校验

对于上述提到的「范围数据」,可以使用 value_parser!(类型).range() 进行范围校验。

arg!(<PORT>)
  .value_parser(value_parser!(u16).range(1..))
3.4 自定义校验

value_parser() 中也可以传自定义的校验函数,该函数的签名需要满足的条件跟我们在介绍 Derive 时一样。

use std::ops::RangeInclusive;
use clap::{arg, command};

fn main() {
    let matches = command!()
        .arg(arg!(<PORT>).value_parser(port_in_range))
        .get_matches();

    println!("port: {:?}", matches.get_one::<u16>("PORT"))
}

const PORT_RANGE: RangeInclusive<usize> = RangeInclusive::new(1, 65535);

fn port_in_range(s: &str) -> Result<u16, String> {
    let port: usize = s
        .parse()
        .map_err(|_|format!("`{s}` is not a port number"))?;
    if PORT_RANGE.contains(&port) {
        Ok(port as u16)
    } else {
        Err(format!(
            "port not in range {}-{}",
            PORT_RANGE.start(),
            PORT_RANGE.end(),
        ))
    }
}
3.5 关联参数
  • **依赖关系:**使用 requires(id | group)
  • 排斥关系:使用 group().multiple(false).required(true)
.group(
    ArgGroup::new("vers")
  			// 表示 "set-ver", "major", "minor", "patch" 必须有一个且只能有一个存在
        .multiple(false)
        .required(true)
        .args(["set-ver", "major", "minor", "patch"]),
)
.arg(
    arg!([INPUT_FILE] "some regular input")
        .value_parser(value_parser!(PathBuf))
        .group("input"),
)
.arg(
    arg!(config: -c <CONFIG>)
        .value_parser(value_parser!(PathBuf))
   			// 表示 -c 需要有 group 为 input 的命令存在才可以使用
        .requires("input"),
)

Derive vs Builder (2) 对比

Derive vs Builder

clap + rpassword 实现加密输入

对于密码、密钥等关键信息的输入,为了信息安全,我们一般会使用加密输出,clap 本身不支持加密输入功能。若你有这方面的需求,可以使用 rpassword crate 辅助完成。

示例:

use clap::Parser;
use rpassword::read_password;

#[derive(Parser)]
#[command(version, author, about, long_about = None)]
struct Cli {
    #[arg(short, long)]
    username: String,

    #[arg(short, long, required = true)]
    password: bool,
}

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

    let password = if cli.password {
        // Prompt user to enter password
        read_password().expect("Failed to read password")
    } else {
        "".to_string()
    };

    // Use username and password to do something
    println!("username: {}, password: {}", cli.username, password);
}

clap_complete 实现自动补全

要实现自动补全,需要在 .zshrc.bashrcSHELL 文件中加入命令自动补全脚本。这时候可以使用 clap_complete 来实现这个功能。

下面的示例目录结构如下:

├── Cargo.lock
├── Cargo.toml
├── build.rs
└── src
    ├── cli.rs
    └── main.rs

首先我们需要引入 clapclap_complete crate,其中 clap_complete 只需在 build 环境下即可,所以我们的 Cargo.tmol 如下:

[package]
name = "myapp"
version = "0.1.0"
edition = "2021"

build = "build.rs"

[dependencies]
clap = { version = "4.5.1" }
dirs = "5.0.1"

[build-dependencies]
clap = { version = "4.5.1"}
clap_complete = "4.5.1"

我们先在 src/cli.rs 中实现一个简单的命令行程序 myapp

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

pub fn build_cli() -> Command {
    Command::new("myapp")
        .about("Tests completions")
        .arg(Arg::new("file")
            .help("some input file"))
        .subcommand(Command::new("test")
            .about("tests things")
            .arg(Arg::new("case")
                .long("case")
                .action(ArgAction::Set)
                .help("the case to test")))
}

我们主要是演示这个自动补全功能,为了省事,src/main.rs 中就不实现具体逻辑了:

mod cli;

fn main() {
    let _m = cli::build_cli().get_matches();
}

接着,我们在项目根目录下实现 build.rs,它将为我们指定的命令生成自动补全脚本:

touch build.rs
use clap_complete::{generate_to, shells::Bash};
use std::env;
use std::io::Error;

include!("src/cli.rs");

fn main() -> Result<(), Error> {
    let outdir = match env::var_os("OUT_DIR") {
        None => return Ok(()),
        Some(outdir) => outdir,
    };

    let mut cmd = build_cli();
    let path = generate_to(
        Bash,
        &mut cmd, // We need to specify what generator to use
        "myapp",  // We need to specify the bin name manually
        outdir,   // We need to specify where to write to
    )?;

    println!("cargo:warning=completion file is generated: {path:?}");

    Ok(())
}

你需要把其中的 myapp 替换为你的命令。

执行构建命令:

cargo build

可以看到输出:

warning: myapp@0.1.0: completion file is generated: "/Users/hedon/RustroverProjects/learn-clap-complete/target/debug/build/myapp-42e401d08c044ca3/out/myapp.bash"
    Finished dev [unoptimized + debuginfo] target(s) in 1.90s

这里会输出生成脚本所在的位置,我这里是 /Users/hedon/RustroverProjects/learn-clap-complete/target/debug/build/myapp-42e401d08c044ca3/out/myapp.bash

我的终端使用的是 zsh:

echo $SHELL      
/bin/zsh

所以我需要将这个文件的内容加到 ~/.zshrc 文件的末尾:

cat /Users/hedon/RustroverProjects/learn-clap-complete/target/debug/build/myapp-42e401d08c044ca3/out/myapp.bash >> ~/.zshrc

重新加载配置文件:

source ~/.zshrc

这个时候你使用 myapp 命令的时候,按 tap 键,就有自动补全了:

➜  ./target/debug/myapp 
--help    -h        \[file\]  help      test 

HTTPie

由于篇幅原因,实战 HTTPie 部分请看:Rust 实战丨HTTPie

与 Go 语言 cobra 比较

Go 的 cobra 也是用于构建命令行应用程序的库,它在 Go 语言生态中非常受欢迎。

为了直观展示这 2 个库构建命令行应用程序的区别,我们来设计一个简单的命令行程序,用 clapcobra 分别实现,以展示如何用这两个库实现相同的功能。

让我们创建一个 CLI 程序,它有一个 greet 子命令,接受一个 -n--name 参数,并打印出一条欢迎信息。

Rust clap 实现

use clap::{ Parser, Subcommand};

#[derive(Parser)]
#[command(bin_name = "greet_app")]
struct Cli {
    #[command(subcommand)]
    sub: Option<Sub>,
}

#[derive(Subcommand)]
enum Sub {
    Greet {
        #[arg(short, long)]
        name: String,
    }
}

fn main() {
    let cli = Cli::parse();
    if let Some(sub) = cli.sub {
        match sub {
            Sub::Greet{name} => println!("greeting: {:?}", name),
        }
    }
}

Go cobra 实现

package main

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
	Use:   "greet_app",
	Short: "A simple greeting application",
	Long:  `This is a simple greeting application with a greet command.`,
}

var greetCmd = &cobra.Command{
	Use:   "greet",
	Short: "Greets a user",
	Long:  `Prints a greeting message for the specified user.`,
	Run: func(cmd *cobra.Command, args []string) {
		name, _ := cmd.Flags().GetString("name")
		fmt.Printf("Hello, %s!\n", name)
	},
}

func init() {
	rootCmd.AddCommand(greetCmd)
	greetCmd.Flags().StringP("name", "n", "", "Sets the name to greet")
	greetCmd.MarkFlagRequired("name")
}

func main() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

输出:

This is a simple greeting application with a greet command.

Usage:
  greet_app [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  greet       Greets a user
  help        Help about any command

Flags:
  -h, --help   help for greet_app

Use "greet_app [command] --help" for more information about a command.

对比

设计哲学和易用性

clap:

  • 使用 Rust 的宏来提供强大的编译时功能,如参数解析、验证等。
  • 利用 Rust 的类型安全特性,减少运行时错误。
  • 支持通过派生宏自动从结构体生成命令行解析代码,简化开发流程。

cobra:

  • 采用更传统的命令式编程模型,直观且易于上手。
  • 通过组合命令对象来构建复杂的命令行应用。
  • 提供了一套完整的生成工具来创建命令和配置,促进了开发速度。

功能和特性

clap:

  • 自动生成帮助信息、版本信息等。
  • 支持多级子命令。
  • 支持自定义验证器和复杂的参数关系(如互斥、依赖等)。

cobra:

  • 支持自动生成帮助文档。
  • 内置命令自动补全脚本生成功能
  • 支持持久化命令行标志到配置文件
  • 通过插件支持增加额外的子命令。
  • 能够轻松地与其他 Go 库集成,如 Viper 用于配置管理。

性能

clap:

  • 由于 Rust 的编译时优化,clap 在解析命令行参数时通常会有更好的性能。
  • 更少的运行时开销,尤其是在处理大量复杂命令行参数时。

cobra:

  • 性能对于大多数命令行应用来说已经足够,但可能不如 clap 优化。
  • Go 的运行时可能会引入额外的开销,尤其是在并发处理时。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1808722.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

双Token方案实现Token自动续期(基于springboot+vue前后端分离项目)

文章目录 前言一、双Token方案介绍1. 令牌类型与功能2.双Token方案的优点3.实现流程 二、具体实现1.后端实现1.1 jwt工具类1.2 响应工具类1.3 实体类1.4 过滤器1.5 controller1.6 启动类 2、前端实现2.1 登录页面2.2 index页面2.3 请求拦截器和响应拦截器 效果展示 前言 更多j…

Windows11下Docker使用记录(五)

目录 准备1. WSL安装cuda container toolkit2. win11 Docker Desktop 设置3. WSL创建docker container并连接cuda4. container安装miniconda&#xff08;可选&#xff09; Docker容器可以从底层虚拟化&#xff0c;使我们能够在 不降级 CUDA驱动程序的情况下使用 任何版本的CU…

【李宏毅-生成式 AI】Spring 2024, HW5:LLM Fine-tuning 实验记录

文章目录 1. Task Overview2. Overall Workflow3. Dataset 介绍4. 代码介绍4.1 环境介绍4.2 下载 dataset4.3 下载并加载模型4.2 Notebook 代码1&#xff09;import 部分2&#xff09;固定 seed3&#xff09;加载 LLM4&#xff09;加载 tokenizer5&#xff09;设置解码参数6&am…

C++期末复习提纲(血小板)

目录 1.this指针 2.静态成员变量 3.面向对象程序设计第一阶段 4.面向对象程序设计第二阶段 5.面向对象程序设计第三阶段 6.简答题 &#xff08;1&#xff09;拷贝构造函数执行的三种情况&#xff1a; &#xff08;2&#xff09;虚析构函数的作用&#xff1a; &#xff…

Java面试_数据库篇_优化,事务,Mysql

Java面试_数据库篇_优化,事务,Mysql 优化如何定位慢查询方案一: 开源工具方案二: Mysql自带慢日志 如何分析慢SQL语句索引介绍索引聚簇索引和非聚簇索引&#xff0c;回表查询覆盖索引&#xff0c;超大分页优化索引创建的原则索引失效 谈谈sql优化的经验 事务事务特性隔离级别un…

算法:位运算题目练习

目录 常见的位运算的操作总结 ①基础位操作 ②给一个数n&#xff0c;确定它的二进制表示中的第x位是0还是1 ③将一个数n的二进制表示的第x位修改成1 ④将一个数n的二进制表示的第x位修改成0 ⑤位图的思想 ⑥提取一个数n二进制表示中最右侧的1 ⑦干掉一个数n二进制表示中…

无人机、机器人10公里WiFi远距离图传模块,实时高清视频传输,飞睿CV5200模组方案,支持mesh自组网模块

在快速发展的物联网时代&#xff0c;远距离无线通信技术已成为连接各种智能设备的关键。无人机、安防监控、机器人等领域对数据传输的距离和速度要求越来越高。 公里级远距离WiFi模组方案可以通过多种技术和策略的结合来实现无人机和机器人之间的高效通信传输。 飞睿智能CV52…

QT C++ QTableWidget 表格合并 setSpan 简单例子

这里说的合并指的是单元格&#xff0c;不是表头。span的意思是跨度、宽度、范围。 setSpan函数需要设定行、列、行跨几格&#xff0c;列跨几格。 //函数原型如下 void QTableView::setSpan(int row, i nt column, 、 int rowSpanCount,/*行跨过的格数*/ int columnSpanCount…

离散数学答疑 5

知识点&#xff1a;单侧连通&#xff0c;强连通&#xff0c;弱连通 前缀码&#xff1a;比如001和00101就不是。因为后者的前三位和前者的重复了 有向图的邻接矩阵求法&#xff1a;横着看 数据结构21-4分钟搞定邻接矩阵_哔哩哔哩_bilibili 可达矩阵是包含自反性的。可达矩阵是…

Excel 将同一分类下的值依次填进分类格右边的格中

表格的第2列是分类&#xff0c;第3列是明细&#xff1a; ABC1S.noAccountProduct21AAAQatAAG32BAAQbIAAW43BAAQkJAAW54CAAQaAAP65DAAQaAAX76DAAQbAAX87DAAQcAAX 需要将同一分类下的值依次填入分类格右边的格中&#xff1a; ABCD1S.noAccountProduct21AAAQatAAG32BAAQbIAAWkJ…

13 RTP包的使用

RTP RTP包最主要的就是Sequence number。 对于发送者来说&#xff0c;视频的每一个帧都有很多包组成。对于接收端来接收的时候是有一个队列进行接收的。这个队列大小都是通过计算的。有了队列之后就会不断的往队列中插入数据。当队列中有的数据超时一直组不成包的时候&#xf…

国资e学快速学习实战教程

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

[FreeRTOS 基础知识] 任务调度 与 链表

文章目录 任务并行的概念RTOS如何实现多任务调度&#xff1f; 任务并行的概念 在生活中&#xff0c;经常出现一心多用的情况。比如你需要一边吃饭一边手机回复信息&#xff0c;这里面就存在两个任务&#xff1a;任务一、吃饭。任务二、手机回复信息。 假如你无法一心多用&…

IO进程线程(十二)进程间通信 共享内存 信号灯集

文章目录 一、共享内存 shared memory&#xff08;shm&#xff09;&#xff08;一&#xff09;特点&#xff08;二&#xff09; 相关API1. 创建共享内存2. 映射共享内存到当前的进程空间3. 取消地址映射4. 共享内存控制 &#xff08;三&#xff09;使用示例&#xff08;四&…

08-指针与数组的结合——数组指针与指针数组的区别

指针与数组的结合 示例 1:指针访问数组元素 通过指针访问数组元素的例子&#xff1a; #include <stdio.h>int main() {int arr[5] {1,2,3,4,5};//int *p1 &arr;int *p1 (int *)&arr; // 需要强制类型转换int *p2 arr;printf("*p1:%d\n", *(p1 …

【SQL】牛客网SQL非技术入门40道代码|练习记录

跟着刷题&#xff1a;是橘长不是局长哦_哔哩哔哩_bilibili 6查询学校是北大的学生信息 select device_id, university from user_profile where university 北京大学 7查找年龄大于24岁的用户信息 select device_id, gender, age, university from user_profile where age…

大模型训练的艺术:从预训练到增强学习的四阶段之旅

文章目录 大模型训练的艺术&#xff1a;从预训练到增强学习的四阶段之旅1. 预训练阶段&#xff08;Pretraining&#xff09;2. 监督微调阶段&#xff08;Supervised Finetuning, SFT&#xff09;3. 奖励模型训练阶段&#xff08;Reward Modeling&#xff09;4. 增强学习微调阶段…

关于 Redis 中集群

哨兵机制中总结到&#xff0c;它并不能解决存储容量不够的问题&#xff0c;但是集群能。 广义的集群&#xff1a;只要有多个机器&#xff0c;构成了分布式系统&#xff0c;都可以称之为一个“集群”&#xff0c;例如主从结构中的哨兵模式。 狭义的集群&#xff1a;redis 提供的…

C++~~期末复习题目讲解---lijiajia版本

目录 1.类和对象 &#xff08;3&#xff09;创建对象的个数 &#xff08;3&#xff09;全局变量&#xff0c;局部变量 &#xff08;4&#xff09;构造函数的执行次数 &#xff08;5&#xff09;静态动态析构和构造顺序 &#xff08;6&#xff09;初始化顺序和声明顺序 &a…

Postman环境变量以及设置token全局变量!

前言百度百科解释&#xff1a; 环境变量&#xff08;environment variables&#xff09;一般是指在操作系统中用来指定操作系统运行环境的一些参数&#xff0c;如&#xff1a;临时文件夹位置和系统文件夹位置等。 环境变量是在操作系统中一个具有特定名字的对象&#xff0c;它…