百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

Rust编程:写个实用的CLI小工具 rust cli

itomcoil 2024-12-19 13:44 22 浏览

功能

  • 命令行解析,处理子命令及各种参数
  • 验证用户输入
  • 将用户输入转换为我们内部能够理解的参数
  • 利用解析好的参数,发送HTTP请求,获得响应
  • 优化输出响应

命令行解析

利用官方推荐的clap库。

首先,在Cargo.toml文件中,添加相关依赖:

[dependencies]
clap = { version = "3", features = ["derive"] } # 命令行解析

然后,在main.rs添加处理命令行解析的相关代码:

use clap::Parser;

// get 子命令

/// feed get with and url and will retrieve the response for you
#[derive(Parser, Debug)]
struct Get {
    /// HTTP 请求的URL
    url: String,
}

// post 子命令。需要输入一个url和若干个可选的 key=value,用于提供 json body

/// feed post with an url and optional key=value pairs.We will post the data
/// as JSON,and retrieve the response for you
#[derive(Parser, Debug)]
struct Post {
    /// HTTP 请求的URL
    url: String,
    /// HTTP 请求的body
    body: Vec<String>,
}

//子命令分别对应不同的HTTP方法,目前只支持get/post
#[derive(Parser, Debug)]
enum SubCommand {
    Get(Get),
    Post(Post),
    // 暂时不支持其他 HTTP 方法
}

// 定义 HTTPie的CLI的主入口,它包含若干个子命令
// 下面 /// 的注释是文档,clap会将其作为CLI 的帮助。

/// A naive httpie implementation with Rust ,can you imagine how easy it is ?
#[derive(Parser, Debug)]
#[clap(version = "1.0", author = "Bin Li <bin@163.com>")]
pub struct Opts {
    #[clap(subcommand)]
    subcmd: SubCommand,
}
  
  fn main() {
    let opts: Opts = Opts::parse();
    println!("{:?}", opts);
}

代码说明:

  • 必须利用 use clap::Parser;导入clap。
  • 为了让CLI的定义变得简单,用到了clap提供的宏#[derive(Parser)],这个宏能够生成一些额外的代码帮我们处理CLI解析工作。
  • 首先定义一个数据结构T,描述CLI都会捕获什么数据,之后通过T::parse() 就可以解析出各种命令行参数,在这里我们定义的数据结构T实则为结构体Opts
  • 其中,parse()函数我们并没有定义,它是宏#[derive(Parser)]自动生成的。

运行:

windows 环境下,在PowerShell中,运行以下命令:

cargo build --quiet ; target/debug/httpie post httpbin.org/post a=1 b=2

如果出现以下错误提示:

error[E0554]: `#![feature]` may not be used on the stable release channel

说明当前编译使用的channel还没有包含#![feature]功能,那就需要切换channel

channel代表我们使用的Rust开发环境是稳定版、试用版还是尝鲜版,分别对应的是stable、beta、nightly,在stable没有的功能,可能在betanightly中就有了。

首先利用以下命令,查看betanightly版本是否有安装:

rustup toolchain list

如果没有安装,则通过以下命令,安装betanightly版本:

rustup toolchain install nightly

最后,需要设置默认的channel:

  • 直接更改当前默认的channel
 rustup default nightly
  • 临时更改
rustup run nightly cargo build
  • 覆盖当前项目使用的channel
 rustup override set nightly

再运行,则成功输出以下结果:

Opts { subcmd: Post(Post { url: "httpbin.org/post", body: ["a=1", "b=2"] }) }

默认情况下,cargo build 编译出来的二进制文件,在项目根目录的 target/debug下,如图:



验证用户输入

以上代码,并没有对用户的输入做任何的校验,例如输入输入以下URL,就会解析出错误结果:

cargo build --quiet ; target/debug/httpie post a=1 b=2 
// 输出以下结果:
Opts { subcmd: Post(Post { url: "a=1", body: ["b=2"] }) }

显而易见,需要做两个验证:

  • URL的验证
  • body的验证

前置条件,需要在Cargo.toml文件中,增加以下依赖:

[dependencies]
anyhow = "1.0" # 错误处理
reqwest = {version="0.11",features=["json"]} #HTTP客户端
mime = "0.3" #处理mime类型

首先,验证URL是否是合法的

clap允许为每个解析出来的值添加 自定义的解析函数。

定义parse_url解析函数

// 需要引入以下crate
use reqwest::Url; 
use anyhow::Result;

fn parse_url(s: &str) -> Result<String> {
    // 这里我们仅仅是检查一下 URL 是否是合法的
    let _url: Url = s.parse()?;

    Ok(s.into())
}

把这个自定义的解析函数,与clap关联起来

// get 子命令

/// feed get with and url and will retrieve the response for you
#[derive(Parser, Debug)]
struct Get {
    /// HTTP 请求的URL
    #[clap(parse(try_from_str = parse_url))]
    url: String,
}

// post 子命令。需要输入一个url和若干个可选的 key=value,用于提供 json body

/// feed post with an url and optional key=value pairs.We will post the data
/// as JSON,and retrieve the response for you
#[derive(Parser, Debug)]
struct Post {
    /// HTTP 请求的URL
    #[clap(parse(try_from_str = parse_url))]
    url: String,
    /// HTTP 请求的body
    body: Vec<String>,
}

然后,是body的验证。

body的内容都是类似 body: ["a=1", "b=2"]格式,也就是说每一项都是 key = value的格式。

所以,需要定义一个数据结构来存储这类信息:

/// 命令行中的 key=value 可以通过 parse_kv_pair 解析成 KvPair结构
#[allow(dead_code)]
#[derive(Debug)]
struct KvPair {
    k: String,
    v: String,
}

也需要自定义一个解析函数,把解析的结果放入KvPair中。也就是说,把满足条件的字符串转换成KvPair结构体。

最优方式就是实现一个Rust 标准库定义的FromStr trait,KvPari结构体实现它之后,就可以直接调用字符串的parse()泛型函数,字符串会直接转换为KvPair,这样很方便地处理字符串到KvPair类型的转换了。

/// 当我们实现 FromStr trait 后,可以用 str.parse() 方法将字符串解析成 KvPair
impl FromStr for KvPair {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        // 使用 = 进行split,这样会得到一个迭代器
        let mut iter = s.split('=');
        let err = || anyhow!(format!("Failed to parse {}", s,));
        Ok(Self {
            // 从迭代器中取出第一个结果作为key,迭代器返回 Some(T)/None
            // 将其转换成 Ok(T)/Err(E),然后调用 ? 处理错误
            k: (iter.next().ok_or_else(err)?).to_string(),
            // 从迭代器中取出第二个结果作为 value
            v: (iter.next().ok_or_else(err)?).to_string(),
        })
    }
}

/// 因为我们为 KvPair 实现了 FromStr,所以,这里可以直接调用s.parse() 得到KvPair
fn parse_kv_pair(s: &str) -> Result<KvPair> {
    s.parse()
}

// post 子命令。需要输入一个 url,和若干个可选的 key=value,用于提供 json body

/// feed post with an url and optional key=value pairs. We will post the data
/// as JSON, and retrieve the response for you
#[derive(Parser, Debug)]
struct Post {
    /// HTTP 请求的 URL
    #[clap(parse(try_from_str = parse_url))]
    url: String,
    /// HTTP 请求的 body
    #[clap(parse(try_from_str=parse_kv_pair))]
    body: Vec<KvPair>,
}

验证

分别输入不同的错误参数,可以看到能成功提示相应的错误信息:

cargo build --quiet
target/debug/httpie post https://httpbin.org/post a=1 b
error: Invalid value "b" for '<BODY>...': Failed to parse b

For more information try --help
target/debug/httpie post http a=1                      
error: Invalid value "http" for '<URL>': relative URL without a base

For more information try --help
target/debug/httpie post https://baidu.com/post a=1 b=2
Opts { subcmd: Post(Post { url: "https://baidu.com/post", body: [KvPair { k: "a", v: "1" }, KvPair { k: "b", v: "2" }] }) }

在不修改主流程的情况下,通过宏、额外的验证函数、trait、trait object等工具,可以实现代码的高度复用且彼此独立。

HTTP请求

前置条件,需要在Cargo.toml文件中,增加以下依赖,增加异步处理HTTP功能:

[dependencies]
mime = "0.3" #处理mime类型
tokio = {version="1",features=["full"]} #异步处理库

为main函数添加宏#[tokio::main],使其变为异步函数。

#[tokio::main]
async fn main() -> Result<()> {
    let opts: Opts = Opts::parse();
    // 生成一个HTTP客户端
    let client = Client::new();
    match opts.subcmd {
        SubCommand::Get(ref args) => get(client, args).await?,
        SubCommand::Post(ref args) => post(client, args).await?,
    };
    Ok(())
}

get 和 post 也设置为异步函数:

async fn get(client: Client, args: &Get) -> Result<()> {
    let resp = client.get(&args.url).send().await?;
    println!("{:?}", resp.text().await?);
    Ok(())
}

async fn post(client: Client, args: &Post) -> Result<()> {
    let mut body = HashMap::new();
    for pair in args.body.iter() {
        body.insert(&pair.k, &pair.v);
    }
    let resp = client.post(&args.url).json(&body).send().await?;
    println!("{:?}", resp.text().await?);
    Ok(())
}

