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;
}