当前位置:   article > 正文

深入探索 Rust 的 clap 库:命令行解析的艺术_`parser` is imported here, but it is only a trait,

`parser` is imported here, but it is only a trait, without a derive macro

版本声明

结论先行

本文将从 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".
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

使用示例:

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

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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

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"));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

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

Usage: derive [OPTIONS] <NAME>

Arguments:
  <NAME>  Specify your name

Options:
  -a, --age <AGE>  Specify your age optionally
  -h, --help       Print help
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

通过观察,可以发现 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

Derive

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

cargo add clap --features derive
  • 1

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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

#[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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

可以看到跟在命令后面有 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,
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

编译:

cargo build --example optional --release
  • 1

执行:

/target/release/examples/optional --help 
  • 1

输出:

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

测试:

➜  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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
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......"),
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

输出:

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

输出:

➜  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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
2.5 变长参数

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

del file1 file2 file3
  • 1

这个时候可以使用 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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

输出:

➜  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"]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

输出:

➜  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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
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),
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

输出:

➜  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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

3. 参数校验

3.1 类型校验

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

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

For more information, try '--help'.
  • 1
  • 2
  • 3

默认支持:

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

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

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/863999
推荐阅读
相关标签