Go CLI 教程:Fortune 克隆
我之前写了两篇 CLI 应用的教程,分别是构建 gololcat 和 gocowsay。在这两篇教程中,我都使用了 fortune
作为输入生成器。
在本文中,我将用 Go 完成这个“管道三部曲”的最后一部分 - gofortune
。
首先,什么是 fortune?参考 维基百科的定义,Fortune 是一款简单的程序,从一个引述数据库中随机显示一条信息。
说简单点,它是一个随机引述生成器。
它的历史可以追溯到 Unix Version 7 (1979)。至今仍然广泛应用。许多 Linux 发行版都默认安装了它,而在 MacOS 上,可以通过 brew install fortune
命令进行安装。
在某些系统上,它还会在使用 shell 时作为欢迎语或告别语。
维基百科上还提到:
许多人选择将 fortune 输入到 cowsay 命令中,以增添对话的幽默感。
而对于我来说,我会使用我的 gocowsay
命令。
好了,先到这里,让我们用 Go 构建一个 Fortune 的克隆版本。
下面是程序的功能概述。
fortunes
文件夹的位置与系统和发行版有关,可以使用构建标志硬编码它,或者使用环境变量。但为了锻炼,我要做一件“肮脏”的事情,直接询问 fortune
,通过执行 fortune -f
命令,并得到如下输出:
输出的第一行包含 fortunes 文件夹的路径。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package main
import ( "fmt" "os/exec" )
func main() { out, err := exec.Command("fortune", "-f").CombinedOutput() if err != nil { panic(err) }
fmt.Println(string(out)) }
|
这段代码完全复制了我得到的输出。看起来 fortune -f
是将输出写入到了 stderr,所以我使用了 CombinedOutput
来获取 stdout
和 stderr
的内容。
但是我只需要第一行,应该如何做呢?下面的代码逐行打印了 stderr
的所有输出内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package main
import ( "bufio" "fmt" "os/exec" )
func main() { fortuneCommand := exec.Command("fortune", "-f") pipe, err := fortuneCommand.StderrPipe() if err != nil { panic(err) } outputStream := bufio.NewScanner(pipe) for outputStream.Scan() { fmt.Println(outputStream.Text()) } }
|
要取得第一行,我可以去掉 for 循环,只扫描第一行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package main
import ( "bufio" "fmt" "os/exec" )
func main() { fortuneCommand := exec.Command("fortune", "-f") pipe, err := fortuneCommand.StderrPipe() if err != nil { panic(err) } fortuneCommand.Start() outputStream := bufio.NewScanner(pipe) outputStream.Scan() fmt.Println(outputStream.Text()) }
|
接下来,我们提取这一行中的路径。
在我的系统中,输出的第一行是 100.00% /usr/local/Cellar/fortune/9708/share/games/fortunes
。我可以通过在第一个 /
字符出现的位置开始截取子字符串:
1 2
| line := outputStream.Text() path := line[strings.Index(line, "/"):]
|
现在,我得到了 fortunes 的路径。我可以索引其中的文件。这里有 .dat
二进制文件和纯文本文件。我打算丢弃二进制文件以及 off/
文件夹,因为后者包含了不文明的引述。
我们首先来索引文件。我使用 path/filepath
包的 Walk
方法来遍历文件树,从 root
开始。我使用它而不是 ioutil.ReadDir()
是因为 fortunes 可能嵌套在子文件夹中。在 WalkFunc
的 visit
函数中,我使用 filepath.Ext()
来丢弃 .dat 文件,使用 f.IsDir()
来丢弃文件夹文件(例如 /off
,但子文件夹中的文件不会被排除),以及所有不文明的引述,方便地位于 /off
下,然后打印每个剩余文件的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| func visit(path string, f os.FileInfo, err error) error { if strings.Contains(path, "/off/") { return nil } if filepath.Ext(path) == ".dat" { return nil } if f.IsDir() { return nil } files = append(files, path) return nil }
func main() { fortuneCommand := exec.Command("fortune", "-f") pipe, err := fortuneCommand.StderrPipe() if err != nil { panic(err) } fortuneCommand.Start() outputStream := bufio.NewScanner(pipe) outputStream.Scan() line := outputStream.Text() root := line[strings.Index(line, "/"):]
err = filepath.Walk(root, visit) if err != nil { panic(err) } }
|
我们将这些文件的值存入切片中,以便稍后随机选择一个。我定义了一个字符串切片 files
,并在 visit()
函数中将其添加进去。在 main()
的末尾,我打印得到的文件数量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| package main
import ( "bufio" "log" "os" "os/exec" "path/filepath" "strings" )
var files []string
func visit(path string, f os.FileInfo, err error) error { if err != nil { log.Fatal(err) } if strings.Contains(path, "/off/") { return nil } if filepath.Ext(path) == ".dat" { return nil } if f.IsDir() { return nil } files = append(files, path) return nil }
func main() { fortuneCommand := exec.Command("fortune", "-f") pipe, err := fortuneCommand.StderrPipe() if err != nil { panic(err) } fortuneCommand.Start() outputStream := bufio.NewScanner(pipe) outputStream.Scan() line := outputStream.Text() root := line[strings.Index(line, "/"):]
err = filepath.Walk(root, visit) if err != nil { panic(err) }
println(len(files)) }
|
现在,我使用 Go 随机数生成器 的功能来从数组中随机选择一个项:
1 2 3 4 5 6 7 8 9 10 11 12 13
| func randomInt(min, max int) int { return min + rand.Intn(max-min) }
func main() {
rand.Seed(time.Now().UnixNano()) i := randomInt(1, len(files)) randomFile := files[i] println(randomFile) }
|
我们的程序现在在每次运行时都会打印一个随机 fortune 文件名。
现在我要做的是扫描文件中的引述,并打印一条随机引述。在每个文件中,引述由位于单独一行上的 %
字符分隔。我可以轻松地检测到这个规律,并将每个引述扫描到一个数组中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| file, err := os.Open(randomFile) if err != nil { panic(err) } defer file.Close()
b, err := ioutil.ReadAll(file) if err != nil { panic(err) }
quotes := string(b)
quotesSlice := strings.Split(quotes, "%") j := randomInt(1, len(quotesSlice))
fmt.Print(quotesSlice[j])
|
这并不是很高效,因为我在切片中扫描了整个 fortune 文件,然后选择了一个随机项,但它可以工作。
那么,这是我们非常基础的 fortune
克隆版本的最终代码。它没有原始 fortune
命令的许多功能,但这是个起点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| package main
import ( "bufio" "fmt" "io/ioutil" "log" "math/rand" "os" "os/exec" "path/filepath" "strings" "time" )
var files []string
func randomInt(min, max int) int { return min + rand.Intn(max-min) }
func visit(path string, f os.FileInfo, err error) error { if err != nil { log.Fatal(err) } if strings.Contains(path, "/off/") { return nil } if filepath.Ext(path) == ".dat" { return nil } if f.IsDir() { return nil } files = append(files, path) return nil }
func main() { fortuneCommand := exec.Command("fortune", "-f") pipe, err := fortuneCommand.StderrPipe() if err != nil { panic(err) } fortuneCommand.Start() outputStream := bufio.NewScanner(pipe) outputStream.Scan() line := outputStream.Text() root := line[strings.Index(line, "/"):]
err = filepath.Walk(root, visit) if err != nil { panic(err) }
rand.Seed(time.Now().UnixNano()) i := randomInt(1, len(files)) randomFile := files[i]
file, err := os.Open(randomFile) if err != nil { panic(err) } defer file.Close()
b, err := ioutil.ReadAll(file) if err != nil { panic(err) }
quotes := string(b)
quotesSlice := strings.Split(quotes, "%") j := randomInt(1, len(quotesSlice))
fmt.Print(quotesSlice[j]) }
|
总结一下,我将 visit
转为了 filepath.Walk
的内联函数参数,并将 files
移动到了 main()
的局部变量中,而不是全局变量中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| package main
import ( "bufio" "fmt" "io/ioutil" "log" "math/rand" "os" "os/exec" "path/filepath" "strings" "time" )
func randomInt(min, max int) int { return min + rand.Intn(max-min) }
func main() { var files []string
fortuneCommand := exec.Command("fortune", "-f") pipe, err := fortuneCommand.StderrPipe() if err != nil { panic(err) } fortuneCommand.Start() outputStream := bufio.NewScanner(pipe) outputStream.Scan() line := outputStream.Text() root := line[strings.Index(line, "/"):]
err = filepath.Walk(root, func(path string, f os.FileInfo, err error) error { if err != nil { log.Fatal(err) } if strings.Contains(path, "/off/") { return nil } if filepath.Ext(path) == ".dat" { return nil } if f.IsDir() { return nil } files = append(files, path) return nil }) if err != nil { panic(err) }
rand.Seed(time.Now().UnixNano()) i := randomInt(1, len(files)) randomFile := files[i]
file, err := os.Open(randomFile) if err != nil { panic(err) } defer file.Close()
b, err := ioutil.ReadAll(file) if err != nil { panic(err) }
quotes := string(b)
quotesSlice := strings.Split(quotes, "%") j := randomInt(1, len(quotesSlice))
fmt.Print(quotesSlice[j]) }
|
现在,可以使用 go build; go install
,我们的三部曲 gofortune
、gocowsay
和 gololcat
就完成了。
tags: [“go”, “CLI”, “fortune”]