2 Daily greetings

Your computer day starts when you sign in. The original term was log in, but because trees are so scarce and signs are so plentiful, the term was changed by the Bush administration in 2007. Regardless of such obnoxious federal overreach, your computer day can start with a cheerful greeting after you sign in or open a terminal window, customized by a tiny C program. To make it so, you will:

  • Review the Linux startup process.

  • Discover where in the shell script to add your greeting.

  • Write a simple greetings program.

  • Modify your greetings program to add the time of day.

  • Update the timestamp with the current moon phase.

  • Enhance your greetings message with a bon mot.

The programs created and expanded upon in this chapter are specific to Linux, macOS, and the Windows Subsystem for Linux (WSL), where a startup script is available for configuring the terminal window. A later section explains which startup scripts are available for the more popular shells. This chapter doesn’t go into creating a daily greeting message when the GUI shell starts.

I suppose you could add a startup message for the Windows terminal screen, the command prompt. It’s possible, but the process bores me, and only hardcore Windows nerds would care, so I’m skipping the specifics. The greetings programs still run at the Windows command prompt, if that’s your desire. Otherwise, you may lodge your complaints with me personally; my email address is found in this book’s introduction. I promise not to answer a single email from a whiny Windows user.

2.1 The shell starts

Linux has a long, involved, and thoroughly exciting boot process. I’m certain that you’re eager to read all the nitty-gritty details. But this book is about C programming. You must seek out a Linux book to know the complete, torrid steps involved with rousing a Linux computer. The exciting stuff relevant to creating a daily greeting happens later, after the operating system completes its morning routine, when the shell starts.

2.1.1 Understanding how the shell fits in

Each user account on a Linux system is assigned a default shell. This shell was once the only interface for Linux. I recall booting into an early version of Red Hat Linux back in the 1990s and the first—and only—thing I saw was a text mode screen. Today things are graphical, and the shell has been shunted off to a terminal window. It’s still relevant at this location, which is great for C programming.

The default shell is configured by the something-or-other. I’m too lazy to write about it here. Again, this isn’t a Linux book. Suffice it to say that your account most likely uses the bash shell—a collision of the words “Bourne again shell,” so my writing “bash shell” is redundant (like ATM machine), but it looks awkward otherwise.

To determine the default shell, start a terminal window. At the prompt, type the command echo $SHELL:

$ echo $SHELL
/bin/bash

Here, the output confirms that the assigned user shell is bash. The $SHELL argument represents the environment variable assigned to the startup shell, which is /bin/bash here. This output may not reflect the current shell—for example, if you’ve subsequently run the sh or zsh or similar command to start another shell.

To determine the current shell, type the command ps -p $$:

$ ps -p $$
  PID TTY          TIME CMD
    7 tty1     00:00:00 bash

This output shows the shell command is bash, meaning the current shell is bash regardless of the $SHELL variable’s assignment.

To change the shell, use the chsh command. The command is followed by the new shell name. Changing the shell affects only your account and applies to any new terminal windows you open after issuing the command. That’s enough Linux for today.

2.1.2 Exploring various shell startup scripts

When a shell starts, it processes commands located in various startup scripts. Some of these scripts may be global, located in system directories. Others are specific to your account, located locally in your home folder.

Startup scripts configure the terminal. They allow you to customize the horrid text-only experience, perhaps adding colors, creating shortcuts, and performing various tasks you may otherwise have to manually perform each time a terminal window opens. Any startup script file located in your home directory is yours to configure.

Given all that, the general advice is not to mess with startup shell scripts. To drive home this point, the shell script files are hidden in your home directory. The filenames are prefixed with a single dot. The dot prefix hides files from appearing in a standard directory listing. This stealth allows the files to be handy yet concealed from a casual user’s attempts to meddle with them.

Because you want to meddle with the shell startup script, specifically to add a personalized greeting, it’s necessary to know the script names. These names can differ, depending upon the shell, though the preferred startup script to edit appears in table 2.1.

Table 2.1 Tediously dry info regarding Linux shell scripts

Shell

Name

Command

Startup filename

Bash

Bash, “Bourne again shell”

/bin/bash

.bash_profile

Tsch

Tee C shell

/bin/tsch

.tcshrc

Csh

C shell

/bin/csh

.cshrc

Ksh

Korn shell

/bin/ksh

.profile

Sh

Bourne shell

/bin/sh

