CHAPTER 7: YOUR INPUT, PLEASE



We've gone over a lot of cool stuff with Squirrel, but so far, we haven't made anything interactive. Most users tend to like programs more when they can give it their own input, and that's what we'll be giving them the power to do.

If you are still using the old version of the downloadable VM, please get the latest version here. At the time of this writing, the browser version has not yet been written to accept input.


LESSON 7.1: GETTING INPUT

To get input from the user, we'll be using the embedded function, getInput(). This function pauses the application and waits for the user to enter a number or string in the console, and then returns that input the same way another function does with the return keyword.

print("What is your name? ");

local input = getInput();

if(input == "Jonny") print("Jonny Bravo?");

getInput() is set to return a string, so if you need a number, you'll have to use the conversion functions we covered earlier.

local input = getInput();

print(input + 5); //Before conversion
print("\n");

input = input.tointeger();

print(input + 5); //After conversion

If you were to enter 5 as your input, the first output would be 55, and the second would be 10.

EXERCISE 7.1

Write a program that asks the user for their first and last names separately, then prints them out together.


LESSON 7.2: BOOLEAN INPUT

Sometimes, you need to ask a user a yes or no question. You could compare the input in an if or switch statement each time, but what if the user answers with something invalid? What if they use weird capitalization? Wouldn't it be easier to have a function that takes care of these posibilities every time? That's what we're going to build in this lesson. Let's start with the basic form of the function:

function confirm(){
	local input = getInput();
	
	switch(input){
		case "yes":
			return true;
			break;
		case "no":
			return false;
			break;
	};
};

In a perfect world, this is all we would need. Sadly, if the user enters anything other than a lowercase "yes" or "no", the function will not return any value at all, and the variable counting on that data will not recieve it, which would break your program. So let's add a safety net.
function confirm(){
	local input = getInput();
	input = input.tolower(); //Make the input all lower case
	
	switch(input){
		case "yes":
			return true;
			break;
		case "no":
			return false;
			break;
	};
};

This will protect us from different cases. Next, we'll need a way to make sure that if the user enters a wrong word, such as if they made a typo and hit enter before noticing it, it will ask them to try again.

function confirm(){
	local input;
	
	while(true){ //This makes it continue to ask over and over again until it gets a correct answer
		input = getInput();
		input = input.tolower(); //Make the input all lower case
		
		switch(input){
			case "yes":
				return true;
				break;
			case "no":
				return false;
				break;
			default: //This lets the user know they need to answer better
				print("Come again? ");
				break;
		};
	};
};

This new version will continuously ask for new input until it recieves a valid answer. However, sometimes a user will just want to type "y" or "n". As we covered with switch statements, you can have multiple conditions, so let's fix our code so that it accepts different inputs in addition.

function confirm(){
	local input;
	
	while(true){ //This makes it continue to ask over and over again until it gets a correct answer
		input = getInput();
		input = input.tolower(); //Make the input all lower case
		
		switch(input){
			case "y":
			case "yes":
				return true;
				break;
			case "n":
			case "no":
				return false;
				break;
			default: //This lets the user know they need to answer better
				print("Come again? ");
				break;
		};
	};
};

Now it will accept single-letter responses in addition to full words. Keep this function in your code for future lessons, as we'll be using it again.

EXERCISE 7.2

Add new cases that will accept different forms of "yes" or "no", maybe something in a different language. Then make a program that asks for a name and confirms whether or not the name entered is correct.


LESSON 7.3: PARSING INPUT

When dealing with text, parsing is when you break the text into words and tags so that it can be easily examined by the program. Let's start building a function that breaks a string down into words using spaces to divide the string.

function parse(str){
	local currentword = "";
	local newarray = [];

	if(str.len() == 0) return ["ERROR: Cannot parse empty string!"];
};

First, we'll need these two variables. One is to store the current word we've found so far, and the other is the array of words that will be returned at the end. The function then checks that the string has a greater length than zero, and if not, it returns a one-element-long array with an error message. An array is returned because code following the function call will be expecting an array type variable. This is useful if you're printing your new array; the error message will show up in its place.

Other methods of error handling are available, but we'll just use this for now.

function parse(str){
	local currentword = "";
	local newarray = [];

	for(local i = 0; i < str.len(); i++){
		if(str.slice(i, i + 1) != " "){
			currentword += str.slice(i, i + 1);
		} else {
			newarray.push(currentword);
			currentword = "";
		};
	};
	newarray.push(currentword);
	
	return newarray;

	if(str.len() == 0) return ["ERROR: Cannot parse empty string!"];
};

In this new for loop, we go from the start of the string and check what each character is. If the character is not a space, it is added to a temporary word. When a space is found, the word is pushed onto the array and then emptied to make room for the next one.

When the loop ends, we push the final word onto the array and return the array. We now have a parsed string that is ready for us to do things with. Using this code, we can look at the result for ourselves:

