Convert "sl" Source Code from C to Rust

Previous = Convert const to Vector of String-containing Vectors

Get User-Provided Options from the Command-line, Using Clap

Page Contents

The original C-version of the program "sl" provides the ability for the user to add command-line options to control things such as which train to display, or to run in "Accident" mode. We also want to add this to our Rust version. You may recall Using Clap to Parse Rust Arguments earlier to get command-line arguments. We'll implement that now in our program.

Add Clap (Command-Line Argument Parser) Dependency to the Project

Back to Top

If you haven't already added Clap-capability to your project, you may remember it's an easy thing to do:

$ cargo add clap --features derive

This should add the dependency to your "Cargo.toml" file, so that it should look something like this:

$ cat Cargo.toml
[package]
name = "sl"
version = "0.1.0"
edition = "2021"

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

[dependencies]
clap = { version = "4.2.1", features = ["derive"] }

Create a Module/Function to Get Command-line Arguments

Back to Top

Below is what your "main.rs" file should look like currently:

main.rs
/* main.rs */

mod convert_to_vec;
mod display;
mod images;

use convert_to_vec::str_to_vecvecstring;
use display::display_image;
use images::*;

fn main() {
    let model_str: &str = D51;
    let model_vecvecstring = str_to_vecvecstring(model_str);
    display_image(model_vecvecstring);
} // end of main()

The image model is not going to be the only thing we might get from the command-line. There will likely be quite a bit of processing to make sure we have good data, so let's put this stuff in its own function, in its own module/file. As a starter, create the following file:

src/get_options.rs
/* get_options.rs */

use crate::images::*;

pub fn get_opts() -> String {
    return PLANE.to_string(); 
} // end of get_opts()

Although not exactly accurate, you can think of the "get_options.rs" file as being one level beneath the "main.rs" file, and you can think of the "main.rs" file as being the same thing as "crate". This is why the "use" statement can't just point to "images"; it needs to start one level up, at the "main", or "crate", level, and then follow the path ("::") to "images".

The "images" module was defined in "main.rs", as a module attached to the "sl" project as a whole, so we don't declare it here in "get_options.rs".

Also, since a &str-type of variable "dies" when program-control leaves the function in which it is used, we need to return the value of our const-type "PLANE" value as something more long-lived than a &-str-based const; that's why we convert it to a String-type, and then return that String.

And modify "main()" as needed:

main.rs
/* main.rs */

mod convert_to_vec;
mod display;
mod get_options;
mod images;

use convert_to_vec::str_to_vecvecstring;
use display::display_image;
use get_options::get_opts;
use images::*;

fn main() {
    let model_str: &str = D51;
    let model_string = get_opts();
    let model_vecvecstring = str_to_vecvecstring(&model_string);
    display_image(model_vecvecstring);
} // end of main()

We're no longer putting our image data in a &str-type of variable, but rather into a String-type of value, because that's what's coming back from the "get_opts()" function. Although not necessary, changing the name of the variable provides a little documentation about what's happening. But when we send it to the "str_to_vecvecstring()" function, we send that with an & pre-fixed, to send a borrowed slice, because a slice is what that function expects.

Use clap to Get Command-Line Options

Back to Top

Which Model?

Back to Top

To use clap, we need to create a struct-type of data definition. Then we ned to create a variable of that type, and fill that variable with arguments provided by clap's parser() function. Then we match that value to the available possibilities, and return the match, if found, otherwise the default of "D51" if not found. Here's the code:

get_options.rs
/* get_options.rs */

