Golang: 打造一個交互式命令行

Go 非常適合於構建命令行應用程序。我們構建了一個名爲 Dolt 的應用, 它是世界上第一個支持版本控制的 SQL 數據庫。我們爲處理 Dolt 的所有子命令和參數編寫了自己的命令行解析器, 但也許我們不應該這樣做。現在有很多很棒的工具可以替代我們自己編寫的解析器, 如果我們今天開始這個項目, 我們可能會選擇使用它們:

spf13/cobra 提供了從簡單的文本命令格式生成代碼的強大支持, 並且可以免費爲你生成 zsh 和其他 shell 的自動補全。charmbracelet/gum 是一個 Golang 工具, 可以生成非常漂亮的命令行提示, 你可以將它們組合到 shell 腳本中。alecthomas/kingpin 是一個非常出色的構建命令行應用程序的庫, 可能最接近我們自己構建的內容。

所以對於這種 Go 擅長的常見用例, 有很多很棒的工具可以使用。但是如果你想在 Go 中構建一個交互式 shell, 你該怎麼做? 選擇的選項就沒有那麼多了。

這篇博客將教你如何使用我們所知道的最好的選擇 abiosoft/ishell, 並討論如何充分利用它。你將學習如何配置交互式 shell 以處理你想要的命令, 如何退出 shell, 以及如何使用該軟件包的內置功能。我們還將展示我們如何使用它來構建 Dolt 內置的 SQL shell 功能。

演示

當你啓動 Dolt 的 SQL shell 時, 你會看到這樣的內容:

% dolt sql
# Welcome to the DoltSQL shell.
# Statements must be terminated with ';'.
# "exit" or "quit" (or Ctrl-D) to exit.
last_insert/main*> show tables;
+-----------------------+
| Tables_in_last_insert |
+-----------------------+
| test |
+-----------------------+
1 row in set (0.00 sec)
last_insert/main*> select * from test;
+------+----+
| name | id |
+------+----+
| one | 1 |
+------+----+
1 row in set (0.00 sec)
last_insert/main*> call dolt_checkout('-b', 'newBranch');
+--------+--------------------------------+
| status | message |
+--------+--------------------------------+
| 0 | Switched to branch 'newBranch' |
+--------+--------------------------------+
1 row in set (0.01 sec)
last_insert/newBranch> call dolt_checkout('main');
+--------+---------------------------+
| status | message |
+--------+---------------------------+
| 0 | Switched to branch 'main' |
+--------+---------------------------+
1 row in set (0.00 sec)
last_insert/main*> exit
Bye

這裏有幾個需要注意的地方:

我們還在 shell 中使用了顏色, 用於提示符和輸出。下面是它在我的終端中的樣子:

所以這就是 shell 的作用, 以及它與普通命令行應用程序的不同之處: 你有一個循環, 不斷接受用戶輸入, 給出答覆或執行其他工作, 直到用戶決定使用預定義的命令退出。

預定義命令還是自由格式?

原始的 abiosoft/ishell 包是爲處理預定義命令而構建的, 每個命令都有一個不同的處理程序。實際使用時, 看起來像這樣:

shell.AddCmd(&ishell.Cmd{
    Name: "login",
    Help: "simulate a login",
    Func: func(c *ishell.Context) {
        // disable the '>>>' for cleaner same line input.
        c.ShowPrompt(false)
        defer c.ShowPrompt(true) // yes, revert after login.
        // get username
        c.Print("Username: ")
        username := c.ReadLine()
        // get password.
        c.Print("Password: ")
        password := c.ReadPassword()
        // ... do something with username and password
        c.Println("Authentication Successful.")
    },
})

然後在運行時, 你會看到:

>>> login
>>> Username: someusername
>>> Password:
Authentication Successful.
>>>

