成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專(zhuān)欄INFORMATION COLUMN

Java 命令行交互輸入庫(kù) JLine 入門(mén)

quietin / 3265人閱讀

摘要:實(shí)際上,中使用庫(kù)的軟件非常多,這使得同時(shí)也成為了一個(gè)事實(shí)上的命令行交互標(biāo)準(zhǔn)。對(duì)語(yǔ)言來(lái)說(shuō),就是這樣一個(gè)幫助你搭建一個(gè)命令行交互界面的庫(kù)。命令行會(huì)將用戶(hù)輸入的一行原樣打印出來(lái)。

我們都知道,軟件的用戶(hù)界面無(wú)非分為 GUI (圖形用戶(hù)界面)和 CLI (命令行用戶(hù)界面)。對(duì)于我們經(jīng)常使用 Linux 的人來(lái)說(shuō),命令行界面一定非常熟悉。無(wú)論是 Shell 里輸入命令的界面,還是如 GDB 等軟件的內(nèi)部交互界面,都是命令行界面。而當(dāng)我們開(kāi)發(fā)自己的軟件,要寫(xiě)認(rèn)真寫(xiě)一個(gè) CLI 的時(shí)候,卻發(fā)現(xiàn)要手寫(xiě)做出一個(gè)好用的命令行界面其實(shí)非常困難。因?yàn)橐粋€(gè)好的命令行界面,在輸入/輸出之外,還要支持一些常見(jiàn)的命令行功能。

對(duì)我而言,一個(gè)合格的命令行軟件界面應(yīng)該支持這三個(gè)功能:

自動(dòng)補(bǔ)全:當(dāng)按下 TAB 鍵時(shí),在當(dāng)前光標(biāo)處進(jìn)行內(nèi)容補(bǔ)全。根據(jù)上下文信息,補(bǔ)全可能是對(duì)命令的補(bǔ)全,也可能是對(duì)文件路徑的補(bǔ)全。

命令歷史:當(dāng)按上/下方向鍵時(shí),可以顯示上一條/下一條命令。

行編輯 (line editing):可以使用 Emacs 快捷鍵進(jìn)行行內(nèi)的編輯功能,例如 Ctrl+A 移動(dòng)光標(biāo)至行首,Ctrl+E 移動(dòng)光標(biāo)至行尾。

熟悉 Linux 的人會(huì)發(fā)現(xiàn),上面這三個(gè)功能都是 GNU Readline 的功能。我們不需要在軟件中手寫(xiě)這幾個(gè)功能,只要用這樣一個(gè)庫(kù)就可以了。實(shí)際上,GNU/Linux 中使用 GNU Readline 庫(kù)的軟件非常多,這使得 GNU Readline 同時(shí)也成為了一個(gè)事實(shí)上的命令行交互標(biāo)準(zhǔn)。GNU Readline 是 C 語(yǔ)言的庫(kù)。我們用其他語(yǔ)言的時(shí)候,就要找對(duì)應(yīng)功能的庫(kù)(這往往是封裝了底層的 GNU Readline 的庫(kù))。對(duì) Java 語(yǔ)言來(lái)說(shuō),JLine 就是這樣一個(gè)幫助你搭建一個(gè)命令行交互界面的庫(kù)。

本文是想通過(guò)一個(gè)例子介紹 JLine3 的基本用法。JLine3 并沒(méi)有一個(gè) "Hello, world!" 的例子,它的 wiki 也寫(xiě)得非常簡(jiǎn)略。雖然有一個(gè)示例的程序 Example.java,但這個(gè)示例比較復(fù)雜,難以理解。希望本文的內(nèi)容能對(duì)你理解 JLine3 的用法有所幫助。

基本框架

我們嘗試為軟件 Fog 設(shè)計(jì)一個(gè)命令行用戶(hù)界面。用戶(hù)可以輸入四種命令:

CREATE [FILE_NAME]
OPEN [FILE_NAME] AS [FILE_VAR]
WRITE TIME|DATE|LOCATION TO [FILE_VAR]
CLOSE [FILE_VAR]

下面我們將一步步地寫(xiě)出 Fog 軟件的命令行界面。首先,用 JLine3 搭建一個(gè)最基礎(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),沒(méi)有進(jìn)行任何特殊的設(shè)置。命令行會(huì)將用戶(hù)輸入的一行原樣打印出來(lái)。當(dāng)用戶(hù)輸入 Ctrl+D (End of line) 時(shí),程序會(huì)退出。

