AlderAutomation

C++ and Cmake: File Manipulation

Last updated: 2 minutes ago

Carrying on from the last two posts about Neovim and Cmake ( Cmake & Neovim & Neovim buffer and tabs ) I am now making a "small" c++ project to go through a novelWriter project folder and produce some analytics. Mostly just doing this to carry on with the Neovim, cmake, and c++ tutorials. But also to get myself back into c++ immersion so I can get back to working on my game. I have found a few more topics for us to touch on.

Notes:

Setup the Date Container: Struct & Cmake

In my project, I need to access a file called ToC.txt which if we do a cat ToC.txt outputs this:

Table of Contents
=================

File Name                  Class      Layout    Document Label
------------------------------------------------------------------------------
content/5a996475adf4e.nwd  NOVEL      DOCUMENT  Title Page

We need to read the file, remove the headers, and store all the data. What do we store the data in? I decided on a Struct, but I could have also used a class or a map. Since this is data only and that data isn't doing anything on its own, I ruled out class (although I later found out that structs can have methods too). It seems to me the biggest difference between a struct and a map is that structs are predefined at compile time and don't change, whereas maps are defined at runtime and can have fields added or removed as the program sees fit (closer to the behaviour of a variable). I decided to go with a struct because that field data will not be changing.

I figured that this would be a good time to introduce another cmake concept, I put the struct into its own .hpp file so that we could add it as a include. In my Project directory:

*A note about file extensions .h vs .hpp: they functionally are the same. .h typically signifies a c/c++ header file while a .hpp signifies a c++ header file. I am using .hpp as I am working in C++ and have no idea if this will work with C *

In the noteStruct.hpp:

// Sept 06 2026
//
// Data structure for the Note

// Pragma once makes it so the header only gets called once even if it is included 
// in many .cpp files 
#pragma once

#include <string>

// simple structure to handle the ToC data 
struct noteStruct {
	std::string fileName; 
	std::string docClass; 
	std::string docLayout; 
	std::string docLabel; 
};

Now in the CMakeLists.txt we can add that include folder so that we can add the noteStruct.hpp to the main file:

That line can be on multiple files as in my CMakeLists.txt. Our entire file should read something like (I am trying to stick close to historical posts):

cmake_minimum_required(VERSION 3.15...4.4)

project(NW_Analyzer VERSION 1.0 
        DESCRIPTION "An app to analyze NW project folder" 
        LANGUAGES CXX
)

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

add_executable(main src/main.cpp)

target_compile_features(main PRIVATE cxx_std_23)

target_include_directories(
    ## main should be your executable name, not projectname
	main PRIVATE
	${PROJECT_SOURCE_DIR}/include
)

If we have done everything right, we can now go into our main.cpp and add:
#include "noteStruct.hpp"
Notice that the include file is in "" and not <>. The <> format is for system and standard library headers, while "" is typically for project made header files.

If you want to make sure this works, you could make a test struct:

noteStruct note; 
note.fileName = "Test"; 
note.docClass = "classTest"; 
note.docLayout = "layoutTest"; 
note.docLabel = "labelTest";

Then run the build command cmake -S . -B build && cmake --build build && ./build/main
Everything should build and produce no errors.

Reading the Data file: Fstream

OK, we have a place to put the data, now we need to read the file, remove headers, and place data.

First thing we need for reading files, is to include the fstream header at the top of the main.cpp file with the rest of the includes:
#include <fstream>

In the main function we can create a file input fstream object: std::ifstream readFile;

Note:
I use the std:: because that tells me and the compiler that the ifstream is coming from the standard library. Many people and tutorials will advise to put "using namespace std;" to save you typing out std:: but if you happen to get two includes with the same function it can cause mental overhead or compile issues.

Note:
fstream creates/opens a file input and output fstream object
ifstream opens a file input fstream object
ofstream creates/opens a file output fstream object

Next we need to open the file and make sure that it does open and not error:

// Path to ToC.txt will be dependent on where the file is stored. Right now I am hard coding it, but later I want to make it a user-defined variable.  
readFile.open("path/to/ToC.txt");  