local data = parse(getInput());
print("\n\n");

if(data != 0) foreach(val in data){
	print(val + "\n");
};
Try entering a full sentence and see how everything separates. Each word should show up on a separate line.

EXERCISE 7.3

This function is actually incomplete. Add another if statement that checks for quotation marks and disables word pushing until a matching quotation mark is found or the end of the string is met. Then add a check that skips over multiple spaces to make sure empty elements are not added to the array. Save the function for later use.


LESSON 7.4: PUTTING IT TOGETHER

If you haven't figured out the parsing function, you can use mine here:

function parse(str){
	local currentword = "";
	local newarray = [];
	local quoting = 0;
	
	if(str.len() == 0) return 0;
	
	for(local i = 0; i < str.len(); i++){
		switch(str.slice(i, i+1)){
			//White space
			case " ":
			case "\n":
				if(currentword != "" && quoting == 0){
					newarray.push(currentword);
					currentword = "";
				} else if(quoting != 0) currentword += str.slice(i, i + 1);
				break;
			//Quotation
			case "\"":
				if(quoting == 0) quoting = 1;
				else if(quoting == 1) quoting = 0;
				else if(quoting == 2) currentword += "\"";
				break;
			case "\'":
				if(quoting == 0) quoting = 2;
				else if(quoting == 2) quoting = 0;
				else if(quoting == 1) currentword += "\'";
				break;
			//Escape characters
			case "\\":
				if(i < str.len() - 1 && quoting != 0){
					switch(str.slice(i + 1, i + 2)){
						case "\\":
							currentword += "\\";
							i++;
							break;
						case "n":
							currentword += "\n";
							i++;
							break;
						case "\"":
							currentword += "\"";
							i++;
							break;
						case "\'":
							currentword += "\'";
							i++;
							break;
					};
				}; else if(quoting == 0){
					switch(str.slice(i + 1, i + 2)){
						case "\\":
							currentword += "\\";
							i++;
							break;
						case "\"":
							currentword += "\"";
							i++;
							break;
						case "\'":
							currentword += "\'";
							i++;
							break;
					};
				};
				break;
			//Everything else
			default:
				currentword += str.slice(i, i + 1);
				break;
		};
	};
	newarray.push(currentword);
	
	return newarray;
};

Copy the functions we've gone over so far into a file called lib.nut. This is where we get to see a cool feature of Squirrel: when you run a script file, Squirrel remembers everything that was defined in that file, allowing it to be used in the next one that is run. By running lib.nut before anything else, you can store code you want to reuse between programs without having to copy it into every single one, making much cleaner code. You can actually rename your library file to anything you want; lib.nut is just a concise name.

Keep this file around for future projects, and be sure to add any other functions you happen to write if you find the need for them.

Our project is to make a simple list system that runs on a command line. It will use four commands: add, delete, print, and exit. Using the parser, we will break user commands into two parts, a function, and an argument. When the user tells the program to add something, it will be parsed, and the argument for add will be pushed into an array. When the user says delete, the program will search for an entry and remove it from the array if it exists. The print command will list all entries found on a separate line each. Lastly, exit will ask the user yes or no, and quit if it gets yes.

Normally, a program like this would store much more information, but for this lesson, we'll just worry about storing names.

To begin, let's add the variables we'll need throughout our program:

local quit = false;
local people = [];
local input = "";
local cmd = [];

First we have a bool to tell when the program is ready to quit, an array to store the list in, a string to store raw input, and another array to store the parsed command. We need to store the raw input to make sure there actually is input before trying to parse an empty string.

Next, we'll begin our program with the main loop.

while(!quit){
};

This loop will continue to run until we tell it to stop by setting quit to true. Next, we'll request input from the user, and if they submit an empty string, then we'll use the continue keyword to skip everything else in the loop, making it try again until it gets a full string. When it does, it passes the string to the parser.

while(!quit){
	print("\n>:");
	input = getInput();
	if(input == "") continue;
	cmd = parse(input);
};

With our input parsed, we'll pass the first element of the cmd array to a switch statement which will have a case for each valid command. In the case of exit, we'll also include the word quit, since users will try either one. A default statement at the end will print a message telling the user they didn't enter a valid command.

while(!quit){
	print("\n>:");
	input = getInput();
	if(input == "") continue;
	cmd = parse(input);

	switch(cmd[0].tolower()){
		case "quit":
		case "exit":
			break;
		case "add":
			break;
		case "delete":
			break;
		case "print":
			break;
	};
};

We set the first word of the input to all lower case so we don't need to worry about users adding weird cases to their input. With this skeleton for our switch statement, we can begin adding the meat of our program!

For quit, the job is simple: ask if the user is sure, and if they say yes, set quit to true. This will tell the while loop to stop. Remember to keep a break statement at the end of each case.
case "quit":
case "exit":
	print("Are you sure you want to quit? ");
	quit = confirm();
	break;

For the add function, we need to do a check to make sure a second word was entered. This is the argument needed by the add function. Then we check if people is empty. If it is, we can push the new entry, otherwise, we check that it doesn't exist already before pushing it to the end. This is done by using a for loop to check each element of the people list. Since 0 is an index of the list, we'll use -1 to indicate no element found. If it isn't found, push the new entry, else, print an error to say the entry already exists. We do this check because in more complex storage, we need to make sure we aren't overwriting anyone else's account.

case "add":
	if(cmd.len() <= 1){
		print("ERROR: String expected.\n");
		break;
	};

	if(people.len() == 0){
		print("Added " + cmd[1] + "\n");
		people.push(cmd[1]);
	} else {
		local nameFound = -1;
		for(local i = 0; i < people.len(); i++){
			if(people[i] == cmd[1]){
				nameFound = i;
				break;
			};
		};
	
		if(nameFound == -1){
			print("Added " + cmd[1] + "\n");
			people.push(cmd[1]);
		} else print("ERROR: Entry " + cmd[1] + " already exists.\n");
	};
	break;

The print function is pretty simple. We just show the number of entries found, then run a quick for loop to print each one.

case "print":				
	print("\n" + people.len() + " ENTRIES FOUND:\n");
	for(local i = 0; i < people.len(); i++){
		if(people[i] != "") print(people[i] + "\n");
	};
	break;

Finally, the delete function. Similar checks are used as in add, and if they pass, a loop runs through to find the index of the entry and use the array.remove() function to delete it. Squirrel will automatically resize the array for us, so we don't need to worry about reallocating any memory or recounting the number of entries.

case "delete":
	if(people.len() == 0){
		print("ERROR: Nothing to delete.\n");
		break;
	};
	
	if(cmd.len() == 1){
		print("ERROR: String expected.\n");
		break;
	};
	
	local nameFound = -1;
	for(local i = 0; i < people.len(); i++){
		if(people[i] == cmd[1]){
			nameFound = i;
			break;
		};
	};
	
	if(nameFound >= 0){
		people.remove(nameFound);
		print("Deleted " + cmd[1] + "\n");
	} else {
		print("ERROR: Entry not found.\n");
	};
	break;

We print an error if the entry is unfound just in case the user mistypes or makes some other mistake. Lastly, the default statement tells them they entered a function name that doesn't match anything. Close your switch statement and run the script. Enter these commands to try it out:

add Joey
add Chandler
add Rachel
print

If all went well, you will have a list of names. Remember how our parser works if you want to enter full names; wrap the name in quotes to have them treated as a single entry. Your output should look like this:

Added Joey

Added Chandler

Added Rachel

3 ENTRIES FOUND:
Joey
Chandler
Rachel

Now it's time for Rachel to move out. Try out the delete command on Rachel so she's no longer on the tennants list. Finally, enter quit or exit and close the program.

You now have a working list manager! As it is now, though, it does not support saving or loading files, so there's not much more you can do with it. That functionality will be added in a future update of the VM.

EXERCISE 7.4

Add a second argument that stores the age alongside the name of each person in your list. Make it so you can delete them just by name. The print command should show the name and age on the same line. Then add a find command that lists all entries that contain the argument string and no others.


LESSSON 7.5: SAFE TYPECASTING

Remember way back when we discussed functions that let you change a value's type? Certain values would cause Squirrel to crash. For example, trying to convert a string into an integer would not work if it didn't begin with a number. We're going to fix this issue with a function that checks whether or not it's safe to convert a string into an integer. If it fails, we can give a custom error message and then offer the users a safety net, such as a default value.
function strint(str){
	if(typeof str == "float" || typeof str == "bool" || typeof str == "double" || typeof str == "integer") return true;
	if(typeof str != "string") return false;
	for(local i = 0; i < 10; i++){
		if(str.slice(0, 1) == i.tostring()) return true;
	};
	return false;
};

The first thing we check is if the argument is a different kind of number. If it is, then of course it's safe to turn into an integer. Then we check if it's not a string, because if it's not a number, and it's not a string, then it's some kind of object that most likely doesn't have a way to support that conversion, and so we say false to say it's unsafe.

Next we test the first character in the string. We only need to chec the first because that's all Squirrel needs; if anything after the number is not a number, too, then it's just cut off. We check for string versions of every number, 0 through 9. As soon as we find one that matches, we return true.

Finally, if all else fails, then the function couldn't find a number at the beginning, so we just return false. And that's it! Save this function into your lib.nut, since we'll be using it in the future.

Are you enjoying this tutorial? If so, why not send a donation? You can do so by clicking the donate link at the top of the screen, and if you do, please attach a note to your payment letting me know it's for the tutorial!

<< PREV