即使我們只寫(xiě)了一個(gè)框架,但此時(shí)程序已經(jīng)擁有了 JLine3 默認(rèn)提供的命令歷史和行編輯功能。此時(shí)按上/下方向鍵時(shí),會(huì)顯示上一條/下一條命令,也可以使用 Ctrl+A、Ctrl+E 等 Emacs 快捷鍵進(jìn)行行內(nèi)編輯。

命令補(bǔ)全 簡(jiǎn)單補(bǔ)全與復(fù)合補(bǔ)全

由于命令補(bǔ)全和程序的命令格式密切相關(guān),所以我們必須自己定義補(bǔ)全的方式。根據(jù) wiki 中所寫(xiě),JLine3 中定義命令補(bǔ)全的方式是:創(chuàng)建一個(gè) Completer 類(lèi)的實(shí)例,將其傳入 LineReader。JLine3 內(nèi)置了多個(gè) completer,其中最常見(jiàn)的是 FileNameCompleter (補(bǔ)全文件名)和 StringsCompleter (根據(jù)預(yù)定義的幾個(gè)字符串進(jìn)行補(bǔ)全,用于命令名或參數(shù)名)。例如,F(xiàn)og 程序的四個(gè)命令分別以 CREATE, OPEN, WRITE, CLOSE 開(kāi)頭,那么我們可以使用一個(gè) StringsCompleter 來(lái)對(duì)命令的第一個(gè)單詞進(jìn)行補(bǔ)全:

Completer commandCompleter = new StringsCompleter("CREATE", "OPEN", "WRITE", "CLOSE");

LineReader lineReader = LineReaderBuilder.builder()
        .terminal(terminal)
        .completer(commandCompleter)
        .build();

然而,這種補(bǔ)全方式只能支持每個(gè)命令的第一個(gè)單詞,我們想要在命令的各種可能的地方都進(jìn)行補(bǔ)全該怎么辦呢?這時(shí)候就需要將 completer 進(jìn)行組合,形成 復(fù)合 completer 。一般情況下,StringsCompleter 這樣的 簡(jiǎn)單 completer 只能負(fù)責(zé)一個(gè)單詞的補(bǔ)全,而要想實(shí)現(xiàn)整條命令的補(bǔ)全,就需要將幾個(gè)不同的 completer 組合起來(lái)使用。ArgumentCompleter 就是用來(lái)補(bǔ)全整條命令的復(fù)合 completer。它可以將若干個(gè) completer 組合在一起,每個(gè) completer 負(fù)責(zé)補(bǔ)全命令中的第 i 個(gè)單詞。以 CREATE 命令為例,這條命令共有兩個(gè)單詞,第一個(gè)單詞需要字符串補(bǔ)全,第二個(gè)單詞需要文件名補(bǔ)全。于是我們使用 ArgumentCompleterStringsCompleterFileNameCompleter 組合起來(lái):

Completer createCompleter = new ArgumentCompleter(
        new StringsCompleter("CREATE"),
        new Completers.FileNameCompleter()
);

LineReader lineReader = LineReaderBuilder.builder()
        .terminal(terminal)
        .completer(createCompleter)
        .build();

根據(jù) ArgumentCompleter 的兩個(gè)參數(shù),在輸入第一個(gè)單詞的時(shí)候會(huì)補(bǔ)全 CREATE,輸入第二個(gè)單詞的時(shí)候會(huì)補(bǔ)全文件名。但實(shí)測(cè)時(shí)會(huì)發(fā)現(xiàn)一個(gè)問(wèn)題:當(dāng)你已經(jīng)輸入了 CREATE 和文件名后,再試圖進(jìn)行補(bǔ)全,在第三個(gè)單詞處試圖補(bǔ)全,還是會(huì)出現(xiàn)文件名的補(bǔ)全。這是因?yàn)椋?b>ArgumentCompleter 在你已經(jīng)“用完了”所有的 completers 之后(即第三個(gè)單詞開(kāi)始),會(huì)默認(rèn)使用最后一個(gè) completer。這并不是我們想要的效果。為了解決這個(gè)問(wèn)題,我們可以在最后添加一個(gè) NullCompleter

Completer createCompleter = new ArgumentCompleter(
        new StringsCompleter("CREATE"),
        new Completers.FileNameCompleter(),
        NullCompleter.INSTANCE
);

LineReader lineReader = LineReaderBuilder.builder()
        .terminal(terminal)
        .completer(createCompleter)
        .build();

NullCompleter 即不進(jìn)行任何補(bǔ)全。這樣,從第三個(gè)單詞開(kāi)始,都不會(huì)進(jìn)行任何多余的補(bǔ)全。

