Rust编程:写个实用的CLI小工具 rust cli
itomcoil 2024-12-19 13:44 36 浏览
功能
- 命令行解析,处理子命令及各种参数
- 验证用户输入
- 将用户输入转换为我们内部能够理解的参数
- 利用解析好的参数,发送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没有的功能,可能在beta和nightly中就有了。
首先利用以下命令,查看beta和nightly版本是否有安装:
rustup toolchain list
如果没有安装,则通过以下命令,安装beta和nightly版本:
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,测试通过:
相关推荐
- selenium(WEB自动化工具)
-
定义解释Selenium是一个用于Web应用程序测试的工具。Selenium测试直接运行在浏览器中,就像真正的用户在操作一样。支持的浏览器包括IE(7,8,9,10,11),MozillaF...
- 开发利器丨如何使用ELK设计微服务中的日志收集方案?
-
【摘要】微服务各个组件的相关实践会涉及到工具,本文将会介绍微服务日常开发的一些利器,这些工具帮助我们构建更加健壮的微服务系统,并帮助排查解决微服务系统中的问题与性能瓶颈等。我们将重点介绍微服务架构中...
- 高并发系统设计:应对每秒数万QPS的架构策略
-
当面试官问及"如何应对每秒几万QPS(QueriesPerSecond)"时,大概率是想知道你对高并发系统设计的理解有多少。本文将深入探讨从基础设施到应用层面的解决方案。01、理解...
- 2025 年每个 JavaScript 开发者都应该了解的功能
-
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发。1.Iteratorhelpers开发者...
- JavaScript Array 对象
-
Array对象Array对象用于在变量中存储多个值:varcars=["Saab","Volvo","BMW"];第一个数组元素的索引值为0,第二个索引值为1,以此类推。更多有...
- Gemini 2.5编程全球霸榜,谷歌重回AI王座,神秘模型曝光,奥特曼迎战
-
刚刚,Gemini2.5Pro编程登顶,6美元性价比碾压Claude3.7Sonnet。不仅如此,谷歌还暗藏着更强的编程模型Dragontail,这次是要彻底翻盘了。谷歌,彻底打了一场漂亮的翻...
- 动力节点最新JavaScript教程(高级篇),深入学习JavaScript
-
JavaScript是一种运行在浏览器中的解释型编程语言,它的解释器被称为JavaScript引擎,是浏览器的一部分,JavaScript广泛用于浏览器客户端编程,通常JavaScript脚本是通过嵌...
- 一文看懂Kiro,其 Spec工作流秒杀Cursor,可移植至Claude Code
-
当Cursor的“即兴编程”开始拖累项目质量,AWS新晋IDEKiro以Spec工作流打出“先规范后编码”的系统工程思维:需求-设计-任务三件套一次生成,文档与代码同步落地,复杂项目不...
- 「晚安·好梦」努力只能及格,拼命才能优秀
-
欢迎光临,浏览之前点击上面的音乐放松一下心情吧!喜欢的话给小编一个关注呀!Effortscanonlypass,anddesperatelycanbeexcellent.努力只能及格...
- JavaScript 中 some 与 every 方法的区别是什么?
-
大家好,很高兴又见面了,我是姜茶的编程笔记,我们一起学习前端相关领域技术,共同进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力在JavaScript中,Array.protot...
- 10个高效的Python爬虫框架,你用过几个?
-
小型爬虫需求,requests库+bs4库就能解决;大型爬虫数据,尤其涉及异步抓取、内容管理及后续扩展等功能时,就需要用到爬虫框架了。下面介绍了10个爬虫框架,大家可以学习使用!1.Scrapysc...
- 12个高效的Python爬虫框架,你用过几个?
-
实现爬虫技术的编程环境有很多种,Java、Python、C++等都可以用来爬虫。但很多人选择Python来写爬虫,为什么呢?因为Python确实很适合做爬虫,丰富的第三方库十分强大,简单几行代码便可实...
- pip3 install pyspider报错问题解决
-
运行如下命令报错:>>>pip3installpyspider观察上面的报错问题,需要安装pycurl。是到这个网址:http://www.lfd.uci.edu/~gohlke...
- PySpider框架的使用
-
PysiderPysider是一个国人用Python编写的、带有强大的WebUI的网络爬虫系统,它支持多种数据库、任务监控、项目管理、结果查看、URL去重等强大的功能。安装pip3inst...
- 「机器学习」神经网络的激活函数、并通过python实现激活函数
-
神经网络的激活函数、并通过python实现whatis激活函数感知机的网络结构如下:左图中,偏置b没有被画出来,如果要表示出b,可以像右图那样做。用数学式来表示感知机:上面这个数学式子可以被改写:...
- 一周热门
- 最近发表
- 标签列表
-
- ps图案在哪里 (33)
- super().__init__ (33)
- python 获取日期 (34)
- 0xa (36)
- super().__init__()详解 (33)
- python安装包在哪里找 (33)
- linux查看python版本信息 (35)
- python怎么改成中文 (35)
- php文件怎么在浏览器运行 (33)
- eval在python中的意思 (33)
- python安装opencv库 (35)
- python div (34)
- sticky css (33)
- python中random.randint()函数 (34)
- python去掉字符串中的指定字符 (33)
- python入门经典100题 (34)
- anaconda安装路径 (34)
- yield和return的区别 (33)
- 1到10的阶乘之和是多少 (35)
- python安装sklearn库 (33)
- dom和bom区别 (33)
- js 替换指定位置的字符 (33)
- python判断元素是否存在 (33)
- sorted key (33)
- shutil.copy() (33)