GitHub项目地址:https://github.com/oMIZUCHIo/WCProject
1.Word Count 项目要求:
wc.exe 是一个常见的工具,它能统计文本文件的字符数、单词数和行数。这个项目要求写一个命令行程序,模仿已有wc.exe 的功能,并加以扩充,给出某程序设计语言源文件的字符数、单词数和行数。
实现一个统计程序,它能正确统计程序文件中的字符数、单词数、行数,以及还具备其他扩展功能,并能够快速地处理多个文件。
具体功能要求:
程序处理用户需求的模式为:
wc.exe [parameter] [file_name]
基本功能列表:
wc.exe -c file.c //返回文件 file.c 的字符数
wc.exe -w file.c //返回文件 file.c 的词的数目
wc.exe -l file.c //返回文件 file.c 的行数
扩展功能:
-s 递归处理目录下符合条件的文件。
-a 返回更复杂的数据(代码行 / 空行 / 注释行)。
空行:本行全部是空格或格式控制字符,如果包括代码,则只有不超过一个可显示的字符,例如“{”。
代码行:本行包括多于一个字符的代码。
注释行:本行不是代码行,并且本行包括注释。一个有趣的例子是有些程序员会在单字符后面加注释:
} //注释
在这种情况下,这一行属于注释行。
[file_name]: 文件或目录名,可以处理一般通配符。
高级功能:
-x 参数。这个参数单独使用。如果命令行有这个参数,则程序会显示图形界面,用户可以通过界面选取单个文件,程序就会显示文件的字符数、行数等全部统计信息。
需求举例:
wc.exe -s -a *.c ===> 返回当前目录及子目录中所有*.c 文件的代码行数、空行数、注释行数。
2.预计开发时间 PSP
PSP2.1 |
Personal Software Process Stages |
预估耗时(分钟) |
实际耗时(分钟) |
Planning |
计划 |
20 |
|
· Estimate |
· 估计这个任务需要多少时间 |
20 |
|
Development |
开发 |
300 |
|
· Analysis |
· 需求分析 (包括学习新技术) |
60 |
|
· Design Spec |
· 生成设计文档 |
10 |
|
· Design Review |
· 设计复审 (和同事审核设计文档) |
0 |
|
· Coding Standard |
· 代码规范 (为目前的开发制定合适的规范) |
10 |
|
· Design |
· 具体设计 |
50 |
|
· Coding |
· 具体编码 |
80 |
|
· Code Review |
· 代码复审 |
30 |
|
· Test |
· 测试(自我测试,修改代码,提交修改) |
60 |
|
Reporting |
报告 |
60 |
|
· Test Report |
· 测试报告 |
30 |
|
· Size Measurement |
· 计算工作量 |
20 |
|
· Postmortem & Process Improvement Plan |
· 事后总结, 并提出过程改进计划 |
10 |
|
合计 |
|
760 |
|
3.解题思路
采用JAVA语言进行编写,并使用IO流进行文件信息的读入;将输入参数和输出结果进行封装,便于后期的更改;使用正则表达式进行部分信息的判断;使用javafx做图形化界面。
遇到的问题:对JAVA正则等部分语法不太熟悉,需要边做项目边不断地查找资料;觉得项目的需求有点模棱两可,后选定我认为的理解进行实现,可能会理解错。
4.设计实现过程
主要组成为:一个主函数类,作为整个项目的入口;一个WC实现类,用于各信息的计算;一个图形化类,用于调起图形化窗口。
其中因字符数,词数,行数的计算可方便的在一次文件IO读取中便进行计算,因此决定将它们在一个方法中实现,再根据要求进行输出,基本不会影响性能
函数:
process:实现类的对外调度口,用于进行参数判断,方法实现,以及结果信息拼接的函数
searchFiles:用于递归处理文件目录
countingSimply:计算基础信息:字符数,词数,行数
countingComplex:计算代码行数,空行数,注释行数
5.代码说明
(1)主函数
public static void main(String[] args) { WCUtil wcUtil = new WCUtil(); Parameter parameter = new Parameter(); if(args == null || args.length == 0){ System.out.println("\n请输入指令"); Scanner scanner = new Scanner(System.in); args = scanner.nextLine().split("\\s+"); } if(!args[0].equals("wc.exe")){ System.out.println("\n当前只可运行wc.exe"); return ; } //最后一个路径为文件路径 String lastParam = args[args.length - 1]; if(!lastParam.equals("-x")){ parameter.setFilePath(lastParam); }else{ parameter.setFrame(true); } for (int i = 1; i < args.length - 1; i++) { switch (args[i]) { case "-c": //计算 file.c 的字符数 parameter.setCountChar(true); break; case "-w": //计算 file.c 的单词数 parameter.setCountWord(true); break; case "-l": //计算 file.c 的行数 parameter.setCountLine(true); break; case "-s": //使用递归处理目录下符合条件的文件 parameter.setRecurrence(true); break; case "-a": //返回更复杂的数据(代码行 / 空行 / 注释行) parameter.setCountComplex(true); break; case "-x": //调用图形化界面 parameter.setFrame(true); break; default: System.out.println("\n指令出错"); } } if(parameter.isFrame()){ Frame.main(args); }else{ System.out.println(wcUtil.process(parameter)); } }
(2)实现类调用方法
/** * @description 对外调用接口 * @param parameter 请求参数 */ String process(Parameter parameter){ //对输入参数进行预处理,判断 参数是否合法,是否含有通配符 String errormsg = judgeFilePath(parameter); if(errormsg != null) return errormsg; //各文件的查询结果集合 List<Count> countList = new ArrayList<>(); //查询文件相应信息 searchFiles(parameter,countList); if(countList.size() == 0) return "\n未有符合条件的文件"; StringBuilder stringBuffer = new StringBuilder(); for(Count count : countList){ stringBuffer.append("\n文件名:").append(count.getFileName()); if(parameter.isCountChar()){ stringBuffer.append("\n字符数:").append(count.getCharNum()); } if(parameter.isCountWord()){ stringBuffer.append("\n词数:").append(count.getWordNum()); } if(parameter.isCountLine()){ stringBuffer.append("\n行数:").append(count.getLineNum()); } if(parameter.isCountComplex()){ stringBuffer.append("\n代码行数:").append(count.getCodeLineNum()); stringBuffer.append("\n空行数:").append(count.getEmptyLineNum()); stringBuffer.append("\n注释行数:").append(count.getNoteLineNum()); } stringBuffer.append("\n"); } return stringBuffer.toString(); }
(3)递归查询文件
/** * @description 查询文件 * @param parameter 命令与文件路径信息封装 */ private void searchFiles(Parameter parameter, List<Count> countList){ File file = new File(parameter.getFilePath()); if (file.isFile() && file.exists()) { //为文件 //当输入文件名不含通配符 或 含通配符且文件名匹配时才查询 if(parameter.getMatchName() == null || (parameter.getMatchName() != null && compareName(file.getName(),parameter.getMatchName()))){ //获取基础信息 Count count = countingSimply(file.getPath()); count.setFileName(file.getName()); //当含有 -s 命令时额外添加 代码行数 等信息 if(parameter.isCountComplex()){ toComplexCount(count,countingComplex(file.getPath())); } countList.add(count); } } else if (! file.exists()){ System.out.println("\n文件不存在"); } else if(file.isDirectory() && !parameter.isRecurrence()){ if(parameter.getMatchName() == null){ System.out.println("\n输入文件夹时,请加入-s命令进行递归查询"); }else{ System.out.println("\n输入文件含通配符时,请加入-s命令对目录进行递归查询"); } }else if (file.isDirectory() && parameter.isRecurrence()) { //为文件夹且需要递归时 File[] files = file.listFiles(); //获取文件列表 if(files != null && files.length != 0) { for (File f : files) { if (f.isDirectory()) { //为文件夹且需要递归 //递归查询子文件 parameter.setFilePath(f.getPath()); searchFiles(parameter,countList); } else if (f.isFile()) { //为文件 //当输入文件名不含通配符 或 含通配符且文件名匹配时才查询 if(parameter.getMatchName() == null || (parameter.getMatchName() != null && compareName(f.getName(),parameter.getMatchName()))) { //获取基础信息 Count count = countingSimply(f.getPath()); count.setFileName(f.getName()); //当含有 -s 命令时额外添加 代码行数 等信息 if(parameter.isCountComplex()){ toComplexCount(count,countingComplex(f.getPath())); } countList.add(count); } } } } } }
(4)计算字符数,词数,行数
/** * @description 获取字符数,词数,行数信息 * @param filePath 文件路径 * @return Count 结果封装 */ private Count countingSimply(String filePath){ int charNum = 0; int wordNum = 0; int lineNum = 0; try { BufferedReader reader = new BufferedReader(new FileReader(filePath)); String line; boolean flag = false; //为false表示 一个单词的结束 //IO逐行读取 while ((line = reader.readLine()) != null) { ++ lineNum; //逐个获取字符 for (int i = 0; i < line.length(); i++) { char c = line.charAt(i); //跳过空格类字符 if (c == ' ' || c == '\n' || c == '\t' || c == '\r') { continue; } ++ charNum; //如果字符为 空格、换行等 则为一个单词的结束 if (!(c >= 'a' && c <= 'z') && !(c >= 'A' && c <= 'Z')) { flag = false; //当flag为false时,表示上一个单词的结束,也代表新单词的开始 }else if (!flag) { flag = true; ++ wordNum; } } } reader.close(); } catch (IOException e) { System.out.println("IO执行出错:" + e.getMessage()); } return new Count(charNum,wordNum,lineNum); }
(5)计算代码行数,空行数,注释行数
其中按需求,多于一个字符的代码的行称为代码行,而注释行的前提是“本行不是代码行”,因此认为代码行优先级高于注释行的判断,因此如: "“code end //note"为代码行而不是注释行(奇怪.jpg)
/** * @description 查询代码行数,空行数,注释行数 * @param filePath 文件路径 */ private Count countingComplex(String filePath){ //单行注释 在单字符后的注释(多字符后的注释算做代码行,注释行的前提是 不是代码行) String singleLineNote01 = "(\\s*)([{};]?)(\\s*)(/{2})(.*)"; /*单行注释*/ //在单字符后的注释(多字符后的注释算做代码行,注释行的前提是 不是代码行) String singleLineNote02 = "(\\s*)([{};]?)(\\s*)(/\\*)(.*)(\\*/)(\\s*)"; // 多行注释开头 /* String muiltNoteStart = "(\\s*)(/\\*)(.*)"; // 多行注释结尾 */ String muiltNoteEnd = "(\\s*)(\\*/)(.*)"; Count count = new Count(); int codeLineNum = 0; int emptyLineNum = 0; int noteLineNum = 0; try{ BufferedReader reader = new BufferedReader(new FileReader(filePath)); String line = null; while ((line = reader.readLine()) != null) { int notBlankNum = 0; // 判断非空格类字符数量 //如果当行匹配注释行,则注释行加一并跳过后面操作,防止影响代码行和空行的计算 if (line.matches(singleLineNote01) || line.matches(singleLineNote02)) { // 单行注释统计 ++ noteLineNum; continue; } // 多行注释统计 if (line.matches(muiltNoteStart)) { //第一行 /* 算做注释行 while (!line.matches(muiltNoteEnd)){ ++ noteLineNum; line = reader.readLine(); } //最后一行 */ 也算做注释行 ++ noteLineNum; continue; } //逐个获取字符,此时已确定此行不是注释行,再根据字符数量判断是否为代码行或空行 for (int i = 0; i < line.length(); i++) { char c = line.charAt(i); ++ notBlankNum; if (c == ' ' || c == '\n' || c == '\t' || c == '\r') { -- notBlankNum; //减去多计算的非空格类数 } } if(notBlankNum > 1){ ++ codeLineNum; }else{ ++ emptyLineNum; } } reader.close(); count.setCodeLineNum(codeLineNum); count.setEmptyLineNum(emptyLineNum); count.setNoteLineNum(noteLineNum); } catch (IOException e) { System.out.println("IO执行出错:" + e.getMessage()); } return count; }
(6)其他
/** * @description 参数判断 */ private String judgeFilePath(Parameter parameter) { if(parameter.getFilePath() == null){ return "\n文件路径不能为空"; }else if(!(parameter.isCountChar() || parameter.isCountWord() || parameter.isCountLine() || parameter.isCountComplex() || parameter.isFrame())){ return "\n请输入 -c -w -l -a -x 中至少一个命令"; } //获取目录路径 String[] paths = parameter.getFilePath().split("\\\\"); String fileName = paths[paths.length - 1]; boolean flag = false; //先判断文件名中是否含有通配符(因为文件夹名中不含?,*字符,所以若含有通配符则为文件类型) for(int i = 0 ; i < fileName.length() ; i ++){ if(fileName.toCharArray()[i] == '*' || fileName.toCharArray()[i] == '?'){ flag = true; } } if(flag){ //含有 ?, * 即为文件类型 StringBuilder sb = new StringBuilder(); for(int i = 0 ; i < paths.length - 1 ; i ++) { if (i == paths.length - 2) { sb.append(paths[i]); } else { sb.append(paths[i]).append("\\"); } } //判断目录合法性 File dirFile = new File(sb.toString()); if(!dirFile.exists() || (dirFile.exists() && !dirFile.isDirectory())){ return "\n文件路径出错"; } //文件路径改为目录路径 parameter.setFilePath(sb.toString()); //设置需匹配的文件名 parameter.setMatchName(fileName); return null; } File file = new File(parameter.getFilePath()); //文件名中不含有通配符时再判断文件是否存在,防止通配符对文件存在判断造成影响 if(!file.exists()){ return "\n文件或文件夹不存在"; } return null; } /** * @description 文件名是否匹配 * @param fileName 实际文件名 * @param matchName 含通配符的文件名 */ private boolean compareName(String fileName, String matchName){ matchName = matchName.replaceAll("\\?","(.?)").replaceAll("\\*","(.*)"); matchName = "^" + matchName + "$"; return fileName.matches(matchName); }
6. 测试运行
测试文件:
测试类:
public class WCTest { @Test public void TestAll(){ System.out.println("________TestBase________"); TestBase(); System.out.println("________TestEx________"); TestEx(); System.out.println("________TestDir________"); TestDir(); System.out.println("________TestMatch________"); TestMatch(); } @Test public void TestBase(){ String param = "wc.exe -c D:\\tmp\\WCTest.txt"; Main.main(param.split("\\s+")); } @Test public void TestEx(){ String param = "wc.exe -c -w -l -a D:\\tmp\\WCTest.txt"; Main.main(param.split("\\s+")); } @Test public void TestDir(){ String param = "wc.exe -c -w -l -a -s D:\\tmp"; Main.main(param.split("\\s+")); } @Test public void TestMatch(){ String param = "wc.exe -c -w -l -a -s D:\\tmp\\WC*.txt"; Main.main(param.split("\\s+")); } @Test public void TestFrame(){ String param = "wc.exe -x"; Main.main(param.split("\\s+")); } }
测试运行:
________TestBase________ 文件名:WCTest.txt 字符数:69 词数:5 行数:13 ________TestEx________ 文件名:WCTest.txt 字符数:69 词数:5 行数:13 代码行数:2 空行数:6 注释行数:5 ________TestDir________ 文件名:Test.txt 字符数:8 词数:1 行数:1 代码行数:1 空行数:0 注释行数:0 文件名:WCTest02.txt 字符数:19 词数:2 行数:4 代码行数:1 空行数:2 注释行数:1 文件名:WCTest.txt 字符数:69 词数:5 行数:13 代码行数:2 空行数:6 注释行数:5 ________TestMatch________ 文件名:WCTest02.txt 字符数:19 词数:2 行数:4 代码行数:1 空行数:2 注释行数:1 文件名:WCTest.txt 字符数:69 词数:5 行数:13 代码行数:2 空行数:6 注释行数:5
图形化界面测试:
7.PSP实际花费时间
PSP2.1 |
Personal Software Process Stages |
预估耗时(分钟) |
实际耗时(分钟) |
Planning |
计划 |
20 |
15 |
· Estimate |
· 估计这个任务需要多少时间 |
20 |
15 |
Development |
开发 |
300 |
430 |
· Analysis |
· 需求分析 (包括学习新技术) |
60 |
90 |
· Design Spec |
· 生成设计文档 |
10 |
10 |
· Design Review |
· 设计复审 (和同事审核设计文档) |
0 |
0 |
· Coding Standard |
· 代码规范 (为目前的开发制定合适的规范) |
10 |
10 |
· Design |
· 具体设计 |
50 |
80 |
· Coding |
· 具体编码 |
80 |
100 |
· Code Review |
· 代码复审 |
30 |
40 |
· Test |
· 测试(自我测试,修改代码,提交修改) |
60 |
100 |
Reporting |
报告 |
60 |
80 |
· Test Report |
· 测试报告 |
30 |
50 |
· Size Measurement |
· 计算工作量 |
20 |
25 |
· Postmortem & Process Improvement Plan |
· 事后总结, 并提出过程改进计划 |
10 |
15 |
合计 |
|
760 |
1060 |
8.项目小结
本次项目我花了较多的时间用于代码的测试和学习新知识上,因此花了远比我认为多的时间,并且最后的结果也仍有一定的问题。
同时在开发前没有很好地进行项目结构的设计,导致在具体的代码实现时出现了将原有的部分结构推翻重弄的情况,虽然因为这个项目不会很复杂,没有花多少时间重弄,但如果今后遇到的结构更为复杂的项目,必会给我带来很大的麻烦,因此我在今后的学习中应多注重项目流程的完整实现。