類(lèi)似地,我們?cè)偌尤?OPEN 命令補(bǔ)全的定義:

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();

這里有兩點(diǎn)需要注意的地方:

CREATE 命令和 OPEN 命令分別定義了 completer,再用 AggregateCompleter 組合起來(lái)。AggregateCompleter 是另一種復(fù)合 completer,將多種可能的補(bǔ)全方式組合到了一起。打比方來(lái)說(shuō),ArgumentCompleter 相當(dāng)于串聯(lián)電路,而 AggregateCompleter 相當(dāng)于并聯(lián)電路。

OPEN 命令的 ArgumentCompleter 中只定義了前三個(gè)單詞的補(bǔ)全方式。這是因?yàn)榈谒膫€(gè)單詞是用戶(hù)定義了文件變量,用戶(hù)可能輸入任何的名字,因此無(wú)法進(jìn)行補(bǔ)全。

動(dòng)態(tài)補(bǔ)全

WRITE 命令的補(bǔ)全與前兩個(gè)稍有不同。根據(jù)程序語(yǔ)義,只有用戶(hù)在 OPEN 命令中定義了的文件變量才能在 WRITE 命令中使用。那么,在補(bǔ)全的時(shí)候也應(yīng)該考慮這一點(diǎn)。我們需要在運(yùn)行時(shí)動(dòng)態(tài)地調(diào)整補(bǔ)全候選詞:每當(dāng)用戶(hù)使用 OPEN 命令打開(kāi)一個(gè)文件后,都調(diào)整 completer,將新的文件變量納入補(bǔ)全候選詞。我們需要知道如何動(dòng)態(tài)地修改 completer。雖然 completer 的創(chuàng)建和傳遞給 LineReader 的過(guò)程是靜態(tài)的,但在程序運(yùn)行時(shí),是通過(guò)調(diào)用 Completer.complete() 來(lái)獲取補(bǔ)全的候選詞的。那么,我們可以繼承 Completer 并重寫(xiě) complete() 方法來(lái)實(shí)現(xiàn)動(dòng)態(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, List candidates) {
        completer.complete(reader, line, candidates);
    }

    public void setFileVars(List fileVars) {
        this.completer = new StringsCompleter(fileVars);
    }
}

當(dāng)調(diào)用 setFileVars() 時(shí),會(huì)重新創(chuàng)建一個(gè)新的 StringsCompleter,從而擴(kuò)充候選詞。而在 REPL 中,只需要在用戶(hù)輸入 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)過(guò)說(shuō),在默認(rèn)情況下,JLine3 已經(jīng)支持命令歷史查找。不過(guò)我們想加上一個(gè)特殊的功能:用戶(hù)輸入的注釋?zhuān)ㄒ?# 開(kāi)頭)不會(huì)進(jìn)入命令歷史,從而在命令歷史查找時(shí)不受注釋內(nèi)容的干擾。

JLine3 中,History 負(fù)責(zé)控制歷史記錄的行為,其默認(rèn)實(shí)現(xiàn)為 DefaultHistory。查看源代碼,我們發(fā)現(xiàn) add() 方法是其核心行為。用戶(hù)輸入的一行命令,會(huì)通過(guò) 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);

    // ...
}

同樣地,我們可以通過(guò)繼承并重寫(xiě) add() 方法,將注釋內(nèi)容過(guò)濾掉,不加入命令歷史:

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 的各個(gè)功能設(shè)計(jì)得比較清晰,有其對(duì)應(yīng)的接口和默認(rèn)實(shí)現(xiàn)。如果我們想自定義一些特性,一般通過(guò)繼承并重寫(xiě)的方式可以做到。JLine3 的源代碼也比較容易理解,遇到困難時(shí),可以自己閱讀源代碼來(lái)尋找線(xiàn)索。

本文中示例程序的完整代碼參見(jiàn) jline3-demo。

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/77015.html

