项目要求
实现一个统计程序,它能正确统计程序文件中的字符数、单词数、行数,以及还具备其他扩展功能,并能够快速地处理多个文件。
具体功能要求:
程序处理用户需求的模式为:
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 返回更复杂的数据(代码行 / 空行 / 注释行)。(实现)
- 高级功能:
-x 参数。这个参数单独使用。如果命令行有这个参数,则程序会显示图形界面,用户可以通过界面选取单个文件,程序就会显示文件的字符数、行数等全部统计信息。
(未实现)
需求举例:
wc.exe -s -a *.c (实现)
返回当前目录及子目录中所有*.c 文件的代码行数、空行数、注释行数。
Github项目地址:https://github.com/kvhong/JAVA-to-bulid-wc.exe
解题思路
主函数思路:通过输入的命令来判断执行各个功能函数,将输入的命令分割为两部分,第一部分是指令,第二部分是文件路径,用指令来调用各个功能函数,文件路径则作为参数输入到各个功能函数中实现功能。
功能函数的共同部分:通过传入的文件路径并用Buffer流读取文件内容,对文件内容进行相应统计然后输出结果,需实现文件不存在或者路径输入错误提示。
词数统计函数:需要去除空行、各种符号,将独立的词统计出来;
字符统计函数:需要去除空行、空格,将其余的内容的每一个单元都示为一个字符统计出来;
行数统计函数:文件中全部行数除结尾空行不算外其他都统计;
空行:TAB、空格、回车形成的空行都算入;
注释行:单独存在//、/*、*/的算入注释行;
代码行:除了空行、注释行外属于代码行;
帮助:将各种命令列出,以更好的帮助使用;
遇到的问题及解决方法
一开始不知道如何对词、字符进行有效的分割,然后在网上学习到可以用正则表达式进行分割:使用下面语句将数字和和中英文标点符号和中文都替换成空格,以通过空格分割出各个词。
str.replaceAll("[\\p{Nd}\\u9fa5-\\uffe5\\p{Punct}\\s&&[^-]]", " ");
使用下面的正则匹配器匹配注释行和空行
Pattern nodeLinePattern = Pattern.compile("((//)|(/\\*+)|((^\\s)*\\*)|((^\\s)*\\*+/))+", Pattern.MULTILINE + Pattern.DOTALL); // 注释匹配器(匹配单行、多行、文档注释) Pattern spaceLinePattern = Pattern.compile("^\\s*$"); // 空白行匹配器(匹配回车、tab键、空格)
设计及代码
该程序通过一个主函数文件和一个方法函数文件组成。主函数文件用于输入参数并判断所输入的各种操作以调用方法函数。方法函数用于实现各种功能:
文件词数统计函数:getwordnumber() 命令:-w 文件路径(须包含完整路径)
//文件词数统计函数 void getwordnumber(String filename) throws IOException { String ret = null; int num=0; String[] strword = null; File file = new File(filename); if(file.exists()) { //读取文件 FileReader fr = new FileReader(filename); br = new BufferedReader(fr); String line = null; StringBuffer sbf = new StringBuffer(); while((line=br.readLine())!= null) { sbf.append(line); String str = sbf.toString(); //正则表达式替换数字和和中英文标点符号和中文 str = str.replaceAll("[\\p{Nd}\\u9fa5-\\uffe5\\p{Punct}\\s&&[^-]]", " "); //按空格将内容分割 strword = str.split("\\s+"); num=strword.length; } ret= "该程序文件中单词数为:"+num; br.close(); fr.close();
System.out.println(ret); }else { System.out.println("文件不存在,请重新输入文件!"); } }
文件字符数统计函数:getCharacternumber() 命令:-c 文件路径(须包含完整路径)
//文件字符统计函数 void getCharacternumber(String filename) throws IOException { String ret = null; int number = 0; String[] strword = null; File file = new File(filename); if(file.exists()) { //读取文件 FileReader fr = new FileReader(filename); br = new BufferedReader(fr); String line = null; String str=null; StringBuffer sbf = new StringBuffer(); while((line=br.readLine())!= null) { sbf.append(line); str = sbf.toString(); strword = str.split("\\s+"); } //通过正则匹配器匹配一个或多个字母和数字 for(int i=0;i<strword.length;i++) { Pattern pattern = Pattern.compile("[0-9a-zA-Z]*"); Matcher matcher = pattern.matcher(strword[i]); if(matcher.find()) { number+=matcher.regionEnd(); } } ret = "该程序文件中的字符数为:"+number; br.close(); fr.close(); System.out.println(ret); }else { System.out.println("文件不存在,请重新输入文件!"); } }
文件行数统计函数:getlinenumber() 命令:-l 文件路径(须包含完整路径)
//文件行数统计函数 void getlinenumber(String filename) throws IOException { String ret = null; int linenum = 0; File file = new File(filename); if(file.exists()) { //读取文件 FileReader fr = new FileReader(filename); //读取文件行数 LineNumberReader lnr = new LineNumberReader(fr); while(lnr.readLine()!= null) { linenum=lnr.getLineNumber(); } ret = "该程序文件中的行数为:"+linenum; lnr.close(); fr.close();
System.out.println(ret); }else { System.out.println("文件不存在,请重新输入文件!"); } }
统计文件或者文件夹中程序文件的空行、代码行、注释行,有递归文件目录功能的函数,将递归功能一同实现在此函数中:diffline() 命令:-a 文件路径或文件夹路径/-s-a 文件路径或文件夹路径
//统计文件或者文件夹中程序文件的空行、代码行、注释行,有递归文件目录功能 void diffline(File file) throws FileNotFoundException { int spaceline = 0; int nodeline = 0; int codeline = 0; if (file == null || !file.exists()) throw new FileNotFoundException(file + ",文件不存在!"); if (file.isDirectory()) { File[] files = file.listFiles(new FileFilter() { public boolean accept(File pathname) { return pathname.getName().endsWith(".java")|| pathname.isDirectory()||pathname.getName().endsWith(".c")||pathname.getName().endsWith(".cpp") ; } }); //递归文件目录 for (File target : files) { diffline(target); } } else { System.out.println("文件名:"+file.getAbsolutePath()); BufferedReader bufr = null; try { // 将指定路径的文件与字符流绑定 bufr = new BufferedReader(new InputStreamReader(new FileInputStream(file))); } catch (FileNotFoundException e) { throw new FileNotFoundException(file + ",文件不存在!" + e); } // 定义匹配每一行的正则匹配器 Pattern nodeLinePattern = Pattern.compile("((//)|(/\\*+)|((^\\s)*\\*)|((^\\s)*\\*+/))+", Pattern.MULTILINE + Pattern.DOTALL); // 注释匹配器(匹配单行、多行、文档注释) Pattern spaceLinePattern = Pattern.compile("^\\s*$"); // 空白行匹配器(匹配回车、tab键、空格) // 遍历文件中的每一行,并根据正则匹配的结果记录每一行匹配的结果 String line = null; try { while((line = bufr.readLine()) != null) { if (nodeLinePattern.matcher(line).find()) { nodeline ++; }else if (spaceLinePattern.matcher(line).find()) { spaceline ++; }else{ codeline ++; } } System.out.println("空行:"+spaceline); System.out.println("代码行:"+codeline); System.out.println("注释行:"+nodeline); } catch (IOException e) { throw new RuntimeException("读取文件失败!" + e); } finally { try { bufr.close(); // 关闭文件输入流并释放系统资源 } catch (IOException e) { throw new RuntimeException("关闭文件输入流失败!"); } } } }
帮助函数:help() 命令:?
//帮助函数 void help(){ System.out.println("-c 文件(须包含文件完整路径) 统计程序文件中的字符数"); System.out.println("-w 文件(须包含文件完整路径) 统计程序文件中的单词数"); System.out.println("-l 文件(须包含文件完整路径) 统计程序文件中的行数"); System.out.println("-a 文件(须包含文件完整路径) 统计程序文件中的空行数、代码行数、注释行数"); System.out.println("-s-a 文件路径或者文件夹路径 递归统计程序文件中的空行数、代码行数、注释行数");
System.out.println("? 帮助"); }
主函数:wctest.java
public class wctest{ private static Scanner scanner; public static void main(String[] args) throws IOException{ String str = null; wcfunction wcf = new wcfunction(); //循环询问命令输入 while(true) { System.out.print("请输入命令:"); //命令输入 scanner = new Scanner(System.in); if(scanner.hasNext()) { str=scanner.nextLine(); } //分割命令,第一个作为判断第二个为文件路径 String[] strword = str.split(" "); if(strword[0].equals("-c")) { wcf.getCharacternumber(strword[1]); }else if(strword[0].equals("-w")) { wcf.getwordnumber(strword[1]); }else if(strword[0].equals("-l")) { wcf.getlinenumber(strword[1]); }else if(strword[0].equals("-a")) { File file = new File(strword[1]); wcf.diffline(file); }else if(strword[0].equals("-s-a")) { File file = new File(strword[1]); wcf.diffline(file); }else if(strword[0].equals("?")) { wcf.help(); } } } }
测试
已将程序打包成wc.exe文件,放在Github项目中。可直接打开exe文件输入命令和文件路径直接使用,也可使用CMD命令行运行wc.exe再输入命令和文件路径使用。
CMD命令行使用:
exe直接使用:
代码覆盖率
代码覆盖率:91.8%
PSP
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
Planning | 计划 | 45 | 60 |
· Estimate | · 估计这个任务需要多少时间 | 45 | 60 |
Development | 开发 | 1290 | 1540 |
· Analysis | · 需求分析 (包括学习新技术) | 80 | 120 |
· Design Spec | · 生成设计文档 | 40 | 60 |
· Design Review | · 设计复审 (和同事审核设计文档) | 40 | 40 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
· Design | · 具体设计 | 120 | 160 |
· Coding | · 具体编码 | 780 | 920 |
· Code Review | · 代码复审 | 80 | 90 |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | 120 |
Reporting | 报告 | 240 | 380 |
· Test Report | · 测试报告 | 120 | 220 |
· Size Measurement | · 计算工作量 | 30 | 40 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 90 | 120 |
合计 | 1575 | 1980 |
总结
先是从课堂上更加意识到制定设计流程的重要性,了解到计划的设计不仅仅只存在于流程,对时间的规划也是非常重要的。在编写代码过程中实质遇到的困难是在如何实现词数的统计、正则表达式以及对文件目录的递归实现。最后通过学习正则表达式实现了对词数的统计,也进一步学习了正则表达式这一较为陌生的概念。也学会了一些新的插件和软件的使用:Eclipse的EclEmma插件用于代码覆盖率的统计;Git Bash软件对本地项目上传到Github使用;exe4j软件对JAVA项目进行EXE文件的打包使用。这次的项目作业对我来说是受益匪浅,对以后的项目实行有很大的帮助。