我之前写了两篇 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 文件夹的路径。
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
的所有输出内容:
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 循环,只扫描第一行:
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
。我可以通过在第一个 /
字符出现的位置开始截取子字符串:
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
下,然后打印每个剩余文件的值。
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()
的末尾,我打印得到的文件数量。
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 随机数生成器 的功能来从数组中随机选择一个项:
// 返回大于等于 min,小于 max 的 int
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 文件名。
现在我要做的是扫描文件中的引述,并打印一条随机引述。在每个文件中,引述由位于单独一行上的 %
字符分隔。我可以轻松地检测到这个规律,并将每个引述扫描到一个数组中:
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
命令的许多功能,但这是个起点。
package main
import (
"bufio"
"fmt"
"io/ioutil"
"log"
"math/rand"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
var files []string
// 返回大于等于 min,小于 max 的 int
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()
的局部变量中,而不是全局变量中。
package main
import (
"bufio"
"fmt"
"io/ioutil"
"log"
"math/rand"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
// 返回大于等于 min,小于 max 的 int
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
就完成了。