3

Minils, a somewhat rusty ls

 1 year ago
source link: https://sebport0.github.io/minils/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Minils, a somewhat rusty ls

Samuel Bassi · 2021-12-19

Chapter 12 of the Rust Book shows how to build minigrep, a tribute to grep that also serves as a recap of the main Rust features show on the previous chapters.

In this post I try to do the same with ls as an exercise. The good: I’ve learned a little bit about std::fs, std::os, std::path and the chrono crate. The bad: my Rust still feels rusty and non-idiomatic(I hope time will solve this).

This is the minils repo. The final result can be found on the main branch but I’ll talk about the details flowing through the other branches as checkpoints. To be more specific, I’ll follow this structure: feature/check-path-is-dir -> feature/list-mode -> refactor -> feature/cli-crate.

How to use

I’ll let myself be extra lazy here and point to the minils README :P

Minils

The start

All of the program functionality is inside src/main.rs. We are using std::env and std::fs. The first let us collect the arguments that started with the program, but there is a catch: we want a vector of Strings, so we must indicate that to the compiler via the type of args. So we end with let args: Vec<String> and our args will looke like this: target/debug/minils and dir/as/arg. We want the second argument, the first one is not useful.

Next we use fs::metadata in combination with the given path as the second argument. Metadata allows us to query all kinds of information about a directory or file, in minils is used to check that the provided path to a directory is, indeed, a directory. If it is not a directory, the program will panic. Otherwise, we walk through the contents of the directory using fs::read_dir and print the contents to stdout.

List mode

With the “list mode” we want to emulate the ls long listing format(-l flag). We will show six columns for each directory entry: permissions(raw bits), size, creation time, last modified time, last access time and the entry. For example, running the program inside the repo give us:

33204 2443 2021-12-19UTC 2021-12-19UTC 2021-12-19UTC Cargo.lock
33204 190 2021-12-19UTC 2021-12-19UTC 2021-12-19UTC Cargo.toml
16893 4096 2021-11-20UTC 2021-12-19UTC 2021-12-19UTC src
16893 4096 2021-11-20UTC 2021-12-19UTC 2021-12-19UTC .git
33204 8 2021-11-20UTC 2021-11-20UTC 2021-12-19UTC .gitignore
16893 4096 2021-12-01UTC 2021-12-01UTC 2021-12-19UTC target

First, we add the chrono crate to Cargo.toml, it should look like this:

[package]
name = "minils"
version = "0.1.0"
edition = "2018"

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

[dependencies]
chrono = "0.4"

The chrono crate makes working with dates and time easier in Rust. We use it to translate metadata io::Result<SystemTime> to datetime format. To achieve this we have to do something like:

use chrono::{DateTime, Utc};

let created: DateTime<Utc> = metadata.created().unwrap().into();
let created = created.date();

let last_modified: DateTime<Utc> = metadata.modified().unwrap().into();
let last_modified = last_modified.date();

let last_accessed: DateTime<Utc> = metadata.accessed().unwrap().into();
let last_accessed = last_accessed.date();

We use into() in combination with the DateTime<Utc> type to translate the io::Result<SystemTime> from unwrap() to a datetime format. In the following line, we take the date from the datetime.

Refactor

Things seems to be working but our code is a little messy. Maybe we can improve it following the minigrep style. To do this, we add a lib.rs file inside the src folder. This file will contain the logic of the program.

Lets add a Config structure, it will hold the path to the directory and also a somewhat improved argument handling:

pub struct Config {
    pub path_to_read: String,
}

impl Config {
    pub fn new(mut args: env::Args) -> Result<Config, &'static str> {
        args.next();

        let path_to_read = match args.next() {
            Some(arg) => arg,
            None => return Err("Path to read argument is needed."),
        };

        Ok(Config { path_to_read })
    }
}

Note that now we are using the iterator that env::args() returns. We use the next() method to avoid getting the path to the executable and to check that the second argument is present. If the path_to_read argument is not present, an error is returned. If everything is ok, we return a new Config.

Next, lets make two new functions new and print_entry. They wrap the previous behaviour: the first reads the contents of the directory and calls the second one to print each of them to the standard output.

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents =
        fs::read_dir(config.path_to_read).expect("Something went wrong when reading from path!");

    for entry in contents {
        print_entry(entry.unwrap())
    }

    Ok(())
}

We have to use the pub keyword to tell the compiler that we want to call the contents of lib.rs from main.rs.

Inside main.rs, we make use of the Config structure and the two functions but also handle the errors and exit with process::exit(1) if something bad happens.

use minils::Config;
use std::{env, process};

fn main() {
    let config = Config::new(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    if let Err(e) = minils::run(config) {
        eprintln!("Application error: {}", e);
        process::exit(1);
    }
}

Until now, the list mode is not optional at all. We solve this on the next section the easy way adding a new crate.

CLI crate

Finally, to make things more interesting we add the structop crate to our program. Like before with chrono, we must go to Cargo.toml and add the new dependency:

structopt = "0.3.25"

Now we need to adapt our code. The Config structure transforms to Cli, which has two variables, path(same as path_to_read from before) and list, which is a boolean that indicates if we want to see more information for each entry:

#[derive(StructOpt)]
/// Use a dir as an argument to list its contents.
pub struct Cli {
    /// List contents from path.
    #[structopt(parse(from_os_str), default_value = ".")]
    pub path: PathBuf,

    /// Show additional permissions, size, creation, last modified and last accessed dates
    /// for each entry.
    #[structopt(short, long)]
    pub list: bool,
}

The structop crate has some very interesting features. It adds a help flag automagically:

cargo run -- -help

minils 0.1.0
Use a dir as an argument to list its contents

USAGE:
    minils [FLAGS] [path]

FLAGS:
    -h, --help       Prints help information
    -l, --list       Show additional permissions, size, creation, last modified and last accessed dates for each entry
    -V, --version    Prints version information

ARGS:
    <path>    List contents from path [default: .]

If you look closely, you will notice that the comments on our Cli struct code are shown on the help output!

Also, the #[structopt(short, long)] will automatically transform our argument to the short and long versions. Nice!

Now we adapt the rest of our functions to the new Cli struct. This consists of adding support for the list flag, and with it we have our optional list mode ready to go.

pub fn run(cli: Cli) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_dir(cli.path).expect("Something went wrong when reading from path!");

    for entry in contents {
        print_entry(entry.unwrap(), cli.list);
    }

    Ok(())
}

fn print_entry(entry: DirEntry, list_flag: bool) {
    let path = entry.path();
    let path_last = path.iter().last().unwrap().to_str().unwrap();

    match list_flag {
        true => print_with_metadata(entry, path_last),
        false => println!("{}", path_last),
    };
}

Finally, in main:

use std::process;
use structopt::StructOpt;

fn main() {
    let args = minils::Cli::from_args();

    if let Err(e) = minils::run(args) {
        eprintln!("Application error: {}", e);
        process::exit(1);
    }
}

The end

This was minils. Something that is totally missing from the code are the unit tests. We could test the functions inside lib.rs. This isn’t hard to do, in fact, unit testing seems to be natively easy in Rust but I’ll try to learn more and come with examples in future posts. For now, I must keep on reading the Rust Book.

See you later!

References


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK