GitHub项目地址:https://github.com/JackyLin18/word-count
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 文件的代码行数、空行数、注释行数。
解题思路:
分析项目要求,核心内容是需要读取指定文件,使用jdk提供的FileReader类和BufferedReader类,可以便捷地实现读取文件和记录字符数、行数的功能。
设计实现过程:
- 项目的代码构成:一共需要编写四个类,分别为:WordCountMain、FileUtil、ParamsUtil、FileFrame。
- WordCountMain类为程序入口类,其中包括main()方法和show()方法。main()方法的功能:调用相关方法对用户输入的参数进行判断和操作,根据用户的输入调用不同的功能方法,输出操作结果。show()方法的功能:打印用户菜单。
- FileUtil类为文件操作类,是项目的核心类,其中包括getFileList()方法,getCharsetsCount()方法,getWordsCount()方法,getLinesCount()方法和getCBNLinesCount()方法共五个静态方法。①getFileList()方法的功能:根据传入的文件是否为文件夹:是文件夹,遍历文件夹中的每一个文件。如果文件名符合通配符,将文件放入List中返回。②getCharsetsCount()方法的功能是:返回指定文件的字符数。③getWordsCount()方法的功能是:返回指定文件的单词数。④getLinesCount()方法的功能是:返回指定文件的行数目。⑤getCBNLinesCount()方法的功能是:返回指定文件的代码行数、空白行数、注释行数。
- ParamsUtil类为参数检查类,用于检查用户输入的参数是否合法,其中包括isExe()方法、isOperation()方法、isFileExist()方法这三个private方法和checkInputParams()方法这一个public方法。
- FileFrame类为用户提供图形界面,便于用户进行文件选择操作。
- 程序运行流程:程序启动,提示用户输入参数,程序通过调用ParamsUtil的checkInputParams()方法判断输入的参数是否合法,如果不合法,要求用户重新输入;如果合法,根据用户输入的参数调用对应的FileUtil类的相关方法,获得文件的相关数据后进行输出反馈给用户。
- 用户图形界面(GUI)说明:用户输入参数 “-frame” 后,调出图形界面。用户在图形界面可选择文件或者文件夹,根据用户选择的文件或者文件夹,程序自动识别出所有匹配的后缀供用户选择。同时,用户可在操作选项中选择需要进行的操作(-c, -w, -l, -a),用户选择完毕后点击确定,程序弹出窗口返回文件信息。
代码说明:
- FileUtil.getFileList(String filePath, String consistent)
1 // 根据传入的路径,如果表示的是一个文件夹,返回该文件夹下的所有符合通配符的子文件
2 public static List<File> getFileList(String filePath, String consistent) {
3 File file = new File(filePath);
4 // 存放遍历过程中的文件夹
5 LinkedList<File> temp_fileList = new LinkedList<>();
6 // 存放需要返回的符合条件的子文件
7 List<File> fileList = new ArrayList<>();
8 // 先将传入的指定文件放入temp_fileList中
9 temp_fileList.add(file);
10 // 如果该文件不是文件夹,将其放置fileList中并返回
11 if (!file.isDirectory()) {
12 // 如果通配符为默认的 ".*" 将所有文件加入fileList
13 if (consistent.equals(".*")) {
14 fileList.add(file);
15 } else {
16 // 如果文件与指定的格式相符,将其加入返回的fileList
17 if (filePath.endsWith(consistent)) {
18 fileList.add(file);
19 }
20 }
21 return fileList;
22 }
23 /*
24 如果temp_fileList中的每一个元素,如果该元素为文件夹,将整个元素放至temp_fileList中,
25 如果该元素为文件,但不符合通配符,不做任何处理
26 如果改文件为文件,且符合通配符,将其放至fileList中
27 */
28 while (!temp_fileList.isEmpty()) {
29 // 遍历temp_fileList
30 for (File f : Objects.requireNonNull(temp_fileList.removeFirst().listFiles())) {
31 // 如果该元素为文件夹,将其放至temp_fileList中
32 if (f.isDirectory()) {
33 temp_fileList.add(f);
34 } else {
35 // 如果通配符为默认的 ".*" 将所有文件加入fileList
36 if (consistent.equals(".*")) {
37 fileList.add(f);
38 } else {
39 // 如果文件与指定的格式相符,将其加入返回的fileList
40 if (f.getName().endsWith(consistent)) {
41 fileList.add(f);
42 }
43 }
44 }
45 }
46 }
47 return fileList;
48 }
- FileUtil.getCharsetsCount(String filePath)
1 // "-c"操作:传入一个文件路径,返回该文件的字符数
2 public static String getCharsetsCount(String filePath) throws IOException {
3 File file = new File(filePath);
4 int charsetsCount = 0;
5 String str = null;
6 // 装饰模式,使其获得多功能
7 FileReader fileReader = new FileReader(file);
8 BufferedReader reader = new BufferedReader(fileReader);
9 while ((str = reader.readLine()) != null) {
10 // 将所有的空格去除
11 str = str.replaceAll(" ", "");
12 charsetsCount += str.length();
13 }
14 fileReader.close();
15 // 返回指定文件文件名及其字符数
16 return "指定文件" + filePath + "的字符数:" + charsetsCount;
17 }
- FileUtil.getWordsCount(String filePath)
1 // "-w"操作:传入一个文件路径,返回该文件的词数
2 public static String getWordsCount(String filePath) throws IOException {
3 File file = new File(filePath);
4 int wordsCount = 0;
5 String str = null;
6 // 装饰模式,使其获得多功能
7 FileReader fileReader = new FileReader(file);
8 BufferedReader reader = new BufferedReader(fileReader);
9 while ((str = reader.readLine()) != null) {
10 // 两个单词之间使用空格分开
11 String[] strArray = str.split(" ");
12 for (String s : strArray) {
13 if (!s.equals("")) {
14 wordsCount++;
15 }
16 }
17 }
18 fileReader.close();
19 // 返回指定文件文件名及其单词数
20 return "指定文件" + filePath + "的单词数:" + wordsCount;
21 }
- FileUtil.getLinesCount(String filePath)
1 // "-l"操作:传入一个文件路径,返回该文件的行数
2 public static String getLinesCount(String filePath) throws IOException {
3 File file = new File(filePath);
4 int linesCount = 0;
5 // 装饰模式,使其获得多功能
6 FileReader fileReader = new FileReader(file);
7 BufferedReader reader = new BufferedReader(fileReader);
8 while ((reader.readLine()) != null) {
9 // 每读取一行,行数加一
10 linesCount++;
11 }
12 fileReader.close();
13 // 返回指定文件文件名及其行数目
14 return "指定文件" + filePath + "的行数目:" + linesCount;
15 }
- FileUtil.getCBNLinesCount(String filePath)
1 // "-a"操作:传入一个文件路径,输出该文件的代码行数、空行数、注释行数
2
3 /**
4 * 代码行:本行包括多于一个字符的代码
5 * 空行:本行全部是空格或格式控制字符,如果包括代码,则只有不超过一个可显示的字符,例如 "{"
6 * 注释行:本行不是代码行,并且本行包括注释。一个有趣的例子是有些程序员会在单字符后面加注释:
7 * } //注释
8 * 在这种情况下,这一行属于注释行
9 */
10 public static List<String> getCBNLinesCount(String filePath) throws IOException {
11 int codeLineCounts = 0; // 代码行数目
12 int blankLineCounts = 0; // 空白行数目
13 int noteLineCounts = 0; // 注释行数目
14 File file = new File(filePath);
15 // 装饰模式,使其获得多功能
16 FileReader fileReader = new FileReader(file);
17 BufferedReader reader = new BufferedReader(fileReader);
18 String str = null;
19 while ((str = reader.readLine()) != null) {
20 // 去掉空格
21 str = str.replaceAll(" ", "");
22 /*
23 如果包含 "//" 但是注释外的字符多于一个,为代码行
24 如果包含 "//" 而且注释外的字符少于或等于一个,为注释行
25 */
26 if (!str.contains("//")) {
27 if (str.length() > 1) {
28 codeLineCounts++;
29 } else {
30 blankLineCounts++;
31 }
32 } else {
33 if (str.substring(0, str.indexOf("//")).length() > 1) {
34 codeLineCounts++;
35 } else {
36 noteLineCounts++;
37 }
38 }
39 }
40 // 返回指定文件文件名及其代码行数、空白行数、注释行数
41 List<String> messages = new ArrayList<>();
42 messages.add("指定文件" + filePath + "的代码行数目:" + codeLineCounts + "\n");
43 messages.add("指定文件" + filePath + "的空白行数目:" + blankLineCounts + "\n");
44 messages.add("指定文件" + filePath + "的注释行数目:" + noteLineCounts + "\n");
45 fileReader.close();
46 return messages;
47 }
- WordCountMain.main(String[] args)
1 // 程序主入口
2 public static void main(String[] args) throws IOException, ClassNotFoundException, UnsupportedLookAndFeelException, InstantiationException, IllegalAccessException {
3 // 提供给用户菜单
4 show();
5 // 读取用户输入的操作
6 BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
7 String input = null;
8 // 创建一个判断输入参数的类实例
9 ParamsUtil paramsUtil = new ParamsUtil();
10 while ((input = reader.readLine()) != null) {
11 if(input.contains("-frame")){
12 new FileFrame();
13 }
14 // 判断用户的输入是否合法,如果合法,按用户要求的操作执行,如果不合法,要求用户重新输入
15 // 对输入进行 "添加通配符" 的处理
16 if (!input.contains("-s")) {
17 // 如果没有输入"-s",自动添加默认的通配符
18 input = input.concat(" .*");
19 } else {
20 // 如果输入了"-s",但没有输入通配符,也添加默认的通配符
21 String[] params = input.split(" ");
22 if (!params[params.length - 1].contains(".")) {
23 input = input.concat(" .*");
24 }
25 }27 if (paramsUtil.checkInputParams(input)) {
28 // 获得用户输入的参数数组
29 String[] params = input.split(" ");
30 int paramsLength = params.length;
31 for (int i = 1; i < paramsLength; i++) {
32 // FileUtil#getFileList()第一个参数为指定的文件路径,第二个参数为指定文件的通配符
33 switch (params[i]) {
34 case "-c":
35 // 输出指定文件的字符数
36 for (File f : FileUtil.getFileList(params[paramsLength - 2],
37 params[paramsLength - 1])) {
38 System.out.println(FileUtil.getCharsetsCount(f.getPath()));
39 }
40 break;
41 case "-w":
42 // 输出指定文件的单词数
43 for (File f : FileUtil.getFileList(params[paramsLength - 2],
44 params[paramsLength - 1])) {
45 System.out.println(FileUtil.getWordsCount(f.getPath()));
46 }
47 break;
48 case "-l":
49 // 输出指定文件的行数
50 for (File f : FileUtil.getFileList(params[paramsLength - 2],
51 params[paramsLength - 1])) {
52 System.out.println(FileUtil.getLinesCount(f.getPath()));
53 }
54 break;
55 case "-a":
56 // 输出指定文件的代码行数、空白行数、注释行数
57 for (File f : FileUtil.getFileList(params[paramsLength - 2],
58 params[paramsLength - 1])) {
59 for(String message:FileUtil.getCBNLinesCount(f.getPath())){
60 System.out.print(message);
61 }
62 }
63 break;
64 default:
65 break;
66 }
67 }
68 }
69 // 再次显示菜单提示用户输入
70 show();
71 }
72 }
- FileFrame
1 public class FileFrame implements ActionListener {
2 JFrame frame = new JFrame("文件选择");
3 JTabbedPane tabPane = new JTabbedPane();// 选项卡布局
4 Container container = new Container();
5 JPanel panel = new JPanel();
6 JLabel directoryLabel = new JLabel("文件目录");
7 JLabel fileLabel = new JLabel("选择文件");
8 JLabel consistentLabel = new JLabel("指定后缀");
9 JLabel operationLabel = new JLabel("选择操作");
10 // 显示选择的文件夹的路径
11 JTextField text1 = new JTextField();
12 // 显示选择的文件的路径
13 JTextField text2 = new JTextField();
14 // 文件夹选择按钮
15 JButton directoryButton = new JButton("选择");
16 // 文件选择按钮
17 JButton fileButton = new JButton("选择");
18 // 后缀名下拉框
19 JComboBox<String> comboBox = new JComboBox<>();
20 // 操作复选框
21 JCheckBox jCheckBoxC = new JCheckBox("-c");
22 JCheckBox jCheckBoxW = new JCheckBox("-w");
23 JCheckBox jCheckBoxL = new JCheckBox("-l");
24 JCheckBox jCheckBoxA = new JCheckBox("-a");
25 // 文件选择器
26 JFileChooser fileChooser = new JFileChooser();
27 // 提交按钮
28 JButton submitButton = new JButton("确定");
29 // 用一个List存放查询结果
30 List<String> messages = new ArrayList<>();
31 // 用一个List存放选择的操作
32 List<JCheckBox> checkBoxList = new ArrayList<>();
33
34 {
35 // 设置按钮格式
36 directoryButton.setBorder(BorderFactory.createRaisedBevelBorder());
37 directoryButton.setFocusPainted(false);
38 fileButton.setBorder(BorderFactory.createRaisedBevelBorder());
39 fileButton.setFocusPainted(false);
40 submitButton.setBorder(BorderFactory.createRaisedBevelBorder());
41 submitButton.setFocusPainted(false);
42 }
43
44 public FileFrame() throws ClassNotFoundException, UnsupportedLookAndFeelException, InstantiationException, IllegalAccessException {
45 String lookAndFeel = UIManager.getSystemLookAndFeelClassName();
46 UIManager.setLookAndFeel(lookAndFeel);
47
48 // 获得显示屏幕的宽度
49 double lx = Toolkit.getDefaultToolkit().getScreenSize().getWidth();
50 // 获得显示屏幕的高度
51 double ly = Toolkit.getDefaultToolkit().getScreenSize().getHeight();
52
53 // 设置窗口出现的位置
54 frame.setLocation(new Point((int) (lx / 2) - 150, (int) (ly / 2) - 150));
55 // 设置窗口的大小
56 frame.setSize(400, 220);
57 // 设置布局
58 frame.setContentPane(tabPane);
59
60 // 设置标签、按钮的位置和大小
61 fileLabel.setBounds(30, 20, 70, 20);
62 text2.setBounds(100, 20, 160, 20);
63 text2.setEditable(false);
64 fileButton.setBounds(270, 20, 80, 20);
65
66 directoryLabel.setBounds(30, 50, 70, 20);
67 text1.setBounds(100, 50, 160, 20);
68 text1.setEditable(false);
69 directoryButton.setBounds(270, 50, 80, 20);
70
71 consistentLabel.setBounds(30, 80, 70, 20);
72 comboBox.setBounds(100, 80, 160, 20);
73
74 operationLabel.setBounds(30, 110, 70, 20);
75 jCheckBoxC.setBounds(100, 110, 40, 20);
76 jCheckBoxW.setBounds(140, 110, 40, 20);
77 jCheckBoxL.setBounds(180, 110, 40, 20);
78 jCheckBoxA.setBounds(220, 110, 40, 20);
79
80 submitButton.setBounds(270, 110, 80, 20);
81
82 // 为三个按钮添加事件处理
83 directoryButton.addActionListener(this);
84 fileButton.addActionListener(this);
85 submitButton.addActionListener(this);
86
87 // 将组件全部添加到container中
88 container.add(directoryLabel);
89 container.add(text1);
90 container.add(directoryButton);
91 container.add(fileLabel);
92 container.add(text2);
93 container.add(fileButton);
94 container.add(submitButton);
95 container.add(consistentLabel);
96 container.add(comboBox);
97 container.add(operationLabel);
98 container.add(jCheckBoxC);
99 container.add(jCheckBoxW);
100 container.add(jCheckBoxL);
101 container.add(jCheckBoxA);
102
103 checkBoxList.add(jCheckBoxC);
104 checkBoxList.add(jCheckBoxW);
105 checkBoxList.add(jCheckBoxL);
106 checkBoxList.add(jCheckBoxA);
107
108 // 设置窗口可见
109 frame.setVisible(true);
110 // 设置关闭窗口结束程序
111 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
112 tabPane.add(container);
113 }
114
115 public void actionPerformed(ActionEvent e) {
116 File directoryChecked = null;
117 File fileChecked = null;
118 // 点击文件夹选择按钮,选择文件夹
119 if (e.getSource().equals(directoryButton)) {
120 // 设置其只能选择到文件夹
121 fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
122 // 打开文件选择器
123 int state = fileChooser.showOpenDialog(null);
124 // 如果关闭文件选择,直接返回;否则将选择的文件的文件名显示在面板上
125 if (state == 1) {
126 return;
127 } else {
128 directoryChecked = fileChooser.getSelectedFile();
129 text1.setText(directoryChecked.getAbsolutePath());
130 comboBox.removeAllItems();
131 for (String s : getConsistentList()) {
132 comboBox.addItem(s);
133 }
134 container.add(comboBox);
135 tabPane.add(container);
136 }
137 }
138 // 点击文件选择按钮,选择文件
139 if (e.getSource().equals(fileButton)) {
140 // 设置其只能选择到文件
141 fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
142 // 打开文件选择器
143 int state = fileChooser.showOpenDialog(null);
144 // 如果关闭文件选择,直接返回;否则将选择的文件的文件名显示在面板上
145 if (state == 1) {
146 return;
147 } else {
148 fileChecked = fileChooser.getSelectedFile();
149 text2.setText(fileChecked.getAbsolutePath());
150 comboBox.removeAllItems();
151 for (String s : getConsistentList()) {
152 comboBox.addItem(s);
153 }
154 container.add(comboBox);
155 tabPane.add(container);
156 }
157 }
158 // 点击提交按钮后弹出窗口,显示查询结果
159 if (e.getSource().equals(submitButton)) {
160 if (!text1.getText().equals("")) {
161 for (File f : FileUtil.getFileList(text1.getText(), (String) comboBox.getSelectedItem())) {
162 addMessages(messages, f, checkBoxList);
163 }
164 }
165 if (!text2.getText().equals("")) {
166 for (File f : FileUtil.getFileList(text2.getText(), (String) comboBox.getSelectedItem())) {
167 addMessages(messages, f, checkBoxList);
168 }
169 }
170 JOptionPane.showMessageDialog(null, messages.toArray(),
171 "选择的文件的查询结果", JOptionPane.PLAIN_MESSAGE);
172 messages = new ArrayList<>();
173 }
174 }
175
176 // 将指定的文件进行指定操作的查询,并将结果放置messages中
177 private void addMessages(List<String> messages, File f, List<JCheckBox> checkBoxList) {
178 try {
179 if (checkBoxList.get(0).isSelected()) {
180 messages.add(FileUtil.getCharsetsCount(f.getPath()) + "\n");
181 }
182 if (checkBoxList.get(1).isSelected()) {
183 messages.add(FileUtil.getWordsCount(f.getPath()) + "\n");
184 }
185 if (checkBoxList.get(2).isSelected()) {
186 messages.add(FileUtil.getLinesCount(f.getPath()) + "\n");
187 }
188 if (checkBoxList.get(3).isSelected()) {
189 messages.addAll(FileUtil.getCBNLinesCount(f.getPath()));
190 }
191 } catch (IOException ex) {
192 ex.printStackTrace();
193 }
194 }
195
196 // 获取选择的后缀
197 private List<String> getConsistentList() {
198 // 先获取选择的文件
199 String directoryPath = text1.getText();
200 String filePath = text2.getText();
201 // 创建一个List存放文件的后缀
202 List<String> consistentList = new ArrayList<>();
203 consistentList.add(".*");
204 // 遍历所有文件,获得选择的文件、文件夹中的子文件的所有后缀名
205 if (!filePath.equals("")) {
206 String s = filePath.substring(filePath.lastIndexOf("."));
207 // 如果List中不存在这个后缀名,将其加入List
208 if (!consistentList.contains(s)) {
209 consistentList.add(s);
210 }
211 }
212 if (!directoryPath.equals("")) {
213 for (File f : FileUtil.getFileList(directoryPath, ".*")) {
214 String s = f.getPath().substring(f.getPath().lastIndexOf("."));
215 // 如果List中不存在这个后缀名,将其加入List
216 if (!consistentList.contains(s)) {
217 consistentList.add(s);
218 }
219 }
220 }
221 return consistentList;
222 }
223 }
测试运行:
- 测试用例:
- 单个测试“-c, -w, -l, -a, -s”每一个操作:
- 同时进行多个操作:
- 调出用户图形界面
- 使用图形界面分别进行只选择文件、只选择文件夹和同时选择文件和文件夹的操作
对指定文件进行操作:
获得文件夹中所有的文件后缀名供用户选择:
对指定文件夹中的文件进行操作:
对选择的文件、文件夹进行操作:
PSP表格:
项目小结:
- 要做好一个项目,要把基本的框架构建好,做好前期的计划工作甚至要比编码要更重要。一个好的计划能让你事半功倍,在没有做好计划之前就动手编码会使你走很多的弯路。一个项目的开发重点不只在于代码的编写,前期的计划、项目分析、规范和设计,后期的复审、测试和改进同样重要。
- 项目完成了基本要求,扩展要求和高级要求,但实际的代码中还存在不足,前期的计划不够完善导致编写代码时还会走不少的弯路,有待改进。