MyWCprj.exe
1. What is MyWCprj.exe?
wc
是linux下一个非常好用的代码统计小工具,可以通过 -c
、-w
、-l
等选项分别进行对指定文件的代码字符数、词组数、行数统计。
应学校软件工程课程的PSP个人项目开发实践需要,特此尝试自己写一个用c++实现的wc
(MyWCprj.exe
)。
该程序仅能确保统计.c .cpp .h文件代码数的正确性。
语法
MyWCprj.exe [parameter] [fileOrDir_name1] [fileOrDir_name2] ……
功能参照
MyWCprj.exe -c file.c //输出文件 file.c 的字符数
MyWCprj.exe -w file.c //输出文件 file.c 的词的数目
MyWCprj.exe -l file.c //输出文件 file.c 的行数
MyWCprj.exe -a file.c //输出文件 file.c
MyWCprj.exe -s fileDir //递归处理目录fileDir及其子目录中代码文件,无其他参数时仅列举目录fileDir及其子目录中代码文件(.c .cpp .h),可配合一个或多个 -c -w -l -a 一起处理。
MyWCprj.exe -l file1.c file2.c file3.c //输出file1.c、file2.c、file3.c的行数
MyWCprj.exe -s -l DirPath1 DirPath2 DirPath3 //递归输出DirPath1、DirPath2、DirPath3目录及其子目录下所有代码文件的行数
测试示例
参考中典型的源代码文件 CommonSrcCode.cpp:
1 #include"WChead.h" 2 3 4 int main(int argc,char *argv[]) 5 { 6 /*cout << "ar num=" << argc << endl; 7 for (int i = 0; i < argc; i++) 8 { 9 cout << "No." << i << " "; 10 cout << "ar =" << argv[i] << endl; 11 } 12 system("PAUSE");*/ 13 14 if (argc < 2) 15 { 16 cout << "" << endl; 17 return -1; 18 } 19 20 class_WordCal myWC; 21 myWC.workMain(argc, argv); 22 23 //system("PAUSE"); 24 return 0; 25 }
测试:
# 运行环境:windows 10 pro,ver 1803,powershell # 使用 dir 命令,可看出 I:\TmpCode 下的示例代码文件 PS ..\MyWCprj\Debug> dir I:\TmpCode Directory: I:\TmpCode Mode LastWriteTime Length Name ------ #典型的源代码文件 CommonSrcCode.cpp -a---- 9/14/2018 9:34 AM 384 CommonSrcCode.cpp #空代码文件 EmptyCode.cpp -a---- 9/14/2018 7:29 PM 0 EmptyCode.cpp #仅有字符‘a‘的代码文件 Onechar.cpp -a---- 9/14/2018 7:32 PM 1 Onechar.cpp #仅有字符串“#pragma once”的代码文件 Oneline.cpp -a---- 9/14/2018 7:34 PM 12 Oneline.cpp #仅有字符串“#define” 的代码文件 Oneword.cpp -a---- 9/14/2018 7:32 PM 7 Oneword.cpp # 使用 .\MyWCprj.exe -s -w -c -l -a I:\TmpCode 组合 # 可看出 MyWCprj.exe 递归输出当前目录下的代码文件对应信息 PS ..\MyWCprj\Debug> .\MyWCprj.exe -s -w -c -l -a I:\TmpCode list_size: 5 No.0 I:\TmpCode\CommonSrcCode.cpp: The Num of chars: 360 I:\TmpCode\CommonSrcCode.cpp The Num of words: 45 I:\TmpCode\CommonSrcCode.cpp The Num of Lines: 24 I:\TmpCode\CommonSrcCode.cpp The Num of Effective lines: 8 I:\TmpCode\CommonSrcCode.cpp The Num of Empty lines: 9 I:\TmpCode\CommonSrcCode.cpp The Num of Comment lines: 8 I:\TmpCode\CommonSrcCode.cpp No.1 I:\TmpCode\EmptyCode.cpp: The Num of chars: 0 I:\TmpCode\EmptyCode.cpp The Num of words: 0 I:\TmpCode\EmptyCode.cpp The Num of Lines: 0 I:\TmpCode\EmptyCode.cpp The Num of Effective lines: 0 I:\TmpCode\EmptyCode.cpp The Num of Empty lines: 1 I:\TmpCode\EmptyCode.cpp The Num of Comment lines: 0 I:\TmpCode\EmptyCode.cpp No.2 I:\TmpCode\Onechar.cpp: The Num of chars: 1 I:\TmpCode\Onechar.cpp The Num of words: 1 I:\TmpCode\Onechar.cpp The Num of Lines: 0 I:\TmpCode\Onechar.cpp The Num of Effective lines: 1 I:\TmpCode\Onechar.cpp The Num of Empty lines: 0 I:\TmpCode\Onechar.cpp The Num of Comment lines: 0 I:\TmpCode\Onechar.cpp No.3 I:\TmpCode\Oneline.cpp: The Num of chars: 12 I:\TmpCode\Oneline.cpp The Num of words: 2 I:\TmpCode\Oneline.cpp The Num of Lines: 0 I:\TmpCode\Oneline.cpp The Num of Effective lines: 1 I:\TmpCode\Oneline.cpp The Num of Empty lines: 0 I:\TmpCode\Oneline.cpp The Num of Comment lines: 0 I:\TmpCode\Oneline.cpp No.4 I:\TmpCode\Oneword.cpp: The Num of chars: 7 I:\TmpCode\Oneword.cpp The Num of words: 1 I:\TmpCode\Oneword.cpp The Num of Lines: 0 I:\TmpCode\Oneword.cpp The Num of Effective lines: 1 I:\TmpCode\Oneword.cpp The Num of Empty lines: 0 I:\TmpCode\Oneword.cpp The Num of Comment lines: 0 I:\TmpCode\Oneword.cpp
2. PSP实践
PSP2.1表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 430 | 120 |
Estimate | 估计这个任务需要多少时间 | 30 | 15 |
Development | 开发 | 400 | 300 |
Analysis | 需求分析 (包括学习新技术) | 150 | 45 |
Design Spec | 生成设计文档 | 20 | 40 |
Design Review | 设计复审 (和同事审核设计文档) | 20 | 60 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范)划 | 10 | 85 |
Design | 具体设计 | 30 | 120 |
Code Review | 代码复审 | 30 | 60 |
Test | 测试(自我测试,修改代码,提交修改) | 30 | 120 |
PReporting | 报告 | 110 | 50 |
Test Report | 测试报告 | 30 | 60 |
Size Measurement | 计算工作量 | 20 | 40 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 60 | 70 |
合计 | 计划 | 430 | 680 |
3. 分析思路
由于曾经有过在linux上逆向分析二进制程序的经验,对于ltrace
、wc
、objdump
、ptrace
、gdb
等二进制分析与调试工具有一定的了解与使用经历,所以对于wc
功能也有一定的了解。
于是乎,去GNU
找WC
命令行工具的C源码,简洁且优雅,在性能与可读性上有非常好的平衡。据其分析之,于是就以C++的方式实现了-c
、-w
、-l
的功能,并初步封装入c++自定义的类class_WordCal
中。
对于-a
功能,要求分析出待统计代码文件的空行、注释行、有效代码行。先是与室友沟通,结果很天马行空,基于字符判断的分析模式利用代码实现很困难且可读性不佳,于是在google中查阅与编译原理、代码预处理相关的网页与资料,结果在csdn论坛中获得启发。依然是源码注释行统计.请进来看看,感谢之至!。可利用状态机的思想:
假定一开始是非注释状态,读入一个字符,如果不是/,保持非注释状态继续读入字符, 如果字符是/,读入下一个字符,如果不是*,保持非注释状态,如果是*,进入注释状态; 在注释状态,类似的读入字符,如果不是*,保持注释状态继续读入字符, 如果字符是*,读入下一个字符,如果不是/,保持注释状态,如果是/,退出注释状态进入非字符状态。
于是自己依据该思想创建了一个结构体:
1 typedef struct { 2 bool bEffective; //有效行标识 3 bool bComm1; // "//" 注释行标识 4 bool bComm2; // "/**/" 注释行标识 5 }AllStatus; //高级统计状态标识结构体
具体的功能思路的代码实现见Github代码有。
至于-s
功能,由于对windows的文件操作不算太熟悉,于是在网上查找目录递归遍历的思路,基本上就是调用诸如_findfirst
、_findnext
的库函数,逐个获取目录下的子目录或文件的路径并存之于对应的string
类的vector
,遇到子目录则对子目录路径进行递归遍历。如此反复,最终可获得一个关于所有子目录路径的vector<string>
,和关于所有代码文件路径的vector<string>
。然后根据每一个代码文件路径实现指定的分析与信息输出功能即可。难点在于如何实现递归遍历上,这对于提升编程能力与启发编程思想来说是一个很好的实践。
4. 大体设计
- 高级统计(-a)状态结构体
AllStatus:
1 typedef struct { 2 bool bEffective; //有效行标识 3 bool bComm1; // "//" 注释行标识 4 bool bComm2; // "/**/" 注释行标识 5 }AllStatus; //高级统计状态标识结构体
- 数据统计类
class_data
:1 class class_Data 2 { 3 public: 4 int iNumOfChar; //存储当前文件的字符数 5 int iNumOfWord; //存储当前文件的单词数 6 int iNumOfLine; //存储当前文件的行数 7 int iNumOfEffectiveLine; //存储当前文件的有效代码行数 8 int iNumOfEmptyLine; //存储当前文件的空行数 9 int iNumOfCommentLine; //存储当前文件的注释行数 10 string strTmpRowStr; //临时存储刚从文件读入的一行代码 11 AllStatus allStatus; //高级统计状态机 12 13 class_Data() 14 { 15 iNumOfChar = 0; 16 iNumOfWord = 0; 17 iNumOfLine = 0; 18 iNumOfEffectiveLine = 0; 19 iNumOfEmptyLine = 0; 20 iNumOfCommentLine = 0; 21 allStatus.bComm1 = false; 22 allStatus.bComm2 = false; 23 allStatus.bEffective = false; 24 allStatus.bEmpty = false; 25 } 26 27 ~class_Data() 28 { 29 30 } 31 32 };
- 总功能类设计class_WordCal:
1 class class_WordCal : public class_Data 2 { 3 public: 4 bool bOpChar; //字符统计输出开关 5 bool bOpLine; //行数统计输出开关 6 bool bOpWord; //词组统计输出开关 7 bool bOpRecursion; //递归统计开关 8 bool bOpAll; //更复杂数据统计开关 9 int iFileNameIndex; //存储文件名的stringVector中带操作的文件名下标 10 ifstream tmpifs; //存储当前打开文件的文件读操作类 11 vector<string> vStrFileOrDirName; //存储当前文件名的stringVector 12 vector<string> vStrCodeFilePath; //存储文件或目录路径的stringVector 13 string strFilePath; //存储当前文件的路径 14 15 16 void Option(int argc, char *argv[]); //功能选择函数 17 void Display(int Index); //统计输出函数 18 void StringSkip(string s, int &pos); //当s中存在‘ ‘ ‘\t‘ ‘\n‘ ‘\r‘时,将pos置于该字符之后 19 void myFileFind(string DirPath); //对传入目录路径下的所有代码文件(.c .cpp .h) 录入至 vStrCodeFilePath 中 20 void DirList(vector<string> &vStr, const string strPath); //从strPath指定的路径中递归查找代码文件,并录入至vStrCodeFilePath中 21 22 int getword(FILE *fp); //从输入流中获取下一个词组,读取至文件结尾或异常情况出现时返回0,返回1则为其他情况 23 Status SetIfstream(vector<string> vStr, int Index); //设置打开文件的文件读操作类 24 //Status CountFromFile(int Index); //(旧代码,不兼容-s统计功能)对vStrFileOrDirName中第Index个文件名对应文件进行代码统计(-l -w -c) 25 //Status CountFromFileAll(int Index); //(旧代码,不兼容-s统计功能)对vStrFileOrDirName中第Index个文件名对应文件进行高级代码统计(-a) 26 27 Status CountFromDir(int Index); //对vector<string>中第Index个目录下的所有代码文件进行递归统计(-s) 28 Status CountFromFile(vector<string> vStr, int Index);//对vector<string>中第Index个文件名对应文件进行代码统计(-l -w -c) 29 Status CountFromFileAll(vector<string> vStr, int Index);//对vector<string>中第Index个文件名对应文件进行高级代码统计(-a) 30 void Display(vector<string> vStr, int Index); //vector<string>中第Index个文件名输出统计函数 31 Status EmptyCheck(string s, int pos); //空行检查与统计 32 33 void CountMain(); //统计功能入口 34 void workMain(int argc, char *argv[]); //类主功能函数入口 35 36 };
5. 关键功能伪码
1. -c -w -l :
1 wc_basic_function(FileName) 2 { 3 File *fp = fopen(FileName, "r"); 4 char c; 5 bool bEnd = false; 6 while(bEnd != true) 7 { 8 while ((c = getc(fp)) != EOF) 9 { 10 if (isalpha(c)) //调用stdarg.h中的isalpha(),判断是否为词组,是则词组数++; 11 { 12 iNumOfWord++; 13 break; 14 } 15 iNumOfChar++; 16 if ((c) == ‘\n‘) 17 { 18 iNumOfLine++; 19 } 20 } 21 while (c != EOF) //由于词组判断完后上一个循环结束,此循环将继续统计行数及字符数。 22 { 23 iNumOfChar++; 24 if ((c) == ‘\n‘) 25 { 26 iNumOfLine++; 27 } 28 if (!isalpha(c)) 29 { 30 break; 31 } 32 c = getc(fp); 33 } 34 bEnd = (c != EOF); 35 } 36 }
2. -a:
1 function_GetFileAllInfo() 2 { 3 Status tmpStatus; 4 allStatus.bComm1 = allStatus.bComm2 = allStatus.bEffective = false; //高级统计状态机初始化 5 iNumOfEffectiveLine = iNumOfEmptyLine = iNumOfCommentLine = 0; //代码行数、空行数、注释行数置零初始化 6 while (!tmpifs.eof()) 7 { 8 pos = 0; 9 allStatus.bEffective = false; 10 getline(tmpifs, strTmpRowStr); //读取文件中的一行代码 11 12 StringSkip(strTmpRowStr, pos); 13 14 tmpStatus = EmptyCheck(strTmpRowStr, pos); //空行检查与统计 15 if (en_true == tmpStatus) 16 { 17 continue; 18 } 19 else if (en_fail == tmpStatus) 20 { 21 return en_fail; 22 } 23 24 while (1) 25 { 26 if (false == allStatus.bComm2 27 && ‘/‘ == strTmpRowStr[pos] 28 && ‘/‘ == strTmpRowStr[pos + 1]) //读取到"//"的情况 29 { 30 if (false == allStatus.bEffective) 31 { 32 iNumOfCommentLine++; 33 } 34 else 35 { 36 iNumOfEffectiveLine++; 37 } 38 break; 39 } 40 41 if (false == allStatus.bComm2 42 && ‘}‘ == strTmpRowStr[pos] 43 && ‘/‘ == strTmpRowStr[pos + 1] 44 && ‘/‘ == strTmpRowStr[pos + 2]) //读取到"}//"的情况 45 { 46 if (false == allStatus.bEffective) 47 { 48 iNumOfCommentLine++; 49 } 50 else 51 { 52 iNumOfEffectiveLine++; 53 } 54 break; 55 } 56 57 if (false == allStatus.bComm2 58 && ‘/‘ == strTmpRowStr[pos] 59 && ‘*‘ == strTmpRowStr[pos + 1]) //读取到"/*"的情况 60 { 61 pos += 2; 62 allStatus.bComm2 = true; 63 continue; 64 } 65 if (true == allStatus.bComm2) // "/**/"的过滤处理 66 { 67 if (‘*‘ == strTmpRowStr[pos] && ‘/‘ == strTmpRowStr[pos + 1]) //读到"*/"时的状态处理 68 { 69 pos++; 70 allStatus.bComm2 = false; 71 iNumOfCommentLine++; 72 } 73 else if (‘\0‘ == strTmpRowStr[pos]) //读取至行末时的处理 74 { 75 if (true == allStatus.bEffective) //若此时为有效代码状态,则代码行数++。例如 printf("hello\n"); /*this is a hello print*/ 76 { 77 iNumOfEffectiveLine++; 78 } 79 else //否则,此处代表纯注释行,则注释行数++ 80 { 81 iNumOfCommentLine++; 82 } 83 break; 84 } 85 pos++; 86 continue; 87 } 88 89 90 if (‘\0‘ == strTmpRowStr[pos]) //运行至行末 91 { 92 if (allStatus.bEffective == false) 93 { 94 if (allStatus.bComm1 || allStatus.bComm2) //纯注释行 95 { 96 ++iNumOfCommentLine; 97 } 98 } 99 else if (allStatus.bEffective == true) 100 { 101 ++iNumOfEffectiveLine; 102 } 103 break; 104 } 105 pos++; 106 allStatus.bEffective = true; 107 } 108 } 109 tmpifs.close(); 110 }
3. -s:
递归算法伪码:
1 class cal{ 2 ... 3 vector<string> vFilePath; 4 vector<string> vDirPath; 5 ... 6 wc_GetFile_main(string strDirPath) //递归遍历目录,并将所有代码文件路径传入FilePath中 7 { 8 string tmpStr; 9 DirList(vDirPath, strDirPath + "\\*"); //从strDirPath + "\\*"指定的路径中递归查找代码文件,并录入至vFilePath中;将strDirPath的子目录名传入vDirPath中。 10 for (i = 0; i < vDirPath.size(); ++i) 11 { 12 tmpPath = strDirPath + "\\" + vDirPath[i]; //子目录路径拼接。 13 myFileFind(tmpPath); //对子目录进行递归操作。 14 } 15 } 16 17 DirList(verctor<string> &vStr,string strPath) //获取当前目录下的子目录名与代码文件路径,分别录入到vStr与vFilePath中。 18 { 19 vector<string> vStrTmp; 20 _finddata_t fileDir; 21 long lfDir = _findfirst(strPath.c_str(), &fileDir); 22 int tmpPos; 23 24 if (-1l == lfDir) //尚未实现具体的文件类型识别,仅进行_findfirst是否成功的判断 25 { 26 cout << "DirList: "<< strPath <<" File or dir not found!" << endl; 27 } 28 else 29 { 30 string tmp = strPath; 31 tmp.erase(tmp.size() - 1, 1);//去除路径中的‘*‘ 32 do 33 { 34 string FileOrDirName(fileDir.name); 35 if (FileOrDirName.find(‘.‘) == -1) //目录则将目录名传入vStrTmp中 36 { 37 vDirPath.push_back(fileDir.name); 38 } 39 else if( 40 ((tmpPos = FileOrDirName.find(".cpp")) != -1 && FileOrDirName[tmpPos + 4] == ‘\0‘) //精确匹配.cpp字串 41 || ((tmpPos = FileOrDirName.find(".c")) != -1 && FileOrDirName[tmpPos + 2] == ‘\0‘) //精确匹配.c字串 42 || ((tmpPos = FileOrDirName.find(".h")) != -1 && FileOrDirName[tmpPos + 2] == ‘\0‘) //精确匹配.h字串 43 )//代码文件则将其路径传入vStrCodeFilePath中 44 { 45 vFilePath.push_back(tmp + FileOrDirName); 46 } 47 } while (_findnext(lfDir, &fileDir) == 0); 48 } 49 _findclose(lfDir); 50 vStr = vStrTmp; 51 } 52 ... 53 };
在获得到所有代码文件路径的vector<string> vFilePath后,逐一对vFilePath内各个路径进行代码统计与信息输出即可,简单的迭代就可以实现。
总结
- 前期的准备工作非常重要,知识学习与储备、类似的程序框架的参考、算法的评估、按需求设计功能模块等等,都是为中期代码开发提供方向与实现基础的,不重视前期工作的话在中期代码开发与后期的测试、文档撰写方面要吃很大的亏,代码体现的思路与实现方式也难以简洁优雅。这次实践中我就是吃了这个亏,导致在具体代码实现时做了很多前期需要做的工作,流程很乱,效率不高。
- 注意待统计代码文件的编码类型,utf-8在此程序中表现良好。曾在测试环节中遇到对unicode编码的文件进行统计,算法失败地一塌糊涂,统计结果与实际也相差甚远。在接近有一个半小时的时间里都是在debug,试图在算法上找错误的根源,无果。最终发现是unicode编码下宽字符串的问题,换用utf-8后表现良好。
- 此次的时间较赶,感觉代码质量还不高,找机会整理好思路,按PSP的流程进行代码重构。
原文地址:https://www.cnblogs.com/HelloGaveu/p/9649580.html