Convert "sl" Source Code from C to Rust

Using Rust's Native Parsing Capability to Parse Program Arguments

Some programs take arguments so that the user can modify the program's behavior. The program we are converting from C to Rust, "sl", takes arguments. You can see what those arguments are by looking at the man page for the program, like so:

$ man sl

Reading Program Arguments

Rust has its own built-in argument-handling capabilities, but they are limited in comparison to what can be done with the third-party crate, "Clap". But before looking at "Clap", let's look briefly at the built-in capabilities.

Move up a directory level and create a new project:

$ cd ..
$ cargo new parse_native

You now have a new "Hello, world!"-printing "main.rs". It should look like this:

fn main() {
  println!("Hello, world!");
} // end of main()

Now compile this new "main.rs" program, and then run it with some program arguments. Nothing special will happen, because the program is not programmed to do anything with arguments.

$ cd parse_native
$ cargo build
$ ./target/debug/parse_native -myargument --yourargument

You can also use Cargo to run your app with arguments. Normally any arguments you provide on the cargo command line will be interpreted by Cargo to be arguments to Cargo itself, but if you separate those arguments with a double-dash, then Cargo will interpret them to be for the program you're running:

cargo run -myargument --yourargument

will generate an error message from Cargo, because Cargo itself doesn't understand those arguments, but:

cargo run -- -myargument --yourargument

will work, because Cargo understands that its job is simply to pass on these arguments to your program. (Of course, as mentioned, those arguments don't currently do anything.)

So let's use the built-in capabilities of Rust to handle a couple of arguments. Instead of printing "Hello, world!", let's just print whatever arguments we provide. We won't have to worry if an argument is a String type or an i32 type or a this or a that type, because all arguments start out as String types.

Imagine a command line like this: cargo run -- apple banana 12 female. Although we see four arguments here, there are really five; the very first argument is the (relative) path to the program, in this case target/debug/parse_native. We'll see that in just a minute.

Each of the arguments, including the path to the program, are placed into String-type variables, accessed via the "args()" function within the std::env library. These Strings can be "collected" into a single "stack" (or array, or vector) variable called a "vector". Here's a simple program that reads the variables, and prints out the count of variables, and the variables themselves:

main.rs
fn main() {
    // We're creating a vector of Strings, named "args", and we're collecting them from the
    // results of calling the "args()" function from the "std::env" library.
    let args: Vec<String> = std::env::args().collect();
    
    // The length of the vector is the number of String-type variables collected.
    println!("Num of args: {}", args.len());
    
    // Loop through each argument in the vector and print it.
    for each_item in args {
        println!("{}", each_item)
    }
} // end of main()

Try it out:

~/projects/RUST/parse_native$ cargo run -- apple banana 12 female
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/parse_native apple banana 12 female`
Num of args: 5
target/debug/parse_native
apple
banana
12
female

You can try other arguments as well. Something to note is that the "12" is not a numeric value. It's a String value that represents a numeric value; you can't do math with it (without some conversion work, at least).

We don't have to use the name "args" for our variable; we can name it pretty much whatever we want. Here are a few alternatives we could have used:

let arguments: Vec<String> = std::env::args().collect();
let myArgs: Vec<String> = std::env::args().collect();
let switches: Vec<String> = std::env::args().collect();
let porgramInputs: Vec<String> = std::env::args().collect();

Notice that I misspelled "program" above in "porgramInputs", to highlight that a misspelling can be acceptable (provided your misspelling is consistent throughout your "porgram").

Parsing the Arguments

The final thing to do is to change the behavior of our program depending on what the program arguments are. We won't be fancy and do any error-checking or bounds-checking or making sure the arguments are given in a consistent manner, etc; it'll just be a quick-and-dirty app to print "I am happy!" or "I am sad!" depending on which first argument we provide to the program. Here's the code:

fn main() {

  let args: Vec<String> = std::env::args().collect();

  if args[1] == "happy"
    { println!("Whoo-hoo! I am happy! {}! {}! {}!", args[1], args[1], args[1]) };
  
  if args[1] == "sad"
    { println!("Boo-hoo! I am sad! {}! {}! {}!", args[1], args[1], args[1]) };

} // end of main()

And the results:

$ cargo run -- happy --sad -doldrummy 12 -54 -"-54"
   Compiling testflight v0.1.0 (/home/kent/projects/rust/parse_native)
    Finished dev [unoptimized + debuginfo] target(s) in 0.25s
     Running `target/debug/parse_native happy --sad -doldrummy 12 -54 --54`
The number of arguments given is: 7
Whoo-hoo! I am happy! happy! happy! happy!

And that's the basics for processing program arguments using Rust's inbuilt capability. However, as mentioned, we're not taking into account the difference between "sad" and "Sad" and "SAD", or printing errors if too many or too few arguments are given, and we're not producing any help information in the case of other bad input, etc. And that's why we might want to turn to an argument parser that is a bit more robust than what Rust provides natively. That's where "Clap" comes in. We'll take a look at that next, in the Use Clap to Parse Arguments lesson.