震驚!原來命令行還可以這么玩?!
你是否:
- 好奇過命令行里那些花里胡哨的進(jìn)度條是如何實(shí)現(xiàn)的?
- 好奇過Spring Boot為什么能夠打印五顏六色的日志?
- 好奇過Python或者PHP等腳本語言的交互式命令行是如何實(shí)現(xiàn)的?
- 好奇過Vim或者Emacs等在Terminal中的編輯器是怎么實(shí)現(xiàn)的?
如果你曾經(jīng)好奇過,或者被這段話勾起了你的好奇心,那么你絕對不能錯(cuò)過這篇文章!
背景
通過本文你可以學(xué)到:
- 何為Ansi Escape Codes以及它們能干什么?
- Ansi Escape Codes的一些高級應(yīng)用。
- JDK9中Jshell的使用。
事先聲明,本文主要參考: http://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html 。原文思路清晰,案例生動(dòng)形象,排版優(yōu)秀,實(shí)為良心之作。但是由于原文是用英語書寫且用Python作為演示,所以本后端小菜雞不要臉地將其翻譯一遍,并且用JDK9的Jshell做演示,方便廣大的Javaer學(xué)習(xí)。
本文所有的代碼已經(jīng)推到Github中,地址為: https://github.com/Lovelcp/blog-demos/tree/master/ansi-escape-codes-tutorial 。強(qiáng)烈建議大家將代碼clone下來跑一下看看效果,加深自己的印象。
環(huán)境
- Mac或Linux或者WIn10操作系統(tǒng)。 除了Win10之外的Windows系統(tǒng)暫時(shí)不支持Ansi Escape Codes。
- 因?yàn)楸疚牟捎肑shell作為演示工具,所以大家需要安裝最近剛正式發(fā)布的JDK9。
OK!一切準(zhǔn)備就緒,讓我們開始吧!
富文本
Ansi Escape Codes最基礎(chǔ)的用途就是讓控制臺顯示的文字以富文本的形式輸出,比如設(shè)置字體顏色、背景顏色以及各種樣式。讓我們先來學(xué)習(xí)如何設(shè)置字體顏色,而不用再忍受那枯燥的黑白二色!
字體顏色
通過Ansi指令(即Ansi Escape Codes)給控制臺的文字上色是最為常見的操作。比如:
- 紅色: \u001b[31m
- 重置: \u001b[0m
絕大部分Ansi Escape Codes都以 \u001b 開頭。讓我們通過Java代碼來輸出一段紅色的 Hello World :
- System.out.print("\u001b[31mHello World");
從上圖中,我們可以看到,不僅 Hello World 是變成了紅色,而且接下來的 jshell> 提示符也變成了紅色。其實(shí)不管你接下來輸入什么字符,它們的字體顏色都是紅色。直到你輸入了其他顏色的Ansi指令,或者輸入了重置指令,字體的顏色才會(huì)不再是紅色。
讓我們嘗試輸入重置指令來恢復(fù)字體的顏色:
- System.out.print("\u001b[0m");
很好! jshell> 提示符恢復(fù)為了白色。所以一個(gè)最佳實(shí)踐就是,最好在所有改變字體顏色或者樣式的Ansi Escape Codes的最后加上重置指令,以免造成意想不到的后果。舉個(gè)例子:
- System.out.print("\u001b[31mHello World\u001b[0m");
當(dāng)然,重置指令可以被添加在任何位置,比如我們可以將其插在 Hello World 的中間,使得 Hello 是紅色,但是 World 是白色:
- System.out.print("\u001b[31mHello\u001b[0m World");
8色
剛才我們介紹了 紅色 以及 重置 命令。基本上所有的控制臺都支持以下8種顏色:
- 黑色: \u001b[30m
- 紅色: \u001b[31m
- 綠色: \u001b[32m
- 黃色: \u001b[33m
- 藍(lán)色: \u001b[34m
- 洋紅色: \u001b[35m
- 青色: \u001b[36m
- 白色: \u001b[37m
- 重置: \u001b[0m
不如將它們都輸出看一下:
- System.out.print("\u001b[30m A \u001b[31m B \u001b[32m C \u001b[33m D \u001b[0m");
- System.out.print("\u001b[34m E \u001b[35m F \u001b[36m G \u001b[37m H \u001b[0m");
注意, A 因?yàn)槭呛谏耘c控制臺融為一體了。
16色
大多數(shù)的控制臺,除了支持剛才提到的8色外,還可以輸出在此之上更加明亮的8種顏色:
- 亮黑色: \u001b[30;1m
- 亮紅色: \u001b[31;1m
- 亮綠色: \u001b[32;1m
- 亮黃色: \u001b[33;1m
- 亮藍(lán)色: \u001b[34;1m
- 亮洋紅色: \u001b[35;1m
- 亮青色: \u001b[36;1m
- 亮白色: \u001b[37;1m
亮色指令分別在原來對應(yīng)顏色的指令中間加上 ;1 。我們將所有的16色在控制臺打印,方便大家進(jìn)行比對:
- System.out.print("\u001b[30m A \u001b[31m B \u001b[32m C \u001b[33m D \u001b[0m");
- System.out.print("\u001b[34m E \u001b[35m F \u001b[36m G \u001b[37m H \u001b[0m");
- System.out.print("\u001b[30;1m A \u001b[31;1m B \u001b[32;1m C \u001b[33;1m D \u001b[0m");
- System.out.print("\u001b[34;1m E \u001b[35;1m F \u001b[36;1m G \u001b[37;1m H \u001b[0m");
從圖中我們可以清晰地看到,下面的8色比上面的8色顯得更加明亮。比如,原來黑色的 A ,在黑色的控制臺背景下,幾乎無法看到,但是一旦通過亮黑色輸出后,對比度變得更高,變得更好辨識了。
256色
最后,除了16色外,某些控制臺支持輸出256色。指令的形式如下:
- \u001b[38;5;${ID}m
讓我們輸出256色矩陣:
- for (int i = 0; i < 16; i++) {
- for (int j = 0; j < 16; j++) {
- int code = i * 16 + j;
- System.out.printf("\u001b[38;5;%dm%-4d", code, code);
- }
- System.out.println("\u001b[0m");
- }
關(guān)于字體顏色我們就介紹到這,接下來我們來介紹背景色。
背景顏色
剛才所說的字體顏色可以統(tǒng)稱為前景色(foreground color)。那么理所當(dāng)然,我們可以設(shè)置文本的背景顏色:
- 黑色背景: \u001b[40m
- 紅色背景: \u001b[41m
- 綠色背景: \u001b[42m
- 黃色背景: \u001b[43m
- 藍(lán)色背景: \u001b[44m
- 洋紅色背景: \u001b[45m
- 青色背景: \u001b[46m
- 白色背景: \u001b[47m
對應(yīng)的亮色版本:
- 亮黑色背景: \u001b[40;1m
- 亮紅色背景: \u001b[41;1m
- 亮綠色背景: \u001b[42;1m
- 亮黃色背景: \u001b[43;1m
- 亮藍(lán)色背景: \u001b[44;1m
- 亮洋紅色背景: \u001b[45;1m
- 亮青色背景: \u001b[46;1m
- 亮白色背景: \u001b[47;1m
首先讓我們看看16色背景:
- System.out.print("\u001b[40m A \u001b[41m B \u001b[42m C \u001b[43m D \u001b[0m");
- System.out.print("\u001b[44m A \u001b[45m B \u001b[46m C \u001b[47m D \u001b[0m");
- System.out.print("\u001b[40;1m A \u001b[41;1m B \u001b[42;1m C \u001b[43;1m D \u001b[0m");
- System.out.print("\u001b[44;1m A \u001b[45;1m B \u001b[46;1m C \u001b[47;1m D \u001b[0m");
值得注意的是,亮色背景并不是背景顏色顯得更加明亮,而是讓對應(yīng)的前景色顯得更加明亮。雖然這點(diǎn)有點(diǎn)不太直觀,但是實(shí)際表現(xiàn)就是如此。
讓我們再來試試256背景色,首先指令如下:
- \u001b[48;5;${ID}m
同樣輸出256色矩陣:
- for (int i = 0; i < 16; i++) {
- for (int j = 0; j < 16; j++) {
- int code = i * 16 + j;
- System.out.printf("\u001b[48;5;%dm%-4d", code, code);
- }
- System.out.println("\u001b[0m");
- }
感覺要被亮瞎眼了呢!至此,顏色設(shè)置已經(jīng)介紹完畢,讓我們接著學(xué)習(xí)樣式設(shè)置。
樣式
除了給文本設(shè)置顏色之外,我們還可以給文本設(shè)置樣式:
- 粗體: \u001b[1m
- 下劃線: \u001b[4m
- 反色: \u001b[7m
樣式分別使用的效果:
- System.out.print("\u001b[1m BOLD \u001b[0m\u001b[4m Underline \u001b[0m\u001b[7m Reversed \u001b[0m");
或者結(jié)合使用:
- System.out.print("\u001b[1m\u001b[4m\u001b[7m BOLD Underline Reversed \u001b[0m");
甚至還可以和顏色結(jié)合使用:
- System.out.print("\u001b[1m\u001b[31m Red Bold \u001b[0m");
- System.out.print("\u001b[4m\u001b[44m Blue Background Underline \u001b[0m");
是不是很簡單,是不是很酷!學(xué)會(huì)了這些,我們已經(jīng)能夠?qū)懗鍪挚犰诺拿钚心_本了。但是如果要實(shí)現(xiàn)更復(fù)雜的功能(比如進(jìn)度條),我們還需要掌握更加牛逼的光標(biāo)控制指令!
光標(biāo)控制
Ansi Escape Code里更加復(fù)雜的指令就是光標(biāo)控制。通過這些指令,我們可以自由地移動(dòng)我們的光標(biāo)至屏幕的任何位置。比如在Vim的命令模式下,我們可以使用 H/J/K/L 這四個(gè)鍵實(shí)現(xiàn)光標(biāo)的上下左右移動(dòng)。
最基礎(chǔ)的光標(biāo)控制指令如下:
- 上: \u001b[{n}A
- 下: \u001b[{n}B
- 右: \u001b[{n}C
- 左: \u001b[{n}D
通過光標(biāo)控制的特性,我們能夠?qū)崿F(xiàn)大量有趣且酷炫的功能。首先我們來看看怎么實(shí)現(xiàn)一個(gè)進(jìn)度條。
進(jìn)度數(shù)字顯示
作為進(jìn)度條,怎么可以沒有進(jìn)度數(shù)字顯示呢?所以我們先來實(shí)現(xiàn)進(jìn)度條進(jìn)度數(shù)字的刷新:
- void loading()throws InterruptedException {
- System.out.println("Loading...");
- for (int i = 1; i <= 100; i++) {
- Thread.sleep(100);
- System.out.print("\u001b[1000D" + i + "%");
- }
- }
從圖中我們可以看到,進(jìn)度在同一行從1%不停地刷新到100%。為了進(jìn)度只在同一行顯示,我們在代碼中使用了 System.out.print 而不是 System.out.println 。在打印每個(gè)進(jìn)度之前,我們使用了 \u001b[1000D 指令,目的是為了將光標(biāo)移動(dòng)到當(dāng)前行的最左邊也就是行首。然后重新打印新的進(jìn)度,新的進(jìn)度數(shù)字會(huì)覆蓋剛才的進(jìn)度數(shù)字,循環(huán)往復(fù),這就實(shí)現(xiàn)了上圖的效果。
PS: \u001b[1000D 表示將光標(biāo)往左移動(dòng)1000個(gè)字符。這里的1000表示光標(biāo)移動(dòng)的距離,只要你能夠確保光標(biāo)能夠移動(dòng)到最左端,隨便設(shè)置多少比如設(shè)置2000都可以。
為了方便大家更加輕松地理解光標(biāo)的移動(dòng)過程,讓我們放慢進(jìn)度條刷新的頻率:
- void loading()throws InterruptedException {
- System.out.println("Loading...");
- for (int i = 1; i <= 100; i++) {
- System.out.print("\u001b[1000D");
- Thread.sleep(1000);
- System.out.print(i + "%");
- Thread.sleep(1000);
- }
- }
現(xiàn)在我們可以清晰地看到:
- 從左到右打印進(jìn)度,光標(biāo)移至行尾。
- 光標(biāo)移至行首,原進(jìn)度數(shù)字還在。
- 從左到右打印新進(jìn)度,新的數(shù)字會(huì)覆蓋老的數(shù)字。光標(biāo)移至行尾。
- 循環(huán)往復(fù)。
Ascii進(jìn)度條
好了,我們現(xiàn)在已經(jīng)知道如何通過Ansi Escape Code實(shí)現(xiàn)進(jìn)度數(shù)字的顯示和刷新,剩下的就是實(shí)現(xiàn)進(jìn)度的讀條。廢話不多說,我們直接上代碼和效果圖:
- void loading()throws InterruptedException {
- System.out.println("Loading...");
- for (int i = 1; i <= 100; i++) {
- int width = i / 4;
- String left = "[" + String.join("", Collections.nCopies(width, "#"));
- String right = String.join("", Collections.nCopies(25 - width, " ")) + "]";
- System.out.print("\u001b[1000D" + left + right);
- Thread.sleep(100);
- }
- }
由上圖我們可以看到,每次循環(huán)過后,讀條就會(huì)增加。原理和數(shù)字的刷新一樣,相信大家閱讀代碼就能理解,這里就不再贅述。
讓我們來點(diǎn)更酷的吧!利用Ansi的光標(biāo) 向上 以及 向下 的指令,我們還可以同時(shí)打印出多條進(jìn)度條:
- void loading(int count)throws InterruptedException {
- System.out.print(String.join("", Collections.nCopies(count, "\n"))); // 初始化進(jìn)度條所占的空間
- List<Integer> allProgress = new ArrayList<>(Collections.nCopies(count, 0));
- while (true) {
- Thread.sleep(10);
- // 隨機(jī)選擇一個(gè)進(jìn)度條,增加進(jìn)度
- List<Integer> unfinished = new LinkedList<>();
- for (int i = 0; i < allProgress.size(); i++) {
- if (allProgress.get(i) < 100) {
- unfinished.add(i);
- }
- }
- if (unfinished.isEmpty()) {
- break;
- }
- int index = unfinished.get(new Random().nextInt(unfinished.size()));
- allProgress.set(index, allProgress.get(index) + 1); // 進(jìn)度+1
- // 繪制進(jìn)度條
- System.out.print("\u001b[1000D"); // 移動(dòng)到最左邊
- System.out.print("\u001b[" + count + "A"); // 往上移動(dòng)
- for (Integer progress : allProgress) {
- int width = progress / 4;
- String left = "[" + String.join("", Collections.nCopies(width, "#"));
- String right = String.join("", Collections.nCopies(25 - width, " ")) + "]";
- System.out.println(left + right);
- }
- }
- }
在上述代碼中:
- 我們首先執(zhí)行 System.out.print(String.join("", Collections.nCopies(count, "\n"))); 打印出多個(gè)空行,這可以保證我們有足夠的空間來打印進(jìn)度條。
- 接下來我們隨機(jī)增加一個(gè)進(jìn)度條的進(jìn)度,并且打印出所有進(jìn)度條。
- 最后我們調(diào)用 向上 指令,將光標(biāo)移回到最上方,繼續(xù)下一個(gè)循環(huán),直到所有進(jìn)度條都到達(dá)100%。
實(shí)際效果如下:
效果真是太棒啦!剩下將讀條和數(shù)字結(jié)合在一起的工作就交給讀者啦。學(xué)會(huì)了這招,當(dāng)你下次如果要做一個(gè)在命令行下載文件的小工具,這時(shí)候這些知識就派上用場啦!
制作命令行
最后,最為酷炫的事情莫過于利用Ansi Escape Codes實(shí)現(xiàn)一個(gè)個(gè)性化的命令行(Command-Line)。我們平常使用的Bash以及一些解釋型語言比如Python、Ruby等都有自己的REPL命令行。接下來,讓我們揭開他們神秘的面紗,了解他們背后實(shí)現(xiàn)的原理。
PS:由于在Jshell中,方向鍵、后退鍵等一些特殊鍵有自己的作用,所以接下來無法通過Jshell演示。需要自己手動(dòng)進(jìn)行編譯運(yùn)行代碼才能看到實(shí)際效果。
一個(gè)最簡單的命令行
首先,我們來實(shí)現(xiàn)一個(gè)最簡單的命令行,簡單到只實(shí)現(xiàn)下面兩種功能:
- 當(dāng)用戶輸入一個(gè)可打印的字符時(shí),比如abcd等,則在控制臺顯示。
- 當(dāng)用戶輸入回車時(shí),另起一行,輸出剛才用戶輸入的所有字符,然后再另起一行,繼續(xù)接受用戶的輸入。
那么這個(gè)最簡單的命令行的實(shí)現(xiàn)代碼會(huì)長這樣:
- import java.io.IOException;
- public class CommandLine{
- public static void main(String[] args)throws IOException, InterruptedException {
- // 設(shè)置命令行為raw模式,否則會(huì)自動(dòng)解析方向鍵以及后退鍵,并且直到按下回車read方法才會(huì)返回
- String[] cmd = { "/bin/sh", "-c", "stty raw </dev/tty" };
- Runtime.getRuntime()
- .exec(cmd)
- .waitFor();
- while (true) {
- String input = "";
- while (true) {
- char ch = (char) System.in.read();
- if (ch == 3) {
- // CTRL-C
- return;
- }
- else if (ch >= 32 && ch <= 126) {
- // 普通字符
- input += ch;
- }
- else if (ch == 10 || ch == 13) {
- // 回車
- System.out.println();
- System.out.print("\u001b[1000D");
- System.out.println("echo: " + input);
- input = "";
- }
- System.out.print("\u001b[1000D"); // 首先將光標(biāo)移動(dòng)到最左側(cè)
- System.out.print(input); // 重新輸出input
- System.out.flush();
- }
- }
- }
- }
好的,讓我們來說明一下代碼中的關(guān)鍵點(diǎn):
- 首先最關(guān)鍵的是我們需要將我們的命令行設(shè)置為raw模式,這可以避免JVM幫我們解析方向鍵,回退鍵以及對用戶輸入進(jìn)行緩沖。大家可以試一下不設(shè)置raw模式然后看一下效果,就可以理解我說的話了。
- 通過 System.in.read() 方法獲取用戶輸入,然后對其ascii值進(jìn)行分析。
- 如果發(fā)現(xiàn)用戶輸入的是回車的話,我們這時(shí)需要打印剛才用戶輸入的所有字符。但是我們需要注意,由于設(shè)置了raw模式,不移動(dòng)光標(biāo)直接打印的話,光標(biāo)的位置不會(huì)移到行首,如下圖:
所以這里需要再次調(diào)用 System.out.print("\u001b[1000D"); 將光標(biāo)移到行首。
好了,讓我們來看一下效果吧:
成功了!但是有個(gè)缺點(diǎn),那就是命令行并沒有解析方向鍵,反而以 [D[A[C[B 輸出(見動(dòng)圖)。這樣我們只能一直往后面寫而無法做到將光標(biāo)移動(dòng)到前面實(shí)現(xiàn)插入的效果。所以接下來就讓我們給命令行加上解析方向鍵的功能吧!
光標(biāo)移動(dòng)
簡單起見,我們僅需實(shí)現(xiàn)按下方向鍵的左右兩鍵時(shí)能控制光標(biāo)左右移動(dòng)。左右兩鍵對應(yīng)的ascii碼分別為 27 91 68 和 27 91 67 。所以我們只要在代碼中加上對這兩串a(chǎn)scii碼的解析即可:
- import java.io.IOException;
- public class CommandLine{
- public static void main(String[] args)throws IOException, InterruptedException {
- // 設(shè)置命令行為raw模式,否則會(huì)自動(dòng)解析方向鍵以及后退鍵,并且直到按下回車read方法才會(huì)返回
- String[] cmd = { "/bin/sh", "-c", "stty raw </dev/tty" };
- Runtime.getRuntime()
- .exec(cmd)
- .waitFor();
- while (true) {
- String input = "";
- int index = 0;
- while (true) {
- char ch = (char) System.in.read();
- if (ch == 3) {
- // CTRL-C
- return;
- }
- else if (ch >= 32 && ch <= 126) {
- // 普通字符
- input = input.substring(0, index) + ch + input.substring(index, input.length());
- index++;
- }
- else if (ch == 10 || ch == 13) {
- // 回車
- System.out.println();
- System.out.print("\u001b[1000D");
- System.out.println("echo: " + input);
- input = "";
- index = 0;
- }
- else if (ch == 27) {
- // 左右方向鍵
- char next1 = (char) System.in.read();
- char next2 = (char) System.in.read();
- if (next1 == 91) {
- if (next2 == 68) {
- // 左方向鍵
- index = Math.max(0, index - 1);
- }
- else if (next2 == 67) {
- // 右方向鍵
- index = Math.min(input.length(), index + 1);
- }
- }
- }
- System.out.print("\u001b[1000D"); // 將光標(biāo)移動(dòng)到最左側(cè)
- System.out.print(input);
- System.out.print("\u001b[1000D"); // 再次將光標(biāo)移動(dòng)到最左側(cè)
- if (index > 0) {
- System.out.print("\u001b[" + index + "C"); // 將光標(biāo)移動(dòng)到index處
- }
- System.out.flush();
- }
- }
- }
- }
效果如下:
It works!但是這個(gè)命令行還不支持刪除,我們無法通過 Backspace 鍵刪去敲錯(cuò)的字符。有了剛才的經(jīng)驗(yàn),實(shí)現(xiàn)刪除功能也十分簡單!
刪除
照著剛才的思路,我們可能會(huì)在處理用戶輸入的地方,加上如下的代碼:
- else if (ch == 127) {
- // 刪除
- if (index > 0) {
- input = input.substring(0, index - 1) + input.substring(index, input.length());
- index -= 1;
- }
- }
但是這段代碼存在點(diǎn)問題,讓我們看一下效果圖:
從圖中我們可以看到:
第一次,當(dāng)我輸入了 11234566 ,然后不停地按下刪除鍵,想要?jiǎng)h掉 34566 ,但是只有光標(biāo)在后退,字符并沒有被刪掉。然后我再按下回車鍵,通過echo的字符串我們發(fā)現(xiàn)刪除實(shí)際上已經(jīng)成功,只是控制臺在顯示的時(shí)候出了點(diǎn)問題。
第二次,我先輸入 123456 ,然后按下刪除鍵,刪掉 456 ,光標(biāo)退到 3 。然后我再繼續(xù)不斷地輸入 0 ,我們發(fā)現(xiàn)隨著 0 覆蓋了原來的 456 顯示的位置。
所以刪除的確產(chǎn)生了效果,但是我們要解決被刪除的字符還在顯示的這個(gè)bug。為了實(shí)現(xiàn)刪除的效果,我們先來學(xué)習(xí)一下Ansi里的刪除指令:
清除屏幕: \u001b[{n}J 為指令。
- n=0 :清除光標(biāo)到屏幕末尾的所有字符。
- n=1 :清除屏幕開頭到光標(biāo)的所有字符。
- n=2 :清除整個(gè)屏幕的字符。
清除行: \u001b[{n}K 為指令。
- n=0 :清除光標(biāo)到當(dāng)前行末所有的字符。
- n=1 :清除當(dāng)前行到光標(biāo)的所有字符。
- n=2 :清除當(dāng)前行。
所以我們的思路就是不管用戶輸入了什么,我們先利用 System.out.print("\u001b[0K"); 清除當(dāng)前行,此時(shí)光標(biāo)回到了行首,這時(shí)再輸出正確的字符。完整代碼如下:
- import java.io.IOException;
- public class CommandLine{
- public static void main(String[] args)throws IOException, InterruptedException {
- // 設(shè)置命令行為raw模式,否則會(huì)自動(dòng)解析方向鍵以及后退鍵,并且直到按下回車read方法才會(huì)返回
- String[] cmd = { "/bin/sh", "-c", "stty raw </dev/tty" };
- Runtime.getRuntime()
- .exec(cmd)
- .waitFor();
- while (true) {
- String input = "";
- int index = 0;
- while (true) {
- char ch = (char) System.in.read();
- if (ch == 3) {
- // CTRL-C
- return;
- }
- else if (ch >= 32 && ch <= 126) {
- // 普通字符
- input = input.substring(0, index) + ch + input.substring(index, input.length());
- index++;
- }
- else if (ch == 10 || ch == 13) {
- // 回車
- System.out.println();
- System.out.print("\u001b[1000D");
- System.out.println("echo: " + input);
- input = "";
- index = 0;
- }
- else if (ch == 27) {
- // 左右方向鍵
- char next1 = (char) System.in.read();
- char next2 = (char) System.in.read();
- if (next1 == 91) {
- if (next2 == 68) {
- // 左方向鍵
- index = Math.max(0, index - 1);
- }
- else if (next2 == 67) {
- // 右方向鍵
- index = Math.min(input.length(), index + 1);
- }
- }
- }
- else if (ch == 127) {
- // 刪除
- if (index > 0) {
- input = input.substring(0, index - 1) + input.substring(index, input.length());
- index -= 1;
- }
- }
- System.out.print("\u001b[1000D"); // 將光標(biāo)移動(dòng)到最左側(cè)
- System.out.print("\u001b[0K"); // 清除光標(biāo)所在行的全部內(nèi)容
- System.out.print(input);
- System.out.print("\u001b[1000D"); // 再次將光標(biāo)移動(dòng)到最左側(cè)
- if (index > 0) {
- System.out.print("\u001b[" + index + "C"); // 將光標(biāo)移動(dòng)到index處
- }
- System.out.flush();
- }
- }
- }
- }
讓我們來看一下效果:
OK,成功了!那么至此為止,我們已經(jīng)實(shí)現(xiàn)了一個(gè)最小化的命令行,它能夠支持用戶進(jìn)行輸入,并且能夠左右移動(dòng)光標(biāo)以及刪除他不想要的字符。但是它還缺失了很多命令行的特性,比如不支持解析像 Alt-f 、 Ctrl-r 等常見的 快捷鍵 ,也不支持輸入U(xiǎn)nicode字符等等。但是,只要我們掌握了剛才的知識,這些特性都可以方便地實(shí)現(xiàn)。比如,我們可以給剛才的命令行加上簡單的語法高亮——末尾如果有多余的空格則將這些空格標(biāo)紅,效果如下:

實(shí)現(xiàn)的代碼也很簡單,可以參考Github項(xiàng)目里的CustomisedCommandLine類。
最后,再介紹一下其他一些有用的Ansi Escape Codes:
- 光標(biāo)向上移動(dòng): \u001b[{n}A 將光標(biāo)向上移動(dòng) n 格。
- 光標(biāo)向下移動(dòng): \u001b[{n}B 將光標(biāo)向下移動(dòng) n 格。
- 光標(biāo)向右移動(dòng): \u001b[{n}C 將光標(biāo)向右移動(dòng) n 格。
- 光標(biāo)向左移動(dòng): \u001b[{n}D 將光標(biāo)向左移動(dòng) n 格。
- 光標(biāo)按行向下移動(dòng): \u001b[{n}E 將光標(biāo)向下移動(dòng) n 行并且將光標(biāo)移至行首。
- 光標(biāo)按行向上移動(dòng): \u001b[{n}F 將光標(biāo)向上移動(dòng) n 行并且將光標(biāo)移至行首。
- 設(shè)置光標(biāo)所在列: \u001b[{n}G 將光標(biāo)移至第 n 列(行數(shù)與當(dāng)前所在行保持一致)。
- 設(shè)置光標(biāo)所在位置: \u001b[{n};{m}H 將光標(biāo)移至第 n 行 m 列,坐標(biāo)原點(diǎn)從屏幕左上角開始。
- 保存光標(biāo)當(dāng)前所在位置: \u001b[{s} 。
- 讀取光標(biāo)上一次保存的位置: \u001b[{u} 。
光標(biāo)按行移動(dòng)的測試代碼參考Github項(xiàng)目里的LineMovementTest類,設(shè)置光標(biāo)位置的測試代碼參考Github項(xiàng)目里的PositionTest類。如果想了解更多的Ansi Escape Codes請參考 維基百科 。