11
\$\begingroup\$

This question Code lines counter prompted me to write my own line counting program. I program on both Windows and Linux and I know about the Linux utility wc.

The command line and output format are based on the wc program, as are the methods for getting the byte count, word count and line count. My comparison is based on the Cygwin wc implementation, since I have Cygwin installed on my Windows 10 computer.

I implemented 7 of the command line switches that Cygwin wc provides:

  • -c, --bytes print the byte counts
  • -m, --chars print the character counts
  • -l, --lines print the newline counts
  • -w, --words print the word counts
  • -L, --max-line-length print the length of the longest line
  • --help display this help and exit
  • --version output version information and exit

The value -L, --max-line-length from Cygwin wc differs by about 2 percent from this implementation, Cygwin wc calculates the size of tabs slightly differently than this implementation does. The source code of Cygwin wc was examined.

I also added 2 command line switches:

  • -R, --subdirectories all files in the directory as well as sub directories
  • -t, --time-execution print the execution time of the program

Execution time is calculated separately for command line parsing with file name expansion and the execution of the analysis of the text files that are input.

The code compiles with the C++17 version of g++ in Cygwin and on my Raspberry Pi 4. I have not tested the g++ versions generated. At the time I tried to build them I didn’t know that std::filesystem requires an additional library to be linked in. I will create a make file for this.

Development Environment:

The code was developed in Visual Studio 2022 on Windows 10, Dell Precision 7740
Processor Intel(R) Core(TM) i7-9850H CPU @ 2.60GHz 2.59 GHz
Installed RAM 64.0 GB (63.8 GB usable)
System type 64-bit operating system, x64-based processor
Pen and touch No pen or touch input is available for this display
Edition Windows 10 Pro
Version 21H2
OS build 19044.2251
Experience Windows Feature Experience Pack 120.2212.4180.0

Program Output:

All test cases were run in PowerShell

In the directory where the code for this program is the command line wc -Llcw *.h *.cpp that uses Cygwin wc reports:

   30   152  1130    87 CmdLineFileExtractor.h
   46   143  1456    75 CommandLineParser.h
   26    86   778    75 Executionctrlvalues.h
   28    60   711    93 FileProcessor.h
   63   275  2515    86 FileStatistics.h
   53   129  1120    72 ProgramOptions.h
   35   115  1100    79 ReportWriter.h
   27    50   470    68 SpecialExceptions.h
   24    44   611    72 StatisticsCollector.h
   38   100   992   103 UtilityTimer.h
  294   846  7587   101 CmdLineFileExtractor.cpp
  256   761  7126   101 CommandLineParser.cpp
   14    14   231    56 Executionctrlvalues.cpp
  104   224  2586    98 FileProcessor.cpp
   44   100  1071    62 FileStatistics.cpp
   26    61   827    67 ProgramOptions.cpp
  196   384  3992    87 ReportWriter.cpp
  100   207  2448    93 StatisticsCollector.cpp
   67   137  1628    82 main.cpp
 1471  3888 38379   103 total

The output from the commandline wcfast.exe -tLlcw *.h *.cpp which uses this implementation is:

finished command line parsing at Tue Nov 15 13:13:40 2022
elapsed time in seconds: 0.0001482


Lines   Words   Bytes   Length of   
of Text         Longest Line    
294     846     7587    105     CmdLineFileExtractor.cpp
30      152     1130    89      CmdLineFileExtractor.h
256     761     7126    104     CommandLineParser.cpp
46      143     1456    77      CommandLineParser.h
14       14      231    57      Executionctrlvalues.cpp
26       86      778    76      Executionctrlvalues.h
104     224     2586    99      FileProcessor.cpp
28       60      711    95      FileProcessor.h
44      100     1071    64      FileStatistics.cpp
63      275     2515    88      FileStatistics.h
67      137     1628    88      main.cpp
26       61      827    69      ProgramOptions.cpp
53      129     1120    73      ProgramOptions.h
196     384     3992    91      ReportWriter.cpp
35      115     1100    81      ReportWriter.h
27       50      470    69      SpecialExceptions.h
100     207     2448    95      StatisticsCollector.cpp
24       44      611    74      StatisticsCollector.h
38      100      992    107     UtilityTimer.h
Lines   Words   Bytes   Length of   
of Text                 Longest Line    
1471    3888    38379   107     

finished  processing and reporting input files Tue Nov 15 13:13:40 2022
elapsed time in seconds: 0.0030075

Note that this implementation provides column headings and Cygwin wc doesn't. The order of the files is different, Cygwin wc processes by file extention while this implementation returns the output in a form similar to ls or dir.

Deep Search:

On Windows 10, Cygwin wc can’t do this.

In the high-level directory where all my non-work projects are (each project is a sub directory of this directory and contains many sub directories) the command line:

wcfast.exe  -RtLwcml *.h *.c *.cpp *.cs

found 565 source code files with 50,146 lines of code and comments, 129,418 words, 1,836,720 bytes with the longest line length being 374 (this last number is shameful). This is the non-work code I’ve written in the last 7 years.

It took 1.03 second to parse the command line and find all the files, it took 0.094 seconds to process the 565 files.

I tried testing from the top level of the boost library on my computer and the program aborted during the search for *.hpp and *.cpp. The search did work in the boost/libs directory. I attempted to correct this from changing from a recursive search for sub directories to an iterative search for directories. While the change improved performance and delayed the abort, the program still aborted which makes me think there is a memory issue involved.

Questions:

  • Are there any memory leaks? Where?
  • Are there any C++ or STL functions I could have used to decrease the amount of code I wrote?
  • Are there any C++20 features I should have used that I didn’t?
  • How can I improve performance?
  • Can I improve the method declarations in the header files?
  • Level of complexity, is any of the code too complex?
  • Modularity, is there too much coupling between classes?
  • Should any of the classes be combined?
  • Are there too many or too few comments?
  • Is the code self documenting?
  • What do you think of the class and method names?

Future

I do plan on expanding the functionality to include lines of code versus lines of comments. I may also add cyclic complexity to the analysis.

Code:

The code on GitHub as posted in the review..

The updated code on GitHub based on the reviews here. This is a work in progress.

Note, I am no longer the only contributor to this project, 2 of the people that posted reviews are now contributing to the project, Toby Speight and Edward, thank you for your contributions. Obviously I won't be posting a followup review because I am no longer the sole author of the code.

If you want to limit your review to C++20 or the most complex code, focus on CommandLineParser.cpp and CommandLineParser.h.

CommandLineParser.cpp

#include <algorithm>
#include <cstring>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>
#include "CommandLineParser.h"
#include "CmdLineFileExtractor.h"
#include "Executionctrlvalues.h"
#include "UtilityTimer.h"

#ifdef _WIN32
static const size_t MinimumCommandLineCount = 1;
#else
// On Linux and Unix argv[0] is the program name so a minimum of 2 arguments
static const size_t MinimumCommandLineCount = 2;
#endif

CommandLineParser::CommandLineParser(int argc, char* argv[],
    std::string progVersion)
    : argCount{ argc }, args{ argv }, useDefaultFlags{ true }
{
    version = progVersion;
    initHelpMessage();
    initDashMaps();
}

void CommandLineParser::findAllFilesToProcess(ExecutionCtrlValues& execVars)
{
    bool searchSubDirs = options.recurseSubDirectories;
    CmdLineFileExtractor fileExtractor(NotFlagsArgs, searchSubDirs);
    fileExtractor.findAllRequiredFiles();
    execVars.filesToProcess = fileExtractor.getFileList();
    execVars.fileSpecTypes = fileExtractor.getFileTypeList();
}

unsigned int CommandLineParser::extractAllArguments()
{
    unsigned int flagCount = 0;

    for (size_t i = 0; i < argCount; i++)
    {
        if (args[i][0] == '-')
        {
            if (args[i][1] == '-')
            {
                processDoubleDashOptions(args[i]);
                flagCount++;
            }
            else
            {
                processSingleDashOptions(args[i]);
                flagCount++;
            }
        }
        else
        {
            NotFlagsArgs.push_back(args[i]);
        }
    }

    return flagCount;
}

bool CommandLineParser::parse(ExecutionCtrlValues& execVars)
{
    UtilityTimer stopWatch;
    // There is no way to determine if -t has been used at this point
    // so start the timer anyway.
    stopWatch.startTimer();

    bool hasFiles = false;

    if (argCount < MinimumCommandLineCount)
    {
        ShowHelpMessage doHelp("Call printHelpMessage");
        throw doHelp;
    }

    unsigned int flagCount = extractAllArguments();
    if (useDefaultFlags)
    {
        SetDefaultOptionsWhenNoFlags();
    }

    findAllFilesToProcess(execVars);
    execVars.options = options;

    if (options.enableExecutionTime)
    {
        stopWatch.stopTimerAndReport("command line parsing at ");
    }

    return hasFiles = execVars.filesToProcess.size() != 0;
}

void CommandLineParser::printHelpMessage()
{
    std::cerr << "\n" << messageProgramName();
    for (auto line : helpMessage)
    {
        std::cerr << line;
    }
    // flush the buffer to make sure the entire message is visible
    std::cerr << std::endl;
}

void CommandLineParser::printVersion()
{
        std::cout << messageProgramName() << ": version: " << version << "\n";
        std::cout << "Packaged by Chernick Consulting\n";
        std::cout << "License GPLv3+: GNU GPL version 3 or later"
            " <http://gnu.org/licenses/gpl.html>.\n";
        std::cout << "This is free software : you are free to change and redistribute it.\n";
        std::cout << "\tThere is NO WARRANTY, to the extent permitted by law.\n";
        std::cout << "\nWritten by Paul A. Chernick\n";
}

/*
 * Flags starting with -- are full strings that need to be processed
 * as strings.
 */
void CommandLineParser::processDoubleDashOptions(char* currentArg)
{
    auto flag = doubleDashArgs.find(currentArg);
    if (flag != doubleDashArgs.end())
    {
        (*flag).second = true;
        useDefaultFlags = false;
        return;
    }

    // The following switches require alternate handling
    if (strncmp(currentArg, "--subdirectories", strlen("--subdirectories")) == 0)
    {
        // Since this is not a column switch it does not affect the default 
        options.recurseSubDirectories = true;
        return;
    }

    if (strncmp(currentArg, "--time-execution", strlen("--time-execution")) == 0)
    {
        // Since this is not a column switch it does not affect the default 
        options.enableExecutionTime = true;
        return;
    }

    if (strncmp(currentArg, "--help", strlen("--help")) == 0)
    {
        ShowHelpMessage doHelp("Call printHelpMessage");
        throw doHelp;
    }

    if (strncmp(currentArg, "--version", strlen("--version")) == 0)
    {
        showVersions sv("Call printVersion");
        throw sv;
    }

    std::cerr << "Unknown flag: " << currentArg << "\n";
}

/*
 * Each character needs to be processed independently.
 */
void CommandLineParser::processSingleDashOptions(char* currentArg)
{
    for (size_t i = 1; i < std::strlen(currentArg); i++)
    {
        auto thisOption = singleDashArgs.find(currentArg[i]);
        if (thisOption != singleDashArgs.end())
        {
            (*thisOption).second = true;
            useDefaultFlags = false;
        }
        else
        {
            switch (currentArg[i])
            {
            case 'R':
                // Since this is not a column switch it does not affect the
                // default  
                options.recurseSubDirectories = true;
                continue;
            case 't':
                // Since this is not a column switch it does not affect the
                // default  
                options.enableExecutionTime = true;
                continue;
            default:
                std::cerr << "Unknown flag: " << currentArg[i] << "\n";
                continue;
            }
        }
    }
}

void CommandLineParser::SetDefaultOptionsWhenNoFlags()
{
    // Based on the default functionality of the wc program.
    options.byteCount = true;
    options.wordCount = true;
    options.lineCount = true;
}

void CommandLineParser::initDashMaps()
{
    doubleDashArgs.insert({ "--bytes", options.byteCount });
    doubleDashArgs.insert({ "--chars", options.charCount });
    doubleDashArgs.insert({ "--lines", options.lineCount });
    doubleDashArgs.insert({ "--max-line-length", options.maxLineWidth });
    doubleDashArgs.insert({ "--words", options.wordCount });

    singleDashArgs.insert({ 'c', options.byteCount });
    singleDashArgs.insert({ 'm', options.charCount });
    singleDashArgs.insert({ 'l', options.lineCount });
    singleDashArgs.insert({ 'L', options.maxLineWidth });
    singleDashArgs.insert({ 'w', options.wordCount });
}

void CommandLineParser::initHelpMessage()
{
    std::string veryLongLine;
    helpMessage.push_back(" file name or file type specification (*.ext)\n");
    helpMessage.push_back("Otions:\n");
    helpMessage.push_back("\t-c, --bytes print the byte counts\n");
    helpMessage.push_back("\t-m, --chars print the character counts\n");
    helpMessage.push_back("\t-l, --lines print the newline counts\n");
    helpMessage.push_back(
        "\t-t, --time-execution print the execution time of the program\n");
    helpMessage.push_back(
        "\t-L, --max-line-length print the length of the longest line\n");
    helpMessage.push_back("\t-w, --words print the word counts\n");
    helpMessage.push_back("\t--help display this help and exit\n");
    helpMessage.push_back("\t--version output version information and exit\n");
    veryLongLine = "\t-R, --subdirectories all files in the"
        " directory as well as sub directories\n";
    helpMessage.push_back(veryLongLine);
    veryLongLine = "\tBy default the -c -l and -w flags are set, setting any"
        " flag requires all flags you want to be set.\n";
    helpMessage.push_back(veryLongLine);
}

std::string CommandLineParser::messageProgramName()
{
    std::string programName =
#ifdef _WIN32
        "wconsteriods"
#else
        // On Linux and Unix argv[0] is the program name;
        (argCount != 0) ? args[0] : "wconsteriods"
#endif
        ;

    return programName;
}

CommandLineParser.h

#ifndef COMMAND_lINE_PARSER_H
#define COMMAND_lINE_PARSER_H

/*
 * Generic class to parse command lines. The public interface should not
 * require modifications for different programs. The help message will
 * need to change on a per program basis. The help message definition may
 * need to move to the ProgramOptions class.
 * 
 * This class should be portable to Windows, Linux and Unix operating
 * systems. There are ifdefs in the implementation for this purpose.
 */
#include <unordered_map>
#include "Executionctrlvalues.h"
#include "SpecialExceptions.h"

class CommandLineParser
{
public:
    CommandLineParser(int argc, char* argv[], std::string progVersion);
    bool parse(ExecutionCtrlValues& execVars);
    void printHelpMessage();
    void printVersion();

protected:
    void processSingleDashOptions(char *currentArg);
    void processDoubleDashOptions(char* currentArg);
    void SetDefaultOptionsWhenNoFlags();
    void initDashMaps();
    void initHelpMessage();
    void findAllFilesToProcess(ExecutionCtrlValues& execVars);
    unsigned int extractAllArguments();
    std::string messageProgramName();

private:
    char** args;
    int argCount;
    std::string version;
    ProgramOptions options;
    std::unordered_map<std::string, bool&> doubleDashArgs;
    std::unordered_map<char, bool&> singleDashArgs;
    std::vector<std::string> helpMessage;
    std::vector<std::string> NotFlagsArgs;
    bool useDefaultFlags;
};

#endif // COMMAND_lINE_PARSER_H

Executionctrlvalues.cpp

#include <vector>
#include <string>
#include "Executionctrlvalues.h"

ExecutionCtrlValues::ExecutionCtrlValues()
{
}

void ExecutionCtrlValues::initFromEnvironmentVariables()
{
    options.initFromEnvironmentVars();
}

Executionctrlvalues.h

#ifndef EXECUTION_CONTROL_VARIABLES_H
#define EXECUTION_CONTROL_VARIABLES_H

/*
 * Storage for environment and commandline argument variables, also stores
 * list of files to process. This class contains all the information to
 * execute the program after the command line has been processed.
 * 
 * This class is fairly generic and can be reused by multiple programs that
 * process command line arguments.
 */
#include <vector>
#include <string>
#include "ProgramOptions.h"

class ExecutionCtrlValues
{
public:
    ExecutionCtrlValues();
    ~ExecutionCtrlValues() = default;
    void initFromEnvironmentVariables();
    ProgramOptions options;
    std::vector<std::string> fileSpecTypes;
    std::vector<std::string> filesToProcess;
};

#endif // EXECUTION_CONTROL_VARIABLES_H

CmdLineFileExtractor.cpp

#include <algorithm>
#include <iostream>
#include <filesystem>
#include <string>
#include <vector>
#include "CmdLineFileExtractor.h"

namespace fsys = std::filesystem;

/*
 * The code here expands wild card file specifications such as "*.cpp" and
 * "*.h" into lists of file names to process. If the -R or --subdirectories
 * command line flag is true then it searches through the current file
 * hierarchy for matching files as well.
 *
 * Originally the file search was implemented as a recursive algorithm, this
 * crashed on Windows 10 when searching for *.hpp in the boost library as a 
 * test case. The code has been rewritten as an iterative solution, first
 * find all the sub directories in the current directory and add them to the
 * sub directory list. Then for each sub directory in the sub directory list
 * search for more sub directories. When all the sub directories have been
 * searched for sub directories find the applicable files in each sub
 * directory.
 * 
 * The booleans in this class indicate the phase of processing completed.
 * The discovered value indicates whether the sub directory has been searched
 *     for more sub directories.
 * The searchedFiles flag indicates if the files in the directory have been
 *     added to the fileList vector.
 */
class SubDirNode
{
public:
    fsys::path fileSpec;
    bool discovered;
    bool searchedFiles;
    SubDirNode(fsys::path path)
        : discovered{ false }, searchedFiles{ false }
    {
        fileSpec = path;
    }
    bool operator==(const SubDirNode& other)
    {
        return fileSpec == other.fileSpec;
    }
    ~SubDirNode() = default;
};

/*
* Internal variables and functions so that #include <fileSystem> does
* not occur every where. It is only needed in this implementation file.
* Normally I put the public interface functions first, but the static
* code needs to be defined before that.
*/
static bool SearchSubDirs;
static std::vector<std::string> fileList;
static std::vector<std::string> fileExtentions;
static std::vector<std::string> nonFlagArgs;
static std::vector<SubDirNode> subDirectories;

/*
 * Search the current directory for sub directories.
 */
static std::vector<SubDirNode> findSubDirs(SubDirNode currentDir)
{
    bool hasSubDirs = false;
    std::vector<SubDirNode> newSubDirs;

    fsys::path cwd = currentDir.fileSpec;

    for (auto it = fsys::directory_iterator(cwd);
        it != fsys::directory_iterator(); ++it)
    {
        if (it->is_directory())
        {
            SubDirNode branch(it->path());
            auto found = std::find(subDirectories.begin(), subDirectories.end(), branch);
            if (found == subDirectories.end())
            {
                // Possible nasty side affects here by adding additional
                // contents to subDirectories
                newSubDirs.push_back(branch);
            }
        }
    }

    return newSubDirs;
}

/*
 * A recursive algorithm can apparently cause a stack overflow on Windows 10 
 * so this is an iterative solution.
 */
static bool discoverSubDirs()
{
    bool discoveredPhaseCompleted = true;
    std::vector<SubDirNode> newSubDirs;

    for (size_t i = 0; i < subDirectories.size(); i++)
    {
        if (!subDirectories[i].discovered)
        {
            std::vector<SubDirNode> tempNewDirs = findSubDirs(subDirectories[i]);
            if (tempNewDirs.size())
            {
                discoveredPhaseCompleted = false;
                newSubDirs.insert(newSubDirs.end(), tempNewDirs.begin(),
                    tempNewDirs.end());
            }
            subDirectories[i].discovered = true;
        }
    }

    // We are done searching the current level, append the new sub directories
    // to the old.
    subDirectories.insert(subDirectories.end(), newSubDirs.begin(), newSubDirs.end());
    return discoveredPhaseCompleted;
}

static void discoverAllSubDirs()
{
    bool discoveryPhaseCompleted = false;
    while (!discoveryPhaseCompleted)
    {
        discoveryPhaseCompleted = discoverSubDirs();
    }
}


static bool containsWildCard(std::string fileSpec)
{
    return fileSpec.find('*') != std::string::npos;
}

static std::string getFileExtention(std::string fname)
{
    std::string fileExtention = "";

    size_t lastDotLocation = fname.find_last_of('.');
    if (lastDotLocation != std::string::npos)
    {
        fileExtention = fname.substr(lastDotLocation);
    }

    return fileExtention;
}

static bool isASpecifiedFileType(std::string notAFlag) noexcept
{
    // Exclude file type specifications since they are not actual file names
    if (containsWildCard(notAFlag))
    {
        return false;
    }

    std::string fileExtension = getFileExtention(notAFlag);
    if (fileExtension.empty())
    {
        return false;
    }

    return std::find(fileExtentions.begin(),
        fileExtentions.end(), fileExtension) != fileExtentions.end();
}

static void searchDirectoryForFiles(SubDirNode currentDir)
{

    for (auto it = fsys::directory_iterator(currentDir.fileSpec);
        it != fsys::directory_iterator(); ++it)
    {
        if (it->is_regular_file() || it->is_character_file())
        {
            std::string fileName{ it->path().generic_string() };
            if (isASpecifiedFileType(fileName))
            {
                fileList.push_back(fileName);
            }
        }
    }
}

static void searchAllDirectoriesForFiles()
{
    for (auto currentDir : subDirectories)
    {
        if (currentDir.discovered && !currentDir.searchedFiles)
        {
            searchDirectoryForFiles(currentDir);
            currentDir.searchedFiles = true;
        }
    }
}

static std::vector<std::string> findAllFileTypeSpecs()
{
    std::vector<std::string> fileSpecTypes;
    // Get all the file specifications and store them in the
    // vector of file specifications. Remove the file specification
    // so that it isn't added to the list of files.
    for (auto notAFlag : nonFlagArgs)
    {
        if (containsWildCard(notAFlag))
        {
            fileSpecTypes.push_back(notAFlag);
        }
    }

    return fileSpecTypes;
}

static std::vector<std::string> getFileTypes()
{
    std::vector<std::string> fileTypes;
    std::vector<std::string> fileSpecTypes = findAllFileTypeSpecs();

    for (auto fileTypeSpec : fileSpecTypes)
    {
        std::string fileExtention = getFileExtention(fileTypeSpec);
        if (!fileExtention.empty())
        {
            fileTypes.push_back(fileExtention);
        }
    }

    return fileTypes;
}

static void addListedFilesToFileList()
{
    for (auto fileSpec : nonFlagArgs)
    {
        if (isASpecifiedFileType(fileSpec))
        {
            fileList.push_back(fileSpec);
        }
    }
}

static void findAllInputFiles()
{
    // if there is nothing to search for quit.
    if (!SearchSubDirs && fileExtentions.size() == 0)
    {
        return;
    }

    // Start in the current working directory  (cwd for non Linux users).
    std::filesystem::path cwd = fsys::current_path();
    if (subDirectories.empty())
    {
        SubDirNode root(cwd);
        root.discovered = (SearchSubDirs) ? false: true;
        subDirectories.push_back(root);
    }

    if (SearchSubDirs)
    {
        discoverAllSubDirs();
    }
    searchAllDirectoriesForFiles();
}

/*
 * Begin public interfaces
 */

CmdLineFileExtractor::CmdLineFileExtractor(
    std::vector<std::string> NonFlagArgs,
    bool searchSubDirs)
{
    // copy is ok, we are not worried about performance here.
    // This class should be instantiated only once per program
    // run. It is only needed by the command line parser.
    nonFlagArgs = NonFlagArgs;
    SearchSubDirs = searchSubDirs;
}

void CmdLineFileExtractor::findAllRequiredFiles() noexcept
{
    fileExtentions = getFileTypes();
    addListedFilesToFileList();
    findAllInputFiles();
}

std::vector<std::string> CmdLineFileExtractor::getFileList() const noexcept
{
    return fileList;
}

std::vector<std::string> CmdLineFileExtractor::getFileTypeList() const noexcept
{
    return fileExtentions;
}

CmdLineFileExtractor.h

#ifndef COMMAND_LINE_FILE_EXTRACTOR_H
#define COMMAND_LINE_FILE_EXTRACTOR_H

/*
 * This class has one purpose, to build a list of files to process for
 * the calling program. It expands wild card strings. It searches the
 * current working directory for files of the specified types. If the
 * search subdirs bool is set it will also search the sub directories
 * of the current working directory. The list of files to process will
 * be full file specifications so that all the proper files are read
 * only once.
 * 
 * What would be protected or private functions and variables are
 * implemented as static functions and variables within the
 * implementation .cpp file to limit the access to the file system to
 * only this class/module.
 */

#include <string>
#include <vector>

class CmdLineFileExtractor
{
public:
    CmdLineFileExtractor(std::vector<std::string> NonFlagArgs, bool searchSubDirs);
    void findAllRequiredFiles() noexcept;
    std::vector<std::string> getFileList() const noexcept;
    std::vector<std::string> getFileTypeList() const noexcept;
};

#endif // COMMAND_LINE_FILE_EXTRACTOR_H

ProgramOptions.cpp

#include <iostream>
#include "ProgramOptions.h"

#ifdef _DEBUG
void ProgramOptions::singleLine(std::string flag, bool flagValue)
{
    std::cout << "Flag " << flag << ": Flag value " <<
        ((flagValue) ? "True" : "False") << "\n";
}

void ProgramOptions::debugPrint()
{
    singleLine("blankLineCount", blankLineCount);
    singleLine("byteCount", byteCount);
    singleLine("charCount", charCount);
    singleLine("codeCount", codeCount);
    singleLine("commentCount", commentCount);
    singleLine("lineCount", lineCount);
    singleLine("maxLineWidth", maxLineWidth);
    singleLine("percentages", percentages);
    singleLine("whitespaceCount", whitespaceCount);
    singleLine("wordCount", wordCount);
    singleLine("recurseSubDirectories", recurseSubDirectories);
    singleLine("enableExecutionTime", enableExecutionTime);
}
#endif

ProgramOptions.h

#ifndef PROGRAM_OPTIONS_STRUCT_H
#define PROGRAM_OPTIONS_STRUCT_H

/*
 * This class is not generic except for the public functions. It is used
 * to contain the the values of the switches/flags on the command line.
 */
class ProgramOptions {
public:
    // Output control variables
    bool blankLineCount;
    bool byteCount;
    bool charCount;
    bool codeCount;
    bool commentCount;
    bool lineCount;
    bool maxLineWidth;
    bool percentages;
    bool whitespaceCount;
    bool wordCount;
    bool enableExecutionTime;
    // input control variables
    bool recurseSubDirectories;

    ProgramOptions()
        : blankLineCount{ false },
        byteCount{ false },
        charCount{ false },
        codeCount{ false },
        commentCount{ false },
        lineCount{ false },
        maxLineWidth{ false },
        percentages{ false },
        whitespaceCount{ false },
        wordCount{ false },
        enableExecutionTime{ false },
        recurseSubDirectories{ false }
    {

    }
    void initFromEnvironmentVars()
    {

    }

#ifdef _DEBUG
    void singleLine(std::string flag, bool flagValue);
    void debugPrint();
#endif // _DEBUG
};

#endif // PROGRAM_oPTIONS_STRUCT_H

SpecialExceptions.h

#ifndef SPECIAL_EXCEPTIONS_H
#define SPECIAL_EXCEPTIONS_H

/*
 * Implements special exceptions for the --help and --version flags.
 */

#include <stdexcept>

class ShowHelpMessage: public std::runtime_error
{
public:
    ShowHelpMessage(const char* msg) : std::runtime_error(msg)
    {
    }
};

class showVersions : public std::runtime_error
{
public:
    showVersions(const char* msg) : std::runtime_error(msg)
    {

    }

};

#endif // SPECIAL_EXCEPTIONS_H

FileProcessor.cpp

#include <iostream>
#include <fstream>
#include <sstream>
#include "StatisticsCollector.h"
#include "FileProcessor.h"
#include "FileStatistics.h"
#include "ReportWriter.h"

static constexpr size_t InputBufferSize = 8 * 1024;

FileProcessor::FileProcessor(std::vector<std::string>& filesToProess, ProgramOptions& progOptions)
    : options{ progOptions }
{
    fileNames = filesToProess;
}

std::string FileProcessor::processAllFiles() noexcept
{
    ReportWriter TotalsReporter(options);
    FileStatistics allFiles;

    std::string resultsToDisplay(TotalsReporter.getColumnHeadingAsOneString());

    for (auto currentFile : fileNames)
    {
        try
        {
            std::string fileResults = processFile(currentFile, allFiles);
            if (!fileResults.empty())
            {
                resultsToDisplay += fileResults;
            }
        }
        catch (std::runtime_error re)
        {
            std::cerr << re.what() << "\n";
        }
    }

    resultsToDisplay += TotalsReporter.getColumnHeadingAsOneString();
    resultsToDisplay += TotalsReporter.getResultText(allFiles);

    return resultsToDisplay;
}

void FileProcessor::processLoop(std::ifstream& inStream,
    FileStatistics& statistics) noexcept
{
    StatisticsCollector fileAnalyzer(statistics);

    std::stringstream inputBuffer;
    std::streambuf* inBuf = inStream.rdbuf();
    inputBuffer << inStream.rdbuf();
    fileAnalyzer.analyzeBuffer(inputBuffer.str());
}

/*
 * Processing a file includes reading the file, analyzing the input to collect
 * the statistics and then pringing the statistics.
 */
std::string FileProcessor::processFile(std::string fileName,
    FileStatistics& totalStats)
{
    std::string fileResults;

    try {
        if (fileName.empty())
        {
            std::string eMsg(
                "Programmer Error: File name is empty in "
                "FileProcessor Constructor!");
            std::runtime_error programmerError(eMsg);
            throw programmerError;
        }

        std::ifstream inStream(fileName);
        if (!inStream.is_open())
        {
            std::string eMsg("Runtime error:  Can't open file " + fileName +
                " for input.");
            std::runtime_error FileInputError(eMsg);
            throw FileInputError;
        }

        FileStatistics statistics(fileName);
        processLoop(inStream, statistics);

        inStream.close();

        ReportWriter ReportFileStatistics(options);
        fileResults = ReportFileStatistics.getResultText(statistics);
        statistics.addTotals(totalStats);
    }

    catch (std::exception ex)
    {
        std::cerr <<
            "Error: unable to complete processing file statistics for "
            << fileName << " Error: " << ex.what() << std::endl;
        fileResults.clear();
    }

    return fileResults;
}

FileProcessor.h

#ifndef FILE_PROCESSOR_H
#define FILE_PROCESSOR_H

#include <string>
#include <vector>
#include <fstream>

#include "FileStatistics.h"
#include "ProgramOptions.h"

class FileProcessor
{
public:
    FileProcessor(std::vector<std::string>& filesToProcess, ProgramOptions& progOptions);
    ~FileProcessor() = default;
    std::string processAllFiles() noexcept;

protected:
    void processLoop(std::ifstream& inStream, FileStatistics& statistics) noexcept;
    std::string processFile(std::string fileName, FileStatistics& totalStats);

private:
    std::vector<std::string> fileNames;
    // The program options are necessary to know what to outout.
    ProgramOptions& options;
};

#endif // FILE_PROCESSOR_H

FileStatistics.cpp

#include "FileStatistics.h"
#include <string>

FileStatistics::FileStatistics()
    : totalLineCount{ 0 },
    codeLineCount{ 0 },
    commentLineCount{ 0 },
    whiteSpaceCount{ 0 },
    wordCount{ 0 },
    characterCount{ 0 },
    codeWithCommentCount{ 0 },
    blankLineCount{ 0 },
    widestLine{ 0 }
{

}

FileStatistics::FileStatistics(std::string inFileName)
    : totalLineCount{ 0 },
    codeLineCount{ 0 },
    commentLineCount{ 0 },
    whiteSpaceCount{ 0 },
    wordCount{ 0 },
    characterCount{ 0 },
    codeWithCommentCount{ 0 },
    blankLineCount{ 0 },
    widestLine{ 0 },
    fileName{ inFileName }
{

}

void FileStatistics::addTotals(FileStatistics &allFiles)
{
    allFiles.totalLineCount += totalLineCount;
    allFiles.codeLineCount += codeLineCount;
    allFiles.commentLineCount += commentLineCount;
    allFiles.whiteSpaceCount += whiteSpaceCount;
    allFiles.wordCount += wordCount;
    allFiles.characterCount += characterCount;
    allFiles.codeWithCommentCount += codeWithCommentCount;
    allFiles.blankLineCount += blankLineCount;
    allFiles.updateWidestLine(widestLine);
}

FileStatistics.h

#ifndef    FILE_STATISTICS_H
#define FILE_STATISTICS_H

/*
 * Class to collect all the statistics about the code in a file.
 * 
 */
#include <string>

class FileStatistics
{
private:
    size_t totalLineCount;
    size_t codeLineCount;
    size_t commentLineCount;
    size_t whiteSpaceCount;
    size_t characterCount;
    size_t wordCount;
    size_t codeWithCommentCount;
    size_t widestLine;
    size_t blankLineCount;
    std::string fileName; 

public:
    FileStatistics();
    FileStatistics(std::string inFileName);
    void setFileName(std::string inFileName) { fileName = inFileName; }
    std::string getFileName() { return fileName; }
    void addTotals(FileStatistics &allFiles);
    void addToLineCount(size_t lineCount) { totalLineCount += lineCount; }
    void setToLineCount(size_t lineCount) { totalLineCount = lineCount; }
    void addToCharCount(size_t charCount) { characterCount += charCount; }
    void setCharCount(size_t charCount) { characterCount = charCount; }
    void addToWordCount(size_t wordCountUpdate) { wordCount += wordCountUpdate; }
    void setWordCount(size_t wordCountUpdate) { wordCount = wordCountUpdate; }
    void addToWhitespace(size_t wsUpdate) { whiteSpaceCount += wsUpdate; }
    // While the inline key word is only a recommendation, hopefully the increment
    // functions can be inline.
    inline void incrementTotalLines() { totalLineCount++; }
    size_t getTotalLines() const { return totalLineCount; }
    inline void incrementCodeLines() { codeLineCount++; }
    size_t getCodeLines() const { return codeLineCount; }
    inline void incrementCommentsLines() { commentLineCount++; }
    size_t getCommentLines() const { return commentLineCount; }
    inline void incrementWhitespace() { whiteSpaceCount++; }
    size_t getWhitespace() const { return whiteSpaceCount; }
    inline void incrementCharacter() { characterCount++; }
    size_t getCharacters() const { return characterCount; }
    inline void incrementWords() { wordCount++; }
    size_t getWords() const { return wordCount; }
    inline void incrementCodeWithComment() { codeWithCommentCount++; }
    size_t getCodeWithComment() const { return codeWithCommentCount; }
    inline void incrementBlankLines() { blankLineCount++; }
    size_t getBlankLines() { return blankLineCount; }
    void updateWidestLine(size_t width) {
        widestLine = (width > widestLine)? width : widestLine;
    }
    size_t getWidestLine() { return widestLine; }
    float getPerecentageOfCode() {
        return static_cast<float>(codeLineCount / totalLineCount);
    }
};

#endif // FILE_STATISTICS_H

StatisticsCollector.cpp

#include <algorithm>
#include <cctype>
#include <filesystem>
#include <iostream>
#include <iterator>
#include <vector>
#include <string>
#include "StatisticsCollector.h"
#include "FileStatistics.h"

static constexpr size_t tabSize = 8;

StatisticsCollector::StatisticsCollector(FileStatistics& fileStats)
    : fileStatistics{ fileStats }
{
}

void StatisticsCollector::analyzeBuffer(std::string inputBuffer) noexcept
{
    std::string::iterator endBuffer = inputBuffer.end();
    std::string::iterator startBuffer = inputBuffer.begin();

    std::uintmax_t bufferSize = std::filesystem::file_size(fileStatistics.getFileName());
    fileStatistics.setCharCount(bufferSize);

    size_t lineCount = std::count(startBuffer, endBuffer, '\n');
    fileStatistics.setToLineCount(lineCount);
    countWordsAndWhiteSpace(inputBuffer);

    std::string::iterator currentChar = startBuffer;
    while (currentChar != endBuffer)
    {
        updateWidestLine(currentChar, endBuffer);
    }
}

void StatisticsCollector::countWordsAndWhiteSpace(std::string& inputBuffer) noexcept
{
    size_t wordCount = 0;
    size_t whiteSpaceCount = 0;
    std::string::iterator currentChar = inputBuffer.begin();
    std::string::iterator endOfInput = inputBuffer.end();
    bool inWord = false;

    for ( ; currentChar != endOfInput; )
    {
        while (isspace(*currentChar))
        {
            whiteSpaceCount++;
            currentChar++;
            if (currentChar == endOfInput)
            {
                break;
            }
        }

        while (!(currentChar == endOfInput) && !isspace(*currentChar))
        {
            inWord = true;
            currentChar++;
            if (currentChar == endOfInput)
            {
                wordCount++;
                inWord = false;
                break;
            }
        }

        if (inWord)
        {
            wordCount++;
            inWord = false;
        }
    }

    fileStatistics.addToWhitespace(whiteSpaceCount);
    fileStatistics.setWordCount(wordCount);
}

void StatisticsCollector::updateWidestLine(std::string::iterator& currentChar,
    std::string::iterator end) noexcept
{
    std::string::iterator endOfLine = std::find(currentChar, end, '\n');
    if (endOfLine != end)
    {
        endOfLine++;
    }
    size_t lineWidth = endOfLine - currentChar;

    // See https://github.com/coreutils/coreutils/blob/master/src/wc.c to
    // observe how tabs are counted.
    size_t tabCount = std::count(currentChar, endOfLine, '\t');
    lineWidth += tabCount * tabSize;


    fileStatistics.updateWidestLine(lineWidth);

    std::string line(currentChar, endOfLine);
    currentChar = endOfLine;
}

StatisticsCollector.h

#ifndef STATISTICS_COLLECTOR_H
#define STATISTICS_COLLECTOR_H

#include <iterator>
#include <string>
#include <vector>
#include "FileStatistics.h"

class StatisticsCollector
{
public:
    StatisticsCollector(FileStatistics& fileStats);
    void analyzeBuffer(std::string inputbuffer) noexcept;

protected:
    void updateWidestLine(std::string::iterator& currentChar,
        std::string::iterator end) noexcept;
    void lineWidth(std::string line) noexcept;
    void countWordsAndWhiteSpace(std::string& inputBuffer) noexcept;

private:
    FileStatistics& fileStatistics;
};

#endif // STATISTICS_COLLECTOR_H

ReportWriter.cpp

#include <iostream>
#include <string>
#include <vector>
#include "ReportWriter.h"

void ReportWriter::printResult(FileStatistics& resultsForOutput) noexcept
{
    std::cout << getResultText(resultsForOutput) << "\n";
}

/*
 * Maintain the order between this function and getColumneHeadingsText().  
 */
std::string ReportWriter::getResultText(FileStatistics& resultsForOutput) noexcept
{
    std::string outString;

    if (options->lineCount)
    {
        outString += std::to_string(resultsForOutput.getTotalLines()) + "\t";
    }

    if (options->wordCount)
    {
        outString += std::to_string(resultsForOutput.getWords()) + "\t";
    }

    if (options->byteCount)
    {
        outString += std::to_string(resultsForOutput.getCharacters()) + "\t";
    }

    if (options->charCount)
    {
        outString += std::to_string(resultsForOutput.getCharacters()) + "\t\t";
    }

    if (options->maxLineWidth)
    {
        outString += std::to_string(resultsForOutput.getWidestLine()) + "\t";
    }

    // End of backwards compatability with wc utility.

    if (options->codeCount)
    {
        outString += std::to_string(resultsForOutput.getCodeLines()) + "\t";
    }

    if (options->commentCount)
    {
        outString += std::to_string(resultsForOutput.getCommentLines()) + "\t";
    }

    if (options->percentages)
    {
        outString +=
            std::to_string(resultsForOutput.getPerecentageOfCode()) + "\t";
    }

    if (options->whitespaceCount)
    {
        outString += std::to_string(resultsForOutput.getWhitespace()) + "\t";
    }
    if (options->blankLineCount)
    {
        outString += std::to_string(resultsForOutput.getBlankLines()) + "\t";
    }

    std::string fileName = correctFileSpec(resultsForOutput.getFileName());
    outString += "\t" + fileName + "\n";

    return outString;
}

/*
 * Maintain the order between this function and getResultText().
 */
std::vector<std::string> ReportWriter::getColumneHeadingsText() noexcept
{
    std::string firstLine;
    std::string secondline;

    if (options->lineCount)
    {
        firstLine += "Lines\t";
        secondline += "of Text\t";
    }

    if (options->wordCount)
    {
        firstLine += "Words\t";
        secondline += "\t";
    }

    if (options->byteCount)
    {
        firstLine += "Bytes\t";
        secondline += "\t";
    }

    if (options->charCount)
    {
        firstLine += "Characters\t";
        secondline += "\t\t";
    }

    if (options->maxLineWidth)
    {
        firstLine += "Length of\t";
        secondline += "Longest Line\t";
    }

    // End of backwards compatability with wc utility.

    if (options->codeCount)
    {
        firstLine += "Lines\t";
        secondline += "of Code\t";
    }

    if (options->commentCount)
    {
        firstLine += "Lines of\t";
        secondline += "Comments\t";
    }

    if (options->percentages)
    {
        firstLine += "Percentage of\t";
        secondline += "Lines of Code\t";
    }

    if (options->whitespaceCount)
    {
        firstLine += "Whitespace\t";
        secondline += "Characters\t";
    }
    if (options->blankLineCount)
    {
        firstLine += "Blank\t";
        secondline += "Lines\t";
    }


    std::vector<std::string> headerRows = { firstLine, secondline };
    return headerRows;
}

std::string ReportWriter::getColumnHeadingAsOneString() noexcept
{
    std::string twolines;
    std::vector<std::string> headingLines = getColumneHeadingsText();
    FileStatistics allFiles;

    std::string twoLines;
    for (auto headingLine : headingLines)
    {
        twoLines += headingLine + "\n";
    }

    return twoLines;
}

void ReportWriter::printColumnHeadings() noexcept
{
    std::vector<std::string> headerRows = getColumneHeadingsText();
    for (auto line : headerRows)
    {
        std::cout << line << "\n";
    }
}

std::string ReportWriter::correctFileSpec(std::string fileSpec) noexcept
{
    if (options->recurseSubDirectories)
    {
        return fileSpec;
    }
    else
    {
        auto newStart = fileSpec.find_last_of('/');
        if (!newStart)
        {
            newStart = fileSpec.find_last_of('\\');
            if (!newStart)
            {
                return fileSpec;
            }
        }
        std::string fileName = fileSpec.substr(newStart + 1);

        
        return fileName;
    }
}

ReportWriter.h

#ifndef REPORT_WRITER_H
#define REPORT_WRITER_H

/*
 * This class prints the output about the file statistics. It can also
 * return a formated string of the output.
 */
#include <memory>
#include <string>    // std::vector included by string
#include "ProgramOptions.h"
#include "FileStatistics.h"

class ReportWriter
{
public:
    // ExecutionCrtlValues is passed in so that the report writer know what
    // output to generate.
    ReportWriter(ProgramOptions& progOptions)
    {
        options = std::make_shared<ProgramOptions>(progOptions);
    }
    void printResult(FileStatistics& resultsForOutput) noexcept;
    std::string getResultText(FileStatistics& resultsForOutput) noexcept;
    void printColumnHeadings() noexcept;
    // Returns 2 lines of properly formated text
    std::vector<std::string> getColumneHeadingsText() noexcept;
    std::string getColumnHeadingAsOneString() noexcept;

private:
    // The program options are necessary to know what to outout.
    std::shared_ptr <ProgramOptions> options;
    std::string correctFileSpec(std::string fileSpec) noexcept;
};

#endif // REPORT_WRITER_H

UtilityTimer.h

#ifndef CC_UTITLTY_TIMER_H
#define CC_UTITLTY_TIMER_H

/*
 * Chernick Consulting Utility timer class.
 * Encapsulates execution timing for all or parts of a program.
 */

#include <chrono>
#include <iostream>
#include <string>

class UtilityTimer
{
public:
    UtilityTimer() = default;
    ~UtilityTimer() = default;
    void startTimer() noexcept
    {
        start = std::chrono::system_clock::now();
    }
    void stopTimerAndReport(std::string whatIsBeingTimed) noexcept
    {
        end = std::chrono::system_clock::now();

        std::chrono::duration<double> elapsed_seconds = end - start;
        std::time_t end_time = std::chrono::system_clock::to_time_t(end);
        double ElapsedTimeForOutPut = elapsed_seconds.count();

        std::cout << "finished " << whatIsBeingTimed << std::ctime(&end_time)
            << "elapsed time in seconds: " << ElapsedTimeForOutPut << "\n" << "\n" << "\n";
    }

private:
    std::chrono::time_point<std::chrono::system_clock> start, end;
};

#endif // CC_UTITLTY_TIMER_H

main.cpp

#include <iostream>
#include <string>
#include <vector>
#include "Executionctrlvalues.h"
#include "CommandLineParser.h"
#include "FileProcessor.h"
#include "ProgramOptions.h"
#include "UtilityTimer.h"

static void mainLoop(ExecutionCtrlValues& executionCtrl)
{
    std::string resultsToDisplay;

    ProgramOptions& options = executionCtrl.options;
    std::vector<std::string> filesToProcess = executionCtrl.filesToProcess;

    FileProcessor fileProcessor(filesToProcess, options);
    resultsToDisplay = fileProcessor.processAllFiles();
    // Yes we want to flush the standard output. All possible output has been
    // collected.
    std::cout << resultsToDisplay << std::endl;
}

int main(int argc, char* argv[])
{
    int exit_status = EXIT_SUCCESS;
    ExecutionCtrlValues executionCtrl;
    std::string versionString("1.0.0");

    CommandLineParser cmdLineParser(argc, argv, versionString);

    try
    {
        executionCtrl.initFromEnvironmentVariables();
        if (cmdLineParser.parse(executionCtrl))
        {
            UtilityTimer stopWatch;
            if (executionCtrl.options.enableExecutionTime)
            {
                stopWatch.startTimer();
            }
            mainLoop(executionCtrl);
            if (executionCtrl.options.enableExecutionTime)
            {
                stopWatch.stopTimerAndReport(
                    " processing and reporting input files ");
            }
        }
    }

    catch (ShowHelpMessage sh)
    {
        cmdLineParser.printHelpMessage();
        cmdLineParser.printVersion();
    }
    catch (showVersions sv)
    {
        cmdLineParser.printVersion();
    }
    catch (std::exception ex)
    {
        std::cerr << "Error: " << ex.what() << "\n";
        exit_status = EXIT_FAILURE;
    }

    return exit_status;
}
\$\endgroup\$
12
  • \$\begingroup\$ Toby Speight has contributed a Makefile for Linux users to use to the GitHub repository. \$\endgroup\$
    – pacmaninbw
    Nov 16, 2022 at 17:27
  • 2
    \$\begingroup\$ "Cygwin wc processes by file extention" - no, it processes files in the exact order they are listed on its command line. I think that sorting the names is an anti-feature - perhaps I'll augment my review? \$\endgroup\$ Nov 16, 2022 at 17:35
  • \$\begingroup\$ @TobySpeight On Linux the shell is handling the order of the files, on Windows the shell doesn't take care of it, so putting the files in the same order would require sorting not currently being done. Add it to your review, maybe add a feature request on GitHub. \$\endgroup\$
    – pacmaninbw
    Nov 16, 2022 at 17:44
  • \$\begingroup\$ Everyone feel free to add issues or feature requests to the GitHub repository. \$\endgroup\$
    – pacmaninbw
    Nov 16, 2022 at 17:46
  • 1
    \$\begingroup\$ Although you're no longer the sole author of what's in your repo, you are still allowed to post it for review as the maintainer if we let you re-license the code under Creative Commons. But it's getting plenty of attention at the moment, so wait until it gets quieter! \$\endgroup\$ Nov 18, 2022 at 18:44

5 Answers 5

10
\$\begingroup\$

Usability

  • Sorting the files in output is surprising, given that commonly-used implementations produce output in the same order that the files were specified. Scripts likely depend on the order matching the arguments
  • Producing the output header lines should be optional, given that the POSIX spec for wc doesn't appear to allow this. That's important, given how often we see a command substitution such as $(wc -l <"$file") in shell scripts.
  • POSIX also specifies that the summary line should have (possibly-translated) total in place of the filename, rather than blank.
  • Columns of numbers are easier to read if right-aligned, like the Cygwin output you show.
  • It only seems to work with files, rather than using standard input when no filenames are specified.

Compiler warnings

Some automated review from GCC:

g++-12 -std=c++20 -Wall -Wextra -Wwrite-strings -Wno-parentheses -Wpedantic -Warray-bounds -Wconversion -Weffc++ -Wuseless-cast   -c -o CmdLineFileExtractor.o CmdLineFileExtractor.cpp
CmdLineFileExtractor.cpp: In constructor ‘SubDirNode::SubDirNode(std::filesystem::__cxx11::path)’:
CmdLineFileExtractor.cpp:37:9: warning: ‘SubDirNode::fileSpec’ should be initialized in the member initialization list [-Weffc++]
   37 |         SubDirNode(fsys::path path)
      |         ^~~~~~~~~~
CmdLineFileExtractor.cpp: In function ‘std::vector<SubDirNode> findSubDirs(SubDirNode)’:
CmdLineFileExtractor.cpp:66:14: warning: unused variable ‘hasSubDirs’ [-Wunused-variable]
   66 |         bool hasSubDirs = false;
      |              ^~~~~~~~~~
g++-12 -std=c++20 -Wall -Wextra -Wwrite-strings -Wno-parentheses -Wpedantic -Warray-bounds -Wconversion -Weffc++ -Wuseless-cast   -c -o CommandLineParser.o CommandLineParser.cpp
In file included from CommandLineParser.cpp:7:
CommandLineParser.h:17:7: warning: ‘class CommandLineParser’ has pointer data members [-Weffc++]
   17 | class CommandLineParser
      |       ^~~~~~~~~~~~~~~~~
CommandLineParser.h:17:7: warning:   but does not declare ‘CommandLineParser(const CommandLineParser&)’ [-Weffc++]
CommandLineParser.h:17:7: warning:   or ‘operator=(const CommandLineParser&)’ [-Weffc++]
CommandLineParser.h:36:16: note: pointer member ‘CommandLineParser::args’ declared here
   36 |         char** args;
      |                ^~~~
CommandLineParser.h: In constructor ‘CommandLineParser::CommandLineParser(int, char**, std::string)’:
CommandLineParser.h:37:13: warning: ‘CommandLineParser::argCount’ will be initialized after [-Wreorder]
   37 |         int argCount;
      |             ^~~~~~~~
CommandLineParser.h:36:16: warning:   ‘char** CommandLineParser::args’ [-Wreorder]
   36 |         char** args;
      |                ^~~~
