You already know how to get user input via command-line arguments. In this lesson we are going to look at another way of getting input from the user: to prompt them for it from within the program itself.

Prompting the user for input

As I’ve mentioned earlier, the terminal is connected to 3 standard communication channels: standard input, standard output and standard error. We have used the two output channels already. Now we are going to read in user input via standard input.

Let’s get started right away.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import "core:fmt"
import "core:os"

main :: proc() {
    buf: [100]byte

    fmt.print("Enter something: ")
    os.read(os.stdin, buf)
    fmt.println("You typed:", buf)
}

Save the program and run it. When prompted to enter something, type “Odin” and hit the ENTER key.

[lorenzo@orthanc odin]$ odin run .
Enter something: Odin
You typed: [79, 100, 105, 110, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

That was probably not what you expected. Those things happen, but let’s look at both the program and the output and see if we can begin to understand why we get the output we get.

We can try to run the program a few more times and see if we can find any patterns. I’ll try typing “Commodore Amiga”.

Enter something: Commodore Amiga
You typed: [67, 111, 109, 109, 111, 100, 111, 114, 101, 32, 65, 109, 105, 103, 97, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

The number 109 repeats twice towards the beginning. Could that correspond to the double m in “Commodore”. This word also has three os in it. The number 111 shows up three times, and two times around those 109s that we speculated could be the letter m.

But then, doesn’t Odin start with the letter o? Why did we get the number 79 instead of 111 in that case? Could there be a difference between capital and lower case letters? We can test for that.

Enter something: oO
You typed: [111, 79, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

At least we are getting consistent results. That’s fine. But there is another little thing to consider. Count the number of letter is the input and compare it to the non-zero values we get in the output. We always seem to be off by one. The last non-zero number is always 10. That’s another thing we have to figure out.

The size of the buf array

To begin with, let’s make sure we know why we get all those zeros at the end. Modify the program so that the length of the array is 10 instead of 100:

buf [10]byte

Now let’s run the program again.

Enter something: Odin
You typed: [79, 100, 105, 110, 10, 0, 0, 0, 0, 0]

So the lenth of the output is going to be related to the size of the variable. What happens is our input in longer than there is space in that array? Let’s try it out.

Enter something: Commodore Amiga
You typed: [67, 111, 109, 109, 111, 100, 111, 114, 101, 32]
lorenzo@orthanc:~/Temporary/odin$ Amiga
bash: Amiga: command not found...

It would seem something went wrong. The array got filled up and then there were some letters left that the program spit back to the terminal. The terminal seems to have tried to run “Amiga” as a program, but since I don’t have a program with that name on my system, the shell interpreter gave me an error message.

I’m not exactly sure how the Windows command-line is going to handle this. But the first part should be the same. The array gets filled up and the program discards all the output that couldn’t fit into the available space.

If the program didn’t discard the information but rather continued to store information outside of the bounds of the array, those letters would end up being stored in memory that was most likely intended for something else, causing the program to crash or behave erratically.

What is a byte?

In the declaration of buf we saw that its type was an array of bytes. So let’s quickly understand what that is as well. So far, you have learned that there is a type to store integer numbers, called int. That wasn’t quite the wholed truth. There are actually several types to store integer numbers. We will dedicate an entire lesson to looking at all the different built-in types in Odin. For now, let’s just look at byte, since that is what we are concerned with here.

Integer types can be either signed (they can be negative) or unsigned (they can only be positive). They also exist in different bit sizes. A byte is an unsigned, 8-bit number, which you can confirm for yourself by looking into the Odin sources:

$ grep byte /opt/odin/base/builtin/builtin.odin
byte :: u8 // alias

Once again, sorry that I don’t know the corresponding command in Windows. However, if you are on that platform, you can see the same thing here.

Here u8 refers to an unsigned 8-bit integer. I am going also going to dedicate a lesson to understanding binary numbers as well as what signed and unsigned mean, but for now just accept that a unsigned 8-bit variable can hold any number between 0 and 255.

A very simple program can confirm that this is the case.

1
2
3
4
5
6
7
8
9
package main

import "core:fmt"
import "core:os"

main :: proc() {
    a: u8 = 255
    b: u8 = 256
}

We try to compile this and we get:

$ odin run .
/home/lorenzo/Temporary/odin/main.odin(8:13) Error: Cannot convert numeric value '256' from '256' to 'u8' from 'untyped integer'
	b: u8 = 256
	        ^~^
	The maximum value that can be represented by 'u8' is '255'

The error message is very clear in this case. But why do we create an array of bytes instead of a string or even an array of runes?

$ grep 'read ::' /opt/odin/core/os/os_linux.odin
read :: proc(fd: Handle, data: []byte) -> (int, Error) {

I used ‘read ::’ as the filter because simply doing ‘read’ spit out too many lines. If you prefer seeing in the browser, it is here.

From this we can see that read is a procedure that takes two arguments: fd which is a Handle and data which is a byte array. The first argument is basically a file and we are simply giving it the special file os.stdin which means read from the keyboard (in the world of Unix, everything is a “file”, even hardware devices as presented to the system as a file). The second argument confirms why we use an array of bytes, because the procedure requires one.

That still doesn’t adequately explain why Odin couldn’t have used a string or an array of runes instead. We will explain that in detail later in the course. For now, unfortunately you are going to have to accept that this is just the way it is.

Summarizing os.read

At this point, let’s quickly summarize what this line does:

os.read(os.stdin, buf)

A bit simplified, the os.read procedure reads from a file and stores the content of that file in a buffer (an array of bytes). By passing in os.stdin as the “file” argument, os.read is going to take whatever characters we type, up to the point where we hit the ENTER key and copy it into the byte array.

But wait! We may have typed characters, but we got numbers back. Why is that now then? Let’s turn to that next.

Letters and numbers

You may have heard at some point that computers “think in numbers”. That is not accurate. They do not think at all first of all. Second, the “numbers” are really not numbers but two distinct voltage states, which we can call voltage high (Vhi) and voltage low (Vlo). We can imagine these two states as representing two distinct concepts such as true and false, yes and no, on and off, or even 0 and 1. Combining multiple wires, each carrying eiher Vhi or Vlo, we can create larger numbers by combining the 0s and 1s that these individual wires carry, just like we can take a 5 and put it in front of a 7 to get 57. Of course, in the computer we are limited to only two distinct digits instead of the 10 that we use in everyday life.

It turns out not to be a problem, as you shall see more about in the lesson on binary numbers. Any number that can be respresented with our everyday numbering system can equally well be represented with a binary number. What happened here is that we only made space in the computers memory for three letters, but we typed four. Just like you can’t get 2 liters of water into a bottle that can only take 1 liter, so you can’t use more memory than you have. (If the program had allowed the extra letter, it would have to be stored in memory reserved for something else, potentially causing memory corruption, something we really don’t want.) The cells in the computer’s memory are only able to store what we will imagine to be numbers, consisting of 0s and 1s. There is no memory cell that can hold a letter of the alphabet, an emoji, a movie or a song. However, you use the computer all the time and you see this. This lesson was typed out on a computer and it consists of lots of text (too much, some may say).

Still, the memory cells only hold numbers. Using software, we can create an abstraction for interpreting those numbers in different ways. Take a number like 79, for instance. In a particular context, it can just be the number 79 that you can add, subtract, mutliply and divide with other numbers. In a different context, it could reprenent the capital letter “O”. In yet another context it sets a color value to a single pixel on the screen or maybe combines with many other numbers to represent a fragment of a song.

The context is vital to understand what a number means inside the computer (or inside your program). That is why types is such an important concept in programming. The type reprents the context. In the context of a string, 79 means “O”, but in the context of an int, it is simply 79.

Converting from numbers into a string

In Odin (as well as in most other languages), a string is a distinct type, that is it provides a context in which to interpret a sequence of numbers in a distinct way. We have got as far as to have a numerical representation of the string in a byte array. Or next task is to convert that into an actual string.

 1package main
 2
 3import "core:fmt"
 4import "core:os"
 5
 6main :: proc() {
 7	buf: [100]byte
 8
 9	fmt.print("Enter something: ")
10	os.read(os.stdin, buf[:])
11	s := string(buf[:])
12	fmt.println("You typed:", s)
13}

I set the buffer back to 100 so that we have enough space for typing up to 100 letters. Run the program and you should get output similar to the one below.

$ odin run .
Enter something: Odin
You typed: Odin

$

There we go, we have created a string context in which our sequence of numbers can be represented as letters instead. (Bear in mind that they are still numbers).

But why is there an empty line between “You typed: Odin” and the prompt (the “$”)? We will have to investigate a bit more. Remember that fmt.print prints out text without jumping down to the next line, whereas fmt.println does jump to the next line after printing the text? Look at the output of the following program, for instance:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import "core:fmt"

main :: proc() {
    fmt.print("ABC")
    fmt.print("DEF")
    fmt.println("GHI")
    fmt.print("JKL")
    fmt.println("MNO")
}

Look at the output of the program and especially where the line breaks and relate it back to the lines of code in the above program.

The trailing 10 in the byte array

I would expect the following program to print “Odin is programming done right” all on one line, if we enter Odin at the prompt, since I am using fmt.print(s).

 1package main
 2
 3import "core:fmt"
 4import "core:os"
 5
 6main :: proc() {
 7	buf: [100]byte
 8
 9	fmt.print("Enter something: ")
10	os.read(os.stdin, buf[:])
11	s := string(buf[:])
12	fmt.print(s)
13    fmt.println(" is programming done right!")
14}

However:

[lorenzo@orthanc odin]$ odin run .
Enter something: Odin
Odin
 is programming done right!

Remember I said everything needs to be represented as a number? I really meant it. Even where a line should break needs to be indicated with a number, in this case the number 10, which we saw in the output at the beginning of the lesson. That is the reason why we had more characters than we thought we had typed. When we hit ENTER to end the input, that character is also copied into the byte array.

Slices

Did you notice how in two places we wrote buf[:]. That means we are converting the whole byte array into a slice (yet anther concept that you will have to wait for another lesson for a full explanation. Simply put, a slice is a piece of an array. As usual, it’s best explained in code.

1
2
3
4
5
6
7
8
package main

import "core:fmt"

main :: proc() {
    s := "Welcome to Odin"
    fmt.println(s[3:8])
}

If you run this, you will get the output “come t”, since the c is the 4th letter (remember, the first letter has index 0) and the e is the 9th letter (the one with index 8). If you wonder why I say the 9th letter, remember that the space in between the words takes one character.

Try it out

Try changing the values of the slice from 3 and 8 to something else. Confirm that in each case you get the output you expect. Also try going out the bounds of the string. What happens if you do this for example: [3:100]? The more you try and study the output the better of an understanding you will have of the capabilities and the limitations, not only of slices, but other things as well.

Trimming the string

Now, let’s put what we know about slices to good use. If I have understood my own discussion well, we could “slice out” that newline character at the end of the string. But how do we know how long the string is? Let’s look that at the os.read procedure again:

read :: proc(fd: Handle, data: []u8) -> (total_read: int, err: Error) {…}

The procedure returns two values: the bytes read and an error. We should always check for errors and deal with them, but for the sake of being able to focus on one thing at a time, we’ll temporarily discard that. More interesting is the first return value: total_read. We will use that to our help!

 1package main
 2
 3import "core:fmt"
 4import "core:os"
 5
 6main :: proc() {
 7	buf: [100]byte
 8
 9	fmt.print("Enter something: ")
10	bytes_read, _ := os.read(os.stdin, buf[:])
11	s := string(buf[:bytes_read-1])
12	fmt.print(s)
13    fmt.println(" is programming done right!")
14}

The magic happens on line 11. We get rid of the trailing newline character by taking the slice buf[:bytes_read-1]. We run the program to confirm that what we think is true and what is actually true are the same.

Enter something: Odin
Odin is programming done right!

Success!

Try it out

Verify that if you do buf[:bytes_read-2], it removes the newline as well as the last letter of whatever you typed. Also, see what happens if you do buf[1:bytes_read-1]. What if we needed to

Dealing with Errors

If for some reason, the os.read call was unable to open the standard input channel, that is an unexpected state for this program and indicates something is wrong. In that case we don’t want to continue. So let’s handle the error condition as well:

package main

import "core:fmt"
import "core:os"

main :: proc() {
    buf: [100]byte

    fmt.print("Enter something: ")
    bytes_read, err := os.read(os.stdin, buf[:])
    if err != nil {
		fmt.eprintln("Unable to open stdin:", err)
		os.exit(-1)
    }
    s := string(buf[:bytes_read-1])
    fmt.print(s)
    fmt.println(" is programming done right!")
}

This should normally not happen. There are times when it does, and will look at that when we get to writing servers. In any case, we should always prepare for any eventuality.

Reading in numbers

Let’s finally look at how we can read in a number from the keyboard. We don’t need to learn anything new. We just apply what we already new into the current program.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "core:fmt"
import "core:os"
import "core:strconv"

main :: proc() {
    buf: [100]byte

    fmt.print("Enter something: ")
    bytes_read, err := os.read(os.stdin, buf[:])
    if err != nil {
		fmt.eprintln("Unable to read from stdin:", err)
		os.exit(-1)
    }

	s := string(buf[:bytes_read-1])
	n, ok := strconv.parse_int(s)
    if !ok {
		fmt.eprintfln("Not a valid number: %s", s)
		os.exit(-1)
    }

    fmt.printfln("I got a number: %d", n)
}

This time I will let you go through the program and explain how it works.

Summary

This lesson has perhaps been a bit heavy on concepts. The good thing is will will revisit each of these again. First we are going to build upon what we have learned in this lesson and learn how to validate and manipulate data.

Exercises

Exercise 1

Write a program that asks the user their name and greets them. Test the program several times and study the output.

  1. type in your own name
  2. hit Enter without typing anything
  3. press space a few times and hit Enter
  4. press space a few times, then type your name and press space a few more times before hitting Enter
  5. type your name in all-uppercase or all-lowercase letters
  6. use characters that typically do not show up in names, such as “@$&*”

In the next few lessons you will learn how to validate input data and to manipulate it in different ways so that you can fix each of the issues that you just encountered in your testing.

Exercise 2

Write a program that asks the user to enter the name of a day of the week. If the user typed “Monday” print “Garfield hates Mondays”. For any other day Tuesday-Friday print “It’s a working day”. For Saturday and Sunday print “Yay! It’s the weekend”. For any other day, print “There is no such day!”

Run the program several times. Check what happens when you type

  1. “Monday”
  2. “Tuesday”
  3. “Saturday”
  4. “MONDAY”
  5. “tuesday”
  6. " Wednesday " (add the spaces)

How would you expect such a program to behave in each of the test cases? How did it actually behave? An important part of writing software is to decide how you want your program to behave in different situations and then to make it do so.

Exercise 3

Look at the following program and try to figure out:

  1. if it will compile without errors
  2. what the output will be, if it can compile

Then try to run it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import "core:fmt"

main :: proc() {
    a: rune = 'A'
    b := a + 2

    fmt.println(b)
}

Was the output what you expected? Can you explain why this happens?

Exercise 4

Type in the following program:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import "core:fmt"
import "core:os"
import "core:strings"

main :: proc() {
    buf: [512]byte

    fmt.print("Enter something: ")
    bytes_read, err := os.read(os.stdin, buf[:])
    if err != nil {
	fmt.eprintln("Unable to read from stdin:", err)
        os.exit(-1)
    }

    s := string(buf[:bytes_read-1])
    lc := strings.to_lower(s)
    fmt.printfln("You wrote: '%s'", lc)
}

Run it a few times, entering the following at the prompt:

  1. Hellope!
  2. mango
  3. I AM HERE!

What output do you get in each case? What line in the above program transforms your input into the output you get?

Try chaning strings.to_lower to strings.to_upper and run the program again a few times. What happens this time?

Can you think of some uses for these two procedures?

Exercise 5

Type in the following program exactly as you see it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import "core:fmt"
import "core:os"
import "core:strings"

main :: proc() {
    buf: [10]byte

    fmt.print("Enter something: ")
    bytes_read, err := os.read(os.stdin, buf[:])
    if err != nil {
	fmt.eprintln("Unable to read from stdin:", err)
        os.exit(-1)
    }

    s := string(buf[:bytes_read-1])
    fmt.printfln("You wrote: '%s'", lc)
}

What happens when you run the program and, at the prompt, you type, “I am learning the Odin programming language”. What causes that to happen? Can you fix the program so that it does what you expect it to?

Exercise 6

Write a program that asks the user to enter a number. If the user entered a valid number that is divisible by 10, print “That is a multiple of 10!”. Otherwise, print “That is not a multiple of 10”.

Exercise 7

Write a program that asks the user to enter 5 numbers. Print “That is an odd number”, if the number the user entered is odd. Otherwise, don’t print anything.

Exercise 8

Write a program that asks the user to enter 10 numbers and prints out the greatest of those numbers.