這很棒, 有很多用途, 但我們想要做一些稍微不同的事情。我們不想要預定義的命令及其各自的處理程序, 而是想要一個更像 REPL 的東西, 我們只是讀取輸入, 直到找到一個分隔符, 然後每次都以相同的方式處理它。對於 Dolt 的 SQL shell, 這意味着讀取一個查詢, 直到我們看到一個 ; 字符, 然後將該查詢發送到數據庫並打印結果, 一次又一次。這在原始包中並不容易實現, 所以我們 fork 了自己的副本來添加這個功能。這就是我們處理上述自由格式 SQL 查詢功能的方式。如果這就是你想要做的, 歡迎使用我們的 fork 而不是原始包。我們將在以下部分演示如何在自由格式模式下配置 shell。

啓動預定義命令的 shell

要啓動你的 shell, 首先選擇一些配置選項並創建一個新的 shell:

rlConf := readline.Config{
    Prompt:                 initialPrompt,
    Stdout:                 cli.CliOut,
    Stderr:                 cli.CliOut,
    HistoryFile:            historyFile,
    HistoryLimit:           500,
    HistorySearchFold:      true,
    DisableAutoSaveHistory: true,
}
shell := ishell.NewWithConfig(&rlConf)

然後添加你的命令。每個命令都是一個 *ishell.Cmd:

shell.AddCmd(&ishell.Cmd{
    Name: "login",
    Help: "simulate a login",
    Func: func(c *ishell.Context) {
        // ...
    },
})
shell.AddCmd(...)
shell.AddCmd(...)

最後, 運行它:

// blocks until shell.Stop() is called by some command
shell.Run()

啓動一個未解釋的 (自由格式)shell

如果你想讓你的 shell 是自由格式的, 你的設置就不同了。具體做法取決於你的配置, 但你需要用額外的配置來創建你的 shell, 以控制行終止符和如何退出 shell:

shellConf := ishell.UninterpretedConfig{
    ReadlineConfig:   &rlConf,
    QuitKeywords:     []string{"quit", "exit", "quit()", "exit()"},
    LineTerminator:   ";",
}
shell := ishell.NewUninterpreted(&shellConf)

然後以未解釋 (自由格式) 模式啓動 shell, 並提供一個單一的函數來處理所有輸入。下面是我們的做法:

shell.Uninterpreted(func(c *ishell.Context) {
    // The entire input line is provided as the single element in c.Args
    query := c.Args[0]
    if len(strings.TrimSpace(query)) == 0 {
        return
    }
    singleLine := strings.ReplaceAll(query, "\n", " ")
    // Add this query to our command history
    if err := shell.AddHistory(singleLine); err != nil {
        shell.Println(color.RedString(err.Error()))
    }
    query = strings.TrimSuffix(query, shell.LineTerminator())
    var nextPrompt string
    var multiPrompt string
    var sqlSch sql.Schema
    var rowIter sql.RowIter
    // Execute the query on the database, then either print the query results or an error if there was one
    func() {
        // We start a new context here so the user can interrupt a long-running query
        subCtx, stop := signal.NotifyContext(initialCtx, os.Interrupt, syscall.SIGTERM)
        defer stop()
        sqlCtx := sql.
        // ...
    }()
})

總之, 這就是我們如何使用 abiosoft/ishell 包來構建一個交互式 shell。我們希望這個教程對你有所幫助! 這段代碼是用 Go 語言編寫的, 它實現了一個交互式的 SQL shell。以下是對代碼的翻譯和解釋:

  1. 創建一個新的上下文, 並將其與 SQL 會話關聯。

  2. 執行查詢並打印結果或錯誤信息。

  1. 更新 shell 的提示符, 以顯示當前的數據庫和分支名稱。

  2. 運行 shell, 直到用戶退出。

此外, 代碼還實現了以下功能:

  1. 安裝 EOF 處理程序, 當輸入結束時停止 shell。

  2. 安裝中斷處理程序, 當用戶按下 Ctrl+C 時控制 shell 的行爲。

  3. 支持通過關鍵字 (如'quit'、'exit') 退出 shell。

  4. 實現了 shell 歷史記錄和自動補全功能。自動補全支持 SQL 關鍵字和表 / 列名的補全。

總的來說, 這段代碼實現了一個功能豐富的交互式 SQL shell, 爲用戶提供了良好的使用體驗。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/JLRFhpJSFeedB1rpawr4wA