相關(guān)文章

  • python入門(mén)

    摘要:零預(yù)備知識(shí)字符編碼計(jì)算機(jī)只能處理數(shù)字,所以為文本需要轉(zhuǎn)化為數(shù)字才能被計(jì)算機(jī)處理,計(jì)算機(jī)里八個(gè)比特作為一個(gè)字節(jié),這是數(shù)據(jù)的存儲(chǔ)基礎(chǔ)單位。 零、預(yù)備知識(shí) 0.1 字符編碼計(jì)算機(jī)只能處理數(shù)字,所以為文本需要轉(zhuǎn)化為數(shù)字才能被計(jì)算機(jī)處理,計(jì)算機(jī)里八個(gè)比特(bit)作為一個(gè)字節(jié)(byte),這是數(shù)據(jù)的存儲(chǔ)基礎(chǔ)單位。計(jì)算機(jī)為了處理文本,有以下三種編碼方式: ASCII碼:只有大小寫(xiě)英文字母,數(shù)字...

    xeblog 評(píng)論0 收藏0
  • maven介紹

    摘要:介紹簡(jiǎn)介編寫(xiě)的用于構(gòu)建系統(tǒng)的自動(dòng)化工具。文件的基本配置介紹什么是,項(xiàng)目對(duì)象模型?;驹O(shè)置協(xié)作關(guān)系組織標(biāo)識(shí),例如,在目錄下,將是目錄。 1. Maven介紹 1.1. 簡(jiǎn)介 java編寫(xiě)的用于構(gòu)建系統(tǒng)的自動(dòng)化工具。 目前版本是2.0.9,注意maven2和maven1有很大區(qū)別,閱讀第三方文檔時(shí)需要區(qū)分版本。 1.2. Maven資源 ? 見(jiàn)官方網(wǎng)站; ? The 5 minu...

    xorpay 評(píng)論0 收藏0
  • python入門(mén),編程基礎(chǔ)概念介紹(變量,條件,函數(shù),循環(huán))

    摘要:該系列文章入門(mén),編程基礎(chǔ)概念介紹變量,條件,函數(shù),循環(huán)中的數(shù)據(jù)類(lèi)型,,,,在中創(chuàng)建對(duì)象學(xué)一門(mén)編程語(yǔ)言正在變得越來(lái)越容易,只要念過(guò)高中甚至是初中小學(xué),能熟練聊和懂得一點(diǎn)點(diǎn)軟件的人,入門(mén)一門(mén)編程語(yǔ)言都不在話(huà)下。 該系列文章: 《python入門(mén),編程基礎(chǔ)概念介紹(變量,條件,函數(shù),循環(huán))》 《python中的數(shù)據(jù)類(lèi)型(list,tuple,dict,set,None)》 《在python...

    Bryan 評(píng)論0 收藏0
  • 以太坊DApp開(kāi)發(fā)入門(mén)教程——Node.js和truffle框架打造區(qū)塊鏈投票系統(tǒng)

    摘要:第一節(jié)課程概述本課程面向初學(xué)者,內(nèi)容涵蓋以太坊開(kāi)發(fā)相關(guān)的基本概念,并將手把手地教大家如何構(gòu)建一個(gè)基于以太坊的完整去中心化應(yīng)用區(qū)塊鏈投票系統(tǒng)。第七節(jié)以太坊世界計(jì)算機(jī)以太坊是一種區(qū)塊鏈的實(shí)現(xiàn)。交易數(shù)據(jù)以太坊中每筆交易都存儲(chǔ)在區(qū)塊鏈上。 第一節(jié) 課程概述 本課程面向初學(xué)者,內(nèi)容涵蓋以太坊開(kāi)發(fā)相關(guān)的基本概念,并將手把手地教大家如何構(gòu)建一個(gè) 基于以太坊的完整去中心化應(yīng)用 —— 區(qū)塊鏈投票系統(tǒng)。 ...

    zebrayoung 評(píng)論0 收藏0
  • 以太坊DApp開(kāi)發(fā)入門(mén)教程——Node.js和truffle框架打造區(qū)塊鏈投票系統(tǒng)

    摘要:第一節(jié)課程概述本課程面向初學(xué)者,內(nèi)容涵蓋以太坊開(kāi)發(fā)相關(guān)的基本概念,并將手把手地教大家如何構(gòu)建一個(gè)基于以太坊的完整去中心化應(yīng)用區(qū)塊鏈投票系統(tǒng)。第七節(jié)以太坊世界計(jì)算機(jī)以太坊是一種區(qū)塊鏈的實(shí)現(xiàn)。交易數(shù)據(jù)以太坊中每筆交易都存儲(chǔ)在區(qū)塊鏈上。 第一節(jié) 課程概述 本課程面向初學(xué)者,內(nèi)容涵蓋以太坊開(kāi)發(fā)相關(guān)的基本概念,并將手把手地教大家如何構(gòu)建一個(gè) 基于以太坊的完整去中心化應(yīng)用 —— 區(qū)塊鏈投票系統(tǒng)。 ...

    MASAILA 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<