編寫友好的命令列應用程式

polarisxu發表於2020-08-05

我來給你講一個故事...

1986 年,Knuth 編寫了一個程式來演示文學式程式設計

這段程式目的是讀取一個文字檔案,找到 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 年前(可能比一些讀者出生的還早)的故事呢? 計算領域已經發生了很多變化了,是吧?

林迪效應 是指如一個技術或者一個想法之類的一些不易腐爛的東西的未來預期壽命與他們的當前存活時間成正比。 太長不看版——老技術還會存在。

如果你不相信的話,看看這些:

現在你應該被說服了吧, 讓我們來討論以下怎麼使你的 Go 命令列程式變得友好。

設計

當你在寫命令列應用程式的時候, 試試遵守 基礎的 Unix 哲學

  • 模組性規則: 編寫通過清晰的介面連線起來的簡單的部件
  • 組合性規則: 設計可以和其他程式連線起來的程式
  • 緘默性規則:當一個程式沒有什麼特別的事情需要說的時候,它就應該閉嘴

這些規則能指導你編寫做一件事的小程式。

  • 使用者需要從 REST API 中讀取資料的功能 ? 他們會將 curl 命令的輸出通過管道輸入到你的程式中
  • 使用者只想要前 n 個結果 ? 他們可以把你的程式的輸出結果通過管道輸入到 head 命令中
  • 使用者指向要第二列資料 ? 如果你的輸出結果以 tab 為分割, 他們就可以把你的輸出通過管道輸入到 cutawk 命令

如果你沒有遵從上述要求 , 沒有結構性的組織你的命令列介面 , 你可能會像下面這種情況一樣的停止。

img

幫助

讓我們來假定你們團隊有一個叫做 nuke-db 的實用工具 。 你忘了怎麼呼叫它然後你:

$ ./nuke-db --help
database nuked    (譯者注:也就說本意想看使用方式,但卻直接執行了)

OMG!

使用 flag 庫 ,你可以用額外的兩行程式碼新增對於 --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 的研究 的引用

看到運動的進度條的人們會有更高的滿意度體驗而且比那些得不到任何反饋的人平均多出三倍的願意等待時間。

旋轉載入

新增一個旋轉載入不需要任何特別的庫

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 年了,命令列應用程式仍然會存在。 它們是自動化的關鍵,如果寫得好,能提供優雅的“類似樂高”的元件來構建複雜的流程。

我希望這篇文章將激勵你成為一個命令列之國的好公民。


作者:Miki Tebeka 譯者:Ollyder 校對:polaris1119

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

相關文章