We ended the last lesson by trying to write a program to add numbers together, that were passed in by the user as command-line arguments. This didn’t work, because command-line arguments are strings and the + operator is not supported for strings.

String to integer conversion

We are going to need a bit of help from another package, called strconv which exists in the core collection. The best way to figure out how something works is to try it out in a little program.

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

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

main :: proc() {
    sa := "12"
    sb := "47"

    a, _ := strconv.parse_int(sa)
    b, _ := strconv.parse_int(sb)

    fmt.println(a + b)
}

For just a few minutes, let’s ignore the underscores in lines 10 and 11. We’ll get to them below. Run this program to confirm that it does print out 59 as you would expect. If you like, try changing the values of sa and sb a few times and see how the output changes. In fact, let’s try to put something that is not an integer in one or even both the variables.

What happens, for instance if the value of sa and sb are 13 and “foo” respectively? You will get 13. If both variable contains things that don’t represent integer numbers, you will get 0.

Now we are ready to look at those underscores.

Dealing with errors

The parse_int procedure is going to try to convert a string into an integer. However, if that doesn’t work, the function will return 0 as well as another value, which represents an error. If we look for parse_int in the file <odin root>/core/strconv/strconv.odin we see this:

parse_int :: proc(s: string, base := 0, n: ^int = nil) -> (value: int, ok: bool) {

Here, s represents the string we pass in, like “14” or “foo”. We can ignore the base and n arguments for now (yes, just like the command-line can take arguments, so can procedures). Let’s instead focus for a moment on what comes after the ->, which is: (value int, ok: bool). This means the procedure returns not one, but two values: and integer and a boolean. The boolean will be true if the conversion could be done or false if it couldn’t.

Great! Now let’s quickly add a bit of code to check that boolean return value. We replace the underscores with a variable and check that its value is not false. If it is we print a message and exit. Simple, right?

package main

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

main :: proc() {
    sa := "foo"
    sb := "13"

    a, ok := strconv.parse_int(sa)
    if !ok {
        fmt.println("not a valid integer:", sa)
        os.exit(-1)
    }

    b, ok := strconv.parse_int(sb)
    if !ok {
        fmt.println("not a valid ingeter:", sb)
        os.exit(-1)
    }

    fmt.println(a + b)
}

Just a quick reminder that !ok should be read as “not ok”. If you find it easier to read, you could write ok == false instead, since the double equal sign is used for comparion.

This looks 100% perfect nothing could go wrong. Except…

$ odin run .
/home/lorenzo/Temporary/odin/main.odin(17:8) Error: Redeclaration of 'ok' in this scope
	at /home/lorenzo/Temporary/odin/main.odin(11:8)
	b, ok := strconv.parse_int(sb)
	   ^

Oh bother! Why is it that nothing ever just works? Actually, it’s just because there’s something very simple we need to look at first. Please bear with me.

A bit more on variable declaration and value

The space between the two curly brackets that surround the body of the main procedure is called a scope. (It can also be called other things, but here we will refer to it as that.) Within a scope, you are not allowed to redeclare a variable. So far, we’ve been declaring variables using the := operator. I normally call that a Pascal assignment operator, some refer to it as a walrus operator instead. This creates a variable and assigns a value to it at the same time. In the follwing line we create the variable name and give it the value Adjei.

name := "Adjei"

If I want to change the value of name later, I use a single = instead.

name := "Adjei" // declaration

/* some other code */

name = "Klufio" // here we change the value of the variable

With this in mind, can you see what is wrong with the program above? Of course, we try to declare the variable ok twice. There are a few ways to overcome this issue. You could for example rename the second ok somthing else, like ok2. That will work.

 1package main
 2
 3import "core:fmt"
 4import "core:os"
 5import "core:strconv"
 6
 7main :: proc() {
 8    sa := "foo"
 9    sb := "13"
10
11    a, ok := strconv.parse_int(sa)
12    if !ok {
13        fmt.println("not a valid integer:", sa)
14        os.exit(-1)
15    }
16
17    b, ok2 := strconv.parse_int(sb)
18    if !ok2 {
19        fmt.println("not a valid ingeter:", sb)
20        os.exit(-1)
21    }
22
23    fmt.println(a + b)

Running this, we get:

$ odin run .
not a valid integer: foo

However, this is not a very good solution. You are creating an extra variable for no good reason. Now it’s easy to think that “this is such a small program, who cares about just one extra variable” and that would be fine. However, I prefer to stick to principles. No matter how trivial a program is, I want to try to do what is right. And we should really not pollute our namespace with variables we don’t need.

Variables are really useful. That doesn’t mean you should create variables that aren’t needed. You are just pollution your own namespace.

So let’s try something else instead.

Predeclaring varaibles

One thing we can do is predeclare our variables. This is different from what we’ve been doing so far, because when do something like name := "John", we don’t tell Odin this is a string, it figures it out by itself. But when we predeclare a variable, we don’t necessarily give it an initial value, so Odin has no information to go by in order to figure out what type to use. So, we have to specify both the name and the type of the variable. Here are a few examples:

name: string
age: int
is_married: bool
books_in_stock: int = 1400

Notice that I said we don’t necessarily give it an initial value. We can as the last example shows. You might wonder why not just say books_in_stock := 1400 instead, and that is true, it would work just as fine in that particular case. However, as we shall see a bit later, there are actually different kinds of integers. Sometimes you will want to ensure that the correct variant is used (you will be doing a lot of that if you start working with a library like Raylib for instance).

Let’s see an updated version of the program using predefined variables.

 1package main
 2
 3import "core:fmt"
 4import "core:os"
 5import "core:strconv"
 6
 7main :: proc() {
 8    a, b: int
 9    ok: bool
10
11    sa := "foo"
12    sb := "13"
13
14    a, ok = strconv.parse_int(sa)
15    if !ok {
16        fmt.println("not a valid integer:", sa)
17        os.exit(-1)
18    }
19
20    b, ok = strconv.parse_int(sb)
21    if !ok {
22        fmt.println("not a valid ingeter:", sb)
23        os.exit(-1)
24    }
25
26    fmt.println(a + b)
27}

Remember that now that we have predeclared the variables, we need to change := to = in lines 14 and 20. One new thing here: if we are declaring more than one variable of exactly the same time, we can put them together as one line 8.

There, this should work. However, looking at it again, don’t lines 14-18 and lines 20-24 look very similar? Larry Wall, creator of Perl, famously said that the three virtues of a great programmer are: laziness, impatience and hubris. I make absolutely no claims to being a great programmer. I can be quite impatience, not sure if I have hubris, but I can guarantee you that I’m lazy. Now, let’s put that laziness to good use, shall we?

Implementing your own procedures

So far, every program we have written has had a single procedure: main. However, we are allowed to write as many procedures as we like. A procedure is a piece of code that optionally takes one or more arguments and optionally returns something to the caller.

Before we go and update the program we’ve been working on, let’s write a program that creates the simplest possible procedure just to demonstrate a few points.

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

import "core:fmt"

main :: proc() {
    a := value()
    b := value()
    c := value()

    fmt.printfln("the values are %d, %d and %d", a, b, c)
}

value :: proc() -> int {
    return 47
}

For some reason, the number 47 is so important it deserves a procedure that does nothing but return that number. The -> int indicates that this procedure will always return one value and one value only and the type of the only value is going to be an int.

Once we have a new procedure, we can call it as many times as we need to. In the above example, it is called 3 times. Each time, it’s return is assigned to a different variable. As a result, we have the variables a, b and c all holding the value 47.

Do you rmemeber back in lesson 3 when we talked about the default order of execution? Procedures are yet another way to alter the order in which things happen. Here is, more or less what happens.

  1. the program starts at the beginning of main
  2. the program jumps to value which returns the number 47, which is assigned to the variable a
  3. the program jumps to value which again returns the number 47, this time assigned to another variable b
  4. the program jumps to value which yet again returns the number 47, this time assigned to a third variable c
  5. back in main, the printfln statement is run and the program terminates

As you can see, there is a lot of jumping going on between main and value. But what if we didn’t want the same value returned all the time. We want to pass in a value and have the double of that returned. Find, let’s replace the value procedure with another one called double, one that will take an argument in addition to returning a value.

package main

import "core:fmt"

main :: proc() {
    a := double(1)
    b := double(2)
    c := double(3)

    fmt.printfln("The doubles are %d, %d and %d", a, b, c)
}

double :: proc(n: int) -> int {
    return n*2
}

As always, run the program to verify that it does why you expect it to do. (You do look at the code and try to figure it out before running, don’t you?) Then study the procedure and compare it to value.

Try it out

What happens if you don’t pass a value into a procedure that takes one? That is what happens if you tried the following?

d := double()

Getting back on track

Let’s now look at how the idea of our own procedures can help us. We want to take all the code that repeats and stick it into a procedure of its own. Look at the code below. The variables s, n and ok are all private to the parse_int_or_fail procedure. They are not accessible in main and do not need to be declared in main.

I called the procedure parse_int_or_fail to indicate to myself (and anybody else who would use the procedure) that this is a potentially fatal procedure. If you don’t give it valid data, it will simply exit the whole program.

package main

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

main :: proc() {
    sa := "foo"
    sb := "13"

    a := parse_int_or_fail(sa)
    b := parse_int_of_fail(sb)

    fmt.println(a + b)
}

parse_int_or_fail :: proc(s) -> int {
    n, ok := strconv.parse_int(s)
    if !ok {
        fmt.println("not a valid integer:", s)
        os.exit(-1)
    }
}

Notice how much cleaner main became all of a sudden? That is one of the benefits of procedures.

At this stage I’m not going to say that this is a good or a bad way of implementing this program. Right now, I just want to show you what is possible and let you practice as much as possible. Once we have built up enough of a practical foundation and we have written enough little programs, we are ready to start reasoning about what might be “good” code as opposed to “bad” code. For now, worry less about that and more about simply just writing as much code as possible.

As it turns out, by implementing a procedure, that whole issue of predeclaring and not redeclaring variables went away by itself. Go figure, that means I could have just introduced you to procedures and we could have skipped the section about variables. But then again, if we hadn’t tried our hands on it, we might never have got this far.

Take a bit of time to think about why the problem of redeclaring ok simply doesn’t happen any more. If you find it useful that out pen and paper and trace how the programs jumps between procedures. Be aware that, every time the procedure is called it “starts over from zero”, so any previous declarations or values will be gone.

Converting command-line arguments to integers

Everying we’ve discussed in the lesson so far came about as a result of one simple thing: we wanted to write a program that prints out the sum of all the command-line arguments passed into it. Let’s get back to that. Before we implement, we should probably ask ourselves what do it if the user passes in somehing that is not an integer. We could:

  1. exit the program
  2. use some default value and continue

We have tried exiting the program so this time, we will keep going. A sane default value might be 0. We probably do want to at least print a warning message so that the user is made aware that something isn’t right.

Let’s start by making sure we can convert command-line arguments into numbers.

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

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

main :: proc() {
    for a in os.args[1:] {
        n, _ := strconv.parse_int(a)
        fmt.printfln("Got the number: %d", n)
    }
}

We try to run it, passing in both integers and non-integers to ensure that it works properly.

$ ./odin 2 3 4 -202 foo
Got the number: 2
Got the number: 3
Got the number: 4
Got the number: -202
Got the number: 0

So far so good. We don’t have to worry about redeclaring a variable since the for loop introduces its own scope and in each iteration of the loop the previous declaration of n is gone so we can safely declare a new n. Also, we are temporarily discarding the ok part of th e parse_int procedure. We will soon bring it back when we decide to print out the warning messages.

Fine, but how do we print out the sum of the numbers? To do that we need to create another variable to keep a running total as we go through the arguments. This sum variable needs to be declared outside of the scope of the for loop, so that it survives each loop iteration and we can print out the sum after we are done parsing the command-line arguments. After the loop, we print out the sum.

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

package main

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

main :: proc() {
    sum := 0

    for a in os.args[1:] {
        n, _ := strconv.parse_int(a)
        sum += n
    }

    fmt.printfln("The sum is: %d", sum)
}

Again test this with some different combinations of command-line arguments. Notice that if parse_int it returns a zero together with the false to indicate that it failed. That means we are simply adding 0 in such a case, which is good because it doesn’t affect the sum at all.

We are functionally done. However, I promised we would print a warning message if the user passed in something that is not a valid integer. This is usually a good idea. The user maybe was in a hurry, meant to type 45, but their finger slipped a bit so they ended up typing 4r instead, without noticing. But printing an error message, we help the user to know that the result is potentially off because they typed in something that was wrong.

I mentioned about “communication channels” in an erlier lesson. We have 3 such channels, 2 for output and 1 for input. The one we should be using for error output is called the standard error channel. There are variants of the print procedures that begin with a e that print to stderr instead of stdout.

Our final program looks like this:

 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:strconv"

main :: proc() {
    sum := 0

    for a in os.args[1:] {
        n, ok := strconv.parse_int(a)
        if !ok {
            fmt.eprintfln("Not a valid number: %s, 0 will be used", a)
        }

        sum += n
    }

    fmt.printfln("The sum is: %d", sum)
}

Test it to make sure that it behaves as expected.

You might wonder why we need two output channels, one for normal output and one for error output. It is because we can choose to redirect any of the standard communication channels (at least on Unix and Unix-like systems like Linux and MacOS, I really don’t know about Windows). To redirect stdout we use > and to redirect stderr we use 2>.

If we wanted to run a program called process-lots-and-lots-of-data and have all normal output go to a file called processing.log and error messages to go to errors.log, the we would type this in our terminal.

$ process-lots-and-lots-of-data > processing.log 2> errors.log

This gives us the benefit of separating “noraml” output from error messages. If you have ever used a web server, like nginx or jetty, you might have noticed that you usually get two log files for each vhost: one normal log and one error log.

String to boolean conversion

If we can convert strings to integers, what about the other type we have learned so far? Is it possible to for us to convert strings into booleans? We could of course look it up. But, we could also just go ahead and give it a try. The procedure to convert a string to an integer was parse_int. The Odin type name for a boolean is bool, by the way. So we could guess that the procedure we need is parse_bool. Let’s give it a try, shall we?

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

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

main :: proc() {
    s := "true"
    b, ok := strconv.parse_bool(s)
    if !ok {
        fmt.println("Could not convert")
        os.exit(-1)
    }

    fmt.printfln("I got: %v", b)
}

This program prints true. You might wonder what the %v placeholder means. It’s a generic placeholder that you can use for pretty much any type. I use it here because there is no, as far as I know at least, any format specifier for the bool type. So %v it is.

Try it out

Modify the string variable s to the following and see what happens:

  1. “false”
  2. “TRUE”
  3. " true" (a space before the t)

Coming Up

Now that we’ve learned quite a bit and you’ve had ample opportunity to practice what you’ve learned, we are are going to take a little bit of time to focus on different ways in which we can use the for loop.

Exercises

Exercise 1

Write a program that prints out only the command-line arguments that are integers and are even. Remember that even numbers are ones for which the remainder of integer division is 0. In case you don’t remember, the modulus operator % is used to get the remainder of integer division.

Exercise 2

Write a program that expects a string followed by one or more numbers. If the first argument is “even”, print only the command-line arguments that are even. If the first argument is “odd”, print only command-line arguments. For any other value of the first argument, print an error message and exit.

Exercise 3

Write a program that prints only numbers that are divisible by both 3 and 5. The Odin operator for logical AND is &&.

Exercise 4

Write a program that reads through all the command-line arguments, which are all expected to be integers. If any argument is not a valid integer, print an error message and exit.

If all the arguments are valid integers, print out the lowest and highest numbers. For instance, if I passed 10, -3, 44, 0, -7, 16, I should get the following ouput:

$ ./exercise-4 10 -3 44 0 -7 16
min: -7
max: 44

Exercise 5

Write a program the prints the average of all the numbers that are passed in via command-line arguments. Convert any arguments that are not valid integers to zeros. Run the program a few times. Are you getting the output you expect. For instance, if you pass in: 2 7 3 4 1, what result would you expect? What result do you actually get? What result do you get if you use a calculator?

If there is a difference between the expected and actual results, why do you think that might be?

Exercise 6

Write a program that parses the command-line arguments and prints only those arguments that are integers and are either less than 10 or greater than 30. The logical OR operator in Odin is ||.

Exercise 7

The following program is meant to print out only the command-line arguments that are divisible by 10. However, there are some bugs that prevent the program from working properly. Fix the bugs. Make sure you test the program to ensure it really is doing what it’s supposed to.

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

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

main :: proc() {
    for a in os.args[2:] {
        n, _ := strconv.parse_int(a)

        if n % 10 >= 0 {
            fmt.println(n)
        }
    }
}

Exercise 8

Write a program that expects a boolean argument followed by one or more integer arguments. Start by making sure that there are at least two arguments (apart form args[0]) and that the first argument really is a boolean. If it any of these fails, print an error message to stderr and exit the program.

If the first argument is true, print only the intger arguments that are greater than on equal to 50. If the first argument is false, only print those integer arguments that are less than 50.

Simply ignore any arguments after the boolean argument that are not valid integers.

Exercise 9

Write a program that only accepts a single command-line argument. Exit with an error message if the number of command-line arguments is not exactly 1. Convert the single argument to a number and verify that it is between 1 and 12. Then print out a multiplication table for that number, from 1 to 12. Look at the following example output:

$ exercise-9 5
5 * 1 = 5
5 * 2 = 10
5 * 3 = 15
5 * 4 = 20
5 * 5 = 25
5 * 6 = 30
5 * 7 = 35
5 * 8 = 40
5 * 9 = 45
5 * 10 = 50
5 * 11 = 55
5 * 12 = 60

If you want a bit of extra practice, you can try to work on the formatting so that the numbers line up properly.

Exercise 10

Write a program that parses the command-line arguments that it receives. For each argument, try to convert it to an integer. If that works, print “x is an integer”, where x is the value of argument.

If the argument could not be converted to an integer, try converting it to a boolean instead. If that works, print “x is a boolean”, where x is the value of the argument.

If the argument couldn’t be converted to either, print “x is neither an integer nor a boolean”, where x is the value of the argument.

Exercise 11

Write a program that takes 2 command-line arguments and prints out the value of 3x + 5y - 14, where x is the the first argument and y is the second one.

Exercise 12

The following program is meant to print the sum of all the positive, even integers.

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

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

main :: proc() {
    sum_of_evens := 0
    for a in os.args[2:] {
        n, _ := strconv.parse_int(a)

        if n % 3 == 0 {
            sum_of_evens += 1
        }
    }

    fmt.printfln("The sum of all even numbers is: %d", n)
}

It currently doesn’t compile. First fix the compile time bugs so that the program can compile. Unfortunately, there are logic errors in the program as well, causing it to run, but to produce bad results. Fix all logic errors so that the program produces the expected results at all time, bearing in mind the requirements.

Finally, modify the program to print out a warning to stderr for any command-line that does not represent a valid integer.