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 的狀態而改變。在我們的 SQL shell 中, 它會顯示你連接的數據庫和分支。
-
你可以通過特定的輸入退出 shell, 在我們的例子中是
quit或exit。
我們還在 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。以下是對代碼的翻譯和解釋:
-
創建一個新的上下文, 並將其與 SQL 會話關聯。
-
執行查詢並打印結果或錯誤信息。
-
如果查詢執行成功, 則根據指定的格式 (表格或垂直) 打印結果。
-
如果查詢執行失敗, 則打印錯誤信息。
-
更新 shell 的提示符, 以顯示當前的數據庫和分支名稱。
-
運行 shell, 直到用戶退出。
此外, 代碼還實現了以下功能:
-
安裝 EOF 處理程序, 當輸入結束時停止 shell。
-
安裝中斷處理程序, 當用戶按下 Ctrl+C 時控制 shell 的行爲。
-
支持通過關鍵字 (如'quit'、'exit') 退出 shell。
-
實現了 shell 歷史記錄和自動補全功能。自動補全支持 SQL 關鍵字和表 / 列名的補全。
總的來說, 這段代碼實現了一個功能豐富的交互式 SQL shell, 爲用戶提供了良好的使用體驗。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/JLRFhpJSFeedB1rpawr4wA