Join and map command line arguments in Rust Is there a more natural way to do this in rust? I'm having a lot of difficulty trying to make this look clean. Type mismatches and borrowing have me stumped. What are some refactors that I could make?

Input  


Take in list of space-separated, command-line arguments


Output  


Book (join all arguments except the last)
Reference (the last argument)
File (looked up via the book)


I abbreviated the books_to_files mapping to only include a couple examples of books with different word lengths.

Examples

In:                             Out:

Genesis 1:1                     Genesis, 1:1, mhc1.txt
Song of Solomon 2:2-4           Song of Solomon, 2:2-4, mhc3.txt
Acts of the Apostles 3:3-9      Acts of the Apostles, 3:3-9, mhc6.txt


Source Code

use std::env;
use std::collections::HashMap;

fn main() {
    let args: Vec&lt;String&gt; = env::args().collect();

    let book: String;
    if args.len() == 3 {
        book = args[1].clone();
    } else if args.len() == 4 {
        book = args[1].clone() + " " + &amp;args[2].clone();
    } else if args.len() == 5 {
        book = args[1].clone() + " " + &amp;args[2].clone() + " " + &amp;args[3].clone();
    } else if args.len() == 6 {
        book = args[1].clone() + " " + &amp;args[2].clone() + " " + &amp;args[3].clone() + " " + &amp;args[4].clone();
    } else {
        panic!("Wrong number of arguments");
    }
    let reference: String = args[args.len()-1].clone(); 
    let file: String = get_file(&amp;book);

    println!("{}, {}, {}", book, reference, file);
}

fn get_file(book: &amp;String) -&gt; String {
    let books_to_files: HashMap&lt;&amp;str, &amp;str&gt; = [
        ("Genesis", "mhc1.txt"),
        ("First Samuel", "mhc2.txt"),
        ("Song of Solmon", "mhc3.txt"),
        ("Acts of the Apostles", "mhc6.txt"),
        ("First Corinthians", "mhc6.txt"),
        ("Galatians", "mhc6.txt"),
    ].iter().cloned().collect();

    books_to_files.get::&lt;str&gt;(&amp;book.to_string()).unwrap().to_string()
}

```
I've collected a few thoughts about possible refactors you could consider below.

Your if chain isn't the most efficient way

As you've written it, your code will only work for titles between one and four words long. This isn't a disaster, and it will probably work for most titles, but we can rewrite your code to be shorter and handle any length of title. A win-win!


We can use Iterator::skip() to throw away the executable name, as we don't care about it anyway. See skip() in the documentation.

let args: Vec&lt;String&gt; = env::args().skip(1).collect();

It'd be a good idea to check that we've been given enough arguments before continuing.

let arg_len: usize = args.len();
if arg_len &lt; 2 {
    panic!("At least two arguments must be supplied!");
}

Now, we can be a bit clever and avoid your if chain all together. Using a range we can get a slice of our args vector corresponding to all but the last element. We can then use join() to turn that slice back into a String. For example, if we started with vec!["Song", "of", "Solomon", "2:2-4"] in args, we would take a slice to get ["Song", "of", "Solomon"] and rejoin them with " " in the middle to get "Song of Solomon".

let book = args[0..(arg_len - 1)].join(" ");



Try to avoid clone() unless necessary

The first thing I noticed when I took a look at your code was that you've used clone() in quite a few places. We've already got rid of a lot of them with the tweak above, and you should try to avoid them all together as allocating when you don't need to is a waste of time and memory. If you can use an &amp;str, do that instead of insisting on all strings being String. As an aside, there is rarely any point bothering with &amp;String — just use &amp;str there.


Putting the above into action, we can let reference be an &amp;str and avoid a clone.

let reference: &amp;str = &amp;args[args.len() - 1];

Let's also change get_book's method signature as suggested above.

fn get_file(book: &amp;str) -&gt; &amp;str {

Now we can get rid of the unnecessary to_string() below.

books_to_files.get::&lt;str&gt;(&amp;book.to_string()).unwrap()

And tweak this line in main() to accept an &amp;str.

let file: &amp;str = get_file(&amp;book);



Avoiding unwrap

Ideally, get_book ought to return an Option instead, so the caller can choose how they want to handle the error. As it is fatal anyway, unwrapping makes little difference, but it's worth bearing in mind as a future improvement.

Try it Online