Old school on the command line with Rust

2020-10-09



Background

Oh wow, I'm probably giving away my age here, but one of my first programming languages was GW Basic back in the early 90s. While cleaning up some old boxes, I discovered faded printouts of old programs I wrote while learning to program. Being curious, I wondered whether I'd be able to recreate these marvels of computer science in Rust. What follows are a number of experiments with various degrees of silliness. You've been warned.

Who this is for

Most likely, this is a waste of time for a seasoned Rust developer. In fact, it's probably of little to no interest to most developers, to be honest.

Instead, this might be useful for peeps learning Rust, as it contains a few simple functions that are used to create the various shapes on the command line.

What I tried to do

Way back when, before Windows, OS/2 and MacOS made GUIs the norm, we used the command line (terminal). All. The. Time. You did have some graphics, but that was mostly for games. Instead, we spent hours (days?) using ASCII to create "pictures" on our monitors. While not exactly art, it did actually serve a purpose as it was a fun way of learning about iteration, loops, logic and the value of planning and testing your work.

So, in these experiments, I tried to recreate some of the output generated by my old GW Basic programs from almost 30 years ago. As an example, calling a function draw_triangle(5), should result in:


                *
                **
                ***
                ****
                *****
                

Fancy huh? Well, ok, but it was, back in 1992. Really!

A bit about the code

I'll be throwing little snippets of code up in this article, so please keep in mind that the code is split into a module to make it easier to organise, hence the pub qualifiers on some functions.

Step one - Output some stars

Let's kick off with making a function that will print out a number of stars, based on a parameter.


                pub fn print_stars(count: i8) {
                    for _ in 1..=count {
                        print!("*");
                    }
                }
                

I used i8 for the parameter size, because it's more than large enough for what I needed to do. So, what happens in this code? Basically, we create a range from 1 to count, inclusive (..= is inclusive) and then print a single star for each value in the range. The _ character tells Rust that we really don't care about the value we are iterating over, we just want the number of iterations to happen.

When we run the above code with a value of 5: print_stars(5), we get this output:


                *****
                

Step two - Use step one

Ah, that's some great writing there! Ok, just kidding. Now that we can output a number of stars, what do we do with this? Let's draw a block with our new function that looks like this:


                ****
                ****
                ****
                

What we want to do, is draw three lines with four stars each. In the following function, we do exactly that. First, we create a range from 1..3, inclusive. Then we use these iterations to call print_stars three times, with a parameter of 4 (the number of stars we want). Because print! does not advance to a new line, we do that ourselves.


                pub fn draw_small_block() {
                    for _ in 1..=3 {
                        print_stars(4);
                        println!();
                    }
                }
                

Now for some triangles

Alright, a block, not bad. But with a small tweak, we can also draw this:


                *
                **
                ***
                ****
                *****
                

The code for drawing a triangle is very similar to drawing a box, with one small exception:


                pub fn draw_triangle(max: i8) {
                    for n in 1..=max {
                        print_stars(n);
                        println!();
                    }
                }
                

This time, we care about the value of each iteration, because it tells us how many stars to draw. Other than that, it is similar to our draw_small_box function.

What about this shape?


                *****
                ****
                ***
                **
                *
                

Turns out, it's pretty easy in Rust, because you can reverse a range with a simple command rev(). The above is made with this code:


                pub fn draw_reverse_triangle(size: i8) {
                    for n in (1..=size).rev() {
                        print_stars(n);
                        println!();
                    }
                }
                

Boxing it up

Mmmm, what if we wanted to draw this shape:


                *****
                *   *
                *   *
                *****
                

Well, the start and end is easy, but the middle looks a bit tricky, doesn't it? I added another function that draws a number of spaces, pretty much like the one that draws stars.


                pub fn print_spaces(count: i8) {
                    for _ in 1..=count {
                        print!(" ");
                    }
                }
                

This, in combination with the draw_stars function, can help us draw this box shape. Let's start with a simple function to draw the box and see how that goes.


                pub fn draw_small_box() {
                    print_stars(5);
                    println!();
                    print!("*");
                    print_spaces(3);
                    println!("*");
                    print!("*");
                    print_spaces(3);
                    println!("*");
                    print_stars(5);
                    println!();
                }
                

Ok... That does work, but wow, it's a mess. Everything is hard coded and there's a lot of confusion about what is happening. I also think there's value in splitting out the code that draws the * * sections, as we could use that for other shapes.

First, let's add a function to print stars and a newline after:


                pub fn println_stars(count: i8) {
                    print_stars(count);
                    println!();
                }
                

With this, we can clean up draw_small_box a bit while adding a parameter for the size of the box:


                pub fn draw_small_box(size: i8) {
                    println_stars(size);
                    print!("*");
                    print_spaces(3);
                    println!("*");
                    print!("*");
                    print_spaces(3);
                    println!("*");
                    println_stars(size);
                }
                

Alright, it's a little bit better, but we still have to ugly code for the middle of the box. Basically, what we now need, is a way to print a *, some spaces and then another *. It would also be useful to have a function that does this and also advances to a new line.


                // Prints a * * shape with a variable amount of space between each *.
                // size parameter is the maximum size of the entire output.
                pub fn print_spaced_stars(size: i8) {
                    print!("*");
                    print_spaces(size - 2);
                    print!("*");
                }
                
                pub fn println_spaced_stars(size: i8) {
                    print_spaced_stars(size);
                    println!();
                }
                

Now, we can update the draw_small_box function to make it easier to read and understand:


                pub fn draw_small_box(size: i8) {
                    println_stars(size);
                    println_spaced_stars(size);
                    println_spaced_stars(size);
                    println_stars(size);
                }
                

Running our updated method with a parameter value of 8 produces this output:


                ********
                *      *
                *      *
                ********
                

Revisiting triangles

Alright, we have seen that we can combine our functions to create interesting output. Let's revisit triangles and try to draw this:

    
                    *
                   **
                  ***
                 ****
                *****
                

Basically, we want to draw some spaces, then some stars. As you can see in the code below, we are using functions we've already created and just combining them in a different way to get the desired output.


                pub fn draw_offset_triangle(size: i8) {
                    for n in 1..=size {
                        print_spaces(size - n);
                        println_stars(n);
                    }
                }
                

Small tweaks, big changes

Take a look at the following code and see if you can figure out what it will output.


                pub fn draw_something(size: i8) {
                    for n in 1..=size {
                        print_spaces(size - n);
                        println_stars(n * 2);
                    }
                }
                

The code will be called with 5 as a parameter. See if you can figure out what it'll output before you scroll down.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

And the output from the draw_something(5) call is:


                    **
                   ****
                  ******
                 ********
                **********
                

One last one

Add the following function:


                pub fn println_offset_stars(offset: i8, stars: i8) {
                    print_spaces(offset);
                    println_stars(stars);
                }
                

Then combine this with draw_something to draw a tree!


                pub fn draw_a_tree() {
                    draw_something(5);
                    for _ in 1..=3 {
                        println_offset_stars(4, 2);
                    }
                }
                

Calling this function results in this being output:


                    **
                   ****
                  ******
                 ********
                **********
                    **
                    **
                    **
                

Conclusion

This is not the best Rust code you'll see, far from it. While there are always ways to improve code, I have found that, for me at least, it helps to do these kind of code exercises. Especially when I'm in the beginning stages of learning a particular programming language.

I'm hoping to revisit this article in the future and add a few more shapes. In the mean time, go ahead and make some for yourself - it's a surprisingly engaging way to learn more about a language.



Return to the blog index