編寫友好的 Go 命令行應用程序
我來給你講...
1986 年,Knuth[1] 編寫了一個程序來演示文學式編程 [2] 。
這段程序目的是讀取一個文本文件,找到 n 個最常使用的單詞,然後有序輸出這些單詞以及它們的頻率。Knuth 寫了一個完美的 10 頁程序。
Doug Mcllory 看到這裏然後寫了 tr -cs A-Za-z '\n' | tr A-Z a-z | sort | uniq -c | sort -rn | sed ${1}q
。
現在是 2019 年了,爲什麼我還要給你們講一個發生在 33 年前(可能比一些讀者出生的還早)的故事呢?計算領域已經發生了很多變化了,是吧?
林迪效應 [3] 是指如一個技術或者一個想法之類的一些不易腐爛的東西的未來預期壽命與他們的當前存活時間成正比。太長不看版——老技術還會存在。
如果你不相信的話,看看這些:
-
oh-my-zsh[4] 在 GitHub 上已經快有了 100,000 個 星星了
-
《命令行中的數據科學》[5]
-
命令行工具能夠比你的 Hadoop 集羣快 235 倍 [6]
-
...
現在你應該被說服了吧, 讓我們來討論以下怎麼使你的 Go 命令行程序變得友好。
設計
當你在寫命令行應用程序的時候, 試試遵守 基礎的 Unix 哲學 [7]
-
模塊性規則:編寫通過清晰的接口連接起來的簡單的部件
-
組合性規則:設計可以和其他程序連接起來的程序
-
緘默性規則:當一個程序沒有什麼特別的事情需要說的時候,它就應該閉嘴
這些規則能指導你編寫做一件事的小程序。
-
用戶需要從 REST API 中讀取數據的功能 ?他們會將
curl
命令的輸出通過管道輸入到你的程序中 -
用戶只想要前 n 個結果 ?他們可以把你的程序的輸出結果通過管道輸入到
head
命令中 -
用戶指向要第二列數據 ?如果你的輸出結果以 tab 爲分割, 他們就可以把你的輸出通過管道輸入到
cut
或awk
命令
如果你沒有遵從上述要求 , 沒有結構性的組織你的命令行接口 , 你可能會像下面這種情況一樣的停止。
幫助
讓我們來假定你們團隊有一個叫做 nuke-db
的實用工具 。你忘了怎麼調用它然後你:
$ ./nuke-db --help
database nuked (譯者注:也就說本意想看使用方式,但卻直接執行了)
OMG!
使用 flag 庫 [8] ,你可以用額外的兩行代碼添加對於 --help
的支持。
package main
import (
"flag" // extra line 1
"fmt"
)
func main() {
flag.Parse() // extra line 2
fmt.Println("database nuked")
}
現在你的程序運行起來是這個樣子:
$ ./nuke-db --help
Usage of ./nuke-db:
$ ./nuke-db
database nuked
如果你想提供更多的幫助 , 使用 flag.Usage
package main
import (
"flag"
"fmt"
"os"
)
var usage = `usage: %s [DATABASE]
Delete all data and tables from DATABASE.
`
func main() {
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), usage, os.Args[0])
flag.PrintDefaults()
}
flag.Parse()
fmt.Println("database nuked")
}
現在 :
$ ./nuke-db --help
usage: ./nuke-db [DATABASE]
Delete all data and tables from DATABASE.
結構化輸出
純文本是通用的接口。然而,當輸出變得複雜的時候, 對機器來說處理格式化的輸出會更容易。最普遍的一種格式當然是 JSON。
一個打印的好的方式不是使用 fmt.Printf
而是使用你自己的既適合於文本也適合於 JSON 的打印函數。讓我們來看一個例子:
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
)
func main() {
var jsonOut bool
flag.BoolVar(&jsonOut, "json", false, "output in JSON format")
flag.Parse()
if flag.NArg() != 1 {
log.Fatal("error: wrong number of arguments")
}
write := writeText
if jsonOut {
write = writeJSON
}
fi, err := os.Stat(flag.Arg(0))
if err != nil {
log.Fatalf("error: %s\n", err)
}
m := map[string]interface{}{
"size": fi.Size(),
"dir": fi.IsDir(),
"modified": fi.ModTime(),
"mode": fi.Mode(),
}
write(m)
}
func writeText(m map[string]interface{}) {
for k, v := range m {
fmt.Printf("%s: %v\n", k, v)
}
}
func writeJSON(m map[string]interface{}) {
m["mode"] = m["mode"].(os.FileMode).String()
json.NewEncoder(os.Stdout).Encode(m)
}
那麼
$ ./finfo finfo.go
mode: -rw-r--r--
size: 783
dir: false
modified: 2019-11-27 11:49:03.280857863 +0200 IST
$ ./finfo -json finfo.go
{"dir":false,"mode":"-rw-r--r--","modified":"2019-11-27T11:49:03.280857863+02:00","size":783}
處理
有些操作是比較耗時的,一個是他們更快的方法不是優化代碼,而是顯示一個旋轉加載符或者進度條。不要不信我,這有一個來自 Nielsen 的研究 [9] 的引用
看到運動的進度條的人們會有更高的滿意度體驗而且比那些得不到任何反饋的人平均多出三倍的願意等待時間。
旋轉加載
添加一個旋轉加載不需要任何特別的庫
package main
import (
"flag"
"fmt"
"os"
"time"
)
var spinChars = `|/-\`
type Spinner struct {
message string
i int
}
func NewSpinner(message string) *Spinner {
return &Spinner{message: message}
}
func (s *Spinner) Tick() {
fmt.Printf("%s %c \r", s.message, spinChars[s.i])
s.i = (s.i + 1) % len(spinChars)
}
func isTTY() bool {
fi, err := os.Stdout.Stat()
if err != nil {
return false
}
return fi.Mode()&os.ModeCharDevice != 0
}
func main() {
flag.Parse()
s := NewSpinner("working...")
for i := 0; i < 100; i++ {
if isTTY() {
s.Tick()
}
time.Sleep(100 * time.Millisecond)
}
}
運行它你就能看到一個小的旋轉加載在運動。
進度條
對於進度條, 你可能需要一個額外的庫如 github.com/cheggaaa/pb/v3
package main
import (
"flag"
"time"
"github.com/cheggaaa/pb/v3"
)
func main() {
flag.Parse()
count := 100
bar := pb.StartNew(count)
for i := 0; i < count; i++ {
time.Sleep(100 * time.Millisecond)
bar.Increment()
}
bar.Finish()
}
結語
現在差不多 2020 年了,命令行應用程序仍然會存在。它們是自動化的關鍵,如果寫得好,能提供優雅的 “類似樂高” 的組件來構建複雜的流程。
我希望這篇文章將激勵你成爲一個命令行之國的好公民。
via: https://blog.gopheracademy.com/advent-2019/cmdline/
作者:Miki Tebeka[10] 譯者:Ollyder[11] 校對:polaris1119[12]
本文由 GCTT[13] 原創編譯,Go 中文網 [14] 榮譽推出,發佈在 Go 語言中文網公衆號,轉載請聯繫我們授權。
參考資料
[1]
Knuth: https://en.wikipedia.org/wiki/Donald_Knuth
[2]
文學式編程: https://en.wikipedia.org/wiki/Literate_programming
[3]
林迪效應: https://en.wikipedia.org/wiki/Lindy_effect
[4]
oh-my-zsh: https://github.com/ohmyzsh/ohmyzsh
[5]
《命令行中的數據科學》: https://www.datascienceatthecommandline.com/
[6]
命令行工具能夠比你的 Hadoop 集羣快 235 倍: https://adamdrake.com/command-line-tools-can-be-235x-faster-than-your-hadoop-cluster.html
[7]
Unix 哲學: http://www.catb.org/esr/writings/taoup/html/ch01s06.html
[8]
flag 庫: https://golang.org/pkg/flag/
[9]
Nielsen 的研究: https://www.nngroup.com/articles/progress-indicators/
[10]
Miki Tebeka: https://blog.gopheracademy.com
[11]
Ollyder: https://github.com/Ollyder
[12]
polaris1119: https://github.com/polaris1119
[13]
GCTT: https://github.com/studygolang/GCTT
[14]
Go 中文網: https://studygolang.com/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/qOhdWkIG5lZG6hV7QHIsaQ