TLDR: I finally bit the bullet and jumped into Rust and I love it.
I am a big user of todoist for everyday task tracking. It’s simple and easy to use, but has one small problem. I can’t do it from my terminal. So I had started to write my own CLI app in Go for that, since I was comfortable with it. It was basically just a wrapper around their REST API. I had written the most basic things already like creating, listing and closing tasks, so I hadn’t done anything much yet.
This was a great way how to try making something in Rust without spending all my time just trying to find a problem to solve.
When writing CLI apps in Go I use urfave/cli for small and very simple projects or bust out spf13/cobra for something bigger.
The only thing that I found that is comparable in Rust is clap which actually works great, but is not really a toolkit for building CLI apps, but an arguments parser.
The good thing is that it was pretty simple to start working on, even though learning how macros work and all the involved magic with them is kinda confusing at start.
But the more I used it the more I loved it. The way how I can leverage enums to create commands and subcommands just feels so smooth. And using them is also feels like magic.
Just look at the example:
#[derive(Subcommand, Debug)]
enum Commands {
/// Work with tasks
Tasks(Tasks),
}
#[derive(Debug, Args)]
struct Tasks {
#[clap(subcommand)]
command: TaskCommands,
}
#[derive(Debug, Subcommand)]
enum TaskCommands {
/// List commands, default to today and overdue
List {
#[clap(long, short)]
filter: Option<String>,
},
/// Create a task
Create {
/// Content of the task
content: Option<String>,
/// Tasks due date
due: Option<String>,
/// Tasks project
project: Option<String>,
},
}
Creates all that is needed to parse arguments and flags from the command line. The comments are used for help text and it looks awesome:
▶ todoist --help
todoist 0.1.0
USAGE:
todoist <SUBCOMMAND>
OPTIONS:
-h, --help Print help information
-V, --version Print version information
SUBCOMMANDS:
help Print this message or the help of the given subcommand(s)
tasks Work with tasks
▶ todoist tasks --help
Work with tasks
USAGE:
todoist tasks <SUBCOMMAND>
OPTIONS:
-h, --help Print help information
SUBCOMMANDS:
create Create a task
help Print this message or the help of the given subcommand(s)
list List commands, default to today and overdue
And then you can easily parse this and work with the options using match statements. And it also doesn’t let you compile if you forget to match something and it’s so empowering. I don’t have to think about all the options, I just fix what the compiler doesn’t like.
fn main() {
let cli = Cli::parse();
let theme = ColorfulTheme::default();
match &cli.command {
Commands::Tasks(tasks) => match &tasks.command {
TaskCommands::List { filter } => {}
TaskCommands::Create { content, due, project } => {},
TaskCommands::Done { id } => {},
TaskCommands::View { id } => {},
},
}
Ok(())
}
To preface this, my dev work started out writing different PHP applications and some Python scripts, and then I moved into Go development space. I have dabbled with Java, C#, JS/TS and some other language that I forget. All of those language have fairly straight forward way how to deal with structuring your project:
Old PHP code would use require/include
, anything newer would use namespaces and use
keywords.
Structure your files in folders and just use whatever you need. I would call this file based structure.
Go uses packages with a pretty straight forward way. Every folder is a package,
every file in the folder is part of the package, and if something is exported from
the file it’s available outside the package using the import
keyword. I would
call this folder based structure.
So the first thing I tried with Rust is the same way I do it in go. I just created a different file in the same directory and tried to use something from it. This doesn’t work. Right away.
▶ ls --tree
.
├── foo
│ └── file.rs
└── main.rs
main.rs
use foo;
fn main() {
bar()
}
foo/file.rs
pub fn bar() {
println!("bar");
}
But this shows an error:
error[E0432]: unresolved import `foo`
--> src/main.rs:1:5
|
1 | use foo;
| ^^^ no external crate `foo`
error[E0425]: cannot find function `bar` in this scope
--> src/main.rs:4:5
|
4 | bar()
| ^^^ not found in this scope
Some errors have detailed explanations: E0425, E0432.
For more information about an error, try `rustc --explain E0425`.
error: could not compile `folders` due to 2 previous errors
This is because of the way how Rust imports work is a bit different. There are
bunch of ways how to do this. Either create a foo/mod.rs
file. In that file
then have to declare the foo/file.rs
as a public module.
src/foo/mod.rs
pub mod file;
Then you can use it in main as a module:
main.rs:
mod foo;
fn main() {
foo::file::bar()
}
And then there is the crate syntax with the use
keyword that also has crate
and self
and super
keywords, but for more info on that the Rust book will do
a lot better job then I ever would.
I really am loving Rust so far. The type system is awesome. The compiler is awesome(although slow, Go has spoiled me). But there is a lot to learn and curve is steep.
But this small project has convinced me that my CLI tools from now on are gonna be in Rust.