CommandLineParser.cpp:19:1: warning:   when initialized here [-Wreorder]
   19 | CommandLineParser::CommandLineParser(int argc, char* argv[],
      | ^~~~~~~~~~~~~~~~~
CommandLineParser.cpp:19:1: warning: ‘CommandLineParser::version’ should be initialized in the member initialization list [-Weffc++]
CommandLineParser.cpp:19:1: warning: ‘CommandLineParser::options’ should be initialized in the member initialization list [-Weffc++]
CommandLineParser.cpp:19:1: warning: ‘CommandLineParser::doubleDashArgs’ should be initialized in the member initialization list [-Weffc++]
CommandLineParser.cpp:19:1: warning: ‘CommandLineParser::singleDashArgs’ should be initialized in the member initialization list [-Weffc++]
CommandLineParser.cpp:19:1: warning: ‘CommandLineParser::helpMessage’ should be initialized in the member initialization list [-Weffc++]
CommandLineParser.cpp:19:1: warning: ‘CommandLineParser::NotFlagsArgs’ should be initialized in the member initialization list [-Weffc++]
CommandLineParser.cpp: In member function ‘unsigned int CommandLineParser::extractAllArguments()’:
CommandLineParser.cpp:41:30: warning: comparison of integer expressions of different signedness: ‘size_t’ {aka ‘long unsigned int’} and ‘int’ [-Wsign-compare]
   41 |         for (size_t i = 0; i < argCount; i++)
      |                            ~~^~~~~~~~~~
In file included from CommandLineParser.cpp:10:
UtilityTimer.h: In constructor ‘constexpr UtilityTimer::UtilityTimer()’:
UtilityTimer.h:16:9: warning: ‘UtilityTimer::start’ should be initialized in the member initialization list [-Weffc++]
   16 |         UtilityTimer() = default;
      |         ^~~~~~~~~~~~
UtilityTimer.h:16:9: warning: ‘UtilityTimer::end’ should be initialized in the member initialization list [-Weffc++]
CommandLineParser.cpp: In member function ‘bool CommandLineParser::parse(ExecutionCtrlValues&)’:
CommandLineParser.cpp:74:22: warning: comparison of integer expressions of different signedness: ‘int’ and ‘const size_t’ {aka ‘const long unsigned int’} [-Wsign-compare]
   74 |         if (argCount < MinimumCommandLineCount)
      |             ~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~
CommandLineParser.cpp:80:22: warning: unused variable ‘flagCount’ [-Wunused-variable]
   80 |         unsigned int flagCount = extractAllArguments();
      |                      ^~~~~~~~~
g++-12 -std=c++20 -Wall -Wextra -Wwrite-strings -Wno-parentheses -Wpedantic -Warray-bounds -Wconversion -Weffc++ -Wuseless-cast   -c -o Executionctrlvalues.o Executionctrlvalues.cpp
Executionctrlvalues.cpp: In constructor ‘ExecutionCtrlValues::ExecutionCtrlValues()’:
Executionctrlvalues.cpp:5:1: warning: ‘ExecutionCtrlValues::options’ should be initialized in the member initialization list [-Weffc++]
    5 | ExecutionCtrlValues::ExecutionCtrlValues()
      | ^~~~~~~~~~~~~~~~~~~
Executionctrlvalues.cpp:5:1: warning: ‘ExecutionCtrlValues::fileSpecTypes’ should be initialized in the member initialization list [-Weffc++]
Executionctrlvalues.cpp:5:1: warning: ‘ExecutionCtrlValues::filesToProcess’ should be initialized in the member initialization list [-Weffc++]
g++-12 -std=c++20 -Wall -Wextra -Wwrite-strings -Wno-parentheses -Wpedantic -Warray-bounds -Wconversion -Weffc++ -Wuseless-cast   -c -o FileProcessor.o FileProcessor.cpp
In file included from FileProcessor.cpp:7:
ReportWriter.h: In constructor ‘ReportWriter::ReportWriter(ProgramOptions&)’:
ReportWriter.h:18:9: warning: ‘ReportWriter::options’ should be initialized in the member initialization list [-Weffc++]
   18 |         ReportWriter(ProgramOptions& progOptions)
      |         ^~~~~~~~~~~~
FileProcessor.cpp: In constructor ‘FileProcessor::FileProcessor(std::vector<std::__cxx11::basic_string<char> >&, ProgramOptions&)’:
FileProcessor.cpp:11:1: warning: ‘FileProcessor::fileNames’ should be initialized in the member initialization list [-Weffc++]
   11 | FileProcessor::FileProcessor(std::vector<std::string>& filesToProess, ProgramOptions& progOptions)
      | ^~~~~~~~~~~~~
FileProcessor.cpp: In member function ‘std::string FileProcessor::processAllFiles()’:
FileProcessor.cpp:34:43: warning: catching polymorphic type ‘class std::runtime_error’ by value [-Wcatch-value=]
   34 |                 catch (std::runtime_error re)
      |                                           ^~
FileProcessor.cpp: In member function ‘void FileProcessor::processLoop(std::ifstream&, FileStatistics&)’:
FileProcessor.cpp:52:25: warning: unused variable ‘inBuf’ [-Wunused-variable]
   52 |         std::streambuf* inBuf = inStream.rdbuf();
      |                         ^~~~~
FileProcessor.cpp: In member function ‘std::string FileProcessor::processFile(std::string, FileStatistics&)’:
FileProcessor.cpp:95:31: warning: catching polymorphic type ‘class std::exception’ by value [-Wcatch-value=]
   95 |         catch (std::exception ex)
      |                               ^~
g++-12 -std=c++20 -Wall -Wextra -Wwrite-strings -Wno-parentheses -Wpedantic -Warray-bounds -Wconversion -Weffc++ -Wuseless-cast   -c -o FileStatistics.o FileStatistics.cpp
In file included from FileStatistics.cpp:1:
FileStatistics.h: In constructor ‘FileStatistics::FileStatistics()’:
FileStatistics.h:18:16: warning: ‘FileStatistics::wordCount’ will be initialized after [-Wreorder]
   18 |         size_t wordCount;
      |                ^~~~~~~~~
FileStatistics.h:17:16: warning:   ‘size_t FileStatistics::characterCount’ [-Wreorder]
   17 |         size_t characterCount;
      |                ^~~~~~~~~~~~~~
FileStatistics.cpp:4:1: warning:   when initialized here [-Wreorder]
    4 | FileStatistics::FileStatistics()
      | ^~~~~~~~~~~~~~
FileStatistics.h:21:16: warning: ‘FileStatistics::blankLineCount’ will be initialized after [-Wreorder]
   21 |         size_t blankLineCount;
      |                ^~~~~~~~~~~~~~
FileStatistics.h:20:16: warning:   ‘size_t FileStatistics::widestLine’ [-Wreorder]
   20 |         size_t widestLine;
      |                ^~~~~~~~~~
FileStatistics.cpp:4:1: warning:   when initialized here [-Wreorder]
    4 | FileStatistics::FileStatistics()
      | ^~~~~~~~~~~~~~
FileStatistics.cpp:4:1: warning: ‘FileStatistics::fileName’ should be initialized in the member initialization list [-Weffc++]
FileStatistics.h: In constructor ‘FileStatistics::FileStatistics(std::string)’:
FileStatistics.h:18:16: warning: ‘FileStatistics::wordCount’ will be initialized after [-Wreorder]
   18 |         size_t wordCount;
      |                ^~~~~~~~~
FileStatistics.h:17:16: warning:   ‘size_t FileStatistics::characterCount’ [-Wreorder]
   17 |         size_t characterCount;
      |                ^~~~~~~~~~~~~~
FileStatistics.cpp:18:1: warning:   when initialized here [-Wreorder]
   18 | FileStatistics::FileStatistics(std::string inFileName)
      | ^~~~~~~~~~~~~~
FileStatistics.h:21:16: warning: ‘FileStatistics::blankLineCount’ will be initialized after [-Wreorder]
   21 |         size_t blankLineCount;
      |                ^~~~~~~~~~~~~~
FileStatistics.h:20:16: warning:   ‘size_t FileStatistics::widestLine’ [-Wreorder]
   20 |         size_t widestLine;
      |                ^~~~~~~~~~
FileStatistics.cpp:18:1: warning:   when initialized here [-Wreorder]
   18 | FileStatistics::FileStatistics(std::string inFileName)
      | ^~~~~~~~~~~~~~
g++-12 -std=c++20 -Wall -Wextra -Wwrite-strings -Wno-parentheses -Wpedantic -Warray-bounds -Wconversion -Weffc++ -Wuseless-cast   -c -o main.o main.cpp
In file included from main.cpp:5:
CommandLineParser.h:17:7: warning: ‘class CommandLineParser’ has pointer data members [-Weffc++]
   17 | class CommandLineParser
      |       ^~~~~~~~~~~~~~~~~
CommandLineParser.h:17:7: warning:   but does not declare ‘CommandLineParser(const CommandLineParser&)’ [-Weffc++]
CommandLineParser.h:17:7: warning:   or ‘operator=(const CommandLineParser&)’ [-Weffc++]
CommandLineParser.h:36:16: note: pointer member ‘CommandLineParser::args’ declared here
   36 |         char** args;
      |                ^~~~
In file included from main.cpp:8:
UtilityTimer.h: In constructor ‘constexpr UtilityTimer::UtilityTimer()’:
UtilityTimer.h:16:9: warning: ‘UtilityTimer::start’ should be initialized in the member initialization list [-Weffc++]
   16 |         UtilityTimer() = default;
      |         ^~~~~~~~~~~~
UtilityTimer.h:16:9: warning: ‘UtilityTimer::end’ should be initialized in the member initialization list [-Weffc++]
main.cpp: In function ‘int main(int, char**)’:
main.cpp:51:32: warning: catching polymorphic type ‘class ShowHelpMessage’ by value [-Wcatch-value=]
   51 |         catch (ShowHelpMessage sh)
      |                                ^~
main.cpp:56:29: warning: catching polymorphic type ‘class showVersions’ by value [-Wcatch-value=]
   56 |         catch (showVersions sv)
      |                             ^~
main.cpp:60:31: warning: catching polymorphic type ‘class std::exception’ by value [-Wcatch-value=]
   60 |         catch (std::exception ex)
      |                               ^~
g++-12 -std=c++20 -Wall -Wextra -Wwrite-strings -Wno-parentheses -Wpedantic -Warray-bounds -Wconversion -Weffc++ -Wuseless-cast   -c -o ProgramOptions.o ProgramOptions.cpp
g++-12 -std=c++20 -Wall -Wextra -Wwrite-strings -Wno-parentheses -Wpedantic -Warray-bounds -Wconversion -Weffc++ -Wuseless-cast   -c -o ReportWriter.o ReportWriter.cpp
In file included from ReportWriter.cpp:4:
ReportWriter.h: In constructor ‘ReportWriter::ReportWriter(ProgramOptions&)’:
ReportWriter.h:18:9: warning: ‘ReportWriter::options’ should be initialized in the member initialization list [-Weffc++]
   18 |         ReportWriter(ProgramOptions& progOptions)
      |         ^~~~~~~~~~~~
g++-12 -std=c++20 -Wall -Wextra -Wwrite-strings -Wno-parentheses -Wpedantic -Warray-bounds -Wconversion -Weffc++ -Wuseless-cast   -c -o StatisticsCollector.o StatisticsCollector.cpp

CmdLineFileExtractor

Inconsistent file naming - why Cmd here, but Command elsewhere?

Inconsistent spelling - extension is written as "extention" in several places.

Class SubDirNode is all public, so should probably be a struct instead.

Move-initialise the path member:

    fsys::path fileSpec;
    bool discovered = false;
    bool searchedFiles = false;
    SubDirNode(fsys::path path)
            : fileSpec{ std::move(path) }
    {
    }

Class SubDirNode has operator ==, which ought to bind constant this; we should also provide !=:

    bool operator!=(const SubDirNode& other) const = default;

Consider providing operator <=> instead, so all comparisons are supported. (Beware: many compilers don't fully support <=> yet).

I don't like the global variables that are shared by all instances of CmdLineFileExtractor (though it's good that they are restricted to file scope). It makes testing harder (even though there's no test suite as yet, that should probably be your next project!). Consider using a pimpl structure as member (that alleviates your concern about transitive includes).

findSubDirs() can be clearer if we express it as a filtered range:

static auto findSubDirs(SubDirNode currentDir)
{
    auto is_missing = [](const SubDirNode& branch){
        return std::ranges::find(subDirectories, branch) == subDirectories.end();
    };

    fsys::path cwd = currentDir.fileSpec;
    auto subdirs = fsys::directory_iterator{cwd}
        | std::views::filter([](auto& f){ return f.is_directory(); })
        | std::views::transform([](auto& f)->SubDirNode { return f.path(); })
        | std::views::filter(is_missing);

    // TODO (C++23): return subdirs | std::ranges::to<std::vector>();
    auto newSubDirs = std::vector<SubDirNode>{};
    std::ranges::copy(subdirs, std::back_inserter(newSubDirs));
    return newSubDirs;
}

(Look, no explicit loops!)

Since we're sorting directory entries, perhaps use a std::set for subDirectories, which would make is_missing() scale much better - or allow set subtraction (std::set_difference) instead.

Obvious simplification here:

      root.discovered = (SearchSubDirs) ? false: true;

That can be simply

        root.discovered = !SearchSubDirs;

CommandLineParser

This looks like something you might want to reuse in other projects, but it currently has all the arguments hard-coded, meaning it's only suitable for reuse-by-copying. It would be better to have a configurable option parser as a library - look at Python's argparse module for an example in a different language. Obviously, only implement the parts you need, as you need them - and write good unit-tests for the library.

Again, std::move() in the initializer list.

I'm not sure why extractAllArguments() returns the number of flag arguments - the information is never used.

Misleading comment:

  // flush the buffer to make sure the entire message is visible
  std::cerr << std::endl;

Thet should be std::cerr << std::flush, I think, since the help string already ends with a newline.

printHelpMessage() looks over-complicated. Why not just store the help message as a string literal? Then initHelpMessage() could be replaced by

    return
        " file name or file type specification (*.ext)\n"
        "Otions:\n"
        "\t-c, --bytes print the byte counts\n"
        "\t-m, --chars print the character counts\n"
        "\t-l, --lines print the newline counts\n"
        "\t-t, --time-execution print the execution time of the program\n"
        "\t-L, --max-line-length print the length of the longest line\n"
        "\t-w, --words print the word counts\n"
        "\t--help display this help and exit\n"
        "\t--version output version information and exit\n"
        "\t-R, --subdirectories all files in the directory as well as sub directories\n"
        "By default the -c -l and -w flags are set; setting any flag"
        " requires all flags you want to be set.\n";

and the result used as initializer.

Then printHelpMessage can be replaced by a simple << (and now we don't need a function for that, we can implement -h/--help option, to print the used to std::cout).

Similarly, singleDashArgs and doubleDashArgs could be value-initialised in the constructor's initializer-list (and perhaps all three could be const).


ExecutionCtrlValues

This one is all public, so should be a struct. We can inline it all into the header:

struct ExecutionCtrlValues
{
    ProgramOptions options = {};
    std::vector<std::string> fileSpecTypes = {};
    std::vector<std::string> filesToProcess = {};

    void initFromEnvironmentVariables()
    {
        options.initFromEnvironmentVars();
    }
};

FileProcessor

Misspelt std::size_t InputBufferSize. Implementations are permitted, but not required, to declare size_t in the global namespace as well as in std. Programs intended to be portable (especially to future compilers!) should always use the qualified version (assuming that none of the deprecated C headers are included (such as <stdlib.h> instead of <cstdlib>)).

Does this class really need a mutable reference to the program options? I don't believe it does, given that ReportWriter can easily accept a const reference.

It certainly doesn't require a mutable for filesToProcess; since it's copying, better to pass by value and then move-construct the member:

FileProcessor::FileProcessor(std::vector<std::string> filesToProcess,
                             const ProgramOptions& progOptions)
    : fileNames { std::move(filesToProcess) },
      options{ progOptions }
{
}

processLoop() buffers the entire file in memory:

  std::stringstream inputBuffer;
  inputBuffer << inStream.rdbuf();
  fileAnalyzer.analyzeBuffer(inputBuffer.str());

That seems wasteful for large files. A word-count utility shouldn't even need to buffer complete lines, never mind whole files. That said, for inputs that you can mmap() (and Boost provides an abstraction for this), then that's more efficient than streaming.


FileStatistics

Misspelt std::size_t throughout.

If we provide in-class initializers:

private:
    std::size_t totalLineCount = 0;
    std::size_t codeLineCount = 0;
    std::size_t commentLineCount = 0;
    std::size_t whiteSpaceCount = 0;
    std::size_t characterCount = 0;
    std::size_t wordCount = 0;
    std::size_t codeWithCommentCount = 0;
    std::size_t widestLine = 0;
    std::size_t blankLineCount = 0;
    std::string fileName = {};

then we can simplify the constructors:

FileStatistics::FileStatistics() = default;

FileStatistics::FileStatistics(std::string inFileName)
    : fileName{ std::move(inFileName) }
{
}

(I added a move-construction there, too).

This looks very wrong:

    float getPerecentageOfCode() {
        return static_cast<float>(codeLineCount / totalLineCount);
    }

I'm not just talking about the misspelling of "percentage"! If we integer divide, we'll get an integer (probably zero), and then cast that to float. I think that static_cast<float>(codeLineCount) / totalLineCount is what's intended. Of course that's still not a percentage, until we multiply by 100. And double is the natural type for floating-point in C++.

However, if we assume that whole-number precision is enough, then we can work in integers:

    std::size_t getPerecentageOfCode() const {
        // 100 * codeLineCount / totalLineCount, plus ½ to round to nearest
        return (codeLineCount * 200 + totalLineCount) / totalLineCount / 2;
    }

Making the signature consistent with the other member functions has advantages when reducing duplication in ReportWriter, too.


main

We need to set locale from the environment, so that the user's expectation of what is whitespace and how bytes map to characters are respected, and so that printing times uses the correct formatting.

This is the way to set user local for both C and C++ functions in the standard library:

    std::locale::global(std::locale(""));

ProgramOptions

Use in-class initialisers here, too - always better than constant values in the constructor's initializer-list.

initFromEnvironmentVars() is misleading at best!

We should probably accept a std::string_view here, rather than forcing a conversion:

void ProgramOptions::singleLine(std::string flag, bool flagValue)
{
  std::cout << "Flag " << flag << ": Flag value " <<
      ((flagValue) ? "True" : "False") << "\n";
}

Consider using the std::boolalpha i/o manipulator instead of hand-converting booleans.

The final "\n" is a single character string, so consider '\n' instead (yes, that's a pointless micro-optimisation, but it shows attention to details!).


ReportWriter

This comment may be true on your platform, but it's not required by C++:

// std::vector included by string

Standard library headers are allowed transitively include other headers, but relying on one platforms inclusions is not portable.

Why do we have a shared pointer to our options, when we never share the ownership? We could just have a plain const ProgramOptions member, or perhaps even a reference if we can depend on users.

Instead of always constructing and appending to a string just to write to an output stream, I would do it the other way around:

std::ostream& ReportWriter::printResult(FileStatistics& resultsForOutput, std::ostream& os)
{
    if (options->lineCount) {
        os << resultsForOutput.getTotalLines() << '\t';
    }

    if (options->wordCount) {
        os << resultsForOutput.getWords() << '\t';
    }

    if (options->byteCount) {
        os << resultsForOutput.getCharacters() << '\t';
    }

    if (options->charCount) {
        os << resultsForOutput.getCharacters() << "\t\t";
    }

    if (options->maxLineWidth) {
        os << resultsForOutput.getWidestLine() << '\t';
    }

    // End of backwards compatability with wc utility.

    if (options->codeCount) {
        os << resultsForOutput.getCodeLines() << '\t';
    }

    if (options->commentCount) {
        os << resultsForOutput.getCommentLines() << '\t';
    }

    if (options->percentages) {
        os << resultsForOutput.getPerecentageOfCode() << '\t';
    }

    if (options->whitespaceCount) {
        os << resultsForOutput.getWhitespace() << '\t';
    }
    if (options->blankLineCount) {
        os << resultsForOutput.getBlankLines() << '\t';
    }

    os << '\t'
       << correctFileSpec(resultsForOutput.getFileName())
       << '\n';

    return os;
}

std::string ReportWriter::getResultText(FileStatistics& resultsForOutput) noexcept
{
    std::ostringstream os;
    printResult(resultsForOutput, os);
    return os.str();
}

I'm not sure why there are two tabs after character count, nor why we output getCharacters() for the byte count (are we pretending that all files are in a single-byte character set, perhaps?).

In getColumneHeadingsText() (shouldn't that be "Column", not "Columne"?), we assume that tabs are at standard 8-char intervals, but some terminals can be set to other tab stops. It's safer to use space characters where we need to match the text headers:

        firstLine  += "Characters\t";
        secondline += "          \t";

We probably should be using std::setw manipulator when writing values, to be consistent with these. And that's how we get the values right-aligned, to improve usability.

In either case, this is all quite tricky for localisation. We probably want to have a structure for each column containing its header lines (without the tabs), use the max length of the two lines to determine the necessary column width (perhaps taking into account a reasonable max value to accommodate), and use that width for printing both header lines and the values. That's probably worth defining a simple structure for (private to the implementation file, of course).

correctFileSpec() looks like it splits names on \ regardless of whether or not that is a directory separator. We don't want that - and std::filesystem::path provides a filename member that looks like what's wanted here.

To get consistent columns and headers, it's worth defining a structure that represents each column:

struct Column {
    bool ProgramOptions::* flag;
    std::size_t (FileStatistics::*count)() const;
    std::string header[2];
    int width;

    Column(bool ProgramOptions::* flag,
           std::size_t (FileStatistics::* count)() const,
           std::string first, std::string second, std::size_t width = 0)
        : flag { flag },
          count { count },
          header { std::move(first), std::move(second) },
          width { static_cast<int>(std::max({header[0].size(), header[1].size(), width})) }
    {}
};

static const Column columns[] =
{
    { &ProgramOptions::lineCount,
      &FileStatistics::getTotalLines,
      "Lines", "of Text", 6 },
    { &ProgramOptions::wordCount,
      &FileStatistics::getWords,
      "Words", "", 7 },
    { &ProgramOptions::byteCount,
      &FileStatistics::getCharacters,
      "Bytes", "", 8 },
    { &ProgramOptions::charCount,
      &FileStatistics::getCharacters,
      "Characters", "", 8 },
    { &ProgramOptions::maxLineWidth,
      &FileStatistics::getWidestLine,
      "Length of", "Longest Line", 5 },
    { &ProgramOptions::codeCount,
      &FileStatistics::getCodeLines,
      "Lines", "of Code", 6 },
    { &ProgramOptions::commentCount,
      &FileStatistics::getCommentLines,
      "Lines of", "Comments", 6 },
    { &ProgramOptions::percentages,
      &FileStatistics::getPerecentageOfCode,
      "Percentage of", "Lines of Code", 2 },
    { &ProgramOptions::whitespaceCount,
      &FileStatistics::getWhitespace,
      "Whitespace", "Characters", 8 },
    { &ProgramOptions::blankLineCount,
      &FileStatistics::getBlankLines,
      "Blank", "Lines", 6 },
};

Then use the same structure for producing the headers and the content:

std::ostream& ReportWriter::printResult(FileStatistics& resultsForOutput, std::ostream& os)
{
    for (auto const& col: columns) {
        if (options.*(col.flag)) {
            os << std::setw(col.width)
               << (resultsForOutput.*col.count)()
               << '\t';
        }
    }

    os << correctFileSpec(resultsForOutput.getFileName())
       << '\n';

    return os;
}

std::string ReportWriter::getResultText(FileStatistics& resultsForOutput) noexcept
{
    std::ostringstream os;
    printResult(resultsForOutput, os);
    return os.str();
}

std::string ReportWriter::getColumnHeadingAsOneString() noexcept
{
    std::ostringstream os;
    printColumnHeadings(os);
    return os.str();
}
std::ostream& ReportWriter::printColumnHeadings(std::ostream& os) noexcept
{
    for (int i = 0;  i <= 1;  ++i) {
        for (auto const& col: columns) {
            if (options.*(col.flag)) {
                os << std::setw(col.width)
                   << (col.header[i])
                   << '\t';
            }
        }
        os << '\n';
    }

    return os;
}

StatisticsCollector

This uses std::uintmax_t without including <cstdint> (this is quite pedantic: although <filesystem> declares a function returning this type, it doesn't have to provide this name).

It misspells std::size_t throughout.

I don't think that we should be passing strings by value or by mutable reference in any of these functions. Just pass by const& (there's only a couple of arguments to change to const_iterator if we use auto for all the locals), or pass std::string_view instead.

This code looks like it counts file bytes, not how many characters they represent:

    std::uintmax_t bufferSize = std::filesystem::file_size(fileStatistics.getFileName());
    fileStatistics.setCharCount(bufferSize);

You've hit the <cctype> trap here:

      while (isspace(*currentChar))

std::isspace() (another misspelling) requires positive values, so you need to convert to unsigned char before the promotion to int:

        while (std::isspace(static_cast<unsigned char>(*currentChar)))

Counting words seems to be based on whitespace/non-whitespace transitions, but I'm not sure that's a useful measure. Personally, I would discount any "word" not containing at least one alphanumeric character. And I'd consider adding some extra rules to allow & and similar as exceptions (but I probably wouldn't consider it very seriously!).

updateWidestLine() doesn't use the std::string line variable that it creates. And I think I'd prefer it to return the new currentChar rather than modifying through the reference.


UtilityTimer

Missing include of <ctime>, needed by std::ctime(). Though I'd advise against that function, as it always produces American output, regardless of locale.

end doesn't need to be a member, since it's set and used only in stopTimerAndReport.

We should probably be using the steady (or perhaps the high-precision) clock for accurate measurement. I'd use an alias:

public:
    using clock = std::chrono::steady_clock;

We'll still need the system clock for printing the time of day, if we really think that's valuable.

It probably makes sense to initialise start to the present time, which will save users needing to explicitly start the timer if it's constructed at the right time.

 private:
    clock::time_point start = clock::now();

stopTimerAndReport() shouldn't be taking its string argument by copy - use a string-view here. I'd use the standard log stream for this, and combine the multiple separate strings:

    void stopTimerAndReport(std::string_view whatIsBeingTimed) 
    {
        clock::time_point end = clock::now();

        std::chrono::duration<double> elapsed_seconds = end - start;
        double ElapsedTimeForOutPut = elapsed_seconds.count();

        using std::chrono::system_clock;
        auto const now = system_clock::to_time_t(system_clock::now());
        std::clog << "finished " << whatIsBeingTimed << std::put_time(std::localtime(&now), "%c")
                  << "\nelapsed time in seconds: " << ElapsedTimeForOutPut << "\n\n\n";
    }

It certainly shouldn't be noexcept, since any of those << can throw.

\$\endgroup\$
7
  • 3
    \$\begingroup\$ I'd advise -Werror, this makes sure warnings are not missed, and superfluous warnings can always be removed with -Wno-. \$\endgroup\$ Nov 16, 2022 at 10:51
  • 1
    \$\begingroup\$ I'm actually using -Werror locally whilst experimenting with improvements @Matthieu. \$\endgroup\$ Nov 16, 2022 at 12:05
  • \$\begingroup\$ Good catch on CommandLineParser::extractAllArguments(). I was using the flag count to indicate if the default columns should be set, but the use of the -t or -R flags caused that to not output the data collected. This is the bug I was fixing when we were chatting in the 2nd Monitor. Obviously I didn't fix all of the code. Is there a difference between std::size_t and size_t? \$\endgroup\$
    – pacmaninbw
    Nov 16, 2022 at 16:17
  • \$\begingroup\$ Thank you for the contribution of the Makefile in the repository. \$\endgroup\$
    – pacmaninbw
    Nov 16, 2022 at 17:25
  • \$\begingroup\$ Incorporated earlier comment re size_t/std::size_t into the answer. \$\endgroup\$ Nov 16, 2022 at 21:13
8
\$\begingroup\$

Generally it looks pretty well structured, but here are some ideas for further improving your code.

Write member initializers in declaration order

The FileStatistics class has this constructor

FileStatistics::FileStatistics()
    : totalLineCount{ 0 },
    codeLineCount{ 0 },
    commentLineCount{ 0 },
    whiteSpaceCount{ 0 },
    wordCount{ 0 },
    characterCount{ 0 },
    codeWithCommentCount{ 0 },
    blankLineCount{ 0 },
    widestLine{ 0 }
{

}

That looks fine, but in fact, characterCount will be initialized before wordCount because members are always initialized in declaration order and wordCount is declared before characterCount in this class. To avoid misleading another programmer, you should swap the order of those such that they match. See C.47. Or better, in this case, see the next suggestion.

Prefer in-class initializers

Instead of writing a constructor like the one above where all it does is initialize data members to constants, better would be to use in-class member initializers. If you do that, the ordering problems go away and you can omit that constructor entirely, letting the compiler generate it. The other constructor then becomes much simpler, too:

FileStatistics::FileStatistics(std::string inFileName)
    : fileName{ inFileName }
{
}

See C.45

Eliminate unused variables

Unused variables are a sign of poor code quality, so eliminating them should be a priority. In this code, findSubdirs sets hasSubDirs to false but never actually uses that variable. My compiler also tells me that. Your compiler is probably also smart enough to tell you that, if you ask it to do so.

Catch polymorphic exceptions by reference

The code current contains this line:

catch (ShowHelpMessage sh)

The problem with that is that if the exception happened to be some other type, it has just been coerced into being a ShowHelpMessage exception. You should catch polymorphic exceptions by refererence instead to avoid object slicing:

catch (const ShowHelpMessage& sh)

See E.15

Be careful with signed and unsigned

The CommandLineParser includes the argCount variable which is declared as an int, but in many places in the code, it's compared with a variable of type size_t which is unsigned. I'd suggest declaring it as unsigned instead, making the appropriate matching change to the constructor.

Don't manually write empty constructors

Don't manually write empty constructors like the one for ExecutionCtrlValues. You can omit both the declaration and the body, or just omit the body and use ExecutionCtrlValues() = default; if you feel the need to call attention to it.

Move operations to compile-time rather than runtime

There is no reason to have helpMessage constructed at runtime. It is a fixed, static string that is entirely known at compilation time. Further, there's no need to have it be a vector of strings. It could simply be a constexpr std::string_view or not declared at all, since it's only used in one place. Simply send it to std::cerr directly as a single string. Similarly, both the doubleDashArgs and singleDashArgs could be defined at compile time and be made const.

Consider reducing the number of classes

There are a lot of classes here, and I question whether all of them are needed. Do we really need a CmdLineFileExtractor or could that instead be a function of the CommandLineParser class? It might even be a static function, depending on whether the functionality can be simplified.

Don't use exceptions for normal control flow

It's a design choice, but I'm not a big fan of using exceptions for normal control flow. For very normal non-error things like printing the help message, something like a simple if or a switch would be more appropriate in my view.

Parse argv starting with argv[1]

In most systems, argv[0] is the name of the program, so it's not a good idea to start parsing the command line arguments there, but rather with argv[1].

Check your spelling

In the user interface help text we are told about the available "Otions" which sound somewhat more restrictive than "options." ;) Also, there's a peculiar mixed case in a comment at the last line of ProgramOptions.h. It's worth taking some time to spell check, especially for things that the end user sees.

Think of the user

It would be nice to be able to ask for help explicitly with a -h or --help option.

\$\endgroup\$
4
  • 1
    \$\begingroup\$ Thanks. Haven't seen you around lately, been wondering if you were okay. Hopefully this is an improvement over the first C++ program you reviewed for me 7 years ago. \$\endgroup\$
    – pacmaninbw
    Nov 16, 2022 at 15:51
  • 5
    \$\begingroup\$ I'm good, thanks, just really busy. :) Yes, it's a big improvement over 7 years ago -- I had to look just now. Nice! \$\endgroup\$
    – Edward
    Nov 16, 2022 at 15:53
  • \$\begingroup\$ I created a branch to refactor CommandLineParser and ProgramOptions. It is called ReworkProgramOptsDev. I moved all the code that could change based on the program implementation from CommandLineParser to ProgramOptions. Let me know what you think, so that I can merge it into master if it is good. \$\endgroup\$
    – pacmaninbw
    Nov 19, 2022 at 20:12
  • \$\begingroup\$ I've started a discussion for the repository. \$\endgroup\$
    – pacmaninbw
    Nov 20, 2022 at 14:08
7
\$\begingroup\$

About "Cygwin wc"

Cygwin is a collection of GNU utilities ported to Windows, along with a .dll that provides all the POSIX functions a program running on Linux expects. So "Cygwin wc" is actually GNU coreutils wc, and is exactly the same as the one you will find on most Linux distributions.

Answers to your questions

  • Are there any memory leaks? Where?

A good way to find them out yourself is to compile your code with the AddressSanitizer enabled. If you were running on Linux, I would recommend using Valgrind as well.

But the great thing about your code is that you don't do any manual memory allocations at all, and in fact the few pointers I see are to C strings, so I don't see how you could have a memory leak. Good job!

  • Are there any C++ or STL functions I could have used to decrease the amount of code I wrote?
  • Convert C-strings to std::string, and/or pass them as std::string_views early, so operations on them get easier. Consider being able to write if (currentArg == "--help") {…}, or for (char opt: currentArg.substr(1)) {…}.

  • Make use of if-with-initializer: if (auto thisOption = singleDashArgs.find(currentArg[i]); thisOption != singleDashArgs.end()) {…}.

  • This is even a C feature: two string literals next to each other will be merged into one. So instead of repeatedly writing cout << … or helpMessage.push_back(…), you can write something like:

    std::cout << messageProgramName() << ": version: " << version << "\n"
                 "Packaged by Chernick Consulting\n"
                 "License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.\n"
                 "This is free software : you are free to change and redistribute it.\n"
                 "\tThere is NO WARRANTY, to the extent permitted by law.\n"
                 "\n"
                 "Written by Paul A. Chernick\n";
    

    You can also consider using C++11's raw string literals for long blurbs of text.

  • Move initialization out of constructor bodies where possible. In SubDirNode::SubDirNode(), move the initialization of fileSpec into the initializer list. Also consider using default member initialization instead of intializer lists where possible.

  • Make more use of range for loops. For example, when iterating over the items in a directory, you can just write: for (auto& dir_entry: fsys::directory_iterator(cwd)) {…}.

  • Use more of std::filesystem. Instead of writing your own function to get the extension from a filename, just use std::filesystem::path::extension(). If you want to recurse over subdirectories, use std::filesystem::recursive_directory_iterator.

  • Are there any C++20 features I should have used that I didn’t?

I don't think so.

  • How can I improve performance?

The performance already looks pretty good. Then again, it's mostly going to be I/O bound anyway, since counting lines and words isn't that hard for the CPU. You could consider adding a thread pool so multiple threads can do work, sometimes that might help with I/O as well.

  • Can I improve the method declarations in the header files?

Use more const for references to things that shouldn't be modified by the method. Consider using Doxygen to document everything that's in the header files.

I also see you = default constructors and destructors, but often there is no reason to do that. It's better to avoid mentioning them at all if they are going to be the default anyway.

  • Level of complexity, is any of the code too complex?
  • Modularity, is there too much coupling between classes?
  • Should any of the classes be combined?

I think you erred on the side of splitting things up and making classes for everything. It's not that the classes themselves are coupled too much, or that the classes are way too complex, but now you have a lot of classes to deal with.

You also have some classes that appear to store no data, like CmdLineFileExtractor. It does store data though, but in static variables. That doesn't sound very reusable. Also consider whether it should have been a class to begin with.

Some classes are indeed more complex than necessary. Consider FileStatistics. It's just a bunch of counters. I would have made it a struct with just the std::size_t member variables and no methods, except maybe an operator+= overload so you can sum statistics easily.

  • Are there too many or too few comments?
  • Is the code self documenting?

Again, use Doxygen to document the code. I think there are a bit too few comments; there are large code blocks where you are doing something complicated where a comment would make it much clearer to a reader what you are trying to achieve.

  • What do you think of the class and method names?

I think you did a very good job on naming things; it's all very clear!

\$\endgroup\$
7
  • 2
    \$\begingroup\$ On converting C-String to std::string => actually, if read-only, I'd advise converting to std::string_view. You'll get the same ease of use, but with a much lighter weight package. \$\endgroup\$ Nov 16, 2022 at 10:49
  • \$\begingroup\$ @MatthieuM.Good point. \$\endgroup\$
    – G. Sliepen
    Nov 16, 2022 at 11:33
  • \$\begingroup\$ I'm always concerned about performance. When I'm really concerned I look at the assembly code generated by different code. I was considering using threads not sure how much it will help. Each file could be processed in a separate thread, but the command line parsing, or at least finding the files to process is the most time consuming and I'm not sure how to multi-thread that portion. \$\endgroup\$
    – pacmaninbw
    Nov 16, 2022 at 17:32
  • \$\begingroup\$ Oh, raw string literals - I always forget them, and one would be useful there. \$\endgroup\$ Nov 16, 2022 at 17:49
  • 1
    \$\begingroup\$ @TobySpeight It can be both an iterator and a range. It looks like an iterator, but there are also begin() and end() overloads. \$\endgroup\$
    – G. Sliepen
    Nov 16, 2022 at 18:09
5
\$\begingroup\$

It's a little verbose. It might be cleaner if we:

  • Avoid using "Action" classes that are just functions in disguise (e.g. CommandLineParser, CmdLineFileExtractor, FileProcessor, ReportWriter). Don't be afraid to use free-functions and pass data around as necessary. See below for an example.

  • Avoid two-step initialization of classes / variables. Declare and initialize in a single statement (e.g. auto executionCtrl = getExecutionCtrlFromEnvironmentVars()).

  • Avoid declaring named variables for temporary values that are only used once (e.g. versionString in main(), doHelp or sv when throwing an exception).

  • Declare variables as close to the point of use as possible and initialize them to useful values straight away (e.g. executionCtrl in main()).

  • Don't do specific output or printing from classes that could be generic and reusable. e.g. For the UtilityTimer class -- add a getElapsedTime() function to the class and do the printing somewhere else.


e.g. CommandLineParser:

class CommandLineParser
{
public:
  CommandLineParser(int argc, char* argv[], std::string progVersion);

  bool parse(ExecutionCtrlValues& execVars);
  void printHelpMessage();
  void printVersion();

protected:
  void processSingleDashOptions(char *currentArg);
  void processDoubleDashOptions(char* currentArg);
  void SetDefaultOptionsWhenNoFlags();
  void initDashMaps();
  void initHelpMessage();
  void findAllFilesToProcess(ExecutionCtrlValues& execVars);
  unsigned int extractAllArguments();
  std::string messageProgramName();

private:
  char** args;
  int argCount;
  std::string version;
  ProgramOptions options;
  std::unordered_map<std::string, bool&> doubleDashArgs;
  std::unordered_map<char, bool&> singleDashArgs;
  std::vector<std::string> helpMessage;
  std::vector<std::string> NotFlagsArgs;
  bool useDefaultFlags;
};

Using a class as a function means we put a whole lot of stuff in the header that shouldn't really be visible at all. It also means we have a lot of "temporary" member data hanging around with unclear or unnecessarily extended lifetimes. If we use a plain function, we can use local variables instead.

So I'd suggest something more like this:

struct ProgramOptions // note: shown here for illustration
{
    // Input options
    bool recurseSubDirectories = false;
    std::vector<std::string> fileTypes;
    std::vector<std::string> files;

    // Output options
    bool blankLineCount = false;
    bool byteCount = false;
    bool charCount = false;
    bool codeCount = false;
    bool commentCount = false;
    bool lineCount = false;
    bool maxLineWidth = false;
    bool percentages = false;
    bool whitespaceCount = false;
    bool wordCount = false;
    bool enableExecutionTime = false;
};

ProgramOptions getProgramOptions(int argc, char* argv[]);

That single function declaration is all that needs to be in the header file! (Well ok, we could declare the two print functions here too). The rest is implementation detail, and can be contained in the .cpp file. Very roughly (not really compiled / tested), something like this:

namespace // note: anonymous namespace limits visibility of its contents to this file only
{

    ProgramOptions getProgramOptionsFromEnvironment()
    {
        // TODO: implement me!
        return ProgramOptions();
    }

    void updateProgramOptionsFromCommandLine(ProgramOptions& options, int argc, char** argv)
    {
        assert(argc > 0);

        for (int i = 1; i != argc; ++i)
        {
            assert(argv[i] != nullptr);

            auto arg = std::string_view(argv[i]);

            // note: attempting to optimize for simplicity ;)
            if      (arg == "-R" || arg == "--subdirectories")  { options.recurseSubDirectories = true; }
            else if (arg == "-t" || arg == "--time-execution")  { options.enableExecutionTime = true; }
            else if (arg == "-c" || arg == "--bytes")           { options.byteCount = true; }
            else if (arg == "-m" || arg == "--chars")           { options.charCount = true; }
            else if (arg == "-l" || arg == "--lines")           { options.lineCount = true; }
            else if (arg == "-L" || arg == "--max-line-length") { options.maxLineWidth = true; }
            else if (arg == "-w" || arg == "--words")           { options.wordCount = true; }

            else if (arg == "--help") { throw ShowHelpMessage(); } // note: don't need to derive from std::runtime_error... we don't need "real" exceptions
            else if (arg == "--version") { throw ShowVersions(); }

            else if (arg.starts_with("--") || arg.starts_with("-")) { std::cerr << "Unknown flag: " << arg << "\n"; } // note: we could throw another thingy here too
            else { options.files.push_back(std::string(arg)); } // note: we could split files / directories / file types here, or do it later.
        }
    }

} // unnamed

ProgramOptions getProgramOptions(int argc, char** argv)
{
    auto options = getProgramOptionsFromEnvironment();
    updateProgramOptionsFromCommandLine(options, argc, argv);

    // use defaults if no specific option is set:
    if (!options.byteCount && !options.lineCount && !options.wordCount)
    {
        options.byteCount = true;
        options.lineCount = true;
        options.wordCount = true;
    }

    return options;
}

We could use a big table to store long argument names, short names, help string, and actions together, but that's probably much more trouble than it's worth.

Anyhoo, by eliminating the unnecessary classes, we simplify the calling code and main() is maybe a bit neater as a result:

int main(int argc, char* argv[])
{
    try
    {
        auto const options = getProgramOptions(argc, argv);

        // notes: 
        // we can always do timing (it's negligible overhead), but don't always print results
        // timer should auto-start for simplicity.
        // timing could be done inside "mainLoop", but I've left it here for now
        // timer should use `std::chrono::steady_clock` instead of the system clock.
        auto timer = UtilityTimer();

        mainLoop(options); // note: "mainLoop" is not a great name for this function!

        if (options.enableExecutionTime)
        {
            ui::reportTiming(timer.getElapsedTime()); // note: we can group free functions by putting them in a namespace, instead of a class.
        }
    }
    catch (ShowHelpMessage const& sh) // note: we can catch by const& instead of by value
    {
        ui::printHelp();
    }
    catch (ShowVersions const& sv)
    {
        ui::printVersion();
    }
    catch (std::exception const& ex)
    {
        ui::printError(ex.what());
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}
\$\endgroup\$
3
\$\begingroup\$

File name enumeration

This seems to be two programs in one. I advise sticking to the design principle of "do one thing and do it well", so instead of reimplementing a good portion of find, just accept a simple list of filenames (possibly via stdin to allow for more than the maximum command-line length - allow NUL-separated names for robustness). That frees your time to concentrate on the real value, and reduces the bloat on users' systems.

On my platform, the wildcard expansion barely works. If I pass a single filename or a list of filenames, I get no output. If I pass *, I get no output. It ignores every filename that doesn't begin with *., which makes it basically unusable:

$ ./wc main.cpp
$ ./wc *.cpp
$ ./wc '*.cpp'
Lines   Words   Bytes
of Text
67      140     1582            main.cpp
104     224     2510            FileProcessor.cpp
26      61      801             ProgramOptions.cpp
188     395     3794            ReportWriter.cpp
251     746     6577            CommandLineParser.cpp
23      45      652             FileStatistics.cpp
287     855     7462            CmdLineFileExtractor.cpp
100     210     2356            StatisticsCollector.cpp
0       0       0               Executionctrlvalues.cpp
Lines   Words   Bytes
of Text
1046    2676    25734

$ ./wc '*'
$ ./wc '*.filters'
Lines   Words   Bytes
of Text
77      126     2762            wconsteroids.vcxproj.filters
Lines   Words   Bytes
of Text
77      126     2762

(Notice we get a "total" line even though only one file was processed, contrary to POSIX specification for wc.

It doesn't even seem to be possible to operate on a single file called *.x.

And it always exits with success status (0) - even when the usage message is printed to stderr.


Split the program into two on Windows

I believe we can split the program into two, assuming we have a working pipe implementation.

  1. Start with a simple implementation that iterates through the command line, processing each file name as it occurs.
  2. Implement an option for the program to accept a list of files on its input stream (probably use NUL as separator here) instead of (or as well as) command line arguments.
  3. On Windows (inside #ifdef), start a child process to process the filename arguments (including -R and any future options affecting which files are read), which writes the filenames to its output stream.
  4. Make sure the two processes are connected appropriately, and enable the files-on-input option in the parent.

Then you have two pieces - the portable wc program and the Windows-specific file-name preprocessor. The latter could be written independently and used by any program, just as find is on Unixy platforms.

\$\endgroup\$
3
  • \$\begingroup\$ Based on the POSIX specification the usage message should be going to stdout, another bug. wc *.cpp works for me not sure why it doesn't work for you yet. \$\endgroup\$
    – pacmaninbw
    Nov 17, 2022 at 15:57
  • 1
    \$\begingroup\$ wc '*.cpp' works for me (passing the * into the program) , but not wc *.cpp which is expanded by the shell. \$\endgroup\$ Nov 17, 2022 at 16:42
  • 1
    \$\begingroup\$ AIUI, usage message given due to wrong usage is a diagnostic message, so it's correct according to "The standard error shall be used only for diagnostic messages". If we were to provide a -h/--help option, then that becomes program output and should go to standard error. Most utilities follow that principle. \$\endgroup\$ Nov 17, 2022 at 16:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.