Programming in D – Tutorial and Reference
Ali Çehreli

Other D Resources

Program Environment

We have seen that main() is a function. Program execution starts with main() and branches off to other functions from there. The definition of main() that we have used so far has been the following:

void main() {
    // ...
}

According to that definition main() does not take any parameters and does not return a value. In reality, in most systems every program necessarily returns a value to its environment when it ends, which is called an exit status or return code. Because of this, although it is possible to specify the return type of main() as void, it will actually return a value to the operating system or launch environment.

The return value of main()

Programs are always started by an entity in a particular environment. The entity that starts the program may be the shell where the user types the name of the program and presses the Enter key, a development environment where the programmer clicks the [Run] button, and so on.

In D and several other programming languages, the program communicates its exit status to its environment by the return value of main().

The exact meaning of return codes depend on the application and the system. In almost all systems a return value of zero means a successful completion, while other values generally mean some type of failure. There are exceptions to this, though. For instance, in OpenVMS even values indicate failure, while odd values indicate success. Still, in most systems the values in the range [0, 125] can be used safely, with values 1 to 125 having a meaning specific to that program.

For example, the common Unix program ls, which is used for listing contents of directories, returns 0 for success, 1 for minor errors and 2 for serious ones.

In many environments, the return value of the program that has been executed most recently in the terminal can be seen through the $? environment variable. For example, when we ask ls to list a file that does not exist, its nonzero return value can be observed with $? as seen below.

Note: In the command line interactions below, the lines that start with # indicate the lines that the user types. If you want to try the same commands, you must enter the contents of those lines except for the # character. Also, the commands below start a program named deneme; replace that name with the name of your test program.

Additionally, although the following examples show interactions in a Linux terminal, they would be similar but not exactly the same in terminals of other operating systems.

# ls a_file_that_does_not_exist
ls: cannot access a_file_that_does_not_exist: No such file
or directory
# echo $?
2      ← the return value of ls
main() always returns a value

Some of the programs that we have written so far threw exceptions when they could not continue with their tasks. As much as we have seen so far, when an exception is thrown, the program ends with an object.Exception error message.

When that happens, even if main() has been defined as returning void, a nonzero status code is automatically returned to the program's environment. Let's see this in action in the following simple program that terminates with an exception:

void main() {
    throw new Exception("There has been an error.");
}

Although the return type is specified as void, the return value is nonzero:

# ./deneme
object.Exception: There has been an error.
...
# echo $?
1

Similarly, void main() functions that terminate successfully also automatically return zero as their return values. Let's see this with the following program that terminates successfully:

import std.stdio;

void main() {
    writeln("Done!");
}

The program returns zero:

# ./deneme
Done!
# echo $?
0
Specifying the return value

To choose a specific return code we return a value from main() in the same way as we would from any other function. The return type must be specified as int and the value must be returned by the return statement:

import std.stdio;

int main() {
    int number;
    write("Please enter a number between 3 and 6: ");
    readf(" %s", &number);

    if ((number < 3) || (number > 6)) {
        stderr.writefln("ERROR: %s is not valid!", number);
        return 111;
    }

    writefln("Thank you for %s.", number);

    return 0;
}

When the entered number is within the valid range, the return value of the program is zero:

# ./deneme
Please enter a number between 3 and 6: 5
Thank you for 5.
# echo $?
0

When the number is outside of the valid range, the return value of the program is 111:

# ./deneme
Please enter a number between 3 and 6: 10
ERROR: 10 is not valid!
# echo $?
111

The value of 111 in the above program is arbitrary; normally 1 is suitable as the failure code.

Standard error stream stderr

The program above uses the stream stderr. That stream is the third of the standard streams. It is used for writing error messages:

When a program is started in a terminal, normally the messages that are written to stdout and stderr both appear on the terminal window. When needed, it is possible to redirect these outputs individually. This subject is outside of the focus of this chapter and the details may vary for each shell program.

Parameters of main()

It is common for programs to take parameters from the environment that started them. For example, we have already passed a file name as a command line option to ls above. There are two command line options in the following line:

# ls -l deneme
-rwxr-xr-x 1 acehreli users 460668 Nov  6 20:38 deneme

The set of command line parameters and their meanings are defined entirely by the program. Every program documents its usage, including what every parameter means.

The arguments that are used when starting a D program are passed to that program's main() as a slice of strings. Defining main() as taking a parameter of type string[] is sufficient to have access to program arguments. The name of this parameter is commonly abbreviated as args. The following program prints all of the arguments with which it is started:

import std.stdio;

void main(string[] args) {
    foreach (i, arg; args) {
        writefln("Argument %-3s: %s", i, arg);
    }
}

Let's start the program with arbitrary arguments:

# ./deneme some arguments on the command line 42 --an-option
Argument 0  : ./deneme
Argument 1  : some
Argument 2  : arguments
Argument 3  : on
Argument 4  : the
Argument 5  : command
Argument 6  : line
Argument 7  : 42
Argument 8  : --an-option

In almost all systems, the first argument is the name of the program, in the way it has been entered by the user. The other arguments appear in the order they were entered.

