陈天 · Rust 编程第一课
陈天
Tubi TV 研发副总裁
23195 人已学习
新⼈⾸单¥68
登录后,你可以任选4讲全文学习
课程目录
已完结/共 65 讲
基础篇 (21讲)
陈天 · Rust 编程第一课
15
15
1.0x
00:00/00:00
登录|注册

04|get hands dirty:来写个实用的CLI小工具

完善HTTPie的语法高亮
下一讲的挑战
撰写代码的过程中感受Rust处理问题的方式
Rust的表现力
打印HTTP响应
发送GET和POST请求
异步处理
验证URL和body
使用clap库进行命令行解析
格式化输出
发送HTTP请求
命令行解析
用Rust实现HTTPie
用Python开发的CLI工具
特别说明
思考题
小结
HTTP请求
CLI处理
功能分析
HTTPie
用Rust写三个实际价值的小应用
"learning by example"挑战
Rust基本语法
主要内容
本文作者:陈天
get hands dirty:来写个实用的CLI小工具

该思维导图由 AI 生成,仅供参考

你好,我是陈天。
在上一讲里,我们已经接触了 Rust 的基本语法。你是不是已经按捺不住自己的洪荒之力,想马上用 Rust 写点什么练练手,但是又发现自己好像有点“拔剑四顾心茫然”呢?
那这周我们就来玩个新花样,做一周“learning by example”的挑战,来尝试用 Rust 写三个非常有实际价值的小应用,感受下 Rust 的魅力在哪里,解决真实问题的能力到底如何。
你是不是有点担心,我才刚学了最基本语法,还啥都不知道呢,这就能开始写小应用了?那我碰到不理解的知识怎么办?
不要担心,因为你肯定会碰到不太懂的语法,但是,先不要强求自己理解,当成文言文抄写就可以了,哪怕这会不明白,只要你跟着课程节奏,通过撰写、编译和运行,你也能直观感受到 Rust 的魅力,就像小时候背唐诗一样。
好,我们开始今天的挑战。

HTTPie

为了覆盖绝大多数同学的需求,这次挑选的例子是工作中普遍会遇到的:写一个 CLI 工具,辅助我们处理各种任务。
我们就以实现 HTTPie 为例,看看用 Rust 怎么做 CLI。HTTPie 是用 Python 开发的,一个类似 cURL 但对用户更加友善的命令行工具,它可以帮助我们更好地诊断 HTTP 服务。
下图是用 HTTPie 发送了一个 post 请求的界面,你可以看到,相比 cURL,它在可用性上做了很多工作,包括对不同信息的语法高亮显示:
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文以实际示例向读者展示了如何使用Rust编写一个实用的CLI小工具,以HTTPie为例进行讲解。作者首先分析了实现HTTPie所需的主要功能,包括命令行解析、发送HTTP请求和友好输出响应。随后介绍了相关库的使用,如clap用于命令行解析、reqwest用于HTTP客户端等。通过示例代码,读者可以了解如何利用Rust的特性来实现类似HTTPie这样的实用工具。文章突出了Rust语言的简洁和强大特点,展示了如何通过宏、trait、泛型函数等工具编写结构良好、容易维护的代码。通过添加验证功能,作者展示了如何使CLI更加健壮,并符合软件开发的开闭原则。这篇文章对于想要学习Rust语言并实践的读者来说,提供了一个很好的学习范例。文章还通过代码行数统计展示了Rust的高效性,最后鼓励读者在未来的课程中继续体验Rust的魅力。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《陈天 · Rust 编程第一课》
新⼈⾸单¥68
立即购买
登录 后留言