pub fn get_opts() -> String {
    use clap::Parser; // Use c[ommand] l[ine] a[rgument] p[arser] to get command-line arguments.
    use crate::images::*;
 
    // Set up program-argument possibilities.
    #[derive(Parser)]
    struct Arguments {
        /// Which model should be used?
        #[arg(long,short, default_value_t = String::from("D51"))]
        model: String,
    } // end of Arguments struct definition

    let args: Arguments = Arguments::parse();
  
    let image_str = match args.model.to_uppercase().as_str() {
      "P" | "PLANE" => PLANE,
      "D" | "D51" => D51,
      "C" | "C51" => C51,
      "L" | "LITTLE" => LITTLE,
      "B" | "BOAT" => BOAT,
      "T" | "TWIN" | "TWINENGINE" => TWINENGINE,
      "M" | "MOTOR" | "MOTORCYCLE" | "CYCLE" => MOTORCYCLE,
      "J" | "JACK" => JACK,
      "F" | "FERRIS" | "MASCOT" => FERRIS,
      _ => D51,
    };
    return image_str.to_string();
    return C51.to_string();
} // end of get_options()

We could have put the "use clap..." statement at the top of the file, outside of the function "get_options()", which would make it global to the file (which doesn't have anything in it but this function, so it's a minor difference), but it's not needed by anything outside of the function, so we put it in the function to keep its scope as localized as possible.

We also don't need the "use crate::images..." line to be global to the file, so we've moved it into the function also.

Because the "clap" crate is separate from the project crate, we don't need to start the "use clap..." line with the top-level "crate" for the "sl" project, like we did for the "use crate..." statement. We instead start at the top-level of the "clap" crate.

We then define the "Arguments"-data type to be a struct, which currently only has one data item, the "model" field, which will hold a String. At the command-line, we can enter a short form ("p" for "plane", etc) or the long form ("plane"), and if that option is not defined on the command-line, the program will default to setting that value to "D51".

We then create a variable named "args" that is of the "Argument"-type, which gets assigned by the clap parser.

Then we use a "match" statement to compare the incoming (uppercased) value in "model" to various permutations of the possible models. This match statement then returns the appropriate const which gets assigned to "image_str".

And finally that value is converted to a String data type and returned from the "get_opts()" function.

So you should be able to run the program in ways like the following:

$ cargo run
$ cargo run -- --model=p
$ cargo run -- -mb
$ cargo run -- --model c51
$ cargo run -- -m CyClE
$ cargo run -- --help

Notice that we did not include the COALCAR image; that's because it's not designed to travel across the screen without being pulled by one of the other craft. But if you want to include it in your list, feel free to do so. But be aware that you can not use "C" as a shortcut for "COALCAR", because "C" is already being used by the "C51". (Well, you could take it from the "C51" and use it for the "COALCAR" instead, if you wanted.)

Will We Fly?

Back to Top

When we have the image actually animated, going across the screen, we'll want to give the option to have the image "fly". On the command-line, a simple "--fly" option will activate this. If the option is not given, the default will be to not fly. Here's how the clap portion of the program looks, to do this:

get_options.rs
/* get_options.rs */

pub fn get_opts() -> (String, bool) {
    use clap::Parser; // Use c[ommand] l[ine] a[rgument] p[arser] to get command-line arguments.
    use crate::images::*;

    // Set up program-argument possibilities.
    #[derive(Parser)]
    struct Arguments {
        /// Which model should be used?
        #[arg(long,short, default_value_t = String::from("D51"))]
        model: String,
 
        /// Should the model fly?
        #[arg(long, short, default_value_t = false)]
        fly: bool,
 
    } // end of Arguments struct definition

    let args: Arguments = Arguments::parse();
  
    let image_str = match args.model.to_uppercase().as_str() {
      "P" | "PLANE" => PLANE,
      "D" | "D51" => D51,
      "C" | "C51" => C51,
      "L" | "LITTLE" => LITTLE,
      "B" | "BOAT" => BOAT,
      "T" | "TWIN" | "TWINENGINE" => TWINENGINE,
      "M" | "MOTOR" | "MOTORCYCLE" | "CYCLE" => MOTORCYCLE,
      "J" | "JACK" => JACK,
      "F" | "FERRIS" | "MASCOT" => FERRIS,
      _ => D51,
    };
 
    let mut fly: bool = false;
    if args.fly {
    } else {
      fly = false;
    }
    
    return (image_str.to_string(), fly);
    return image_str.to_string();

} // end of get_options()

