Previous chapter To contents Next chapter

Chapter 2, A more elaborate example

To illustrate several of the fundamental points of Pike we will now introduce an example program, that will be extended as we go. We will build a database program that keeps track of a record collection and the songs on the records. In the first version we hard-code our "database" into the program. The database is a mapping where the index is the record name and the data is an array of strings. The strings are of course the song names. The default register consists of one record.
#!/usr/local/bin/pike

mapping (string:array(string)) records =
([
    "Star Wars Trilogy" : ({
        "Fox Fanfare",
        "Main Title",
        "Princess Leia's Theme",
        "Here They Come",
        "The Asteroid Field",
        "Yoda's Theme",
        "The Imperial March",
        "Parade of the Ewoks",
        "Luke and Leia",
        "Fight with Tie Fighters",
        "Jabba the Hut",
        "Darth Vader's Death",
        "The Forest Battle",
        "Finale"
    })
]);
We want to be able to get a simple list of the records in our database. The function list_records just goes through the mapping records and puts the indices, i.e. the record names, in an array of strings, record_names. By using the builtin function sort we put the record names into the array in alphabetical order which might be a nice touch. For the printout we just print a header, "Records:", followed by a newline. Then we use the loop control structure for to traverse the array and print every item in it, including the number of the record, by counting up from zero to the last item of the array. The builtin function sizeof gives the number of items in an array. The printout is formatted through the use of sprintf which works more or less like the C function of the same name.
void list_records()
{
    int i;
    array (string) record_names=sort(indices(records));

    write("Records:\n");
    for(i=0;i<sizeof(record_names);i++)
        write(sprintf("%3d: %s\n", i+1, record_names[i]));
}
If the command line contained a number our program will find the record of that number and print its name along with the songs of this record. First we create the same array of record names as in the previous function, then we find the name of the record whose number (num) we gave as an argument to this function. Next we put the songs of this record in the array songs and print the record name followed by the songs, each song on a separate line.
void show_record(int num)
{
    int i;
    array (string) record_names = sort(indices (records));
    string name=record_names[num-1];
    array (string) songs=records[name];
    
    write(sprintf("Record %d, %s\n",num,name));
    for(i=0;i<sizeof(songs);i++)
        write(sprintf("%3d: %s\n", i+1, songs[i]));
}
The main function doesn't do much; it checks whether there was anything on the command line after the invocation. If this is not the case it calls the list_records function, otherwise it sends the given argument to the show_record function. When the called function is done the program just quits.
int main(int argc, array (string) argv)
{
    if(argc <= 1)
    {
        list_records();
    } else {
        show_record((int) argv[1]);
    }
}

2.1 Taking care of input

Now, it would be better and more general if we could enter more records into our database. Let's add such a function and modify the main() function to accept "commands".

2.1.1 add_record()

Using the method Stdio.Readline()->read() we wait for input which will be put into the variable record_name. The argument to ->read() is printed as a prompt in front of the user's input. Readline takes everything up to a newline character. Now we use the control structure while to check whether we should continue inputting songs. The while(1) means "loop forever", because 1 is always true. This program does not in fact loop forever, because it uses return to exit the function from within the loop when you type a period. When something has been read into the variable song it is checked. If it is a "." we return a null value that will be used in the while statement to indicate that it is not ok to continue asking for song names. If it is not a dot, the string will be added to the array of songs for this record, unless it's an empty string. Note the += operator. It is the same as saying records[record_name]=records[record_name]+({song}).
void add_record()
{
    string record_name=Stdio.Readline()->read("Record name: ");
    records[record_name]=({});
    write("Input song names, one per line. End with '.' on its own line.\n");
    while(1)
    {
        string song;
        song=Stdio.Readline()->read(sprintf("Song %2d: ",
                                                                sizeof(records[record_name])+1));
        if(song==".")
             return;
        if (strlen(song))
            records[record_name]+=({song});
    }
}

2.1.2 main()

The main function now does not care about any command line arguments. Instead we use Stdio.Readline()->read() to prompt the user for instructions and arguments. The available instructions are "add", "list" and "quit". What you enter into the variables cmd and args is checked in the switch() block. If you enter something that is not covered in any of the case statements the program just silently ignores it and asks for a new command. In a switch() the argument (in this case cmd) is checked in the case statements. The first case where the expression equals cmd then executes the statement after the colon. If no expression is equal, we just fall through without any action. The only command that takes an argument is "list" which works as in the first version of the program. If "list" receives an argument, that record is shown along with all the songs on it. If there is no argument it shows a list of the records in the database. When the program returns from either of the listing functions, the break instruction tells the program to jump out of the switch() block. "add" of course turns control over to the function described above. If the command given is "quit" the exit(0) statement stops the execution of the program and returns 0 (zero) to the operating system, telling it that everything was ok.
int main(int argc, array(string) argv)
{
    string cmd;
    while(cmd=Stdio.Readline()->read("Command: "))
    {
        string args;
        sscanf(cmd,"%s %s",cmd,args);

        switch(cmd)
        {
        case "list":
            if((int)args)
            {
                show_record((int)args);
            } else {
                list_records();
            }
            break;

        case "quit":
            exit(0);

        case "add":
            add_record();
            break;
        }
    }
}

2.2 Communicating with files

