摘要:實際上,中使用庫的軟件非常多,這使得同時也成為了一個事實上的命令行交互標準。對語言來說,就是這樣一個幫助你搭建一個命令行交互界面的庫。命令行會將用戶輸入的一行原樣打印出來。
我們都知道,軟件的用戶界面無非分為 GUI (圖形用戶界面)和 CLI (命令行用戶界面)。對于我們經(jīng)常使用 Linux 的人來說,命令行界面一定非常熟悉。無論是 Shell 里輸入命令的界面,還是如 GDB 等軟件的內(nèi)部交互界面,都是命令行界面。而當我們開發(fā)自己的軟件,要寫認真寫一個 CLI 的時候,卻發(fā)現(xiàn)要手寫做出一個好用的命令行界面其實非常困難。因為一個好的命令行界面,在輸入/輸出之外,還要支持一些常見的命令行功能。
對我而言,一個合格的命令行軟件界面應該支持這三個功能:
自動補全:當按下 TAB 鍵時,在當前光標處進行內(nèi)容補全。根據(jù)上下文信息,補全可能是對命令的補全,也可能是對文件路徑的補全。
命令歷史:當按上/下方向鍵時,可以顯示上一條/下一條命令。
行編輯 (line editing):可以使用 Emacs 快捷鍵進行行內(nèi)的編輯功能,例如 Ctrl+A 移動光標至行首,Ctrl+E 移動光標至行尾。
熟悉 Linux 的人會發(fā)現(xiàn),上面這三個功能都是 GNU Readline 的功能。我們不需要在軟件中手寫這幾個功能,只要用這樣一個庫就可以了。實際上,GNU/Linux 中使用 GNU Readline 庫的軟件非常多,這使得 GNU Readline 同時也成為了一個事實上的命令行交互標準。GNU Readline 是 C 語言的庫。我們用其他語言的時候,就要找對應功能的庫(這往往是封裝了底層的 GNU Readline 的庫)。對 Java 語言來說,JLine 就是這樣一個幫助你搭建一個命令行交互界面的庫。
本文是想通過一個例子介紹 JLine3 的基本用法。JLine3 并沒有一個 "Hello, world!" 的例子,它的 wiki 也寫得非常簡略。雖然有一個示例的程序 Example.java,但這個示例比較復雜,難以理解。希望本文的內(nèi)容能對你理解 JLine3 的用法有所幫助。
基本框架我們嘗試為軟件 Fog 設(shè)計一個命令行用戶界面。用戶可以輸入四種命令:
CREATE [FILE_NAME] OPEN [FILE_NAME] AS [FILE_VAR] WRITE TIME|DATE|LOCATION TO [FILE_VAR] CLOSE [FILE_VAR]
下面我們將一步步地寫出 Fog 軟件的命令行界面。首先,用 JLine3 搭建一個最基礎(chǔ)的 REPL (Read-Eval-Print Loop) 框架:
Terminal terminal = TerminalBuilder.builder() .system(true) .build(); LineReader lineReader = LineReaderBuilder.builder() .terminal(terminal) .build(); String prompt = "fog> "; while (true) { String line; try { line = lineReader.readLine(prompt); System.out.println(line); } catch (UserInterruptException e) { // Do nothing } catch (EndOfFileException e) { System.out.println(" Bye."); return; } }
這里除了設(shè)置命令提示符 (prompt),沒有進行任何特殊的設(shè)置。命令行會將用戶輸入的一行原樣打印出來。當用戶輸入 Ctrl+D (End of line) 時,程序會退出。
即使我們只寫了一個框架,但此時程序已經(jīng)擁有了 JLine3 默認提供的命令歷史和行編輯功能。此時按上/下方向鍵時,會顯示上一條/下一條命令,也可以使用 Ctrl+A、Ctrl+E 等 Emacs 快捷鍵進行行內(nèi)編輯。
命令補全 簡單補全與復合補全由于命令補全和程序的命令格式密切相關(guān),所以我們必須自己定義補全的方式。根據(jù) wiki 中所寫,JLine3 中定義命令補全的方式是:創(chuàng)建一個 Completer 類的實例,將其傳入 LineReader。JLine3 內(nèi)置了多個 completer,其中最常見的是 FileNameCompleter (補全文件名)和 StringsCompleter (根據(jù)預定義的幾個字符串進行補全,用于命令名或參數(shù)名)。例如,F(xiàn)og 程序的四個命令分別以 CREATE, OPEN, WRITE, CLOSE 開頭,那么我們可以使用一個 StringsCompleter 來對命令的第一個單詞進行補全:
Completer commandCompleter = new StringsCompleter("CREATE", "OPEN", "WRITE", "CLOSE"); LineReader lineReader = LineReaderBuilder.builder() .terminal(terminal) .completer(commandCompleter) .build();
然而,這種補全方式只能支持每個命令的第一個單詞,我們想要在命令的各種可能的地方都進行補全該怎么辦呢?這時候就需要將 completer 進行組合,形成 復合 completer 。一般情況下,StringsCompleter 這樣的 簡單 completer 只能負責一個單詞的補全,而要想實現(xiàn)整條命令的補全,就需要將幾個不同的 completer 組合起來使用。ArgumentCompleter 就是用來補全整條命令的復合 completer。它可以將若干個 completer 組合在一起,每個 completer 負責補全命令中的第 i 個單詞。以 CREATE 命令為例,這條命令共有兩個單詞,第一個單詞需要字符串補全,第二個單詞需要文件名補全。于是我們使用 ArgumentCompleter 將 StringsCompleter 和 FileNameCompleter 組合起來:
Completer createCompleter = new ArgumentCompleter( new StringsCompleter("CREATE"), new Completers.FileNameCompleter() ); LineReader lineReader = LineReaderBuilder.builder() .terminal(terminal) .completer(createCompleter) .build();
根據(jù) ArgumentCompleter 的兩個參數(shù),在輸入第一個單詞的時候會補全 CREATE,輸入第二個單詞的時候會補全文件名。但實測時會發(fā)現(xiàn)一個問題:當你已經(jīng)輸入了 CREATE 和文件名后,再試圖進行補全,在第三個單詞處試圖補全,還是會出現(xiàn)文件名的補全。這是因為,ArgumentCompleter 在你已經(jīng)“用完了”所有的 completers 之后(即第三個單詞開始),會默認使用最后一個 completer。這并不是我們想要的效果。為了解決這個問題,我們可以在最后添加一個 NullCompleter:
Completer createCompleter = new ArgumentCompleter( new StringsCompleter("CREATE"), new Completers.FileNameCompleter(), NullCompleter.INSTANCE ); LineReader lineReader = LineReaderBuilder.builder() .terminal(terminal) .completer(createCompleter) .build();
NullCompleter 即不進行任何補全。這樣,從第三個單詞開始,都不會進行任何多余的補全。
類似地,我們再加入 OPEN 命令補全的定義:
Completer createCompleter = new ArgumentCompleter( new StringsCompleter("CREATE"), new Completers.FileNameCompleter(), NullCompleter.INSTANCE ); Completer openCompleter = new ArgumentCompleter( new StringsCompleter("OPEN"), new Completers.FileNameCompleter(), new StringsCompleter("AS"), NullCompleter.INSTANCE ); Completer fogCompleter = new AggregateCompleter( createCompleter, openCompleter ); LineReader lineReader = LineReaderBuilder.builder() .terminal(terminal) .completer(fogCompleter) .build();
這里有兩點需要注意的地方:
CREATE 命令和 OPEN 命令分別定義了 completer,再用 AggregateCompleter 組合起來。AggregateCompleter 是另一種復合 completer,將多種可能的補全方式組合到了一起。打比方來說,ArgumentCompleter 相當于串聯(lián)電路,而 AggregateCompleter 相當于并聯(lián)電路。
OPEN 命令的 ArgumentCompleter 中只定義了前三個單詞的補全方式。這是因為第四個單詞是用戶定義了文件變量,用戶可能輸入任何的名字,因此無法進行補全。
動態(tài)補全WRITE 命令的補全與前兩個稍有不同。根據(jù)程序語義,只有用戶在 OPEN 命令中定義了的文件變量才能在 WRITE 命令中使用。那么,在補全的時候也應該考慮這一點。我們需要在運行時動態(tài)地調(diào)整補全候選詞:每當用戶使用 OPEN 命令打開一個文件后,都調(diào)整 completer,將新的文件變量納入補全候選詞。我們需要知道如何動態(tài)地修改 completer。雖然 completer 的創(chuàng)建和傳遞給 LineReader 的過程是靜態(tài)的,但在程序運行時,是通過調(diào)用 Completer.complete() 來獲取補全的候選詞的。那么,我們可以繼承 Completer 并重寫 complete() 方法來實現(xiàn)動態(tài)的候選詞調(diào)整。
public class FileVarsCompleter implements Completer { Completer completer; public FileVarsCompleter() { this.completer = new StringsCompleter(); } @Override public void complete(LineReader reader, ParsedLine line, Listcandidates) { completer.complete(reader, line, candidates); } public void setFileVars(List fileVars) { this.completer = new StringsCompleter(fileVars); } }
當調(diào)用 setFileVars() 時,會重新創(chuàng)建一個新的 StringsCompleter,從而擴充候選詞。而在 REPL 中,只需要在用戶輸入 OPEN 命令后,調(diào)用 setFileVars() 即可。
public class Fog { private static List命令歷史fileVars = new ArrayList<>(); private static FileVarsCompleter fileVarsCompleter = new FileVarsCompleter(); public static void main(String[] args) throws IOException { // ... Completer writeCompleter = new ArgumentCompleter( new StringsCompleter("WRITE"), new StringsCompleter("TIME", "DATE", "LOCATION"), new StringsCompleter("TO"), fileVarsCompleter, NullCompleter.INSTANCE ); Completer fogCompleter = new AggregateCompleter( createCompleter, openCompleter, writeCompleter ); // ... String prompt = "fog> "; while (true) { String line; try { line = lineReader.readLine(prompt); System.out.println(line); if (line.startsWith("OPEN")) { fileVars.add(line.split(" ")[3]); fileVarsCompleter.setFileVars(fileVars); } } catch (UserInterruptException e) { // Do nothing } catch (EndOfFileException e) { System.out.println(" Bye."); return; } } } }
前面已經(jīng)過說,在默認情況下,JLine3 已經(jīng)支持命令歷史查找。不過我們想加上一個特殊的功能:用戶輸入的注釋(以 # 開頭)不會進入命令歷史,從而在命令歷史查找時不受注釋內(nèi)容的干擾。
JLine3 中,History 負責控制歷史記錄的行為,其默認實現(xiàn)為 DefaultHistory。查看源代碼,我們發(fā)現(xiàn) add() 方法是其核心行為。用戶輸入的一行命令,會通過 add() 方法加入命令歷史中。
@Override public void add(Instant time, String line) { Objects.requireNonNull(time); Objects.requireNonNull(line); if (getBoolean(reader, LineReader.DISABLE_HISTORY, false)) { return; } // ... internalAdd(time, line); // ... }
同樣地,我們可以通過繼承并重寫 add() 方法,將注釋內(nèi)容過濾掉,不加入命令歷史:
public final class FogHistory extends DefaultHistory { private static boolean isComment(String line) { return line.startsWith("#"); } @Override public void add(Instant time, String line) { if (isComment(line)) { return; } super.add(time, line); } }
然后我們這樣設(shè)置 LineReader:
LineReader lineReader = LineReaderBuilder.builder() .terminal(terminal) .completer(fogCompleter) .history(new FogHistory()) .build();總結(jié)
我們發(fā)現(xiàn),JLine3 的各個功能設(shè)計得比較清晰,有其對應的接口和默認實現(xiàn)。如果我們想自定義一些特性,一般通過繼承并重寫的方式可以做到。JLine3 的源代碼也比較容易理解,遇到困難時,可以自己閱讀源代碼來尋找線索。
本文中示例程序的完整代碼參見 jline3-demo。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/77015.html
摘要:該系列文章入門,編程基礎(chǔ)概念介紹變量,條件,函數(shù),循環(huán)中的數(shù)據(jù)類型,,,,在中創(chuàng)建對象學一門編程語言正在變得越來越容易,只要念過高中甚至是初中小學,能熟練聊和懂得一點點軟件的人,入門一門編程語言都不在話下。 該系列文章: 《python入門,編程基礎(chǔ)概念介紹(變量,條件,函數(shù),循環(huán))》 《python中的數(shù)據(jù)類型(list,tuple,dict,set,None)》 《在python...
摘要:第一節(jié)課程概述本課程面向初學者,內(nèi)容涵蓋以太坊開發(fā)相關(guān)的基本概念,并將手把手地教大家如何構(gòu)建一個基于以太坊的完整去中心化應用區(qū)塊鏈投票系統(tǒng)。第七節(jié)以太坊世界計算機以太坊是一種區(qū)塊鏈的實現(xiàn)。交易數(shù)據(jù)以太坊中每筆交易都存儲在區(qū)塊鏈上。 第一節(jié) 課程概述 本課程面向初學者,內(nèi)容涵蓋以太坊開發(fā)相關(guān)的基本概念,并將手把手地教大家如何構(gòu)建一個 基于以太坊的完整去中心化應用 —— 區(qū)塊鏈投票系統(tǒng)。 ...
摘要:第一節(jié)課程概述本課程面向初學者,內(nèi)容涵蓋以太坊開發(fā)相關(guān)的基本概念,并將手把手地教大家如何構(gòu)建一個基于以太坊的完整去中心化應用區(qū)塊鏈投票系統(tǒng)。第七節(jié)以太坊世界計算機以太坊是一種區(qū)塊鏈的實現(xiàn)。交易數(shù)據(jù)以太坊中每筆交易都存儲在區(qū)塊鏈上。 第一節(jié) 課程概述 本課程面向初學者,內(nèi)容涵蓋以太坊開發(fā)相關(guān)的基本概念,并將手把手地教大家如何構(gòu)建一個 基于以太坊的完整去中心化應用 —— 區(qū)塊鏈投票系統(tǒng)。 ...
閱讀 2589·2023-04-26 03:00
閱讀 1405·2021-10-12 10:12
閱讀 4200·2021-09-22 15:33
閱讀 2927·2021-09-22 15:06
閱讀 1540·2019-08-30 15:44
閱讀 2152·2019-08-30 13:59
閱讀 541·2019-08-30 11:24
閱讀 2421·2019-08-29 17:07