Not only have we added the capability to specify the "fly" option, we're not having to return that value from the function. Accordingly, we have to make some modifications to the function's definition and return statement, as well as to the calling routine:

main.rs
/* main.rs */

mod convert_to_vec;
mod display;
mod get_options;
mod images;

use convert_to_vec::str_to_vecvecstring;
use display::display_image;
use get_options::get_opts;

fn main() {
    let (model_string, fly) = get_opts();
    let model_vecvecstring = str_to_vecvecstring(&model_string);
    display_image(model_vecvecstring);
    println!("The option to fly = {}.", fly); // Temporary test of what we get back in "fly".
} // end of main()

Give it a try:

$ cargo run
$ cargo run -- --model=p
$ cargo run -- --fly
$ cargo run -- --model c51 -f
$ cargo run -- -m CyClE --fly
$ cargo run -- --help

Of course, we're not animated yet, so we won't actually fly, but you should see a message on screen telling you the state of the "fly" option.

Now instead of the "get_opts()" function returning a string composed of the model image, it returns that string, and a boolean value of "true" or "false". We put them both into a "shopping bag", and send that "shopping bag" back to the calling routine. A "shopping bag" like this, that holds multiple variables, is called a "tuple". But just to be confusing, technically, this is not a tuple. And even if it was, it should be noted that a "tuple" has nothing to do with "two", even though this contains two values.

A true tuple would have a name on the outer shopping bag, and would not have names for the inner values. Instead, the inner values would be referenced by an index number that corresponds to the order in which the values were placed into the bag. For example, here's a real tuple with our same values:

