Gigi Labs

Please follow Gigi Labs for the latest articles. Programmer's Ranch no longer has its domain, so please update your bookmarks and links to programmersranch.blogspot.com.

Monday, May 13, 2013

C# Basics: Command Interpreter Using Methods

Hello! :)

Today we're going to write a program similar to the Windows command prompt, Linux shell, or DOS. It's just a program that accepts commands from the user, and if it recognises the commands, it does something particular. We'll use this program to learn how to use methods, but we'll also learn a few other things as we go along. This article is admittedly longer than usual; rather that split it into two parts, I'm simply not going to post anything new tomorrow, so you can read this over two days if you like.

Let's start with this code:


            while (true)
            {
                String command = Console.ReadLine();

                if (command == "exit")
                    break;
            }

Here, we're accepting a line of text from the user and storing it in the String variable command. Instead of keeping a boolean variable to determine when to quit the loop, as in "C#: ASCII Art Game (Part 1)", we just loop forever (since the while condition is always true). When an "exit" command is received, the break statement allows us to break out of the loop anyway. Try that now.

Next, we add a little spice to our program. First, we set the console title:

            Console.Title = "Command Interpreter";

...and then, we make the user's input show up in white, by replacing the line containing Console.ReadLine() as follows:

                Console.ForegroundColor = ConsoleColor.White;
                String command = Console.ReadLine();
                Console.ResetColor();

Now, let's test what we have so far, by supporting an "echo" command. An "echo" command simply returns whatever you write after it. For example, if you write:

echo I can has cheezburger

...then you'd get:

I can has cheezburger

The only problem here is that we need to separate the command ("echo") from the parameters ("I can has cheezburger"). One way to do this is to break the command string into an array of individual words, as follows:

                String[] words = command.Split();

The command itself is now stored in words[0], while the parameters (if they exist) are in words[1] onwards. Finally, let's change the part where the "exit" command is being handled, and change it into a switch statement. The full code so far should look like this:

            Console.Title = "Command Interpreter";

            while (true)
            {
                Console.ForegroundColor = ConsoleColor.White;
                String command = Console.ReadLine();
                Console.ResetColor();

                String[] words = command.Split();

                switch (words[0])
                {
                    case "exit":
                        return;
                    case "echo":
                        for (int i = 1; i < words.Length; i++)
                        {
                            if (i > 1)
                                Console.Write(' ');
                            Console.Write(words[i]);
                        }
                        Console.WriteLine();
                        break;
                }
            }



For the "echo" command, we used a technique similar to that used in yesterday's article, "C# Basics: Morse Code Converter Using Dictionaries". You'll notice that the break statement used by the exit command has been changed to a return statement. That's because break has a different meaning when used in a switch statement, so we can't really use it there. Instead, return allows us to leave the Main() method completely. We can now test the program:


You'll notice that the code in the Main() method is beginning to grow, and will continue growing as we add more commands. We can organise our code by using methods. Methods are also called functions or subroutines in other languages, or even prodecures if they don't return anything. Take the following code:

                Console.ForegroundColor = ConsoleColor.White;
                String command = Console.ReadLine();
                Console.ResetColor();


...and move it into a new method outside Main(), as follows:

        static String ReceiveInput()
        {
            Console.ForegroundColor = ConsoleColor.White;
            String command = Console.ReadLine();
            Console.ResetColor();
            return command;
        }


This functionality is now defined in this method called ReceiveInput() which returns a String (i.e. it returns the input from the user). In the place where this code originally was, we now need only call this method:

                String command = ReceiveInput();

You'll notice that we now have two command variables declared: one in Main(), and one in ReceiveInput(). That's actually allowed, because they are in different methods. We say they are declared in a different scope.

Now, let's organise the code for the "echo" command in a similar way. Create a new method as follows:

        static void Echo(String[] words)
        {
            for (int i = 1; i < words.Length; i++)
            {
                if (i > 1)
                    Console.Write(' ');
                Console.Write(words[i]);
            }
            Console.WriteLine();
        }

This method has a void return type: that means it doesn't return anything - it just runs the statements within its body. This method is also different from the previous one in that it takes the parameter words. Methods can take any number of parameters in order to receive input data that they will be working with.

The code for the "echo" command can now be simplified as follows:

                    case "echo":
                        Echo(words);
                        break;

If you run the program now, you'll notice it works exactly the same as before.

Now, let's support another command called "total":

        static int Total(String[] words)
        {
            int total = 0;
            try
            {
                for (int i = 1; i < words.Length; i++)
                    total += Convert.ToInt32(words[i]);
            }
            catch (Exception)
            {
                Console.WriteLine("Invalid input");
            }

            return total;
        }

...and...

                    case "total":
                        int total = Total(words);
                        Console.WriteLine(total);
                        break;

The "total" command just calculates the sum of the numbers passed as parameters. Example output:




Finally, let's now support an "average" command:

        static int Average(String[] words)
        {
            int total = Total(words);
            int average = total / (words.Length - 1);
            return average;
        }

...and...

                    case "average":
                        int average = Average(words);
                        Console.WriteLine(average);
                        break;

You'll notice we are using Total() directly to calculate the sum used by the average, instead of repeating the code in Total(). One of the great benefits of methods is that they allow code reuse and eliminate duplicate code - something that plagued my earlier article, "The ASCII Table (C#)". A best practice in programming is the Don't Repeat Yourself (DRY) principle.

Output:


You'll remember from "C# Basics: Arithmetic and Exceptions" that we aren't getting any decimal point in divisions because we are working with integers.

That's it for today! We wrote a little command interpreter, learned how to create our own methods, and learned a lot of new stuff in the process including the break statement, the return statement, and variable scoping. As I wrote earlier, since this was extra long, there will be no article tomorrow!

Here's the full code for today's article:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CsCommandInterpreter
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.Title = "Command Interpreter";

            while (true)
            {
                String command = ReceiveInput();

                String[] words = command.Split();

                switch (words[0])
                {
                    case "exit":
                        return;
                    case "echo":
                        Echo(words);
                        break;
                    case "total":
                        int total = Total(words);
                        Console.WriteLine(total);
                        break;
                    case "average":
                        int average = Average(words);
                        Console.WriteLine(average);
                        break;
                }
            }
        }

        static String ReceiveInput()
        {
            Console.ForegroundColor = ConsoleColor.White;
            String command = Console.ReadLine();
            Console.ResetColor();
            return command;
        }

        static void Echo(String[] words)
        {
            for (int i = 1; i < words.Length; i++)
            {
                if (i > 1)
                    Console.Write(' ');
                Console.Write(words[i]);
            }
            Console.WriteLine();
        }

        static int Total(String[] words)
        {
            int total = 0;
            try
            {
                for (int i = 1; i < words.Length; i++)
                    total += Convert.ToInt32(words[i]);
            }
            catch (Exception)
            {
                Console.WriteLine("Invalid input");
            }

            return total;
        }

        static int Average(String[] words)
        {
            int total = Total(words);
            int average = total / (words.Length - 1);
            return average;
        }
    }
}

As an exercise, test the program above and see if you can find any bugs (there are at least two). Solve them.

Come back on Wednesday for the next article! :)

1 comment: