Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

0. 前言

Lingling insurance

自己刚开始学习Rust不久, 写这个简单的cli也是为了练手, 代码不优雅也请各路大神指点, 我定虚心求教! 起初接触到rust是因为WebAssembly, 有不少人用rust编译为WSAM, 作为一个合格的前端娱乐圈工作者, 还是希望对此有一定的了解.

该CLI实现一个持久化的TODO list. 那废话不多说, 咱们开始吧!

1. 开始

1.1 rust安装

如果你是用的是Macos或者是linux都可以简单的通过下面的命令安装rust

curl <https://sh.rustup.rs> -sSf | sh

如果你使用的window系统, 可以上rustup下载对应exe文件.

1.2 cargo的使用

cargorust包内自带的包管理工具, 类似npm, 同时也提供了一些cli来编译或者初始化一个项目

cargo init

// 初始化一个项目
cargo init xxx-project 

执行结束以后会在执行目录下生产下面的目录结构

|
  .git/
  |
  .gitignore
  |
  Cargo.toml
  |
  src/

其中需要解释的是Cargo.toml文件, 其作用和npmpackage.json相同, 对项目信息和包依赖进行一些说明

[package]
name = "rust-cli"
version = "0.1.0"
authors = ["rogerjluo"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
serde = { version = "1.0", features = ["derive"] }
clap = "~2.32"
serde_json = "1.0"
chrono = "0.4"

cargo build

改命令是将项目进行预编译, 安装你在toml文件中的依赖至target文件夹内, 并生成或者更新Cargo.lock文件对依赖的版本进行管理(作用类似package-lock.json).

cargo build
   Compiling proc-macro2 v1.0.24
   Compiling autocfg v1.0.1
    ...
    ...
   Compiling chrono v0.4.19
   Compiling rust-cli v0.1.0 (/Users/rogerjluo/Documents/project/rust-cli)
    Finished dev [unoptimized + debuginfo] target(s) in 28.25s

cargo run build

编译后执行二进制文件, 比如在src/main.rs文件中写入

fn main () {
  println!("Hello World!")
}
cargo run build
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/rust-cli build`
Hello World!

2. 文件存储和读取

因为我们要做一个持久化数据的todolist, 所以文件操作是必不可少的, 这里就用到了标准库中的std::io::{Read, Write}以及std::fs.

首先我们需要声明一些struct

struct InternalFile {
  path: String,
  pub data: String,
  pub actual_size: usize,
}

pub struct FileSystem {
  files: HashMap<String, InternalFile>,
}

写文件

这里使用HashMap的原因是不希望多次写文件, 所以将多次写入操作合并之后再写入.

// 将写入数据先写入到内存中
pub fn write(&mut self, path: &str, data: &str) {
  self.files.insert(
    calculate_hash(&path.to_string().as_bytes()),
    InternalFile {
      path: path.to_string(),
      data: data.to_string(),
      actual_size: data.len(),
    },
  );
}

// 将文件系统数据转存到文件
pub fn save<T: AsRef<Path>>(&self, path: T) {
  let mut f = fs::File::create(&path).unwrap();
  for (_, file) in &self.files {
    f.write(&file.data.as_bytes()).unwrap();
  }
}

读文件

pub fn load(&self, path: String) -> String {
  let path_name = Path::new(&path);
  let display = path_name.display();

  let mut file = match File::open(&path_name) {
    Ok(file) => file,
    Err(_why) => {
      // 如果文件不存在则创建一个文件
      self.save(path);
      panic!("文件不存在, 已经新建存储文件, 请重试");
    },
  };

  let mut s = String::new();
  match file.read_to_string(&mut s) {
    Err(why) => panic!("无法打开文件 {}: {}", display,
    why.to_string()),
    Ok(_) => println!(""),
  }
  s // 返回文件内容
}

3. JSON的序列化和反序列化

当然为了方便对文件内容进行操作, 我们对写入和读取进行了序列化和反序列的过程, 使用到的是serde_json, 实现类似JS中的JSON.parse以及JSON.stringify

序列化

serde_json::to_string(TodoList)

反序列化

let data: TodoList = match serde_json::from_str(&file) {
  Ok(x) => x,
  Err(_why) => {
    // 如果文件为空或者出错, 则创建一个空的list返回
    TodoList {
      todos: HashMap::new(),
    }
  },
};

这里我们是可以指定反序列化的目标结构, 如果结构不匹配则无法完成反序列化, 但是前提是需要在struct上加上一些声明

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct TodoTask {
  content: String,
  is_done: bool,
  c_time: String,
  u_time: String,
  index: String,
}

#[derive(Serialize, Deserialize)]
pub struct TodoList {
  pub todos: HashMap<String, Vec<TodoTask>>
}

4. clap

基础的功能有了, 接下来我们需要获取命令行中传入的参数并执行不同的逻辑, cargocargo run -- [参数]可以将参数传入到程序中, 但我们这里将使用一个叫做clap的库来简化这个过程.

let help_instru = r#"命令名:
    add(a)          -p [Content]                  : 添加当天的todo
    list_all(la)                                  : 打印全部todo
    list_daily(ld)  -p [Date: YYYY-MM-DD]         : 打印某一天的todo, 不带参数默认打印当天
    remove(rm)      -p [Date: YYYY-MM-DD] [Index] : 删除某一项todo
    toggle(t)       -p [Date: YYYY-MM-DD] [Index] : 改变某一项todo的状态, 完成/待完成
    list_week(lw)   -p [Date: YYYY-MM-DD]         : 打印某一周的todo, 方便生成周报
  "#;
  let matches = App::new("Todo")
    .version("0.0.1")
    .author("rogerjluo")
    .about("rust实现的todolist")
    .arg(Arg::with_name("MODE")
      .short("m")
      .long("mode")
      .takes_value(true)
      .help(help_instru))
    .arg(Arg::with_name("PARAMETERS")
      .short("p")
      .long("params")
      .multiple(true)
      .takes_value(true)
      .help("参数"))
    .get_matches();

进行了声明之后, 我们如何在程序中获取到传入的参数呢

// 如果是参数只支持单个value, 此处match的名字与之前声明的Arg::with_name("MODE")对应
if let Some(mode) = matches.value_of("MODE") {
  match mode {
    "add" => {

    },
    "list_all" => {

    },
    // ....
    _ => println!("没有此命令")
  }
}

// 支持multiple value
if let Some(params) = matches.values_of("PARAMETERS") {
  let p_list: Vec<&str> = params.collect();
  let date = p_list[0]; // 参数一
  let index = p_list[1]; // 参数二
} else {
  println!("请输入参数: -p [Date: YYYYMMDD] [Index] \n")
}

5. 举个栗子 🌰

这里我们只列举其中的两个具体实现~ 如果感兴趣可以去仓库中查看别的代码, 代码地址在文末.

写入一项todo

这里我们只写入当天的todo, 所以传入的参数只需要写入的内容即可, 命令为: cargo run -- -m add -p HelloWorld

"add" => {
  if let Some(params) = matches.values_of("PARAMETERS") {
    let p_list: Vec<&str> = params.collect();
    let content = p_list[0];
    // add_task的作用就是将新的todo插入到对应日期的todo列表中, 这里就不列举代码了
    let newlist = task::add_task(data, content.to_string());
    file_system.write("todo", &serde_json::to_string(&TodoList {
      todos: newlist
    }).unwrap());
    file_system.save(file_name);
  } else {
    println!("请输入参数: -p [Content] \n")
  }
},

执行后, 我们会看到指定写入文件内会有下面的信息

{"todos":{"2020-12-12":[{"content":"HelloWorld","is_done":false,"c_time":"2020-12-12 07:21:31","u_time":"2020-12-12 07:21:31","index":"0"}]}}

列举某一天所在周的全部todo

我们要实现这个功能, 需要计算出某一日期所在的周在该年中的index, 这里借助了chrono库提供的api实现

use chrono::prelude::*;
use chrono::{NaiveDate};
pub fn get_weekday_index(date: &str) -> u32 {
  let utc = NaiveDate::parse_from_str(date, "%Y-%m-%d").unwrap();
  // utc.ordinal()可以获取日期在该年中的index
  let day_index = utc.ordinal();
  (day_index / 7) + 1
}
"list_week" | "lw" => {
  let date;
  // 这里判断是否传入日期参数, 没有传入默认为当天
  if let Some(params) = matches.values_of("PARAMETERS") {
    let p_list: Vec<&str> = params.collect();
    date = p_list[0].to_string();
  } else {
    date = util::get_cur_ymd();
  }
  let week_index = util::get_weekday_index(&date);
  for val in data.todos.keys() {
    let c_week_index = util::get_weekday_index(val);
    // 判断是否在同一周, 如果是则打印
    if c_week_index == week_index {
      task::list_daily(&data, val.to_string());
    }
  }
},

执行命令为: cargo run -- -m lw

cargo run -- -m lw               
   Compiling rust-cli v0.1.0 (/Users/rogerjluo/Documents/project/rust-cli)
    Finished dev [unoptimized + debuginfo] target(s) in 2.00s
     Running `target/debug/rust-cli -m lw`

2020-12-12:

0. [Need Handle] HelloWorld

6. 结尾

文章只是介绍了项目中的部分代码以及功能, 完整版已经实现的功能包括:

  • 写入新的todo
  • 更改todo状态, 待处理和已完成间进行切换
  • 删除某个todo
  • 列出所有todo
  • 列出本周的todo list
  • 列出某一天的todo list

(这里没有列出所有的代码, 如果你感兴趣, 请移步到: git) 如果觉得还不错, 帮忙点个star吧, 初学者举步维艰

如果大家对代码有什么建议的话希望能告诉我, 这将继续鞭策我前进, 感谢~

About

Simple TODO CLI powered by rust

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages