In the last lesson, we learned what variables are and how to use them. We also briefly touched upon the if
statement. In this lesson, we are going to learn how to go on step further towards writing dynamic programs by accepting external input. This could come in many forms:
- the user could be prompted to type something that gets captured into a variable
- the input could come from a file that is read
- the input could come from another program on your computer, via a so-called pipe (something we will look at later)
- the input could come from so-called command-line arguments
Allowing the user to pass command-line arguments is probably the simplest of those, so that is what we are going to look into first.
What are commmand-line arguments?
Let’s say you want to create a folder called “work” from your terminal. On a Linux or Unix system, you would type:
$ mkdir work
On Windows it is just slightly different:
C:/Home/Lorenzo/> md work
In both cases, the first “word” is the command, that is mkdir
on a Unix system, md
on Windows. What follows after, that is work
, is a command-line argument, it is extra information that a program needs to work with. In the case of creating a folder, if you don’t supply a name, how does your operating system know the name of the folder?
We are going learn how to accept command-line arguments into our own programs.
Arrays
To work with command-line arguments, we will first need to import the core:os
package. That will give us access to os.args
, which is an array of strings. You have seen what a string is already, but we haven’t encounered arrays yet. This is going to be a bit wrong, but for now, think of an array as a list of strings. You can access individual items in this list via an index number. The index number of the first item in the list is 0, not 1.
In the beginning it’s really easy to think of the first item in an array as 1 and not 0. If your program doesn’t run like you expect it to, check to make sure you didn’t make a mistake here.
Notice that I have not explained what the import
statement means yet. I will, but there is so much to explain in the beginning that you are going get used to the fact that you can actually use something without fully understanding it.
If your not sure if you need it, try leaving it out in your program and notice what happens when you try to run.
You can get the length of an array by using a special procedure called len
. Let’s give this a try first. Create a folder called args
and in that folder a file called main.org
or args.odin
if you so prefer. There is a reason why in this particular case I gave you a specific folder name as we shall soon see. Here is the program.
|
|
Compiling
I left out a little detail about Odin: it is a compiled language. What that means is that the Odin compiler will take your source code file and turn it into something that your computer understands, called machine language. When you type odin run .
, what happens is that the odin compiler first compiles all the source files in the current directory, produces a program which by default is named just like the parent folder and finally runs the program.
However, when we are working with command-line arguments
, it is easier for us to compile first and then run the resulting program manually. To compile, we just change odin run .
to odin build .
. That wasn’t too hard, was it?
If you didn’t get any errors, type ls
on a Linux/Unix machine or dir
on a Windows machine and you should see a program file. It will be called args
on Linux/Unix, but args.exe
on Windows. That is your program.
To run this program, you simply type:
$ ./args
And the program should respond with 1, as follows.
$ ./args
1
Somethings seems to be wrong here, but let’s just run a few more tests. Each command-line argument is separated by at least one space. Try the following:
$ ./args foo bar
3
And let’s give it one final test.
$ ./args foo bar bim baz quux
6
Each one of them is off by one. I ran the command alone, I got 1, I passed in foo and bar, I got 3, I passed in 5 command-line arguments, I got 6. What is happening here?
The value of args[0]
From our tests, it would seem that the very first item of the os.args
array always contains something. We can extend our little args
program to show us what it is.
package main
import "core:fmt"
import "core:os"
main :: proc() {
fmt.println("Number of command-line arguments:", len(os.args))
fmt.println("Value of first-command-line argument:", os.args[0])
fmt.println("Value of the rest:", os.args[1:])
}
Compile the updated program with odin build .
and then try running the program a few times, with different number of command-line arguments each time. Do you recognize what the first argument is now? Of course, it’s the name of the program itself!
$ ./args foo bar bim baz
Number of command-line arguments: 5
Value of first-command-line argument: ./args
Value of the rest: ["foo", "bar", "bim", "baz"]
Slices
But wait, what is that weird os.args[1:]
thingy I see up in that latest version of the program? It’s called a slice and it’s something we will look into in more detail later. For now, you just have to know that what we are saying is, give me all the elements of the slice from the element with index 1 (the second item) to the end. if you put a number after the colon, you would limit the value further. Look at the following, slightly modified version of above program.
package main
import "core:fmt"
import "core:os"
main :: proc() {
fmt.println("Number of command-line arguments:", len(os.args))
fmt.println("Value of first-command-line argument:", os.args[0])
fmt.println("Value of the rest:", os.args[1:3])
}
Save the program, compile it and run it a few times. What happens when you using the following command line arguments?
- foo bar bim baz
- the quick brown fox jumped over the lazy dog
- foo
You might be surprised about the output of the first two and you might wonder what happened in the last case. Let me explain. the slice [1:3]
will contain the items from the original array with indexes 1 and 2, that is the first number (1) to the last but one (3-1=2).
But what about the test, with only one argument? I suppose you got some output similar to this:
$ ./args foo
Number of command-line arguments: 2
Value of first-command-line argument: ./args
/home/lorenzo/Temporary/args/main.odin(9:46) Invalid slice indices 1:3 is out of range 0..<2
Illegal instruction (core dumped)
The reason for this is that you are trying to take slice out of something that doesn’t exist. The array only has 2 elements: 0 and 1. you are asking for elements 1 and 2. Since element 2 doesn’t exist, the program throws an error.
Notice that this is not a compile-time error, this is a runtime error. When compiling, the Odin compiler has no way of knowing how many command-line arguments the user is going to pass in to the program, so as long as you don’t violate any Odin syntax rules, the program will compile just fine.
It is when you run the program that it crashes, hence runtime error.
Make sure you understand the bounds of your array before you slice it. As you can see, the program will simply crash. You could always check the length of the array before try to slice.
Loops
Now that we’ve got all that out of the way, let’s try to print out our command-line arguments, one at a time, on separate lines. To do that we will use a loop. In this case we want to loop over the elements in the args[1:]
slice. Here is an updated version of our args program.
Write a program that counts the number of command-line arguments that is passed to it. if the count is not 3, print “This program requires 2 arguments”. Otherwise print “Thank you”.
1package main
2
3import "core:fmt"
4import "core:os"
5
6main :: proc() {
7 fmt.println("Number of command-line arguments:", len(os.args))
8 fmt.println("Value of first-command-line argument:", os.args[0])
9
10 for a in os.args[1:] {
11 fmt.println(a)
12 }
13}
As before, compile the program and try running it a few times, passing in different numbers of arguments each time. (I always prefer to run programs first so that I can an idea of what happens before I try to look into the code.) The for
statement is used to repeat some code over and over again. In this version we are basically saying, “take each item of the args[1:] slice and copy it into the temporary variable a
, one at a time.” The variable a is only available in the loop block (delimited by a pair of curly brackets.)
What if we wanted to print a sequence number before each argument? Well, it’s a good thing that the for loop allows us to add a second temporary variable that will act like a counter.
1package main
2
3import "core:fmt"
4import "core:os"
5
6main :: proc() {
7 fmt.println("Number of command-line arguments:", len(os.args))
8 fmt.println("Value of first-command-line argument:", os.args[0])
9
10 for a, i in os.args[1:] {
11 fmt.println(i, a)
12 }
13}
Okay, that’s fine I guess. But isn’t it a bit annoying that it starts counting from zero? That doesn’t look so good. It’s easily fixed. Try this:
1package main
2
3import "core:fmt"
4import "core:os"
5
6main :: proc() {
7 fmt.println("Number of command-line arguments:", len(os.args))
8 fmt.println("Value of first-command-line argument:", os.args[0])
9
10 for a, i in os.args[1:] {
11 fmt.println(i+1, a)
12 }
13}
Formatting output
It’s getting better. Upon further testing, there is still a little something that bothers me.
$ ./args a b c d e f g h i j k l
Number of command-line arguments: 13
Value of first-command-line argument: ./odin
1 a
2 b
3 c
4 d
5 e
6 f
7 g
8 h
9 i
10 j
11 k
12 l
If I pass in 10 or more arguments, they are no longer aligned. Also, I would really like to put a period right after the number. Let me introduce you to fmt.println
’s more powerful sibling, fmt.printfln
. This procedure allows us to put placeholders inside the string that will be substited by variable values. Moreover, we can give extra information about how to format these placeholders.
To start off with, the index number is clearly an integer. The placeholder for an integer is %d
. Next, let’s consider the width we want to give to this placeholder.
I consider it highly unlikely that a user would pass in 100 or more arguments, but we can still make provision for 3 digits. That would allow us up to 999 arguments, before the alignment breaks. That is fine. Now our placeholder is going to be %3d
. This will print 001 instead of 1, which might not be what we want, so we put a space right before the 3, indicating “pad with spaces”, % 3d
.
After the index number, we want to print the value of the command-line argument. The placeholder for a string is %s
. So our total formatting string is going to look as follows: % 3d. %s
The printfln
procedure expects you to pass in the variables that will subsitute the placeholders in the order that the placeholders appear in the formatting string. The index number comes first so we pass it in first, followed by the value of the argument. This gives us the following:
fmt.printfln("% 3d. %s", i, a)
Replace the line fmt.println(i+1, a)
with the above, save, compile and try the program out again. Now it will hopefully look acceptable.
Try it out.
- Remove the space in the placeholder for the integer, so you get
%3d
. Compile and run. What happens to the output? - Change the integer placeholder to
%-3d
. What output do you get when you compile and run? - Change the width of the integer placeholder:
% 5d
. What happens to the output? - Add a number before the string placeholder as well:
%10s
. How does this affect the output?
Acting on command-line arguments
Finally, let’s combine what we have learned here with some things we alread know. We want to create a greeter program that greets the people whose names are passed in as arguments.
|
|
I called the variable name instead of a, because I’m expecting names from the command-line, so it makes more sense. I could of course have called it something else, but you will in general want to try to use variable names that are descriptive.
Compile this program and run it. Pass in a few names and see the result. Let’s try to greet John, Kukuwa and Nii Okai, for instance.
$ ./greet John Kukuwa Nii Okai
Hi there, John!
Hi there, Akua!
Hi there, Nii!
Hi there, Okai!
Oh dear! We’ve run into another problem. Nii Okai’s name has been broken up and printed on two different lines. This is actually a problem from Odin, but from the terminal itself (or rather the command-line interpreter that powers the terminal). It is that interpreter that hands the arguments to the our program and Odin can only work with what it gets.
The terminal’s interpreter considers anything separated by spaces as a new argument. Hence “Nii” is one argument and “Okai” is another. However, it turns out we can fix this quite easily. We simply quote the entire name. Try this:
$ ./greet John Kukuwa "Nii Okai"
Now you should get the expected output, hopefully. Now, suppose I’m not on talking terms with John, for whatever reason, so I don’t want the program to greet him. We recently learned about the if
statement, so let’s put it to use.
|
|
Compile and run this version of the program. If you pass in a list of names, including “John”, it should ignore John. This is done with an if
statment combined with a continue
. The continue basically says skip to the next loop iteration.
Attempting to add numbers from the command-line
Now, I have this great idea. I want to write a program that takes numbers on the command line, adds them up and prints out the sum. This should be simple, right? Let’s put it together.
|
|
That’s not going to work. As usual, the error message tells us everything we need to know. We cannot add a string to an integer, which makes sense, after all what is 15 + pen? It makes no sense.
If we look at the Odin package documentation we see that args
is an array of strings, as I mentioned at the top. In Odin, “13” and 13 are two different things, the former is a string and the latter is an integer. If we want our sum program to work, we are going to have to convert the string “13” into the number 13. Fortunately, that is exaclty what we are going to look at in the next lesson.
By the way, sum += number
is a short way of saying sum = sum + number
. And yes, that makes absolutely no sense in math, since it would evaluate to 0 = number. If number was 1, we would end up with 0 = 1. In Odin, the single equal sing is not a comparison operator, but an assigment one. So sum = sum + number
, will evaluate the right-hand side and then assign the result of that operation to the left hand side. If sum was 12 and number was 2, it would perform the operation 12 + 2 before assigning the result back to the sum
varible.
We will look more at arithmetic operatioWrite a program that counts the number of command-line arguments that is passed to it. if the count is not 3, print “This program requires 2 arguments”. Otherwise print “Thank you”.ns as well in the next lesson.
Exercises
Answers to the exercises can be found here
Exercise 1
If you have an array of 10 elements, what are the index numbers of the first and last elements?
Exercise 2
Assume you have an array called numbers
, which has been initialized as follows:
values := [10]int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100}
What would the following slices return?
values[2:4]
values[5:10]
values[5:11]
values[6:2]
Some of these may prevent you from building the program (compile-time errors) and some compile fine but cause the program to crash when run. For each line that is not able to compile, take not of the error message you get, as well as if it failed at compile time or at runtime.
When you write the program, notice that values
is just an array like os.args
is, so any operation you have learned on the arguments array can equally be perfomed on any other array.
Exercise 3
Write a program that requires 2 command-line arguments. Test that the length of os.args
is exactly 3, otherwise print “This program requires 2 arguments” and exit the program. If two arguments were passed in, print “Thank you!”.
The !=
operator means not equal to. You can use os.exit(-1)
to exit the program prematurely.
Why do we say that we require 2 arguments, but we test for 3 arguments?
Exercise 4
Write a program that prints out only the command-line arguments whose index number is an even number. That is, passing in this command-line:
$ ./exercise-2 January February March April May June July August September October November December
The program should print out
February
April
June
August
October
December
Don’t include os.args[0]
in the output.
The %
returns the modulus, or remainder of division. A number n, is even if n % 2 == 0
.
Exercise 5
Greet every person whose name is passed in as a command-line argument with a greeting that is appropriate for the time of the day. You can use “good morning”, “good afternoon”, “good evening” and “good night” as you did in the last lesson, if you want.
Exercise 6
The follwing program prints out a table of a few classical games and their release years. It uses two arrays, one for game titles and one for release years. (Later you will learn a better method for doing this.) However, the table is not very well formatted. Look at the fmt.printfln
statements and fix the formatting so that the table looks neat and everything is aligned properly. You probably don’t want any zeros in front of the year either.
|
|
Exercise 7
I wanted to write a program that in which the user passes in a boolean argument. If the value of the argument is true
then print “Welcome!”, but if it is false
print “I cannot continue”. I thought the following program should work, but I get an error tryin to compile it.
|
|
Use the compiler error message to try to explain why the program doesn’t work. Note that you aren’t expected to be able to fix it at this stage, merely try to reason about what is going wrong.
Exercise 8
Fix any errors in the following program so that it compiles and prints out all the command-line arguments.
package app
import "core:fmt"
main :: proc() {
for a in args[2:] {
fmt.println(arg)
}
}
Exercise 9
Modify the program in Exercise 9 (or rather, your fixed version of it) to print out “You did not pass in any command-line arguments” if the user didn’t pass any arguments to the program.
Exercise 10
We saw that os.args[0]
holds the name of the program itself. Write a small program that just prints out this value. Compile and run the program to ensure it works like you expect.
Let’s verify that it really is so. Rename the program file. If the original program was called exercise-10
(or exercise-10.exe
on Windows). You can do the following to rename the file in Linux/Unix/MacOS (assuming you are in the folder containing the program file):
$ mv exercise-10 my-program
On Windows, you would use the ren
command instead:
$ ren exercise-10.exe my-program.exe
Run the program again, using ./my-program
and verify that the program output has changed.
Exercise 11
Try the following program out.
|
|
Based on the output you get, try to explain what for r in s
does.
Exercise 12
Why doesn’t the following program work as expected? Fix it.
|
|