// checking if the open errored and exiting app
if (readFile.fail()) {

    // see note about these two statements & use only one
    std::cout << "Failed to open file." << "\n"; 
    std::cout << "Failed to open file." << std::endl; 

    exit(1); 
}

Note on using std::endl or using a
: They both create a new line character but the endl flushes out the output buffer immediately which gets the output to the screen immediately. Endl could also slow down the program because it's doing more work by flushing the buffer everytime you use it.

Parsing the Data: Getline, Sstream, Vector & Struct

Now we need to remove the extraneous header lines from the file. Let's create a string to hold the file data:
std::string fileData
the getline() function reads an entire line of string at once and its parameters are:
getline(stream, string, delimiter)
stream: the stream where the string is, in our case readFile
string: the string were we are going to hold the string from the stream
delimiter: optional. The default is
.

For this particular ToC file we are going to use the following:

getline(readFile, fileData)
getline(readFile, fileData)
getline(readFile, fileData)
getline(readFile, fileData)
getline(readFile, fileData)
getline(readFile, fileData)

Each time we use the getline() it reassigns the fileData variable, which overwrites the data in the variable before it can be used for anything else.

Onto the "fun" part. For the next part we are going to need to go back to top of file and add in some includes:

// Vector for storing all of the noteStructs
#include <vector>
// Sstream for handling each line into smaller strings 
#include <sstream>
// Should already have this include, but noteStruct to hold data
#include "noteStruct.hpp"

Now, create a new vector to hold all of our data structs in. This can be placed anywhere between main's header and the while loop that we are about to create:
std::vector<noteStruct> notes;

Next, create a while loop that basically says "while getline can still getline, run this code". Each iteration takes each line and puts it into a new istringstream, creates a new noteStruct, populates the data into the data structure, and then puts the data into our vector.

	// loop over the file stream until no more data
	while (getline(readFile, fileData)) {

		// place each getline string into incoming string stream 
		std::istringstream iss(fileData); 
		
		// create new noteStruct to hold data
		noteStruct note; 

        // populate data struct from isstream
		iss >> note.fileName; 
		iss >> note.docClass; 
		iss >> note.docLayout; 
		// this line to deal with spaces in Label 
        // std::ws consumes any white space before the label 
        // getline puts rest of string (including inner white space) into the note.doclabel
		std::getline(iss >> std::ws, note.docLabel);

        // now we put the data struct into the vector. 
		notes.push_back(note); 
	}

Closing File & Testing: fstream.close()

The last thing to do is to properly close the file stream, and to see if the vector populated the data structures properly. After the while loops, we'll add:

// close the file properly
readFile.close();

// Checking the vector for data. 
// could do it by single element. Pick any element number and any field
std::cout << notes[3].docLabel << "\n";  

// could also check all with a for loop
for ( noteStruct note : notes ) {
    std::cout << note.docLabel << "\n"; 
}

Conclusion

We now know how to open a file, work with vectors, work with structs, and use includes in cmake. We're doing awesome! Below I will include some reference materials that I used in making this post; trying to be as accurate as possible for you all. Next post is going to detour a little bit from C++ (just going to leave that cliffhanger right there :P ).

If you want me to elaborate on anything, please message me on Discord, Mastodon, X, or email me. I’ll be happy to go into more detail one-on-one or to create more posts.

Reference Materials

sstream

https://www.geeksforgeeks.org/cpp/stringstream-c-applications/

vector

https://www.w3schools.com/cpp/cpp_ref_vector.asp#gsc.tab=0
https://www.w3schools.com/cpp/cpp_vectors.asp

structs

https://www.w3schools.com/cpp/cpp_structs.asp

header files

https://www.geeksforgeeks.org/c/header-files-in-c-cpp-and-its-uses/
https://www.learncpp.com/cpp-tutorial/header-files/

Main first vs Function first

https://stackoverflow.com/questions/21718361/whats-the-difference-between-declaring-functions-before-or-after-main

https://www.geeksforgeeks.org/cpp/function-prototypes-in-cpp/

C++ Reference Book

Gaddis, T. (2014). Starting out with C++: From control structures through objects (8th ed.). Pearson.

std::endl

https://en.cppreference.com/cpp/io/manip/endl

getline()

https://www.geeksforgeeks.org/cpp/getline-string-c/

noaibadge