Write a Minimal Main Function

Before we dive into more advanced features, let’s start by writing a minimal main function. This allows us to verify that our project is correctly set up, builds successfully, and that our application can be executed.

  1. Create and open the file:

    $ nano robot-escape/src/main.cpp
    
  2. Add the following code:

    <project>/robot-escape/src/main.cpp
    #include <erbsland/all_conf.hpp>
    
    #include <filesystem>
    #include <iostream>
    #include <format>
    
    using namespace el::conf;
    
    int main(int argc, char **argv) {
        if (argc < 2) {
            std::cout << "Usage: " << argv[0] << " <config-file>\n";
            return 1;
        }
    
        const auto configFile = std::filesystem::path{argv[1]};
        try {
            Parser parser;
            const auto source = Source::fromFile(configFile);
            const auto doc = parser.parseOrThrow(source);
    
            auto width = doc->getIntegerOrThrow(u8"field.width");
            auto height = doc->getIntegerOrThrow(u8"field.height");
    
            std::cout << std::format("Field width = {}, height = {}\n", width, height);
            return 0;
        } catch (const Error &error) {
            std::cerr << error.toText().toCharString() << "\n";
            return 1;
        }
    }
    

Code Breakdown

Header Includes and Namespace

#include <erbsland/all_conf.hpp>
#include <filesystem>
#include <iostream>
#include <format>
using namespace el::conf;

We include all headers of the configuration parser in one line via all_conf.hpp. This is a convenience for small examples—keeping things compact and readable.

However, in real-world applications, we recommend including only the headers you actually use, like this:

#include <erbsland/conf/Parser.hpp>
#include <erbsland/conf/Source.hpp>
#include <erbsland/conf/Error.hpp>
#include <filesystem>
#include <iostream>
#include <format>

void myFunction() {
    using el::conf::Parser;
    using el::conf::Source;
    using el::conf::Error;
    // ...
}

Avoiding using namespace in production code helps keep your namespaces clean and prevents name collisions.

Handling Command-Line Input

if (argc < 2) {
    std::cout << "Usage: " << argv[0] << " <config-file>\n";
    return 1;
}

const auto configFile = std::filesystem::path{argv[1]};

We check whether a configuration file path is provided as a command-line argument. If not, we print a helpful usage message and exit.

There’s no need to manually check whether the file exists—the parser will handle that for us.

Parsing the Configuration File

Parser parser;
const auto source = Source::fromFile(configFile);
const auto doc = parser.parseOrThrow(source);

We create a Parser instance (a lightweight object), load the file into a Source, and parse it.

The parseOrThrow() function throws an Error exception on failure (e.g., missing file, syntax error). We catch this below and print the error message in a readable form.

Accessing Parsed Values

auto width = doc->getIntegerOrThrow(u8"field.width");
auto height = doc->getIntegerOrThrow(u8"field.height");

std::cout << std::format("Field width = {}, height = {}\n", width, height);

We use getIntegerOrThrow to read two values from the config. If the path doesn’t exist or the type is wrong, an exception is thrown and caught in the same block as parsing errors.

If everything works, the application prints out the values from your configuration file.

The Current Project State

At this point, your project directory structure should look like this, with the newly added components marked:

robot-escape
    ├── erbsland-cpp-configuration
    ├── robot-escape
    │   ├── src
    │   │   └── main.cpp             # [new] The main entry point
    │   └── CMakeLists.txt
    └── CMakeLists.txt

Compile and Run the Application →