.profile

Zsh

Z shell

/bin/zsh

.zshrc

For example, for the bash shell, I recommend editing the startup script .bash_profile to add your greeting. Other startup scripts may run when the shell starts, but this is the script you can modify.

To view your shell’s startup script, use the cat command in a terminal window. Follow the command with the shell’s startup filename. For example:

$ cat ~/.bash_profile

The ~/ pathname is a shortcut for your home directory. After you issue the preceding command, the contents of the shell startup script vomit all over the text screen. If not, the file may not exist and you need to create it.

When you see the file’s contents, somewhere in the morass you can stick your greetings program on a line by itself. The rest of the script shouldn’t be meddled with—unless you’re adept at coding in the scripting language and crafting brilliant startup scripts, which you probably aren’t.

2.1.3 Editing the shell startup script

Shell startup scripts are plain text files. They consist of shell commands, program names, and various directives, which makes the script work like a programming language. The script is edited like any text file.

I could wax eloquent for several pages about shell scripting, but I have a dental appointment in an hour and this book is about C programming. Still, you should note two relevant aspects of a startup shell script: the very first line and the file’s permissions.

To interpret the lines of text in a startup script, the very first line of the file directs the shell to use a specific program to process the remaining lines in the file. Traditionally, the first line of a Unix shell script is:

#!/bin/sh

This line starts with the #, which makes it a comment. The exclamation point, which the cool kids tell me is pronounced “bang,” directs the shell to use the /bin/sh program (the original Bourne shell) to process the remaining lines of text in the file. The command could be anything, from a shell like bash to a utility like expect.

All shell scripts have their executable permissions bit set. If the file exists, this setting is already made. Otherwise, if you’re creating the shell script, you must bless it with the executable bit after the file is created. Use the chmod command with the +x switch, followed by the script filename:

chmod +x .bash_profile

Issuing this command is required only when you initially create the script.

Within the startup script, my recommendation is to set your greetings program on a line by itself at the end of the script. You can even prefix it with a comment, starting the line before with the # character. The cool kids have informed me that # is pronounced “hash.”

For practice, edit the terminal window’s startup script: open a terminal window and use your favorite text editor to open the shell’s startup script, as noted in table 2.1. For example, on my Linux system, I type:

vim ~/.bash_profile

Add the following two lines at the bottom of the script, after all the stuff that looks impressive and tempting:

# startup greetings
echo "Hello" $LOGNAME

The first line is prefixed with a #. (I hope you said “hash” in your head.) This tag marks the line as a comment.

The second line outputs the text "Hello" followed by the contents of environment variable $LOGNAME. This variable represents your login account name.

Here’s sample output:

Hello dang

My account login is dang, as shown. This line of text is the final output generated by the shell startup script when the terminal window first opens. The C programs generated for the remainder of this chapter replace this line, outputting their cheerful and interesting messages.

When adding your greetings program to the startup script, it’s important that you specify its pathname, lest the shell script interpreter freak out. The path can be full, as in:

/home/dang/cprog/greetings

Or it can use the ~/ home directory shortcut:

~/cprog/greetings

In both cases, the program is named greetings, and it dwells in the cprog directory.

2.2 A simple greeting

All major programming projects start out simple and have a tendency to grow into complex, ugly monsters. I’m certain that Excel began its existence as a quick-and-dirty, text mode calculator—and now look at it. Regardless, it’s good programming practice not to begin a project by coding everything you need all at once. No, it’s best to grow the project, starting with something simple and stupid, which is the point of this section.

2.2.1 Coding a greeting

The most basic greetings program you can make is a simple regurgitation of the silly Hello World program that ushers in the pages of every introductory C programming book since Moses. Listing 2.1 shows the version you could write for your greetings program.

Listing 2.1 Source code for greet01.c

#include <stdio.h>
 
