Making a CLI application in Rust

Today, I thought I would do something different. Instead of learning a new concept, I felt I could use what I have learned in the past week to create a small command-line application.

I'm gonna keep this one really simple so that I can complete it within a couple of hours.

Defining the objective

The aim is to create a command-line encyclopedia. To do this, I will be using Wikipedia internally.

My app should do the following things:

  1. Take a single command-line argument.

  2. Fire a GET HTTP request to the Wikipedia page of that argument.

  3. Scrap the first paragraph from it and display it in the command line.

Dependencies

In order to achieve my objective I will be using the following dependencies:

  • reqwest -> To fire the HTTP request to Wikipedia.

  • select -> To parse the HTML body to extract the first paragraph.

If you're following along, you can add the following in your Cargo.toml file under the [dependencies] section.

reqwest = { version = "0.11", features = ["blocking", "json"] }
select = "0.6.0-alpha.1"

Implementation

Let's first start off by just accepting a command-line argument.

Ultimately, we want to call our application in the following way:

cargo run pokemon

Where 'pokemon' should be picked up as the search string.

Rust has an args() function in the env we can use to achieve this.

use std::env;

fn main() {

    let args: Vec<String> = env::args().collect();
    let input = &args[1];
}

Next, we need to create the URL string using our argument.

All we have to do is prefix our argument with en.wikipedia.org/wiki.

For example, to get information about pokemon, we should go to en.wikipedia.org/wiki/pokemon

We can utilize the push_str() method of String to concatenate the 2 strings. This should get us our final URL to use.

let mut url = String::from("https://en.wikipedia.org/wiki/");
url.push_str(&input);

The next step is to fire an API call to get the HTML of the page we're interested in.

Since our application only fires a single request on-demand, we can just use a blocking call to achieve this.

reqwest provides a reqwest::blocking::get() function for this purpose. We will just pass it our URL we created in the previous step. This will get us the whole HTTP response object.

Since we are only interested in the HTML body, we can call the text() method on the Response object to get the body as a string reference.

let resp = reqwest::blocking::get(&url).expect("Api call failed.");
let resp = resp.text().expect("Failed while extracting body.");

Now, the only thing left to do is to extract the summary from the HTML body and print it on the screen.

We can use the select library to achieve this.

If you inspect the Wikipedia page, you will see that the paragraphs are in a <p> tag without any CSS id or class attached to it. This means we cannot directly reference the paragraph we want.

But, if you go through the HTML you will realize that the summary is the second <p> tag in the document. So, what we can do is pull the content of the first 2 <p> tags and ignore the first one.

Obviously, this approach is not ideal since it depends heavily on the way Wikipedia is structuring its HTML and It will probably fail for a lot of cases. But for our purposes it's okay. We're just trying to learn Rust.

Here, we will just select the first 2 <p> tags and skip the first one. Then we can just extract the text and print it to the console.

let document = Document::from(&resp[..]);
for node in document.select(Name("p")).take(2).skip(1) {
    let tag = node.next().unwrap().text();
    println!("{}", tag);
}

And that's it! We can run our code now in the following way.

cargo run pokemon

And it will output the following summary:

The franchise began as Pocket Monsters: Red and Green (later released outside of Japan as Pokémon Red and Blue), a pair of video games for the original Game Boy handheld system that were developed by Game Freak and published by Nintendo in February 1996. It soon became a media mix franchise adapted into various different media.[8] Pokémon is  estimated to be the highest-grossing media franchise of all time. The Pokémon video game series is the fourth best-selling video game franchise of all time with more than 380 million copies sold[9] and one billion mobile downloads,[10] and it spawned an anime television series that has become the most successful video game adaptation[11] of all time with over 20 seasons and 1,000 episodes in 183 countries.[9] The Pokémon Trading Card Game is the highest-selling trading card game of all time[12] with over 34.1 billion cards sold. In addition, the Pokémon franchise includes the world's top-selling toy brand,[13] an anime film series, a live-action film (Detective Pikachu), books, manga comics, music, merchandise, and a temporary theme park. The franchise is also represented in other Nintendo media, such as the Super Smash Bros. series, where various Pokémon characters are playable.

Full code for the CLI app

use select::document::Document;
use select::predicate::{Name};
use std::env;

fn main() {

    let args: Vec<String> = env::args().collect();
    let input = &args[1];

    // Fire api call and extract body.
    // Create url. https://en.wikipedia.org/wiki/<enter user input here>
    let mut url = String::from("https://en.wikipedia.org/wiki/");
    url.push_str(&input);

    // Fire api call.
    let resp = reqwest::blocking::get(&url).expect("Api call failed.");
    let resp = resp.text().expect("Failed while extracting body.");

    // Parse body and extract field.
    let document = Document::from(&resp[..]);
    for node in document.select(Name("p")).take(2).skip(1) {
        let tag = node.next().unwrap().text();
        println!("{}", tag);
    }
}