let options: (String, boolean = ("<\")))><".to_string(), false);
println("The picture of the model = {}", options.0);
println!("And the boolean value of 'fly' = {}.", options.1);

"options" is the name of this tuple "shopping bag", and its first-indexed value is a String variable, and its second-indexed value is a boolean variable.

A "shopping bag" that has a name on the outside, and a name for each individually-labeled item within it which are not accessed according to order, is called a "struct". We've been using a struct for our clap-related code.

Will We Be In Accident Mode?

Back to Top

Adding an Accident mode option is just as easy as adding the Fly option was:

get_options.rs
/* get_options.rs */

pub fn get_opts() -> (String, bool, bool) {
    use clap::Parser; // Use c[ommand] l[ine] a[rgument] p[arser] to get command-line arguments.
    use crate::images::*;

    // Set up program-argument possibilities.
    #[derive(Parser)]
    struct Arguments {
        /// Which model should be used?
        #[arg(long,short, default_value_t = String::from("D51"))]
        model: String,
 
        /// Should the model fly?
        #[arg(long, short, default_value_t = false)]
        fly: bool,
 
        /// Accident?
        #[arg(long, short, default_value_t = false)]
        oops: bool,
 
    } // end of Arguments struct definition

    // Get arguments from command-line, if any.
    let args: Arguments = Arguments::parse();

    // Process "model" option.
    let image_str = match args.model.to_uppercase().as_str() {
      "P" | "PLANE" => PLANE,
      "D" | "D51" => D51,
      "C" | "C51" => C51,
      "L" | "LITTLE" => LITTLE,
      "B" | "BOAT" => BOAT,
      "T" | "TWIN" | "TWINENGINE" => TWINENGINE,
      "M" | "MOTOR" | "MOTORCYCLE" | "CYCLE" => MOTORCYCLE,
      "J" | "JACK" => JACK,
      "F" | "FERRIS" | "MASCOT" => FERRIS,
      _ => D51,
    };
 
    // Process "fly" option.
    let mut fly: bool = false;
    if args.fly {
      fly = true;
    } else {
      fly = false;
    }
 
    // Process Accident ("oops") option.
    let mut oops: bool = false;
    if args.oops {
      oops = true;
    } else {
      oops = false;
    }
 
    return (image_str.to_string(), fly, oops);

} // end of get_options()
main.rs
/* main.rs */

mod convert_to_vec;
mod display;
mod get_options;
mod images;

use convert_to_vec::str_to_vecvecstring;
use display::display_image;
use get_options::get_opts;

fn main() {
    let (model_string, fly, oops) = get_opts();
    let model_vecvecstring = str_to_vecvecstring(&model_string);
    display_image(model_vecvecstring);
    println!("The option to fly = {}.", fly); // Temporary test of what we get back in "fly".
    println!("The Accident (\"oops\") option = {}.", oops); // Temporary test of what we get back in "oops".
} // end of main()

Give it a try:

$ cargo run -- --fly --oops

Will We Be Able to Interrupt the Running of the Program?

Back to Top

The last option available in the C-version of "sl" is the option to interrupt the program with a break signal (Ctrl-C for most PC users). Again, it's as easy to add the option as it is the other options:

get_options.rs
/* get_options.rs */

pub fn get_opts() -> (String, bool, bool, bool) {
    use clap::Parser; // Use c[ommand] l[ine] a[rgument] p[arser] to get command-line arguments.
    use crate::images::*;

    // Set up program-argument possibilities.
    #[derive(Parser)]
    struct Arguments {
        /// Which model should be used?
        #[arg(long,short, default_value_t = String::from("D51"))]
        model: String,
 
        /// Should the model fly?
        #[arg(long, short, default_value_t = false)]
        fly: bool,
        
        /// Accident?
        #[arg(long, short, default_value_t = false)]
        oops: bool,
 
        /// Interrupt program-run?
        #[arg(long, short, default_value_t = false)]
        interruptable: bool,

    } // end of Arguments struct definition

    // Get arguments from command-line, if any.
    let args: Arguments = Arguments::parse();

    // Process "model" option.
    let image_str = match args.model.to_uppercase().as_str() {
      "P" | "PLANE" => PLANE,
      "D" | "D51" => D51,
      "C" | "C51" => C51,
      "L" | "LITTLE" => LITTLE,
      "B" | "BOAT" => BOAT,
      "T" | "TWIN" | "TWINENGINE" => TWINENGINE,
      "M" | "MOTOR" | "MOTORCYCLE" | "CYCLE" => MOTORCYCLE,
      "J" | "JACK" => JACK,
      "F" | "FERRIS" | "MASCOT" => FERRIS,
      _ => D51,
    };

    // Process "fly" option.
    let mut fly: bool = false;
    if args.fly {
      fly = true;
    } else {
      fly = false;
    }

    // Process Accident ("oops") option.
    let mut oops: bool = false;
    if args.oops {
      oops = true;
    } else {
      oops = false;
    }
 
    // Process "interruptible" option.
    let mut interruptible: bool = false;
    if args.interruptible {
      interruptible = true;
    } else {
      interruptible = false;
    }

 
    return (image_str.to_string(), fly, oops, interruptible);

} // end of get_options()
main.rs
/* main.rs */

mod convert_to_vec;
mod display;
mod get_options;
mod images;

use convert_to_vec::str_to_vecvecstring;
use display::display_image;
use get_options::get_opts;

fn main() {
    let (model_string, fly, oops, interruptible) = get_opts();
    let model_vecvecstring = str_to_vecvecstring(&model_string);
    display_image(model_vecvecstring);
    println!("The option to fly = {}.", fly); // Temporary test of what we get back in "fly".
    println!("The Accident (\"oops\") option = {}.", oops); // Temporary test of what we get back in "oops".
    println!("The \"interruptible\" option {}.", interruptible); // Temporary test of what we get back in "interruptible".
    
} // end of main()

Give it a try:

$ cargo run -- --fly --oops --interruptible

What Speed?

Back to Top

The last argument we want to add is an option to control the speed of the animation. This option is not in the original C-version of "sl", but it seems handy enough to include it. You should know the process by now.

get_options.rs
/* get_options.rs */

pub fn get_opts() -> (String, bool, bool, bool, i32) {
    const DELAY: f64 = 100.0;
    
    use clap::Parser; // Use c[ommand] l[ine] a[rgument] p[arser] to get command-line arguments.
    use crate::images::*;

    // Set up program-argument possibilities.
    #[derive(Parser)]
    struct Arguments {
        /// Which model should be used?
        #[arg(long,short, default_value_t = String::from("D51"))]
        model: String,
 
        /// Should the model fly?
        #[arg(long, short, default_value_t = false)]
        fly: bool,
        
        /// Accident?
        #[arg(long, short, default_value_t = false)]
        oops: bool,
 
        /// Interrupt program-run?
        #[arg(long, short, default_value_t = false)]
        interruptable: bool,
  
        /// What speed? Zoom|Fast|Normal|Slow|Crawl
        #[arg(long, short, default_value_t = String::from("NORMAL"))]
        speed: String,

    } // end of Arguments struct definition

    // Get arguments from command-line, if any.
    let args: Arguments = Arguments::parse();

    // Process "model" option.
    let image_str = match args.model.to_uppercase().as_str() {
      "P" | "PLANE" => PLANE,
      "D" | "D51" => D51,
      "C" | "C51" => C51,
      "L" | "LITTLE" => LITTLE,
      "B" | "BOAT" => BOAT,
      "T" | "TWIN" | "TWINENGINE" => TWINENGINE,
      "M" | "MOTOR" | "MOTORCYCLE" | "CYCLE" => MOTORCYCLE,
      "J" | "JACK" => JACK,
      "F" | "FERRIS" | "MASCOT" => FERRIS,
      _ => D51,
    };

    // Process "fly" option.
    let mut fly: bool = false;
    if args.fly {
      fly = true;
    } else {
      fly = false;
    }

    // Process Accident ("oops") option.
    let mut oops: bool = false;
    if args.oops {
      oops = true;
    } else {
      oops = false;
    }

    // Process "interruptible" option.
    let mut interruptible: bool = false;
    if args.interruptible {
      interruptible = true;
    } else {
      interruptible = false;
    }
 
    // Process "speed" option.
    let delay = match args.speed.to_uppercase().as_str() {
      "Z" | "ZOOM" => DELAY * 0.10, // 10% of the normal delay (10 times faster animation)
      "F" | "FAST" => DELAY * 0.50, // 50% (twice as fast as normal)
      "N" | "NORM" | "NORMAL" => DELAY, // 100%
      "S" | "SLOW" => DELAY * 2.0, // 200%
      "C" | "CRAWL" => DELAY * 5.0, // 500% (five times slower than normal)
      _ => DELAY,
    };

 
    return (image_str.to_string(), fly, oops, interruptible,delay as u64);

} // end of get_options()
main.rs
...
fn main() {
    let (model_string, fly, oops, interruptible, delay) = get_opts();
    let model_vecvecstring = str_to_vecvecstring(&model_string);
    display_image(model_vecvecstring);
    println!("The option to fly = {}.", fly); // Temporary test of what we get back in "fly".
    println!("The Accident (\"oops\") option = {}.", oops); // Temporary test of what we get back in "oops".
    println!("The \"interruptible\" option = {}.", interruptible); // Temporary test of what we get back in "interruptible".
    println!("The \"speed\" option = {}.", speed); // Temporary test of what we get back in "speed".
} // end of main()
...

Give it a try:

$ cargo run -- --speed=zoom

Temporarily Turn Off Warnings

Back to Top

When we compile this program, we get lots of warnings abour unused variables. Whereas it's good to get these warnings, we are intentionally not using the variables at the moment. There are several things we could do to deal with these warnings, including doing nothing, and just living with them, but an easy way to turn them off temporarily is to, at the top of the "main.rs" file, before any other code, issue a compiler directive to allow unused items. There are several variants even of this option, but the simplest that probably works the best for us, is to do this, which will turn off the warnings project-wide:

main.rs
#![allow(unused)]
/* main.rs */

mod convert_to_vec;
mod display;
mod get_options;
mod images;
...

Once we have these options in use, we'll want to remove this directive, so that we'll start getting warnings again, which are usually beneficial.

Let's move on to the next task, Use ncurses to Draw the Image.