Now if we want to save the database and also be able to retrieve previously stored data we have to communicate with the environment, i.e. with files on disk. Now we will introduce you to programming with objects. To open a file for reading or writing we will use one of the programs which is builtin in Pike called Stdio.File. To Pike, a program is a data type which contains code, functions and variables. A program can be cloned which means that Pike creates a data area in memory for the program, places a reference to the program in the data area, and initializes it to act on the data in question. The methods (i.e. functions in the object) and variables in the object Stdio.File enable us to perform actions on the associated data file. The methods we need to use are open, read, write and close. See
chapter 9 "File I/O" for more details.

2.2.1 save()

First we clone a Stdio.File program to the object o. Then we use it to open the file whose name is given in the string file_name for writing. We use the fact that if there is an error during opening, open() will return a false value which we can detect and act upon by exiting. The arrow operator (->) is what you use to access methods and variables in an object. If there is no error we use yet another control structure, foreach, to go through the mapping records one record at a time. We precede record names with the string "Record: " and song names with "Song: ". We also put every entry, be it song or record, on its own line by adding a newline to everything we write to the file.
Finally, remember to close the file.
void save(string file_name)
{
    string name, song;
    Stdio.File o=Stdio.File();

    if(!o->open(file_name,"wct"))
    {
        write("Failed to open file.\n");
        return;
    }

    foreach(indices(records),name)
    {
        o->write("Record: "+name+"\n");
        foreach(records[name],song)
            o->write("Song: "+song+"\n");
    }

    o->close();
}

2.2.2 load()

The load function begins much the same, except we open the file named file for reading instead. When receiving data from the file we put it in the string file_contents. The absence of arguments to the method o->read means that the reading should not end until the end of the file. After having closed the file we initialize our database, i.e. the mapping records. Then we have to put file_contents into the mapping and we do this by splitting the string on newlines (cf. the split operator in Perl) using the division operator. Yes, that's right: by dividing one string with another we can obtain an array consisting of parts from the first. And by using a foreach statement we can take the string file_contents apart piece by piece, putting each piece back in its proper place in the mapping records.
void load(string file_name)
{
    string name="ERROR";
    string file_contents,line;

    Stdio.File o=Stdio.File();
    if(!o->open(file_name,"r"))
    {
        write("Failed to open file.\n");
        return;
    }

    file_contents=o->read();
    o->close();

    records=([]);

    foreach(file_contents/"\n",line)
    {
        string cmd, arg;
        if(sscanf(line,"%s: %s",cmd,arg))
        {
            switch(lower_case(cmd))
            {
            case "record":
                name=arg;
                records[name]=({});
                break;

             case "song":
                 records[name]+=({arg});
                 break;
            }
        }
    }
}

2.2.3 main() revisited

main() remains almost unchanged, except for the addition of two case statements with which we now can call the load and save functions. Note that you must provide a filename to load and save, respectively, otherwise they will return an error which will crash the program.
case "save":
    save(args);
    break;

case "load":
    load(args);
    break;

2.3 Completing the program

Now let's add the last functions we need to make this program useful: the ability to delete entries and search for songs.

2.3.1 delete()

If you sell one of your records it might be nice to able to delete that entry from the database. The delete function is quite simple. First we set up an array of record names (cf. the list_records function). Then we find the name of the record of the number num and use the builtin function m_delete() to remove that entry from records.
void delete_record(int num)
{
    array(string) record_names=sort(indices(records));
    string name=record_names[num-1];

    m_delete(records,name);
}

2.3.2 search()

Searching for songs is quite easy too. To count the number of hits we declare the variable hits. Note that it's not necessary to initialize variables, that is done automatically when the variable is declared if you do not do it explicitly. To be able to use the builtin function search(), which searches for the presence of a given string inside another, we put the search string in lowercase and compare it with the lowercase version of every song. The use of search() enables us to search for partial song titles as well. When a match is found it is immediately written to standard output with the record name followed by the name of the song where the search string was found and a newline. If there were no hits at all, the function prints out a message saying just that.
void find_song(string title)
{
    string name, song;
    int hits;

    title=lower_case(title);

    foreach(indices(records),name)
    {
        foreach(records[name],song)
        {
            if(search(lower_case(song), title) != -1)
            {
                write(name+"; "+song+"\n");
                hits++;
            }
        }
    }

    if(!hits) write("Not found.\n");
}

2.3.3 main() again

Once again main() is left unchanged, except for yet another two case statements used to call the search() and delete functions, respectively. Note that you must provide an argument to delete or it will not work properly.
case "delete":
    delete_record((int)args);
    break;

case "search":
    find_song(args);
    break;

2.4 Then what?

Well that's it! The example is now a complete working example of a Pike program. But of course there are plenty of details that we haven't attended to. Error checking is for example extremely sparse in our program. This is left for you to do as you continue to read this book. The complete listing of this example can be found in
appendix B "Register program". Read it, study it and enjoy!

2.5 Simple exercises

  • Make a program which writes hello world 10 times.
  • Modify hello_world.pike to write the first argument to the program.
  • Make a program that writes a hello_world program to stdout when executed.
  • Modify the register program to store data about programs and diskettes instead of songs and records.
  • Add code to the register program that checks that the user typed an argument when required. The program should notify the user and wait to receive more commands instead of exiting with an error message.
  • Add code to the register program to check that the arguments to show_record and delete_records are numbers. Also make sure that the number isn't less than one or bigger than the available number of records.
  • Rewrite the register program and put all the code in main().

  • Previous chapter To contents Next chapter