优化响应

用不同的颜色打印HTTP header 和 HTTP body。

前置条件,需要在Cargo.toml文件中,增加以下依赖,实现 HTTP header和body的高亮区分。

[dependencies]
mime = "0.3" #处理mime类型
colored = "2" #命令终端多彩显示
jsonxf = "1.1" #JSON pretty print 格式化
syntect = "4" # 语法高亮

优化HTTP header打印:

// 打印服务器返回的 HTTP header
fn print_headers(resp: &Response) {
    for (name, value) in resp.headers() {
        println!("{}: {:?}", name.to_string().green(), value);
    }
    println!();
}
// 打印服务器版本号 + 状态码
fn print_status(resp: &Response) {
    let status = format!("{:?} {}", resp.version(), resp.status()).blue();
    println!("{}\n", status);
}
// 将服务器返回的 content-type 解析成 Mime 类型
fn get_content_type(resp: &Response) -> Option<Mime> {
    resp.headers()
        .get(header::CONTENT_TYPE)
        .map(|v| v.to_str().unwrap().parse().unwrap())
}

优化HTTP body 打印:

fn print_syntect(s: &str, ext: &str) {
    // Load these once at the start of your program
    let ps = SyntaxSet::load_defaults_newlines();
    let ts = ThemeSet::load_defaults();
    let syntax = ps.find_syntax_by_extension(ext).unwrap();
    let mut h = HighlightLines::new(syntax, &ts.themes["base16-ocean.dark"]);
    for line in LinesWithEndings::from(s) {
        let ranges = h.highlight(line, &ps);
        let escaped = as_24_bit_terminal_escaped(&ranges[..], true);
        print!("{}", escaped);
    }
}

/// 打印服务器返回的 HTTP body
fn print_body(m: Option<Mime>, body: &str) {
    match m {
        // 对于 "application/json" pretty print
        Some(v) if v == mime::APPLICATION_JSON => print_syntect(body, "json"),
        Some(v) if v == mime::TEXT_HTML => print_syntect(body, "html"),

        // 其他 mime type 直接输出
        _ => println!("{}", body),
    }
}

/// 打印整个响应
async fn print_resp(resp: Response) -> Result<()> {
    print_status(&resp);
    print_headers(&resp);
    let mime = get_content_type(&resp);
    let body = resp.text().await?;
    print_body(mime, &body);
    Ok(())
}

修改post和 get函数,让其返回优化的响应信息:

/// 处理 get 子命令
async fn get(client: Client, args: &Get) -> Result<()> {
    let resp = client.get(&args.url).send().await?;
    Ok(print_resp(resp).await?)
}

/// 处理 post 子命令
async fn post(client: Client, args: &Post) -> Result<()> {
    let mut body = HashMap::new();
    for pair in args.body.iter() {
        body.insert(&pair.k, &pair.v);
    }
    let resp = client.post(&args.url).json(&body).send().await?;
    Ok(print_resp(resp).await?)
}
    
    /// 程序的入口函数,因为在http请求时我们使用了异步处理,所以这里引入tokio
#[tokio::main]
async fn main() -> Result<()> {
    let opts: Opts = Opts::parse();
    let mut headers = header::HeaderMap::new();
    // 为HTTP客户端添加一些缺省的HTTP头
    headers.insert("X-POWERED-BY", "Rust".parse()?);
    headers.insert(header::USER_AGENT, "Rust Httpie".parse()?);
    // 生成一个HTTP客户端
    let client = Client::builder().default_headers(headers).build()?;
    let result = match opts.subcmd {
        SubCommand::Get(ref args) => get(client, args).await?,
        SubCommand::Post(ref args) => post(client, args).await?,
    };
    Ok(result)
}

验证,输入以下命令:

cargo build --quiet
 target/debug/httpie post https://httpbin.org/post greeting=bin name=soft

可以看到有颜色优化的输出结果:



添加Test

仅在cargo test时才能编译

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_print_status() {
        assert!(parse_url("abc").is_err());
        assert!(parse_url("http://abc.xxy").is_ok());
        assert!(parse_url("https://httpbin.org/post").is_ok());
    }

    #[test]
    fn test_print_headers() {
        assert!(parse_kv_pair("a").is_err());
        assert_eq!(
            parse_kv_pair("a=1").unwrap(),
            KvPair {
                k: "a".into(),
                v: "1".into()
            }
        );

        assert_eq!(
            parse_kv_pair("a=").unwrap(),
            KvPair {
                k: "a".into(),
                v: "".into()
            }
        );
    }
}