全部留言(115)

  • 最新
  • 精选
  • 胖胖的奥利奥
    置顶
    2023年 1 月 28 日可運行代碼: extern crate clap; use anyhow::{anyhow, Ok, Result}; use clap::Parser; use reqwest::Url; use std::str::FromStr; // 定义 HTTPie 的 CLI 主入口,包含多个命令 // 下面 /// 的注释是文档, clap 会将其当成是 CLI 的帮助 /// A naive httpie implementation wite Rust, can you imagine how easy it is? #[derive(Parser, Debug)] struct Opts { #[clap(subcommand)] subcmd: SubCommand, } /// 子命令分别对应不同的 HTTP 方法,暂时只支持 GET / POST 方法 #[derive(Parser, Debug)] enum SubCommand { Get(Get), Post(Post), } #[derive(Parser, Debug)] struct Get { #[arg(value_parser=parse_url)] url: String, } #[derive(Parser, Debug)] struct Post { #[arg(value_parser=parse_url)] url: String, #[arg(value_parser=parse_kv_pair)] body: Vec<KvPair>, } #[derive(Debug, Clone, PartialEq)] struct KvPair { k: String, v: String, } impl FromStr for KvPair { type Err = anyhow::Error; fn from_str(s: &str) -> Result<Self, Self::Err> { // 使用 = 进行 split,这会得到一个迭代器 let mut split = s.split('='); let err = || anyhow!(format!("Failed to parse {}", s)); Ok(Self { // 从迭代器中取第一个结果作为 key,迭代器返回 Some(T)/None // 我们将其转换成 Ok(T)/Err(E),然后用 ? 处理错误 k: (split.next().ok_or_else(err)?).to_string(), // 从迭代器中取第二个结果作为 value v: (split.next().ok_or_else(err)?).to_string(), }) } } fn parse_kv_pair(s: &str) -> Result<KvPair> { s.parse() } fn parse_url(s: &str) -> Result<String> { // check url let _url: Url = s.parse()?; Ok(s.into()) } fn main() { let opts = Opts::parse(); println!("{:?}", opts); }
    2023-01-28归属地:广东
    2
    7
  • Faith信
    置顶
    rustc 1.58.1 不能编译的参考老师github代码依赖修改

    编辑回复: 👍

    2022-07-20
    1
  • linuxfish
    置顶
    遇到一个问题,提醒下刚开始学的同学: 老师使用了 clap 包的 Pre-releases 版本,Pre-releases 版本并不保证 API 的稳定。 cargo 在安装依赖的时候会自动使用【最新】的 Pre-releases 版本(尽管你在 Cargo.toml 中指定了一个老版本) 当前 clap 包的最新版本是 v3.0.0-beta.5,若按照课程(get hands dirty:来写个实用的CLI小工具)里步骤操作,会编译不过。 不过老师在 Github 上的代码已经更新成依赖 v3.0.0-beta.5,可以照着那个写。 当然,还是建议把课程里的代码也更新下,或者用红字提示下【代码已过期,请参考 Github 上的最新代码】,不然新手会比较懵逼 参考:https://doc.rust-lang.org/cargo/reference/resolver.html#pre-releases

    作者回复: 谢谢!github 上的已经更新到 clap 3 正式版本了

    2021-12-29
    3
    5
  • Quincy
    置顶
    /// 打印服务器返回的 HTTP body fn print_body(m: Option<Mime>, body: &String) { match m { // 对于 "application/json" 我们 pretty print Some(v) if v == mime::APPLICATION_JSON => { // println!("{}", jsonxf::pretty_print(body).unwrap().cyan()) print_syntect(body); } // 其他 mime type,我们就直接输出 _ => println!("{}", body), } } fn print_syntect(s: &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("json").unwrap(); let mut h = HighlightLines::new(syntax, &ts.themes["base16-ocean.dark"]); for line in LinesWithEndings::from(s) { let ranges: Vec<(Style, &str)> = h.highlight(line, &ps); let escaped = as_24_bit_terminal_escaped(&ranges[..], true); println!("{}", escaped); } }

    作者回复: 非常棒!你似乎是第一个贴出来思考题答案的!我也更新了一下代码库,和你的代码基本一样你可以对比一下。两个点:1. print_syntect 可以再加一个参数 ext,这样灵活性更高;2. 打印时用 print! 效果更好一些。

    2021-08-31
    14
  • 王槐铤
    置顶
    环境 cargo --version cargo 1.52.0 (69767412a 2021-04-21) rustc --version rustc 1.52.1 (9bc8c42bb 2021-05-09) 编译程序代码 clap 库部分报 8 | #![doc = include_str!("../README.md")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 把 Cargo.toml 里 clap 依赖 clap = "=3.0.0-beta.4" 改为 clap = "=3.0.0-beta.2" clap_derive = "=3.0.0-beta.2" 即可通过 具体原因 详见 https://github.com/dfinity/agent-rs/pull/260

    作者回复: 👍

    2021-08-30
    2
    9
  • Tyr
    置顶
    这堂课的源代码可以在这里找到:https://github.com/tyrchen/geektime-rust/tree/master/04_httpie

    作者回复: 有同学反应不能编译通过,问题出在 clap 上。请升级到 Rust 1.54 重新编译。 另外,reqwest 默认的 TLS 使用的是系统的 openssl,对于某些 linux 用户如果没有安装好 openssl,可能会导致编译不过,对此,你可以修改 Cargo.toml: ```toml reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] } # HTTP 客户端 ``` 让 reqwest 使用 rustls。

    2021-08-30
    10
    21
  • qinsi
    习惯了 npm install 的可以试试 cargo-edit: $ cargo install cargo-edit $ cargo add anyhow colored jsonxf mime $ cargo add clap --allow-prerelease $ cargo add reqwest --features json $ cargo add tokio --features full

    作者回复: 好建议!

    2021-08-30
    3
    51
  • Arthur
    对Rust里的derive, impl, trait等概念,和Java/C++中面向对象编程概念里的封装、继承、方法等概念,有怎样的类比和不同,一直模糊不清,希望老师后面能讲到

    作者回复: 继承的概念,在 Rust 里是没有的。而且我们要避免使用这样的思维去建模。很多时候我们提到继承,其实并不是想用继承,而是想通过继承使用多态。在 Rust 下我们可以通过泛型,trait,trait object 实现面向对象语言中主要的多态手段。 封装在任何编程语言中都或多或少存在,它本质上是一种对复杂性的控制。Rust 下 struct / enum / mod 都提供了封装的能力。把若干数据放在某个结构下,若干函数放在摸个模块下,只暴露该暴露的信息出去,这就是封装。 方法可以理解为第一个参数为 self/this 这样的指向调用者自己的特殊函数,在 Python、Javascript 都有类似的概念。Rust 下可以为数据结构 impl 方法,或者 impl trait 来实现接口的方法。注意 Rust 下的方法可以消费 self(第一个参数是 self 而非 &self),其它大多数语言只能消费 &self。

    2021-09-05
    13
  • Marvichov
    查了下colorize trait的doc (https://docs.rs/colored/2.0.0/colored/trait.Colorize.html), 没看到这个trait impl for String啊, 为啥可以call blue on String type呢? ``` format!("{:?} {}", resp.version(), resp.status()).blue(); ``` 老师知道这里面发生了什么转换么?

    作者回复: 注意看 Colorize trait 的定义,它的方法 consume 的都是 self,而非 &self。所以当 impl Colorize for &str 时,self = &str。 在调用方法时,编译器会先看数据结构是否有对应的方法,如果有,按照方法的 signature,传 self / &self / &mut self。如果没有,再看引入的 trait 是否有对应的方法,必要时会根据 self 的类型做 auto Deref。所以这里编译器可以找到 blue(),因为它第一个参数 self = &str,String 可以 Deref 到 &str,所以可以调用。但如果 Colorize 的方法使用 &self,此时 &self = &&str,String 无法 Deref 到 &&str,所以编译器报错。 你可以看这个小例子: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=dbd526df998fc8701ea9855c9e7de73c ```rust pub trait Foo { fn foo(self); } pub trait Bar { fn bar(&self); } pub trait Baz { fn baz(&self); } impl Foo for &str { fn foo(self) { println!("Foo: {}", self); } } impl Bar for &str { fn bar(&self) { println!("Bar: {}", self); } } impl Baz for str { fn baz(&self) { println!("Baz: {}", self); } } fn main() { let s = String::from("Tyr"); // foo 第一个参数是 self = &str,String 可以 auto Deref 到 &str,所以可以调用 s.foo(); // bar 第一个参数是 &self = &&str,String 无法 auto Deref 到 &&str // s.bar(); // baz 第一个参数是 &self,但因为 impl Baz for str {}, 所以 &self = &str // 和 foo 类似,可以调用 s.baz(); } ```

    2021-09-07
    2
    12
  • qinsi
    疑问:查了下reqwest似乎是依赖tokio运行时的,是否意味着用了reqwest就必须用tokio而不能用其他的运行时比如async-std?

    作者回复: 如果想用 async-std,可以用 surf。我建议不要在 async-std 下花时间了。tokio 生态已经占据了绝对优势。

    2021-08-30
    12
收起评论
显示
设置
留言
99+
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部