int main()
{
    printf("Hello, Dan!
");
 
    return(0);
}

Don’t build. Don’t run. If you do, use this command to build a program named greetings:

clang -Wall greet01.c -o greetings

You may substitute clang with your favorite-yet-inferior compiler. Upon success, the resulting program is named greetings. Set this program into your shell’s startup script, adding the last line that looks like this:

greetings

Ensure that you prefix the program name with a pathname—either the full pathname, like this:

/home/dang/bin/greetings

or a relative pathname:

~/bin/greetings

The startup script cannot magically locate program files, unless you specify a path, such as my personal ~/bin directory shown in the examples. (I also use my shell startup script to place my personal ~/bin directory on the search path—another Linux trick found in another book somewhere.)

After the startup script is updated, the next terminal window you open runs a startup script that outputs the following line, making your day more cheerful:

Hello, Dan!

And if your name isn’t Dan, then the greeting is more puzzling than cheerful.

2.2.2 Adding a name as an argument

The initial version of the greetings program is inflexible. That’s probably why you didn’t code it and are instead eager to modify it with some customization.

Consider the modest improvement offered in listing 2.2. This update to the code allows you to present the program with an argument, allowing it to be flexible.

Listing 2.2 Source code for greet02.c

#include <stdio.h>
 
int main(int argc, char *argv[])
{
    if( argc<2)                              
        puts("Hello, you handsome beast!");
    else
        printf("Hello, %s!
",argv[1]);      
 
    return(0);
}

The argument count is always 1 for the program name; if so, a default message is output.

The first word typed after the program name is represented as argv[1] and is output here.

Build this code into a program and thrust it into your shell’s startup script as written in the ancient scrolls but also in the preceding section:

greetings Danny

The program now outputs the following message when you open a new terminal window:

Hello, Danny!

This new message is far more cheerful than the original but still begging for some improvement.

2.3 The time of day

One of the first programs I wrote for my old DOS computer greeted me every time I turned on the computer. The program was similar to those created in the last two sections, which means it was boring. To spice it up, and inspired by my verbal interactions with humans I encounter in real life, I added code to make the greeting reflect the time of day. You can do so as well with varying degrees of accuracy.

2.3.1 Obtaining the current time

Does anyone really know what time it is? The computer can guess. It keeps semi-accurate time because it touches base with an internet time server every so often. Otherwise, the computer’s clock would be off by several minutes every day. Trust me, computers make lousy clocks, but this truth doesn’t stop you from plucking the current time from its innards.

The C library is rife with time functions, all defined in the time.h header file. The time_t data type is also defined in the header. This positive integer value (long data type, printf() placeholder %ld) stores the Unix epoch, the number of seconds ticking away since midnight January 1, 1970.

The Unix epoch is a great value to use in your greetings program. For example, imagine your joy at seeing—every day when you start the terminal—the following jolly message:

Hello, Danny, it's 1624424373

Try to hold back any emotion.

Of course, the time_t value must be manipulated into something a bit more useful. Listing 2.3 shows some sample code. Be aware that many time functions, such as time() and ctime() used in the code for time01.c, require the address of the time_t variable. Yup, they’re pointers.

Listing 2.3 Source code for time01.c

#include <stdio.h>
#include <time.h>                                   
 
int main()
{
    time_t now;
 
    time(&now);                                     
    printf("The computer thinks it's %ld
",now);
    printf("%s",ctime(&now));                       
 
    return(0);
}

The time.h header file is required, lest the compiler become cross with you.

The time() function requires the time_t variable’s address, prefixed here with the & address-of operator.

The ctime() function requires a pointer argument and returns a string appended with a newline.

Here is sample output from the resulting program:

The computer thinks it's 1624424373
Tue Jun 22 21:59:33 2021

The output shows the number of seconds of tick-tocking since 1970. This same value is swallowed by the ctime() function to output a formatted time string. This result may be acceptable in your greetings program, but time data can be customized further. The key to unlocking specific time details is found in the localtime() function, as the code in listing 2.4 demonstrates.

Listing 2.4 Source code for time02.c

#include <stdio.h>
#include <time.h>
 
int main()
{
    time_t now;
    struct tm *clock;                                         
 
    time(&now);
    clock = localtime(&now);
    puts("Time details:");
    printf(" Day of the year: %d
",clock->tm_yday);
    printf(" Day of the week: %d
",clock->tm_wday);          
    printf("            Year: %d
",clock->tm_year+1900);     
    printf("           Month: %d
",clock->tm_mon+1);         
    printf("Day of the month: %d
",clock->tm_mday);
    printf("            Hour: %d
",clock->tm_hour);
    printf("          Minute: %d
",clock->tm_min);
    printf("          Second: %d
",clock->tm_sec);
 
    return(0);
}

Because localtime() returns a pointer, it’s best to declare the structure as a pointer.

The first day of the week is 0 for Sunday.

You must add 1900 to the tm_year member to get the current year; you will forget this.

The tm_mon member ranges from 0 to 11.

I formatted the code in listing 2.4 with oodles of spaces so that you could easily identify the tm structure’s members. These variables represent the time tidbits that the localtime() function extracts from a time_t value. Ensure that you remember to adjust some values as shown in listing 2.4: the year value tm_year must be increased by 1900 to reflect the current, valid year; the month value tm_mon starts with zero, not one.

The output is trivial, so I need not show it—unless you send me a check for $5. Still, the point of the code is to show how you can obtain useful time information with which to properly pepper your terminal greetings.

2.3.2 Mixing in the general time of day

The program I wrote years ago for my DOS computer was called GREET.COM. It was part of my computer’s AUTOEXEC.BAT program, which ran each time I started my trusty ol’ IBM PC. Because I’m fond of nostalgia, I’ve kept a copy of the program. Written in x86 Assembly, it still runs under DOSBox. Ah, the sweet perfume of the digital past. Smells like ozone.

Alas, I no longer have the source code for my GREET.COM program. From memory (and disassembly), I see that the code fetches the current hour of the day and outputs an appropriate time-of-day greeting: good morning, good afternoon, or good evening. You can code the same trick—though in C for your current computer and not in x86 Assembly for an ancient IBM PC.

Pulling together resources from the first chunk of this chapter, listing 2.5 shows a current version of my old greetings program.

Listing 2.5 Source code for greet03.c

#include <stdio.h>
#include <time.h>
 
int main(int argc, char *argv[])
{
    time_t now;
    struct tm *clock;
    int hour;
 
    time(&now);
    clock = localtime(&now);
    hour = clock->tm_hour;     
 
    printf("Good ");
    if( hour < 12 )            
        printf("morning");
    else if( hour < 17 )       
        printf("afternoon");
    else                       
        printf("evening");
 
    if( argc>1 )               
        printf(", %s",argv[1]);
 
    putchar('
');
 
    return(0);
}

This statement is a convenience to avoid using clock->tm_hour over and over.

Before noon, say “Good morning.”

From noon to 5:00 P.M., say “Good afternoon.”

Otherwise, it’s evening.

Check for and output the first command-line argument.

Assuming that the built program is named greetings, that the user types in Danny as the command-line argument, and that it’s 4 o’clock in the afternoon, here is the code’s output:

Good afternoon, Danny

This code effectively replicates what I wrote decades ago as my GREET.COM program. The output is a cheery, time-relevant greeting given the current time of day.

For extra humor, you can add a test for early hours, such as midnight to 4:00 AM. Output some whimsical text such as “Working late?” or “Are you still up?” Oh, the jocularity! I hope your sides don’t hurt.

2.3.3 Adding specific time info

Another way to treat yourself when you open a terminal window is to output a detailed time string. The simple way to accomplish this task is to output the greeting followed by a time string generated by the ctime() function. Here are the two relevant lines of code:

printf(“Good day, %s
”,argv[1]);
printf(“It’s %s”,ctime(&now));

These two statements reflect code presented earlier in this chapter, so you get the idea. Still, the program is lazy. Better to incorporate the strftime() function, which formats a timestamp string according to your specifications.

The strftime() function works like printf(), with a special string that formats time information. The function’s output is saved in a buffer, which your code can use later. The code shown in listing 2.6 demonstrates.

Listing 2.6 Source code for greet04.c

#include <stdio.h>
#include <time.h>
 
int main(int argc, char *argv[])
{
    time_t now;
    struct tm *clock;
    char time_string[64];        
 
    time(&now);
    clock = localtime(&now);     
 
    strftime(time_string,64,"Today is %A, %B %d, %Y%nIt is %r%n",clock);
 
    printf("Greetings");
    if( argc>1 )
        printf(", %s",argv[1]);
    printf("!
%s",time_string);
 
    return(0);
}

Storage for the string filled by the strftime() function

You must fill a localtime() tm structure to make the strftime() function work.

You can review the man page for strftime() to discover all the fun placeholders and what they do. Like the printf() function, the placeholders are prefixed by a % character. Any other text in the formatting string is output as is. Here are the highlights from the strftime() statement in listing 2.6:

02-00_UN01

The output reflects the time string generated and stored in the time_string[] buffer. The time string appears after the general greeting as covered earlier in this chapter:

Greetings, Danny!
Today is Wednesday, June 23, 2021
It is 04:24:47 PM

At this point, some neckbeard might say that all this output can easily be accomplished by using a shell scripting language, which is the native tongue of the shell startup and configuration file anyway. Yes, such people exist. Still, as a C programmer, your job is to offer more insight and power to the greeting. Such additions aren’t possible when using a sad little shell scripting language. So there.

2.4 The current moon phase

My sense is that most programmers operate best at night. So why bother programming a moon phase greeting when you can just pop your head out a window and look up?

You’re correct: the effort is too much trouble, especially when you can write a C program to get a good approximation of the moon phase while remaining safely indoors. You can even delight yourself with this interesting tidbit every time you start a terminal window. Outside? It’s overrated.

2.4.1 Observing moon phases

The ancient Mayans wrote the first moon phase algorithm, probably in COBOL. I’d print a copy of the code here, but it’s just easier to express the pictogram: it’s a little guy squatting on a rock, extending a long tongue, wearing a festive hat, and wearing an angry expression on his face. Programmers know this stance well.

The moon goes through phases as it orbits the Earth. The phases are based on how much of the moon is exposed to sunlight as seen from Earth. Figure 2.1 illustrates the moon’s orbit. The sunny side always faces the sun, though from the Earth we see different portions of the moon illuminated. These are the moon’s phases.

02-01

Figure 2.1 The moon’s orbit affects how much of the illuminated side is visible from Earth.

The phases as they appear from an earthling’s perspective are named and illustrated in figure 2.2. During its 28-day journey, the moon’s phase changes from new (no illumination) to full and back to new again. Further, half the time, the moon is visible (often barely) during daylight hours.

02-02

Figure 2.2 Moon phases as seen from Earth

The phases shown in figure 2.2 follow the moon’s progress from new to full and back again. The latter waning phases happen in the morning, which is why they’re only popular with men named Wayne.

2.4.2 Writing the moon phase algorithm

Without looking outside right now, can you tell the moon phase?

Yes, I assume that you’re reading this book at night. Programmers are predictable. Congratulations if you’re reading this book during the day—outside, even. Regardless of the time, the moon has a current phase. Not a moody teenager phase, but one of the moon how-much-is-illuminated thingies covered in the preceding section.

To determine the moon phase without looking outside or in a reference, you use an algorithm. These are abundant and available on the internet as well as carved into Mayan tablets. The key is the moon’s predictable cycle, which can be mapped to days, months, and years. The degree of accuracy of the algorithm depends on a lot of things, such as your location and the time of day. And if you want to be exact, you must use complex geometry and messy stuff I don’t even want to look at through one eye half-shut.

Listing 2.7 shows the moon_phase() function. It contains an algorithm I found years ago, probably on the old ARPANET. My point is: I don’t know where it came from. It’s mostly accurate, which is what I find of typical moon phase algorithms that don’t use complex and frightening math functions.

Listing 2.7 The moon_phase() function

int moon_phase(int year,int month,int day)
{
    int d,g,e;
 
    d = day;
    if(month == 2)
        d += 31;
    else if(month > 2)
        d += 59+(month-3)*30.6+0.5;
    g = (year-1900)%19;
    e = (11*g + 29) % 30;
    if(e == 25 || e == 24)
        ++e;
    return ((((e + d)*6+5)%177)/22 & 7);
}

The algorithm presented in listing 2.7 requires three arguments: the integers year, month, and day. These are the same as values found in the members of a localtime() tm structure: tm_year+1900 for the year, tm_mon for the month (which starts with 0 for January), and tm_day for the day of the month, starting with 1.

Here’s how I’m going to explain how the algorithm works: I’m not. Seriously, I have no clue what’s going on. I just copied down the formula from somewhere and—by golly—it mostly works. Mostly.

Insert the code from listing 2.7 into your favorite greetings program. If you paste it in above the main() function, it won’t require a prototype. Otherwise, prototype it as:

int moon_phase(int year,int month,int day);

The function returns an integer in the range of 0 to 7 representing the eight moon phases shown earlier in figure 2.2, and in that order. An array of strings representing these phases, matching up to the value returned by the moon_phase() function, looks like this:

char *phase[8] = {
        "waxing crescent", "at first quarter",
        "waxing gibbous", "full", "waning gibbous",
        "at last quarter", "waning crescent", "new"
};

You can craft the rest of the code yourself. I’ve included it as moon.c in this book’s code repository as described in the introduction, which you haven’t read.

With this knowledge in hand, you can easily add the moon phase as output to your terminal program’s initial greeting. One thing you don’t want to do, however, is use this moon phase algorithm to accurately predict the moon phase. Seriously, it’s for fun only. Don’t use this algorithm to launch a manned rocket to the moon. I’m looking at you, Italy.

2.4.3 Adding the moon phase to your greeting

You can add the moon_phase() function to any of the source code samples for the greetings series of programs listed in this chapter. You need to fetch time-based data, which the moon_phase() function requires to make its calculation. You also need an array of strings to output the current moon phase text based on the value the function returns.

Listing 2.6, showing the greet04.c source code, is the best candidate for modification. Make the following changes:

Add a declaration in the main() function for integer variable mp to hold the value returned from the moon_phase() function:

int mp;

Add the following two statements after the last printf() statement in the existing code, just before the return:

mp = moon_phase(clock->tm_year+1900,clock->tm_mon,clock->tm_mday);
printf("The moon is %s
",phase[mp]);

You could combine these statements into a single printf() statement, eliminating the need for the mp variable: Insert the moon_phase() function call (the first line) into the brackets in the printf() statement. The result is a painfully long line of code, which is why I split it up. I’d choose readability over a long line of code any day.

A final copy of greet05.c can be found in this book’s GitHub repository. Here is sample output:

$ greetings Danny
Greetings, Danny!
Today is Thursday, June 24, 2021
It is 10:02:33 PM
The moon is full

Imagine the delight your users will have, seeing such a meaty message at the start of their terminal window day. They’ll lean back and smile, giving a thankful nod as they say, “I appreciate the scintillating details, my programmer friend. Glad I don’t have to venture outside tonight. Thank you.”

2.5 A pithy saying

The fortune program has been a staple of shell startup scripts since the old days, back when some Unix terminals were treadle powered. It remains available today, easily installed from your distro’s package manager; search for “fortune.”

The name “fortune” comes from the fortune cookie. The idea is to generate a pithy saying, or bon mot, which you can use as fresh motivation to start your day. These are inspired by the desserts provided at some Chinese restaurants, which serve the purpose of holding down the paper ticket more than they provide any nutritional value.

Here is an example of a digital fortune cookie, output from the fortune program:

$ fortune
There is no logic in the computer industry.
               --Dan Gookin

It’s possible to replicate the fortune program output, providing you have a database of pithy sayings and a program eager to pluck out a random one.

2.5.1 Creating a pithy phrase repository

The fortune program comes with one or more databases of witticisms. It’s from this database that the fortune cookie message is retrieved and output on the screen. You could borrow from this list, but that’s cheating. It’s also silly, because the fortune program is already written. You’d learn nothing. For shame!

Your goal is to write your own version of the pithy phrase database. It need not be quotes or humor, either. The list could contain tips about using the computer, reminders about IT security, and other important information, like the current, trendy hairstyles.

I can imagine several ways to configure the list. This planning is vital to writing good code: a well-organized list means you have less coding to do. The goal is to pluck a random phrase from the repository, which means an organized file is a must. Figure 2.3 outlines the process for writing code to pluck a random, pithy phrase from a list or database.

02-03

Figure 2.3 The process for reading a random, pithy quote from a file

I can imagine several approaches to formatting the file, as covered in table 2.2.

Table 2.2 Approaches to storing sayings for easy access

File format/data

Pros

Cons

Basic text file

Simple to maintain using existing tools

The file must be read and indexed every time the program runs.

Formatted file with an initial item count reflecting the number of entries

Item count can be read instantly

The item count must be updated as the list is modified.

Hash table with indexed entries

Easy to read and access each record

You will most likely need a separate program to maintain the list, which is more coding to do.

I prefer the basic text file for my list, which means more overhead is required in order to fetch a random entry. It also means that I don’t need to write a list maintenance program. Another benefit is that anyone can edit the sayings file, adding and removing entries at their whim.

Eschewing all other options, my approach is to read the file a line at a time, storing and indexing each line in memory. The file needs to be read only once with this method, so it’s what I choose to do. The downside? I must manage memory locations, also known as pointers.

Fret not, gentle reader.

The bonus of my approach (forgetting pointers for the moment) is that you can use any text file for your list. Files with short lines of text work best; otherwise, you must wrap the text on the terminal screen, which is more work. The file pithy.txt can be found in this book’s GitHub repository.

2.5.2 Randomly reading a pithy phrase

My pithy-phrase greetings program reads lines of text from the repository file, allocating storage space for each string read. As the lines are read and stored, an index is created. This index is a pointer array, but one created dynamically by allocating storage as the file is read. This approach is complex in that it involves those horrifying pointer-pointer things (two-asterisk notation) and liberal use of the malloc() and realloc() function. I find such activity enjoyable, but I also enjoy natto. So there.

As with any complex topic in programming, the best way to tackle the project is to code it one step at a time. The first step is to read a text file and output its contents. The code in listing 2.8 accomplishes this first task by reading lines of text from the file pithy.txt. Remember, this code is just the start. The pointer insanity is added later.

Listing 2.8 Source code for pithy01.c

#include <stdio.h>
#include <stdlib.h>
 
#define BSIZE 256
 
int main()
{
    const char filename[] = "pithy.txt";   
    FILE *fp;
    char buffer[BSIZE];                    
    char *r;
 
    fp = fopen(filename,"r");
    if( fp==NULL )
    {
        fprintf(stderr,"Unable to open file %s
",filename);
        exit(1);
    }
 
    while( !feof(fp) )                     
    {
        r = fgets(buffer,BSIZE,fp);        
        if( r==NULL )
            break;
        printf("%s",buffer);               
    }
 
    fclose(fp);
 
    return(0);
}

The file pithy.txt is assumed to be in the same directory as the program.

The buffer is used to read text from the file; the size is a guess, set as defined constant BSIZE (line 4).

Loops as long as the file isn’t empty

The variable r ensures that fgets() doesn’t mess up and read beyond the end of the file; if so, the loop stops.

Outputs all the lines in the file

The purpose of pithy01.c is to read all the lines from the file. That’s it. Each line is stored in char array buffer[] and then output. The same buffer is used over and over.

The program’s output is a dump of the contents of file pithy.txt. For a release program, your code must ensure that the proper path to pithy.txt (or whatever file you choose) is confirmed and made available.

Build and run to prove it works. Fix any problems. When it’s just right, move on to the next step: use a pointer and allocate memory to store the strings read. Remember, the final program stores all the file’s strings in memory. Because the number of strings is unknown, this allocation method works better than guessing an array size.

To proceed with the next improvement, a new variable entry is introduced. It’s a char pointer, which must be allocated based on the size of the line read from the file. Once allocated, the contents of buffer[] are copied into the memory chunk referenced by pointer entry. It’s this string that’s output, not the contents of buffer[].

Another improvement is to count the number of items read from the file. For this task, the int variable items is added, initialized, and incremented within the while loop.

Here are the updates to the code: Add a line to include the string.h header file, required for the strcpy() function:

#include <string.h>

In the variable declarations part of the code, add char pointer entry and int variable items:

char *r,*entry;
int items;

Before the while loop, initialize variable items to zero:

items = 0;

Within the while loop, memory is allocated for variable entry. The pointer must be tested to ensure memory is available. Then the contents of buffer[] are copied to entry, the contents of entry output, and the items variable incremented. Here is the chunk of code to replace the existing printf() statement in the original program:

entry = (char *)malloc(sizeof(char) * strlen(buffer)+1);     
if( entry==NULL )
{
    fprintf(stderr,"Unable to allocate memory
");
    exit(1);
}
strcpy(entry,buffer);
printf("%d: %s",items,entry);
items++;

Enough storage for the string, plus one for the null character

These updates, found in the online repository in pithy02.c, only change the output by prefixing each line read with its item number, starting with zero for the first line read from the file. While this update may seem tiny, it’s necessary to continue with the next step, which is dynamically storing all the strings read from the file into memory.

As the program sits now, it allocates a series of buffers to store the strings read. Yet the addresses for these buffers are lost in memory. To resolve this issue, a pointer-pointer is required. The pointer-pointer, or address of a pointer, keeps track of all the string’s memory locations. This improvement is where the code earns its NC-17 rating.

To track the strings stored in memory, make these improvements to pithy02.c, which now becomes pithy03.c:

Add a second int variable, x, used in a later for loop. Also add the pointer-pointer variable list_base:

int items,x;
char **list_base;

The list_base variable keeps track of the entry pointers allocated later in the code. But first, the list_base pointer must be allocated itself. Insert this code just after the file is opened and before the while loop:

list_base = (char **)malloc(sizeof(char *) * 100);
if( list_base==NULL )
{
    fprintf(stderr,"Unable to allocate memory
");
    exit(1);
}

The illustration in figure 2.4 shows what’s happening with the first statement allocating variable list_base. It’s a pointer to a pointer, which requires the ** notation. The items it references are character pointers. The size of the list is 100 entries, which is good enough—for now.

02-04

Figure 2.4 How the terrifying pointer-pointer buffer is allocated

Within the while loop, remove the printf() statement. Outputting statements takes place outside the loop. In place of the printf() statement, add this statement below the strcpy() statement:

*(list_base+items) = entry;

Using the offset provided by the items count, this statement copies the address stored in pointer variable entry into the list maintained at location list_base. Only the address is copied, not the entire string. This statement represents crazy pointer stuff—and it works. Figure 2.5 illustrates how the crazy kit ’n’ kaboodle looks.

02-05

Figure 2.5 The list_base and items variables help store strings allocated by the entry pointer.

Finally, after the file is closed, output all the items with this for loop:

for( x=0; x<items; x++ )
    printf("%s",*(list_base+x));

In this loop, variable x sets the offset in the list of addresses: *(list_base+x) references each line of text read from the file, now stored in memory.

At this point, the program effectively reads all the text from the file, stores the text in memory, and keeps track of each string. Before a random string can be plucked out of the lot, care must be taken to consider when more than 100 lines are read from the file.

When memory is allocated for the list_base variable, only 100 pointers can be stored in that memory chunk. If the value of variable items creeps above 100, a memory overflow occurs. To prevent this catastrophe, the code must reallocate memory for list_base. This way, if the file that’s read contains more than 100 lines of text, they can be stored in memory without the program puking all over itself.

To reallocate memory, or to increase the size of an already-created buffer, use the realloc() function. Its arguments are the existing buffer’s pointer and the new buffer size. Upon success, the contents of the old buffer are copied into the new, larger buffer. For the size of list_base to be increased, it must be reallocated to another 100 char pointer-sized chunks.

Only one change is required in order to update the code. The following lines are inserted at the end of the while loop, just after the items variable is incremented:

if( items%100==0 )                                                         
{
    list_base = (char **)realloc(list_base,sizeof(char *)*(items+100));    
    if( list_base==NULL )
    {
        fprintf(stderr,"Unable to reallocate memory
");
        exit(1);
    }
}

Every time items is exactly divisible by 100 . . .

. . . existing storage is increased by 100 pointer-size chunks.

This update is saved as pithy04.c. The code runs the same as the program generated from pithy03.c, though if the file that’s read contains more than 100 lines of text, each is properly allocated, stored, and referenced without disaster.

The program is now ready to do its job: to select and output a random item from the file. The final step is to remove the for loop at the end of the code; it’s no longer needed, as the program is required to output only one random line from the file.

Start by including the time.h header file:

#include <time.h>

Replace the declaration for its int variable x with a declaration for new variable saying:

int items,saying;

Three lines are added to the end of the code, just above the return statement:

srand( (unsigned)time(NULL) );
saying = rand() % (items-1);
printf("%s",*(list_base+saying));

This is the final update to the code, available in the online repository as pithy05.c. When run, the program extracts a random line from the file, outputting its text.

As I wrote earlier in this section, this approach is only one way to resolve the problem. It’s quick and it works, which is good enough to add a pithy saying to your shell startup script.

One final note: the program doesn’t release any memory directly. Normally, the end of a function would be dotted with free() statements, one for each memory chunk allocated. Because the entire code dwells within the main() function, freeing memory isn’t necessary. The memory allocated is freed when the program quits. Had the allocation taken place in a function, however, it’s necessary to release the allocation or risk losing the memory chunk and potentially causing a memory overflow.

2.5.3 Adding the phrase to your greeting code

If your goal is to modify a single greetings program for the shell’s startup script, your next task is to add the code from the pithy series of programs into your greetings program. Such a task would keep all your efforts in a single program and all the output on a single line in the shell startup script.

Because the pithy program is kinda fun, I’m not incorporating it into my previous greetings program code. Instead, I’ll leave it as its own line in the shell startup script. That way, I can also run the program from the command prompt any time I need to be humored or am in need of levity. You can work to incorporate the pithy program into your greetings program on your own.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset