七爪源码:如何在 Go 中构建文本过滤、日志简化工具

轻松删除匹配的文本行。 非常适合在调试时减少日志!


我们在建造什么?

在软件工程中,我们经常解析日志文件以了解程序的内部状态。如果存在频繁但不相关(嘈杂)的日志,这可能会很乏味,尤其是在作为分布式团队进行调试时。为此,我在 Go 中编写了一个简单的程序来删除任何包含子字符串(或一系列子字符串中的一个)的文本行,并生成一个包含除匹配项之外的所有内容的新文件。多年来,这已被证明是无价的——无论是为了我的理智,还是为了那些在我离开的地方开始调试系统的人的理智。

标准工具可以帮助过滤现有的日志行,但这个程序允许您做相反的事情 - 遍历文件,在找到它们时删除嘈杂的日志!


设计工具

在我们开始编码之前,让我们考虑一下如何使用这个工具。鉴于这是一个相当技术性的产品并且只需要一个简单的界面,命令行界面 (CLI) 似乎是合适的。

输入呢?我们需要提供文件源、从该源中删除的关键短语,以及我们是否希望文件在适当的位置进行编辑。如果没有就地编辑,则可选的增强可以是为输出文件提供路径,或者使用输出文件路径的存在来确定就地编辑。

现在,我们要交付什么?我们可以为各种操作系统和架构构建和分发二进制文件,甚至可以部署它们(人工制品,有人吗?)。同样,由于这是一个技术产品(并且免费/开源!),我们应该很好地提供带有构建和使用说明的 README.md,并希望我们的用户制作他们自己的二进制文件。


构建工具

接下来,我们将定义构建此工具所需的技术位。在我们的简单案例中,我们需要以下组件:

  • 标志解析(用于用户提供的选项)
  • 帮助输出(万一工具使用不当)
  • 一种确定文本行是否与给定输入字符串匹配的方法
  • 要写入的输出/目标文件
  • 一种从输入中剪切匹配文本行的方法
  • 如果“就地”编辑,则可以替换原始文件
  • 基准/测试(我们将在另一篇文章中介绍)

让我们从我们的主运行循环开始。这将处理用户输入标志和编辑文本文件的基本逻辑。

func main() {cfg := getUserInput()log.Printf("Trimming file %q of lines with key phrases: %v (in place: %t)
", cfg.inputPath, cfg.keys, cfg.inplace)if err := transformInput(cfg); err != nil {log.Fatalf("failed to cut lines: %s", err)}}

在这里,我们获取用户提供的用于修剪文件的参数,然后执行修剪(如果有任何错误,我们会退出)。

要获取用户提供的参数,我们将定义一个配置结构并使用标准库中的标志包,如下所示:

type config struct {inputPath stringkeys      []stringinplace   bool}func getUserInput() *config {inputPath := flag.String("file", "", "file to modify")keysRaw := flag.String("keys", "", "keys to search for in lines - separate multiple keys with '|'")inplace := flag.Bool("inplace", false, "edit the file (don't create a copy)")flag.Parse()if *inputPath == "" || *keysRaw == "" {helpAndExit()}keys := strings.Split(*keysRaw, "|")return &config{inputPath: *inputPath,keys:      keys,inplace:   *inplace,}}

我们将 config 定义为方便我们自己和未来的读者——我们可以发送对 config 的引用,集中定义和任何可能的文档,而不是在函数之间传递三个或更多参数。如果我们决定扩展工具(提示,提示),它也使将来的修改更容易。

然后我们根据上面的要求定义我们的输入——输入文件路径的字符串,要删除的关键短语的字符串,以及指示文件是否应该就地编辑的布尔值。我们为每个标志定义名称、默认值和用法/帮助解释器字符串。请注意,每个返回指针都指向指示的类型(而不是指示的类型本身)。 flags.Parse() 将用户提供的标志从 os.Args[1:] 解析到这些指针变量中。从那里,我们验证标志是否被适当地提供并将它们返回给调用者。

接下来是转换输入的业务逻辑。下面,我们清理任何预先存在的临时输出文件并生成一个输出文件,该文件是 cfg.inputPath 的所有内容,没有任何包含 cfg.keys 中任何键的行,最后,替换原始文件(如果用户指定到位) .

func transformInput(cfg *config) error {tempDstPath := cfg.inputPath + ".tmp"// clear any pre-existing output fileos.Remove(tempDstPath)if err := transformInputImpl(cfg.inputPath, tempDstPath, cfg.keys); err != nil {// clean up after ourselves if there was an erroros.Remove(tempDstPath)return err}if cfg.inplace {return os.Rename(tempDstPath, cfg.inputPath)}return nil}

最后,我们构建实际的逻辑来解析输入文件,检查任何提供的键,并生成输出:

// generate a temp file that includes all of `inputPath` except for anything matching `keys`func transformInputImpl(inputPath, tempDstPath string, keys []string) (retErr error) {sourceFile, err := os.Open(inputPath)if err != nil {log.Fatalf("failed opening source file: %v", err)}defer sourceFile.Close()outFile, err := os.OpenFile(tempDstPath, os.O_WRONLY|os.O_CREATE, 0600)if err != nil {log.Fatalf("failed creating output file: %v", err)}defer func() {if e := outFile.Close(); retErr == nil {retErr = e}}()bw := bufio.NewWriter(outFile)defer func() {if e := bw.Flush(); retErr == nil {retErr = e}}()first := truescanner := bufio.NewScanner(sourceFile)for scanner.Scan() {line := scanner.Text()if !substrInLine(line, keys) {b := strings.Builder{}if !first {b.WriteString("
")}b.WriteString(line)if _, err := bw.WriteString(b.String()); err != nil {return err}first = false}}return nil}// check if any of `keys` are in `line`func substrInLine(line string, keys []string) bool {for _, key := range keys {if strings.Contains(line, key) {return true}}return false}

我们首先打开提供的输入文件(如果有任何问题,则放弃)。请注意,我们使用的是命名错误返回参数——我们稍后会讨论。

由于我们正在处理文件,因此我们应该记住通过延迟 sourceFile.Close() 自己进行清理(从技术上讲,我们不需要对此处的只读文件执行此操作,但这仍然是一个好习惯。您可以深入了解更多这里)。

然后,我们使用与源文件相同的文件前缀和目录创建或打开输出文件,使其可写。我们将推迟成功关闭它,但在这里我们关心错误,因为它可能表明输出文件不完整。因此,如果之前没有错误,我们会将返回错误值设置为由 outFile.Close() 生成的任何错误。

日志文件可能非常大。因此,此代码可能会执行许多小写操作,从而损害性能。为了缓解这种情况,我们将使用 bufio(另一个标准库包)来提供缓冲 I/O。从本质上讲,这会将许多小写入分批成更少但更大的写入,从而减少性能开销。我们还将在源文件上使用缓冲读取器。

打开源文件和目标文件后,我们将生成一个缓冲写入器。 为了避免最后丢失任何数据,我们将推迟 Flush() 调用以强制将缓冲区中的任何内容写入目标文件,并且我们将根据需要再次设置返回错误参数。

然后,我们在源文件上生成一个缓冲阅读器,并逐行扫描内容。 如果该行不包含任何键作为子字符串,我们将其写入缓冲写入器(最终写入目标文件)。 在所有行都被读取或延迟启动后。假设没有错误,我们将 Flush() 缓冲区将剩余的任何内容写入文件,关闭文件并成功返回。


使用工具

使用该工具的形式为:

go run main.go -file="" -keys=""(和一个可选的 -inplace 用输出替换源文件)。

如果您克隆项目,您可以在 example/input.txt 上测试一些修剪操作,如下所示:

删除以“hello”为前缀的所有内容:

去运行 main.go -file="example/input.txt -keys="hello"

删除所有包含“世界”的内容(即删除所有行):

去运行 main.go -file="example/input.txt -keys="world"

删除包含“大”或“小”字样的所有内容:

去运行 main.go -file="example/input.txt -keys="big|small"

您可以查看自述文件以获取更多信息并进行试验以了解您的想法!


进一步的改进

我们一起构建了一个简单而有效的工具。 尽管如此,仍然可以做出明显的改进。

将正则表达式 (regex) 作为输入而不是一个或多个键用于子字符串搜索会很好。 您可以在本系列的第 2 部分中找到它。


关注七爪网,获取更多APP/小程序/网站源码资源!

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章