This example shows how to create a basic C++ executable including some basic argument parsing. You may learn from this:
- Create a basic C++ program using Entangled.
- Use ArgAgg to parse command line arguments.
- Use fmtlib to do easy (C#, Python, etc. -like) string formating in C++.
- Setting up a basic
Makefile
. - An annoying drinking song.
This program will be so tiny, that it will fit into a single source file:
«99-bottles/src/99-bottles.cc»=
This source file includes some headers, a version number and three functions. The bottles
function helps us create gramatically correct sentences with an arbitrary (though \(\le 0\)) number of bottles. print_song
is the core function generating the 99-bottles song and main
will handle command-line arguments.
First, we’ll include some headers that we’ll need:
«includes»=
The cstdlib
file includes basic functions and constants from the C stdlib.h
header. iostream
contains the C++ library for IO. Next to that we include the ArgAgg and fmtlib headers.
Argument parsing
An executable in C/C++ always has a main function of the signature
The arguments argc
and argv
give the number of arguments given on the command line and an array of null-terminated C-strings respectively. If we were writing C, we would use the getopt
library to parse the given arguments. This C interface is tedious. It’s 2019 and we’re programming C++, so we use ArgAgg (say this out loud and twist your face in a painful contortion just to annoy who ever is sitting next to you)!
ArgAgg is a header-only library that simplifies argument parsing greatly. We may declare the possible command-line arguments and flags as follows:
«declare-arguments»=
It is considered good behaviour to always include the "--help"
and "--version"
arguments (see for instance: tldp.org and the GNU Coding Standards). For good measure we also include an argument that gives the number of bottles that we’ll start the song with.
Using the argagg::parser
instance we can parse the command-line arguments stored in argc
and argv
. Since this operation may fail, we perform it inside a try
, catch
block:
«parse-arguments»=
Now that we have parsed the arguments, we can implement the different ways the program will execute. If either the "--help"
or "--version"
flags was given, we print the requested information and exit the program.
«print-help-or-version»=
If none of those flags was given we continue with the main program. We extract the number of bottles that we want to start with and print the song:
In one line we extract the argument n
, parse it to unsigned
and give a default value, should the argument not been given.
Now we can compose the body of the main function:
Formatting a drinking song
Now, to the problem of 99 bottles! One part of the problem is that towards the end of the song, the grammar of the text is changing slightly due to the singularity of the number one. We’ll create a little helper function to figure out how to communicate the number of bottles we have left.
«bottles-function»=
The fmt::format
function formats a string using "{}"
as a placeholder for whatever arguments follow. This function is much easier to use than the native C++ method of opening a std::ostringstream
, using iomanip
and so on. There is an overview of the format string syntax on the fmtlib homepage.
Now, we may write out the song:
«print-song-function»=
void print_song(unsigned n)
{
unsigned i = n;
while (i > 0)
{
fmt::print(
"{0} of beer on the wall, "
"{0} of beer.\n",
bottles(i));
fmt::print(
"Take one down and pass it around, "
"{} of beer on the wall.\n",
bottles(--i));
}
fmt::print(
"No more bottles of beer on the wall, "
"no more bottles of beer.\n"
"Go to the store and buy some more, "
"{} of beer on the wall.\n",
bottles(n));
}
Song synthesis
To compile the 99-bottles
program, you can use GCC directly.
mkdir build
g++ src/99-bottles.cc -lfmt -o build/99-bottles
But it is much better to provide a Makefile
. We will create a Makefile
that is generic enough to compile your program without too much modification, even as your source code expands.
We make sure all our build files end up in a single folder, preferably called build
(hint: this folder can be added to your .gitignore
).
We search for all files with the .cc
extension (warning: for bigger projects this will slow down compilation considerably). make
does not easily support scanning for source files, but we may use a shell command.
We can use string manipulation present in make
to create the target object files and dependency files. The latter store the dependencies of the source files on header files, so that if a header file changes, the source file is recompiled.
The fmtlib
library requires linking using -lfmt
, we next define the compiler and linker commands and their arguments.
«compile-flags»=
Now that we have defined all these variables we can setup the make targets. The first target given is also the default one, in this case build
. Since both build
and clean
don’t create any files themselves, these targets have to be declared as phony. This means make
won’t search for a file called build
(which is our build directory!) to see if the build
rule should be executed.
In this case build
depends only on the target executable 99-bottles
, making sure that this target is getting built.
«declare-target-rules»=
Last we define the pattern rules. These tell make
how to build a generic target adhering to some pattern from sources of a similar pattern. The first rule we give compiles a source file to an object file, the second rule links all objects files into an executable. We used several handy make
smart variables here:
$(@D)
is the directory that contains the target.$<
is the name of the first prerequisite, in this case the source file.$^
is the list of all prerequisites, in this case the object files.$@
is the name of the target file.
There are more of these listed in the make
documentation.
Note also, that the -MMD
flag to the compiler is actually telling GCC to create the aforementioned header dependency files. These files follow a make
syntax generating a set of rules that is included using the -include
statement.
«declare-pattern-rules»=
The complete Makefile
:
«99-bottles/Makefile»=
Now, compiling is as simple as typing make
. If compilation is successful we can run the program.
$ ./build/99-bottles -n 3
3 bottles of beer on the wall, 3 bottles of beer.
Take one down and pass it around, 2 bottles of beer on the wall.
2 bottles of beer on the wall, 2 bottles of beer.
Take one down and pass it around, 1 bottle of beer on the wall.
1 bottle of beer on the wall, 1 bottle of beer.
Take one down and pass it around, no more bottles of beer on the wall.
No more bottles of beer on the wall, no more bottles of beer.
Go to the store and buy some more, 3 bottles of beer on the wall.