golang版代码行数统计命令行工具

2,382 阅读6分钟

「这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战」。

前言:

经常做某种类型的项目的同学,一定会碰到下面的问题,统计代码(代码/注释)行数,整理品质数据。
还不只是统计,还需要写一堆报告,有 BUG或缺陷 的话,还得写什么直接原因、根本原因、其它地方有没有同样问题、临时解决方案、长期解决方案、预防方案、加强教育之类的。
有时候,真的会折腾到吐。
人在江湖,事情还是要做的,所以效率为王。
先从统计代码行数下手吧。网上也有很多统计代码行数的插件或工具,只是有的时候可能没法儿定制。
于是用 golang 试着写了一个简单的代码行数统计命令行工具。

相关代码:hellogo/step at main · bettersun/hellogo (github.com)

golang版代码统计命令行工具

简单功能点:

  • 可自定义注释规则
  • 可设定需要忽略统计的目录和文件
  • 可设定需要统计的代码文件类型
  • 可统计单文件,也可统计指定目录下的目标文件
  • 统计结果输出文本或者json,使用 json 是考虑后面可以加上 GUI。 输出 Excel 有时间再写。

统计规则

  1. 注释定义文件中定义各种类型代码文件的注释规则,相同注释规则的代码文件类型合并到一起。
    注释规则中包含单行注释规则,多行注释规则。
  2. 按行读取代码文件,每行代码按照注释规则的正则表达式来检查是单行注释,还是多行注释,还是代码。

统计流程

  1. 命令行运行程序,指定参数,可指定多个,也可混合指定目录和文件。
  2. 读取程序配置文件的内容,根据配置规则,获取参数目录的代码文件(包括参数指定的文件)
  3. 按照统计规则进行统计,统计完后加一个汇总处理。
  4. 输出统计结果到文件(文件名和文件类型可在配置文件中指定,现仅支持输出到文本或 json )。

外部文件

注释定义文件

文件名固定:define.yaml

例:

# C风格注释
- fileExtension:
    - .c
    - .cpp
    - .cs
    - .dart
    - .go
    - .php
    - .java
    - .js
  # 单行注释
  singleLine:
    - //
  # 多行注释开始(多行有多个标志的情况,相同位置的开始标志和结束标志必须配对)
  multiLineStart:
    - /*
  # 多行注释结束
  multiLineEnd:
    - '*/'
# Python
- fileExtension:
    - .py
  singleLine:
    - #
  multiLineStart:
    - "'''"
    - '"""'
  multiLineEnd:
    - "'''"
    - '"""'
# CSS
- fileExtension:
    - .css
  singleLine:
    -
  multiLineStart:
    - "/*"
  multiLineEnd:
    - '*/'
# VB/VBS
- fileExtension:
    - .vb
    - .vbs
  singleLine:
    - "'"
  multiLineStart:
    -
  multiLineEnd:
    -

程序配置文件

文件名固定:config.yaml

例:

# 统计目标文件扩展名(.+小写,多个用逗号连接,不指定则统计忽略目录和忽略文件之外的所有文件)
#codeFileExt: .java,.c,.go,.yaml, ,    ,,
codeFileExt:
# 结果文件类型
#  0: txt 1:json
resultFileType: 3
# 结果文件名(不含扩展名)
resultFileName: step_result
# 忽略目录(多个用逗号连接)
ignorePath: .git,.svn,.idea, ,    ,,
# 忽略文件(多个用逗号连接)
ignoreFile: .gitignore,.DS_Store, ,    ,,

结构体

注释定义

// CommentDefine 代码注释定义
type CommentDefine struct {
   // 扩展名(多个)
   FileExtension []string `yaml:"fileExtension"`
   // 单行注释标志
   SingleLine []string `yaml:"singleLine"`
   // 多行注释开始
   MultiLineStart []string `yaml:"multiLineStart"`
   // 多行注释结束
   MultiLineEnd []string `yaml:"multiLineEnd"`
}

配置

// Config 程序的配置文件
type Config struct {
   // 统计目标文件扩展名(.+小写,多个用逗号连接)
   CodeFileExt string `yaml:"codeFileExt"`
   // 忽略目录(多个用逗号连接)
   IgnorePath string `yaml:"ignorePath"`
   // 忽略文件(多个用逗号连接)
   IgnoreFile string `yaml:"ignoreFile"`
   // 结果文件类型
   // 0: txt 1:json
   ResultFileType string `yaml:"resultFileType"`
   // 结果文件名(不含扩展名)
   ResultFileName string `yaml:"resultFileName"`
}

代码行数统计相关

// CommentRegExp 代码注释统计用正则表达式
type CommentRegExp struct {
   // 扩展名
   FileExtension string
   // 有单行注释
   HasSingleLineMark bool
   // 有多行注释
   HasMultiLineMark bool

   // 空行正则表达式
   RegExEmptyLine *regexp.Regexp
   // 单行注释正则表达式
   RegExSingleLine []*regexp.Regexp
   // 写在单行的多行注释正则表达式 即使用多行注释开始和多行注释结束来写的一行注释
   RegExSingleLineStartEnd []*regexp.Regexp
   // 多行注释开始正则表达式
   RegExMultiLineStart []*regexp.Regexp
   // 多行注释结束正则表达式
   RegExMultiLineEnd []*regexp.Regexp
}

// StepInfo 代码行数信息
type StepInfo struct {
   CommentDefined bool   `json:"commentDefined"` // 存在注释配置 true:存在 false:不存在
   Counted        bool   `json:"counted"`        // 已统计标志 true:已统计 false:未统计
   ExInfo         string `json:"exInfo"`         // 扩展信息

   File          string `json:"file"`          // 文件全路径
   FileName      string `json:"fileName"`      // 文件名
   TotalStep     int    `json:"totalStep"`     // 总行数
   EmptyLineStep int    `json:"emptyLineStep"` // 空行数
   CommentStep   int    `json:"commentStep"`   // 注释行数
   SourceStep    int    `json:"sourceStep"`    // 代码行数
   ValidStep     int    `json:"validStep"`     // 有效行数(注释+代码)
}

// StepSummary 代码行数信息汇总
type StepSummary struct {
	StepInfo         []StepInfo `json:"stepInfo"`      // 代码行数统计结果
	CountedFileCount int        `json:"fileCount"`     // 统计文件总数
	FlatFile         []string   `json:"flatFile"`      // 无注释定义文件
	UnCountedFile    []string   `json:"unCountedFile"` // 未统计文件

	TotalStep     int `json:"totalStep"`     // 总行数
	EmptyLineStep int `json:"emptyLineStep"` // 空行总行数
	CommentStep   int `json:"commentStep"`   // 注释总行数
	SourceStep    int `json:"sourceStep"`    // 代码总行数
	ValidStep     int `json:"validStep"`     // 有效总行数(注释+代码)
}

正则表达式字符串转正则表达式

多行注释开始结束例:

start := Escape(def.MultiLineStart[i])
end := Escape(def.MultiLineEnd[i])

cmtRegExp.RegExSingleLineStartEnd[i] =
   regexp.MustCompile(`^[\s]*(` + start + `).*(` + end + `)[\s]*$`)

需要先转义一下,再转换成正则表达式

转义函数:

// Escape 转义
func Escape(sRegExp string) string {
   v := sRegExp
   v = strings.ReplaceAll(v, `*`, `\*`)
   v = strings.ReplaceAll(v, `/`, `\/`)
   return v
}

这里只转义了 * 和 / 。

主要统计逻辑

if !isMultiLineComment { // 不是多行注释
   if cmtRegExp.RegExEmptyLine.MatchString(line) { //空行
      emptyLineStep++
   } else if _, isMatchSingle := MatchIn(line, cmtRegExp.RegExSingleLine); cmtRegExp.HasSingleLineMark && isMatchSingle { // 单行注释
      commentStep++
   } else if _, isMatchSingleStartEnd := MatchIn(line, cmtRegExp.RegExSingleLineStartEnd); cmtRegExp.HasMultiLineMark && isMatchSingleStartEnd { // 多行注释标志开始结束的单行注释
      commentStep++
   } else if matchIndex, isMatch = MatchIn(line, cmtRegExp.RegExMultiLineStart); cmtRegExp.HasMultiLineMark && isMatch { // 多行注释 开始
      commentStep++
      isMultiLineComment = true
   }
} else if cmtRegExp.HasMultiLineMark { // 有多行注释
   if isMultiLineComment {
      if cmtRegExp.RegExEmptyLine.MatchString(line) { //多行注释里的空行
         emptyLineStep++
      } else {
         //多行注释
         commentStep++
         //多行注释结束
         if cmtRegExp.RegExMultiLineEnd[matchIndex].MatchString(line) { //多行注释 结束
            isMultiLineComment = false
         }
      }
   }
}

空行和单行注释的判断很简单,多行注释,需要先在开始时加一个多行注释开始的标志,多行注释结束时再计算注释行数,并把相关标志复原。

还有一个小问题没解决,像下面这种情况:

/* print Hello, World */ printf("Hello, World!\n");

单行内容里,同时有多行注释开始和多行注释结束,最后还有代码的情况,正则表达式验证的地方还需要再改一下。
除了压缩后的代码文件,极少有这种情况,所以暂时还没修改。
也没有充分测试,应该还有其它BUG,欢迎指正。

运行命令

各参数间用空格隔开

Mac:

./main /xx/project/hello /xx/go/test.go /xx/yy/zz.yaml /xx/bs 

Win:

main.exe D:\xx\project\hello D:\xx\go\test.go E:\xx\yy\zz.yaml E:\xx\bs