通过IDE运行Test case,测试通过:



相关推荐

Excel新函数TEXTSPLIT太强大了,轻松搞定数据拆分!

我是【桃大喵学习记】,欢迎大家关注哟~,每天为你分享职场办公软件使用技巧干货!最近我把WPS软件升级到了版本号:12.1.0.15990的最新版本,最版本已经支持文本拆分函数TEXTSPLIT了,并...

Excel超强数据拆分函数TEXTSPLIT,从入门到精通!

我是【桃大喵学习记】,欢迎大家关注哟~,每天为你分享职场办公软件使用技巧干货!今天跟大家分享的是Excel超强数据拆分函数TEXTSPLIT,带你从入门到精通!TEXTSPLIT函数真是太强大了,轻松...

看完就会用的C++17特性总结(c++11常用新特性)

作者:taoklin,腾讯WXG后台开发一、简单特性1.namespace嵌套C++17使我们可以更加简洁使用命名空间:2.std::variant升级版的C语言Union在C++17之前,通...

plsql字符串分割浅谈(plsql字符集设置)

工作之中遇到的小问题,在此抛出问题,并给出解决方法。一方面是为了给自己留下深刻印象,另一方面给遇到相似问题的同学一个解决思路。如若其中有写的不好或者不对的地方也请不加不吝赐教,集思广益,共同进步。遇到...

javascript如何分割字符串(javascript切割字符串)

javascript如何分割字符串在JavaScript中,您可以使用字符串的`split()`方法来将一个字符串分割成一个数组。`split()`方法接收一个参数,这个参数指定了分割字符串的方式。如...

TextSplit函数的使用方法(入门+进阶+高级共八种用法10个公式)

在Excel和WPS新增的几十个函数中,如果按实用性+功能性排名,textsplit排第二,无函数敢排第一。因为它不仅使用简单,而且解决了以前用超复杂公式才能搞定的难题。今天小编用10个公式,让你彻底...

Python字符串split()方法使用技巧

在Python中,字符串操作可谓是基础且关键的技能,而今天咱们要重点攻克的“堡垒”——split()方法,它能将看似浑然一体的字符串,按照我们的需求进行拆分,极大地便利了数据处理与文本解析工作。基本语...

go语言中字符串常用的系统函数(golang 字符串)

最近由于工作比较忙,视频有段时间没有更新了,在这里跟大家说声抱歉了,我尽快抽些时间整理下视频今天就发一篇关于go语言的基础知识吧!我这我工作中用到的一些常用函数,汇总出来分享给大家,希望对...

无规律文本拆分,这些函数你得会(没有分隔符没规律数据拆分)

今天文章来源于表格学员训练营群内答疑,混合文本拆分。其实拆分不难,只要规则明确就好办。就怕规则不清晰,或者规则太多。那真是,Oh,mygod.如上图所示进行拆分,文字表达实在是有点难,所以小熊变身灵...

Python之文本解析:字符串格式化的逆操作?

引言前面的文章中,提到了关于Python中字符串中的相关操作,更多地涉及到了字符串的格式化,有些地方也称为字符串插值操作,本质上,就是把多个字符串拼接在一起,以固定的格式呈现。关于字符串的操作,其实还...

忘记【分列】吧,TEXTSPLIT拆分文本好用100倍

函数TEXTSPLIT的作用是:按分隔符将字符串拆分为行或列。仅ExcelM365版本可用。基本应用将A2单元格内容按逗号拆分。=TEXTSPLIT(A2,",")第二参数设置为逗号...

Excel365版本新函数TEXTSPLIT,专攻文本拆分

Excel中字符串的处理,拆分和合并是比较常见的需求。合并,当前最好用的函数非TEXTJOIN不可。拆分,Office365于2022年3月更新了一个专业函数:TEXTSPLIT语法参数:【...

站长在线Python精讲使用正则表达式的split()方法分割字符串详解

欢迎你来到站长在线的站长学堂学习Python知识,本文学习的是《在Python中使用正则表达式的split()方法分割字符串详解》。使用正则表达式分割字符串在Python中使用正则表达式的split(...

Java中字符串分割的方法(java字符串切割方法)

技术背景在Java编程中,经常需要对字符串进行分割操作,例如将一个包含多个信息的字符串按照特定的分隔符拆分成多个子字符串。常见的应用场景包括解析CSV文件、处理网络请求参数等。实现步骤1.使用Str...

因为一个函数strtok踩坑,我被老工程师无情嘲笑了

在用C/C++实现字符串切割中,strtok函数经常用到,其主要作用是按照给定的字符集分隔字符串,并返回各子字符串。但是实际上,可不止有strtok(),还有strtok、strtok_s、strto...