Rust๋ก todo CLI ๋ง๋ค๊ธฐ ๐ฅ
34538 ๋จ์ด beginnersprogrammingrusttutorial
์์ํ์!
์์ํ๊ธฐ
๋จผ์ Cargo๋ก ์ ํ๋ก์ ํธ๋ฅผ ๋ง๋ญ๋๋ค.
cargo new todo
๊ทธ๋ฐ ๋ค์
Cargo.toml
ํ์ผ์ ๋ค์ ์ข
์์ฑ์ ์ถ๊ฐํฉ๋๋ค.chrono = "0.4.22"
colorize = "0.1.0"
rand = "0.8.5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.85"
๊ฐ ์ข ์์ฑ์ด ์ํํ๋ ์์ ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
chrono
๋ ํ์ฌ ๋ ์ง์ ์๊ฐ์ ๊ฐ์ ธ์ค๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค. colorize
๋ ์ถ๋ ฅ ์์์ ์ง์ ํ๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค. rand
๋ ์์์ ID๋ฅผ ์์ฑํ๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค. serde
๋ฐ serde_json
๋ JSON ํ์ผ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค. ํด๋ ๊ตฌ์กฐ ๋ง๋ค๊ธฐ
src
ํด๋๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.src
โฃ app
โ โ mod.rs # The app module
โฃ structs
โ โ mod.rs # The structs
โฃ todo
โ โ mod.rs # Todo related functions
โฃ utils
โ โ mod.rs # Utility functions
โ main.rs # The main file
์ด์
main.rs
ํ์ผ์์ ๋ชจ๋ ๋ชจ๋์ ๊ฐ์ ธ์ต๋๋ค.mod utils;
mod structs;
mod todo;
mod app;
fn main() {
// ...
}
๊ตฌ์กฐ์ฒด ๋ง๋ค๊ธฐ
์์ํ๊ธฐ ์ ์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ๋ฐ ์ฌ์ฉํ ๊ตฌ์กฐ์ฒด๋ฅผ ๋ง๋ค์ด ๋ณด๊ฒ ์ต๋๋ค.
structs/mod.rs
ํ์ผ์์ ๋ค์ ๊ตฌ์กฐ์ฒด๋ฅผ ์์ฑํฉ๋๋ค.use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Todo {
pub created_at: String,
pub title: String,
pub done: bool,
pub id: u32,
pub updated_at: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ConfigFile {
pub data: Vec<Todo>,
}
์ ํธ๋ฆฌํฐ ํจ์ ์์ฑ
utils/mod.rs
ํ์ผ์์ ์ข
์์ฑ์ ๊ฐ์ ธ์ค๊ณ ์ ํธ๋ฆฌํฐ ํจ์๋ฅผ ์์ฑํฉ๋๋ค.use crate::structs;
use chrono;
use colorize::*;
use rand::prelude::*;
use serde_json::from_str;
use serde_json::Result;
use std::{fs, io::Write};
์ฒซ ๋ฒ์งธ ๊ธฐ๋ฅ์ ๊ธ๋ก๋ฒ ๋ฐ์ดํฐ ํ์ผ์ด ์กด์ฌํ์ง ์๋ ๊ฒฝ์ฐ ์์ฑํ๋ ๊ฒ์ ๋๋ค.
pub fn init() {
// Check if folder exists
if !fs::metadata("C:\\.todobook").is_ok() {
fs::create_dir("C:\\.todobook").unwrap(); // Create folder
// Create file
let mut file = fs::File::create(DATA_FILE).unwrap();
// Write to file
file.write_all(b"{\"data\":[]}").unwrap();
println!("{} {}", "Created folder and file".green(), DATA_FILE);
}
// Check if file exists
else if !fs::metadata(DATA_FILE).is_ok() {
// Create file
let mut file = fs::File::create(DATA_FILE).unwrap();
// Write to file
file.write_all(b"{\"data\":[]}").unwrap();
println!("{} {}", "Created file".green(), DATA_FILE);
}
}
๋ค์ ๊ธฐ๋ฅ์ ๋ช ๋ น์ค์์ ์ธ์๋ฅผ ์ฝ๋ ๊ฒ์ ๋๋ค. ํจ์๋ฅผ ์์ฑํ๊ธฐ ์ ์
Command
๋ชจ๋์์ structs
๋ผ๋ ๊ตฌ์กฐ์ฒด๋ฅผ ์ ์ํฉ๋๋ค.pub struct Command {
pub command: String,
pub arguments: String,
}
์ด์
get_args
ํจ์๋ฅผ ๋ง๋ค ์ ์์ต๋๋ค.pub fn get_args() -> structs::Command {
let args = std::env::args().collect::<Vec<String>>(); // Get arguments and collect them into a vector
let command = args.get(1).unwrap_or(&"".to_string()).to_string(); // Get command or set it to an empty string
let arguments = args.get(2).unwrap_or(&"".to_string()).to_string(); // "" arguments or ""
structs::Command { command, arguments } // Return the command and arguments
}
๋ค์ ํจ์๋ ํ์์คํฌํ๋ฅผ ๋ฐํํฉ๋๋ค.
pub fn get_timestamp() -> String {
let now = chrono::Local::now();
let timestamp = now.format("%m-%d %H:%M").to_string();
timestamp
}
๊ทธ๋ฐ ๋ค์ ์์์ ID๋ฅผ ์์ฑํ๋ ํจ์๋ฅผ ๋ง๋ญ๋๋ค.
pub fn get_id() -> u32 {
// Genrate number between 1 and 1000
let mut rng = rand::thread_rng();
let id: u32 = rng.gen_range(1..1000);
id + rng.gen_range(1..1000)
}
๋ค์ ํจ์๋ JSON ํ์ผ์์ ๋ฐ์ดํฐ๋ฅผ ์ฝ๋ ๊ฒ์ ๋๋ค.
pub fn get_todos() -> Result<Vec<structs::Todo>> {
let data = fs::read_to_string(DATA_FILE).unwrap();
let todos: structs::ConfigFile = from_str(&data)?;
Ok(todos.data)
}
๋ง์ง๋ง ๊ธฐ๋ฅ์ ๋ฐ์ดํฐ๋ฅผ JSON ํ์ผ์ ์ฐ๋ ๊ฒ์ ๋๋ค.
pub fn save_todos(todos: Vec<structs::Todo>) {
let config_file = structs::ConfigFile { data: todos };
let json = serde_json::to_string(&config_file).unwrap();
let mut file = fs::File::create(DATA_FILE).unwrap();
file.write_all(json.as_bytes()).unwrap();
}
๊ทธ๋ฆฌ๊ณ ์ด๊ฒ์ด ์ ํธ๋ฆฌํฐ ๊ธฐ๋ฅ์ ๋ํ ๊ฒ์ ๋๋ค.
todo ํจ์ ๋ง๋ค๊ธฐ
์ด์ ํ ์ผ์ ์ถ๊ฐ, ์ ๊ฑฐ ๋ฐ ๋์ดํ๋ ๋ฐ ์ฌ์ฉํ ํจ์๋ฅผ ๋ง๋ญ๋๋ค.
todo/mod.rs
ํ์ผ์์ ์ข
์์ฑ์ ๊ฐ์ ธ์ต๋๋ค.use crate::structs::Todo;
use crate::utils;
use colorize::*;
์ฒซ ๋ฒ์งธ ๊ธฐ๋ฅ์ todo๋ฅผ ์ถ๊ฐํ๋ ๊ฒ์ ๋๋ค.
pub fn add(title: String) {
if title.len() < 1 { // Check if title is empty
println!("{}", "No title provided".red());
return;
}
let mut todos = utils::get_todos().unwrap(); // Get todos
let todo = Todo {
created_at: utils::get_timestamp(),
title,
done: false,
id: utils::get_id(),
updated_at: utils::get_timestamp(),
};
todos.push(todo); // Push todo to todos
utils::save_todos(todos); // Save todos
println!("{}", "Added todo".green());
}
๋ค์ ๊ธฐ๋ฅ์ todos๋ฅผ ๋์ดํ๋ ๊ฒ์ ๋๋ค.
pub fn list() {
let todos = utils::get_todos().unwrap();
if todos.len() == 0 {
println!("{}", "No todos".red());
return;
}
println!(
"{0: <5} | {1: <20} | {2: <20} | {3: <20} | {4: <20}",
"ID", "Title", "Created at", "Updated at", "Done"
);
println!();
for todo in todos {
println!(
"{0: <5} | {1: <20} | {2: <20} | {3: <20} | {4: <20}",
todo.id,
todo.title,
todo.created_at,
todo.updated_at,
if todo.done { "Completed ๐ธ".green() } else { "No ๐ฟ".red() }
);
}
}
๊ทธ๋ฐ ๋ค์ ํ ์ผ์ ์๋ฃ๋ก ํ์ํ๋ ํจ์๋ฅผ ๋ง๋ญ๋๋ค.
pub fn done(id: String) {
let mut todos = utils::get_todos().unwrap();
let id = id.parse::<u32>().unwrap_or(0);
let exists = todos.iter().any(|todo| todo.id == id);
if !exists {
println!("{}", "Todo not found".red());
return;
}
for todo in &mut todos {
if todo.id == id {
todo.done = true;
todo.updated_at = utils::get_timestamp();
}
}
utils::save_todos(todos);
println!("{}", "Marked todo as done".green());
}
๋ค์ ๊ธฐ๋ฅ์ todo๋ฅผ ์ ๊ฑฐํ๋ ๊ฒ์ ๋๋ค.
pub fn remove(id: String) {
let mut todos = utils::get_todos().unwrap();
let id = id.parse::<u32>().unwrap_or(0);
let exists = todos.iter().any(|todo| todo.id == id);
if !exists {
println!("{}", "Todo not found".red());
return;
}
todos.retain(|todo| todo.id != id);
utils::save_todos(todos);
println!("{}", "Removed todo".green());
}
์ด์ todo ์ฑ์ ๋ง๋๋ ๋ฐ ํ์ํ ๋ชจ๋ ๊ธฐ๋ฅ์ด ์์ต๋๋ค. ํตํฉํ๊ณ ์๋ํ๋๋ก ํด์ผ ํฉ๋๋ค.
๊ธฐ๋ฅ ํตํฉ
app/mod.rs
ํ์ผ์์ ์ข
์์ฑ์ ๊ฐ์ ธ์ต๋๋ค.use crate::todo::*;
use crate::utils;
use colorize::*;
main.rs
ํ์ผ์์ ํธ์ถ๋ ์์ ํจ์๋ฅผ ๋ด๋ณด๋
๋๋ค.pub fn start() {
// ...
}
์กด์ฌํ์ง ์๋ ๊ฒฝ์ฐ ๋จผ์ ๋ฐ์ดํฐ ํ์ผ์ ํ์ธํ๊ณ ์์ฑํฉ๋๋ค.
utils::init();
๊ทธ๋ฐ ๋ค์ ๋ช ๋ น๊ณผ ์ธ์๋ฅผ ์ป์ต๋๋ค.
let args = utils::get_args();
๊ทธ๋ฐ ๋ค์ ๋ช ๋ น์ ์ผ์น์ํค๊ณ ์ ์ ํ ํจ์๋ฅผ ํธ์ถํฉ๋๋ค.
match args.command.as_str() {
"a" => add(args.arguments),
"l" => list(),
"d" => done(args.arguments),
"r" => remove(args.arguments),
"q" => std::process::exit(0),
_ => {
/// SHOW HELP
}
}
๋์์ ์ด๋ ๊ฒ ํ๊ฒ ์ต๋๋ค.
println!("{}", " No command found - Showing help".black());
let help = format!(
"
{} {}
{}
-----
Help:
Command | Arguments | Description
{} text Add a new todo
{} List all todos
{} id Mark a todo as done
{} id Delete a todo
",
"Welcome to".grey(),
"TodoBook".cyan(),
"Simple todo app written in Rust".black(),
"a".cyan(),
"l".blue(),
"d".green(),
"r".red()
);
println!("{help}");
์ด์
main.rs
ํ์ผ์์ ๋ค์ ํจ์ ํธ์ถ์ ์ถ๊ฐํฉ๋๋ค.fn main() {
app::start();
}
์ด์
cargo run
๋ฅผ ์ฌ์ฉํ์ฌ ์ฑ์ ์คํํ ์ ์์ต๋๋ค. ๋ค์๊ณผ ๊ฐ์ ๋ด์ฉ์ด ํ์๋์ด์ผ ํฉ๋๋ค.๊ฒฐ๋ก
์ฝ์ด ์ฃผ์ ์ ๊ฐ์ฌํฉ๋๋ค. ์ด ํํ ๋ฆฌ์ผ์ ์ฆ๊ฒผ๊ธฐ๋ฅผ ๋ฐ๋๋๋ค. ์ง๋ฌธ์ด ์์ผ์๋ฉด ์๊ฒฌ์ ์์ ๋กญ๊ฒ ์ง๋ฌธํ์ญ์์ค. ์์ค ์ฝ๋here๋ ํ์ธํ ์ ์์ต๋๋ค.
Reference
์ด ๋ฌธ์ ์ ๊ดํ์ฌ(Rust๋ก todo CLI ๋ง๋ค๊ธฐ ๐ฅ), ์ฐ๋ฆฌ๋ ์ด๊ณณ์์ ๋ ๋ง์ ์๋ฃ๋ฅผ ๋ฐ๊ฒฌํ๊ณ ๋งํฌ๋ฅผ ํด๋ฆญํ์ฌ ๋ณด์๋ค https://dev.to/posandu/creating-a-todo-cli-with-rust-ai3ํ ์คํธ๋ฅผ ์์ ๋กญ๊ฒ ๊ณต์ ํ๊ฑฐ๋ ๋ณต์ฌํ ์ ์์ต๋๋ค.ํ์ง๋ง ์ด ๋ฌธ์์ URL์ ์ฐธ์กฐ URL๋ก ๋จ๊ฒจ ๋์ญ์์ค.
์ฐ์ํ ๊ฐ๋ฐ์ ์ฝํ ์ธ ๋ฐ๊ฒฌ์ ์ ๋ (Collection and Share based on the CC Protocol.)
์ข์ ์นํ์ด์ง ์ฆ๊ฒจ์ฐพ๊ธฐ
๊ฐ๋ฐ์ ์ฐ์ ์ฌ์ดํธ ์์ง
๊ฐ๋ฐ์๊ฐ ์์์ผ ํ ํ์ ์ฌ์ดํธ 100์ ์ถ์ฒ ์ฐ๋ฆฌ๋ ๋น์ ์ ์ํด 100๊ฐ์ ์์ฃผ ์ฌ์ฉํ๋ ๊ฐ๋ฐ์ ํ์ต ์ฌ์ดํธ๋ฅผ ์ ๋ฆฌํ์ต๋๋ค