It is completely up to the program how it makes use of the arguments. The following program prints its two arguments in reverse order:

import std.stdio;

int main(string[] args) {
    if (args.length != 3) {
        stderr.writefln("ERROR! Correct usage:\n" ~
                        "  %s word1 word2", args[0]);
        return 1;
    }

    writeln(args[2], ' ', args[1]);

    return 0;
}

The program also shows its correct usage if you don't enter exactly two words:

# ./deneme
ERROR! Correct usage:
  ./deneme word1 word2
# ./deneme world hello
hello world
Command line options and the std.getopt module

That is all there is to know about the parameters and the return value of main(). However, parsing the arguments is a repetitive task. The std.getopt module is designed to help with parsing the command line options of programs.

Some parameters like "world" and "hello" above are purely data for the program to use. Other kinds of parameters are called command line options, and are used to change the behaviors of programs. An example of a command line option is the -l option that has been passed to ls above.

Command line options make programs more useful by removing the need for a human user to interact with the program to have it behave in a certain way. With command line options, programs can be started from script programs and their behaviors can be specified through command line options.

Although the syntax and meanings of command line arguments of every program is specific to that program, their format is somewhat standard. For example, in POSIX, command line options start with -- followed by the name of the option, and values come after = characters:

# ./deneme --an-option=17

The std.getopt module simplifies parsing such options. It has more capabilities than what is covered in this section.

Let's design a program that prints random numbers. Let's take the minimum, maximum, and total number of these numbers as program arguments. Let's require the following syntax to get these values from the command line:

# ./deneme --count=7 --minimum=10 --maximum=15

The getopt() function parses and assigns those values to variables. Similarly to readf(), the addresses of variables must be specified by the & operator:

import std.stdio;
import std.getopt;
import std.random;

void main(string[] args) {
    int count;
    int minimum;
    int maximum;

    getopt(args,
           "count", &count,
           "minimum", &minimum,
           "maximum", &maximum);

    foreach (i; 0 .. count) {
        write(uniform(minimum, maximum + 1), ' ');
    }

    writeln();
}
# ./deneme --count=7 --minimum=10 --maximum=15
11 11 13 11 14 15 10

Many command line options of most programs have a shorter syntax as well. For example, -c may have the same meaning as --count. Such alternative syntax for each option is specified in getopt() after a | character. There may be more than one shortcut for each option:

    getopt(args,
           "count|c", &count,
           "minimum|n", &minimum,
           "maximum|x", &maximum);

It is common to use a single dash for the short versions and the = character is usually either omitted or substituted by a space:

# ./deneme -c7 -n10 -x15
11 13 10 15 14 15 14
# ./deneme -c 7 -n 10 -x 15  
11 13 10 15 14 15 14

getopt() converts the arguments from string to the type of each variable. For example, since count above is an int, getopt() converts the value specified for the --count argument to an int. When needed, such conversions may also be performed explicitly by to.

So far we have used std.conv.to only when converting to string. to can, in fact, convert from any type to any type, as long as that conversion is possible. For example, the following program takes advantage of to when converting its argument to size_t:

import std.stdio;
import std.conv;

void main(string[] args) {
    // The default count is 10
    size_t count = 10;

    if (args.length > 1) {
        // There is an argument
        count = to!size_t(args[1]);
    }

    foreach (i; 0 .. count) {
        write(i * 2, ' ');
    }

    writeln();
}

The program produces 10 numbers when no argument is specified:

# ./deneme
0 2 4 6 8 10 12 14 16 18
# ./deneme 3
0 2 4
Environment variables

The environment that a program is started in generally provides some variables that the program can make use of. The environment variables can be accessed through the associative array interface of std.process.environment. For example, the following program prints the PATH environment variable:

import std.stdio;
import std.process;

void main() {
    writeln(environment["PATH"]);
}

The output:

# ./deneme
/usr/local/bin:/usr/bin

std.process.environment provides access to the environment variables through the associative array syntax. However, environment itself is not an associative array. When needed, the environment variables can be converted to an associative array by using toAA():

    string[string] envVars = environment.toAA();
Starting other programs

Programs may start other programs and become the environment for those programs. A function that enables this is executeShell, from the std.process module.

executeShell executes its parameter as if the command was typed at the terminal. It then returns both the return code and the output of that command as a tuple. Tuples are array-like structures, which we will see later in the Tuples chapter:

import std.stdio;
import std.process;

void main() {
    const result = executeShell("ls -l deneme");
    const returnCode = result[0];
    const output = result[1];

    writefln("ls returned %s.", returnCode);
    writefln("Its output:\n%s", output);
}

The output:

# ./deneme
ls returned 0.
Its output:
-rwxrwxr-x. 1 acehreli acehreli 1359178 Apr 21 15:01 deneme

Summary
Exercises
  1. Write a calculator program that takes an operator and two operands as command line arguments. Have the program support the following usage:
    # ./deneme 3.4 x 7.8
    26.52
    

    Note: Because the * character has a special meaning on most terminals (more accurately, on most shells), I have used x instead. You may still use * as long as it is escaped as \*.

  2. Write a program that asks the user which program to start, starts that program, and prints its output.