golang cli
I’ve written two CLI app tutorials to build gololcat and gocowsay. In both I used fortune
as the input generator.
我编写了两个CLI应用程序教程来构建gololcat和gocowsay 。 在这两种方式中,我都将fortune
用作输入生成器。
In this article, I’ll complete the pipe triology with gofortune
.
在本文中,我将使用gofortune
完成管道gofortune
。
![](https://flaviocopes.com/go-tutorial-fortune/triologydraw.png)
What is fortune, first? As Wikipedia says, Fortune is a simple program that display a pseudorandom message from a database of quotations.
首先是什么运气 ? 正如Wikipedia所说 ,Fortune是一个简单的程序,可显示来自报价数据库的伪随机消息。
Basically, a random quote generator.
基本上,是一个随机报价生成器。
It has a very long history dating back to Unix Version 7 (1979). It’s still going strong. Many Linux distributions preinstall it, and on OSX you can install it using brew install fortune
.
它的历史可以追溯到Unix版本7(1979)。 它仍然很强大。 许多Linux发行版都预先安装了它,并且可以在OSX上使用brew install fortune
来安装它。
On some systems, it’s used as a greeting or parting message when using shells.
在某些系统上,使用外壳程序时,它将用作问候或离别消息。
Wikipedia also says
维基百科还说
Many people choose to pipe fortune into the cowsay command, to add more humor to the dialog.
许多人选择将财富运用于Cowsay命令中,以增加对话框的幽默感。
That’s me! Except I use my gocowsay
command.
那是我! 除了我使用gocowsay
命令。
Enough with the intro, let’s build a fortune clone with Go.
足够介绍之后, 让我们使用Go构建一个财富克隆 。
Here’s a breakdown of what our program will do.
这是我们程序的功能细分。
![](https://flaviocopes.com/go-tutorial-fortune/algorithm.png)
The fortunes folder location depends on the system and distribution, being a build flag. I could hardcode it or use an environment variable but as an exercise, I’ll do a dirty thing and ask fortune
directly, by executing it with the -f
flag, which outputs:
财富文件夹的位置取决于系统和发行版,它们是构建标记。 我可以对其进行硬编码,也可以使用环境变量,但是作为练习,我将通过-f
标志执行它来做一件肮脏的事情,并直接请求fortune
,该标志将输出:
![](https://flaviocopes.com/go-tutorial-fortune/folder.png)
The first line of the output contains the path of the fortunes folder.
输出的第一行包含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))
}
This snippet replicates the output exactly as I got it. It seems that fortune -f
writes the output to stderr, that’s why I used CombinedOutput
, to get both stdout
and stderr
.
此代码段完全按照我的方式复制输出。 似乎fortune -f
将输出写入stderr,这就是为什么我使用CombinedOutput
来获取stdout
和stderr
。
But, I just want the first line. How to do it? This prints all the output of stderr
line-by-line:
但是,我只想要第一行。 怎么做? 这将逐行打印stderr
所有输出:
package main
import (
"bufio"
"fmt"
"os/exec"
)
func main() {
fortuneCommand := exec.Command("fortune", "-f")
pipe, err := fortuneCommand.StderrPipe()
if err != nil {
panic(err)
}
for outputStream.Scan() {
fmt.Println(outputStream.Text())
}
}
To get just the first line, I remove the for loop, and just scan the first line:
为了只获得第一行,我删除了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())
}
![](https://flaviocopes.com/go-tutorial-fortune/first-line.png)
Now let’s pick that line and extract the path.
现在,我们选择那条线并提取路径。
On my system the first line of the output is 100.00% /usr/local/Cellar/fortune/9708/share/games/fortunes
. Let’s make a substring starting from the first occurrence of the /
char:
在我的系统上,输出的第一行是100.00% /usr/local/Cellar/fortune/9708/share/games/fortunes
。 让我们从第一次出现的/
char开始创建一个子字符串:
line := outputStream.Text()
path := line[strings.Index(line, "/"):]
Now I have the path of the fortunes. I can index the files found in there. There are .dat
binary files, and plain text files. I’m going to discard the binary files, and the off/
folder altogether, which contains offensive fortunes.
现在,我有了发财之路。 我可以索引在那里找到的文件。 有.dat
二进制文件和纯文本文件。 我将舍弃二进制文件和off/
文件夹,其中包含令人讨厌的财富。
Let’s first index the files. I use the path/filepath
package Walk
method to iterate the file tree starting from root
. I use it instead of ioutil.ReadDir()
because we might have nested folders of fortunes. In the WalkFunc
visit
I discard .dat files using filepath.Ext()
, I discard the folder files (e.g. /off
, but not the files in subfolders) and all the offensive fortunes, conveniently located under /off
, and I print the value of each remaining file.
首先让我们为文件建立索引。 我使用path/filepath
包Walk
方法来迭代从root
开始的文件树。 我用它代替ioutil.ReadDir()
因为我们可能有嵌套的财富文件夹。 在WalkFunc
visit
我使用filepath.Ext()
丢弃.dat文件,我丢弃了文件夹文件(例如/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)
}
}
Let’s put those values in a slice, so I can later pick a random one: I define a files
slice of strings and I append to that in the visit()
function. At the end of main()
I print the number of files I got.
让我们将这些值放在一个片中,以便以后可以选择一个随机值:我定义了一个字符串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))
}
I now use the Go random number generator functionality to pick a random item from the array:
现在,我使用Go随机数生成器功能从数组中选择一个随机项:
// Returns an int >= min, < max
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)
}
Our program now prints a random fortune filename on every run.
现在,我们的程序在每次运行时都会打印一个随机的财富文件名。
What I miss now is scanning the fortunes in a file, and printing a random one. In each file, quotes are separated by a %
sitting on a line on its own. I can easily detect this pattern and scan every quote in an array:
我现在想念的是扫描文件中的运气,然后随机打印。 在每个文件中,引号由一个单独的行上的%
隔开。 我可以轻松检测到此模式并扫描数组中的每个引号:
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])
This is not really efficient, as I’m scanning the entire fortune file in a slice and then I pick a random item, but it works:
这并不是真正有效,因为我要扫描一个切片中的整个财富文件,然后选择一个随机项目,但是它可以工作:
![](https://flaviocopes.com/go-tutorial-fortune/working.png)
So, here’s the final version of our very basic fortune
clone. It misses a lot of the original fortune
command, but it’s a start.
因此,这是我们非常基本的fortune
克隆的最终版本。 它错过了很多原始的fortune
命令,但这只是一个开始。
package main
import (
"bufio"
"fmt"
"io/ioutil"
"log"
"math/rand"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
var files []string
// Returns an int >= min, < max
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])
}
Wrapping up, I move visit
as an inline function argument of filepath.Walk
and move files
to be a local variable inside main()
instead of a global file variable:
总结起来,我将visit
作为filepath.Walk
的内联函数参数移动。将files
移动到main()
内的局部变量而不是全局文件变量中:
package main
import (
"bufio"
"fmt"
"io/ioutil"
"log"
"math/rand"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
// Returns an int >= min, < max
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])
}
I can now go build; go install
and the triology gofortune
gocowsay
and gololcat
is completed:
我现在go build; go install
go build; go install
然后完成三部曲gofortune
gocowsay
和gololcat
:
![](https://flaviocopes.com/go-tutorial-fortune/triology.png)
golang cli