Review - Makefiles & Command Line Arguments

Thomas J. Kennedy

Contents:

All example C++ code (e.g., from recorded lectures) makes use of makefiles. You will use makefiles in each C++ assignment. Makefiles are used to perform compilation and linking steps. For C++, this takes the form of:

  1. Compiling C++ implementation files (e.g., .cpp, .cc, .cxx) to *.o files.
  2. Linking .o files to create one or more executables (binaries).

Writing a makefile is an exercise best left to CS 252 (Introduction to Unix/Linux) or an equivalent course. You are required to use makefiles in this course.

1 A Quick makefile

Most of my examples make use of a quick-and-dirty makefile.

makefile
MAINPROG=cmd_line_example

SOURCES:=$(wildcard *.cpp)
OBJECTS=$(SOURCES:.cpp=.o)
FLAGS=-std=c++17 -fsanitize=leak,address -Wall -Wpedantic -fuse-ld=gold

all: $(SOURCES) $(MAINPROG)

$(MAINPROG): $(OBJECTS)
	g++ $(FLAGS) $(OBJECTS) -o $@

.cpp.o:
	g++ $(FLAGS) -c $< -o $@

clean:
	rm *.o $(MAINPROG)

Let us break everything down…

MAINPROG=cmd_line_example

The first line is a variable. It is used to specify the name for the final executable. In this example, the final executable will be named cmd_line_example.

SOURCES:=$(wildcard *.cpp)
OBJECTS=$(SOURCES:.cpp=.o)

The SOURCES line uses a wild card (i.e., .cpp) to grab a list of all cpp files in the current directory. The OBJECTS line constructs a list of .o files that need to be compiled (one per cpp file).

FLAGS=-std=c++17 -fsanitize=leak,address -Wall -Wpedantic -fuse-ld=gold

The FLAGS line specifies what compilation flags need to be used:

all: $(SOURCES) $(MAINPROG)

When make or make all is run, compile everything (all cpp files) and build the main program.

$(MAINPROG): $(OBJECTS)
	g++ $(FLAGS) $(OBJECTS) -o $@
.cpp.o:
	g++ $(FLAGS) -c $< -o $@

This is a meta-rule that tells the makefile how to compile each cpp file.

clean:
	rm *.o $(MAINPROG)

When make clean is run, delete all .o files and delete the final executable.

2 Command Line Arguments

Most (almost all) examples in will make use of command line arguments. Take a look at…

cmd_line_ex.cpp
#include <iostream>
#include <iomanip>

using namespace std;

/**
 * Echo all command line arguments, one per line in the form:
 *
 *   idx: arg
 *
 * where idx is the index (0, 1, ...) and arg is the argument.
 *
 * @param argc number of arguments
 * @param argv 2D array of C-style (null terminated) strings
 * @param outs output destination (e.g., cerr or cout)
 */
void echo_arguments(const int argc, char** argv, ostream& outs=std::cerr)
{
    // There is always at least one argument, i.e., the program name
    outs << right << setw(3) << 0 << ": " << argv[0] << "\n";
    outs << "\n";

    for (int i = 1; i < argc; i++) {
        // Technically "right" does not need to be repeated, but
        // we would probably tweak formatting more in the future
        outs << right << setw(3) << i << ": " << argv[i] << "\n";
    }
}

int main(int argc, char** argv)
{
    echo_arguments(argc, argv);

    return 0;
}

cmd_line_ex.cpp is a simple program. All it does is echo any entered command line arguments using the echo_arguments function.

void echo_arguments(const int argc, char** argv, ostream& outs=std::cerr)
{
    // There is always at least one argument, i.e., the program name
    outs << right << setw(3) << 0 << ": " << argv[0] << "\n";
    outs << "\n";

    for (int i = 1; i < argc; i++) {
        // Technically "right" does not need to be repeated, but
        // we would probably tweak formatting more in the future
        outs << right << setw(3) << i << ": " << argv[i] << "\n";
    }
}

The first argument (i.e., argv[0]) is a special case. It is always the program name, and it is supplied automatically.

2.1 Playing with Command Line Arguments

Save a copy of

makefile
MAINPROG=cmd_line_example

SOURCES:=$(wildcard *.cpp)
OBJECTS=$(SOURCES:.cpp=.o)
FLAGS=-std=c++17 -fsanitize=leak,address -Wall -Wpedantic -fuse-ld=gold

all: $(SOURCES) $(MAINPROG)

$(MAINPROG): $(OBJECTS)
	g++ $(FLAGS) $(OBJECTS) -o $@

.cpp.o:
	g++ $(FLAGS) -c $< -o $@

clean:
	rm *.o $(MAINPROG)

and

cmd_line_ex.cpp
#include <iostream>
#include <iomanip>

using namespace std;

/**
 * Echo all command line arguments, one per line in the form:
 *
 *   idx: arg
 *
 * where idx is the index (0, 1, ...) and arg is the argument.
 *
 * @param argc number of arguments
 * @param argv 2D array of C-style (null terminated) strings
 * @param outs output destination (e.g., cerr or cout)
 */
void echo_arguments(const int argc, char** argv, ostream& outs=std::cerr)
{
    // There is always at least one argument, i.e., the program name
    outs << right << setw(3) << 0 << ": " << argv[0] << "\n";
    outs << "\n";

    for (int i = 1; i < argc; i++) {
        // Technically "right" does not need to be repeated, but
        // we would probably tweak formatting more in the future
        outs << right << setw(3) << i << ": " << argv[i] << "\n";
    }
}

int main(int argc, char** argv)
{
    echo_arguments(argc, argv);

    return 0;
}

..then open a Linux terminal (e.g., SSH into a CS Linux server or open a local Linux terminal). Run make to compile cmd_line_ex.cpp.

  1. Run ./cmd_line_example

    You should see…

      0: ./cmd_line_example
    
  2. Run ./cmd_line_example 1 2 77 The letter Q "Hello, and..."

    You should see…

      0: ./cmd_line_example
    
      1: 1
      2: 2
      3: 77
      4: The
      5: letter
      6: Q
      7: Hello, and...!
    

Do not forget… The double quotes are required for any argument that contains spaces.

2.2 Using Command Line Arguments

Most programs will not simply echo command line arguments. Many programs will use command line arguments to specify input files. Consider the following excerpt…

    if (argc < 2) {
        cout << "No input file provided." << "\n";
        cout << "Usage: " << argv[0] << " input_file" << "\n";
        return 1;
    }

    ifstream shapesFile(argv[1]);

    if (!shapesFile) {
        cout << "Error: " << argv[1] << "could not be opened" << "\n";
        return 2;
    }

The first conditional block checks that the required number of arguments were supplied by the user.

    if (argc < 2) {

In this excerpt, only one argument is required… an input file name. Note that the first true argument (argv[1]) is the input file name. The first supplied argument (argv[0]) is the executable name.