自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

震驚!原來命令行還可以這么玩?!

移動(dòng)開發(fā)
你是否好奇過命令行里那些花里胡哨的進(jìn)度條是如何實(shí)現(xiàn)的?好奇過Spring Boot為什么能夠打印五顏六色的日志?好奇過Python或者PHP等腳本語言的交互式命令行是如何實(shí)現(xiàn)的?好奇過Vim或者Emacs等在Terminal中的編輯器是怎么實(shí)現(xiàn)的?

你是否:

  • 好奇過命令行里那些花里胡哨的進(jìn)度條是如何實(shí)現(xiàn)的?
  • 好奇過Spring Boot為什么能夠打印五顏六色的日志?
  • 好奇過Python或者PHP等腳本語言的交互式命令行是如何實(shí)現(xiàn)的?
  • 好奇過Vim或者Emacs等在Terminal中的編輯器是怎么實(shí)現(xiàn)的?

如果你曾經(jīng)好奇過,或者被這段話勾起了你的好奇心,那么你絕對不能錯(cuò)過這篇文章!

震驚!原來命令行還可以這么玩?!

背景

通過本文你可以學(xué)到:

  1. 何為Ansi Escape Codes以及它們能干什么?
  2. Ansi Escape Codes的一些高級應(yīng)用。
  3. 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 :

  1. System.out.print("\u001b[31mHello World"); 

 震驚!原來命令行還可以這么玩?!

從上圖中,我們可以看到,不僅 Hello World 是變成了紅色,而且接下來的 jshell> 提示符也變成了紅色。其實(shí)不管你接下來輸入什么字符,它們的字體顏色都是紅色。直到你輸入了其他顏色的Ansi指令,或者輸入了重置指令,字體的顏色才會(huì)不再是紅色。

讓我們嘗試輸入重置指令來恢復(fù)字體的顏色:

  1. System.out.print("\u001b[0m"); 

 震驚!原來命令行還可以這么玩?!

很好! jshell> 提示符恢復(fù)為了白色。所以一個(gè)最佳實(shí)踐就是,最好在所有改變字體顏色或者樣式的Ansi Escape Codes的最后加上重置指令,以免造成意想不到的后果。舉個(gè)例子:

  1. System.out.print("\u001b[31mHello World\u001b[0m"); 

 震驚!原來命令行還可以這么玩?!

當(dāng)然,重置指令可以被添加在任何位置,比如我們可以將其插在 Hello World 的中間,使得 Hello 是紅色,但是 World 是白色:

  1. 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

不如將它們都輸出看一下: 

  1. System.out.print("\u001b[30m A \u001b[31m B \u001b[32m C \u001b[33m D \u001b[0m");  
  2. 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)行比對: 

  1. System.out.print("\u001b[30m A \u001b[31m B \u001b[32m C \u001b[33m D \u001b[0m");  
  2. System.out.print("\u001b[34m E \u001b[35m F \u001b[36m G \u001b[37m H \u001b[0m");  
  3. System.out.print("\u001b[30;1m A \u001b[31;1m B \u001b[32;1m C \u001b[33;1m D \u001b[0m");  
  4. 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色。指令的形式如下:

  1. \u001b[38;5;${ID}m 

讓我們輸出256色矩陣: 

  1. for (int i = 0; i < 16; i++) { 
  2.     for (int j = 0; j < 16; j++) { 
  3.         int code = i * 16 + j; 
  4.         System.out.printf("\u001b[38;5;%dm%-4d", code, code); 
  5.     } 
  6.     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色背景: 

  1. System.out.print("\u001b[40m A \u001b[41m B \u001b[42m C \u001b[43m D \u001b[0m");  
  2. System.out.print("\u001b[44m A \u001b[45m B \u001b[46m C \u001b[47m D \u001b[0m");  
  3. System.out.print("\u001b[40;1m A \u001b[41;1m B \u001b[42;1m C \u001b[43;1m D \u001b[0m");  
  4. 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背景色,首先指令如下:

  1. \u001b[48;5;${ID}m 

同樣輸出256色矩陣: 

  1. for (int i = 0; i < 16; i++) { 
  2.     for (int j = 0; j < 16; j++) { 
  3.         int code = i * 16 + j; 
  4.         System.out.printf("\u001b[48;5;%dm%-4d", code, code); 
  5.     } 
  6.     System.out.println("\u001b[0m"); 

 震驚!原來命令行還可以這么玩?!

感覺要被亮瞎眼了呢!至此,顏色設(shè)置已經(jīng)介紹完畢,讓我們接著學(xué)習(xí)樣式設(shè)置。

樣式

除了給文本設(shè)置顏色之外,我們還可以給文本設(shè)置樣式:

  • 粗體: \u001b[1m
  • 下劃線: \u001b[4m
  • 反色: \u001b[7m

樣式分別使用的效果:

  1. System.out.print("\u001b[1m BOLD \u001b[0m\u001b[4m Underline \u001b[0m\u001b[7m Reversed \u001b[0m"); 

 震驚!原來命令行還可以這么玩?!

或者結(jié)合使用:

  1. System.out.print("\u001b[1m\u001b[4m\u001b[7m BOLD Underline Reversed \u001b[0m"); 

 震驚!原來命令行還可以這么玩?!

甚至還可以和顏色結(jié)合使用: 

  1. System.out.print("\u001b[1m\u001b[31m Red Bold \u001b[0m");  
  2. 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)控制指令!

[[211306]]

光標(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ù)字的刷新: 

  1. void loading()throws InterruptedException { 
  2.     System.out.println("Loading..."); 
  3.     for (int i = 1; i <= 100; i++) { 
  4.         Thread.sleep(100); 
  5.         System.out.print("\u001b[1000D" + i + "%"); 
  6.     } 

 震驚!原來命令行還可以這么玩?!

從圖中我們可以看到,進(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)度條刷新的頻率: 

  1. void loading()throws InterruptedException { 
  2.     System.out.println("Loading..."); 
  3.     for (int i = 1; i <= 100; i++) { 
  4.         System.out.print("\u001b[1000D"); 
  5.         Thread.sleep(1000); 
  6.         System.out.print(i + "%"); 
  7.         Thread.sleep(1000); 
  8.     } 

 震驚!原來命令行還可以這么玩?!

現(xiàn)在我們可以清晰地看到:

  1. 從左到右打印進(jìn)度,光標(biāo)移至行尾。
  2. 光標(biāo)移至行首,原進(jìn)度數(shù)字還在。
  3. 從左到右打印新進(jìn)度,新的數(shù)字會(huì)覆蓋老的數(shù)字。光標(biāo)移至行尾。
  4. 循環(huán)往復(fù)。

Ascii進(jìn)度條

好了,我們現(xiàn)在已經(jīng)知道如何通過Ansi Escape Code實(shí)現(xiàn)進(jìn)度數(shù)字的顯示和刷新,剩下的就是實(shí)現(xiàn)進(jìn)度的讀條。廢話不多說,我們直接上代碼和效果圖: 

  1. void loading()throws InterruptedException { 
  2.     System.out.println("Loading..."); 
  3.     for (int i = 1; i <= 100; i++) { 
  4.         int width = i / 4; 
  5.         String left = "[" + String.join("", Collections.nCopies(width, "#")); 
  6.         String right = String.join("", Collections.nCopies(25 - width, " ")) + "]"
  7.         System.out.print("\u001b[1000D" + left + right); 
  8.         Thread.sleep(100); 
  9.     } 

 震驚!原來命令行還可以這么玩?!

由上圖我們可以看到,每次循環(huán)過后,讀條就會(huì)增加。原理和數(shù)字的刷新一樣,相信大家閱讀代碼就能理解,這里就不再贅述。

讓我們來點(diǎn)更酷的吧!利用Ansi的光標(biāo) 向上 以及 向下 的指令,我們還可以同時(shí)打印出多條進(jìn)度條: 

  1. void loading(int count)throws InterruptedException { 
  2.     System.out.print(String.join("", Collections.nCopies(count"\n"))); // 初始化進(jìn)度條所占的空間 
  3.     List<Integer> allProgress = new ArrayList<>(Collections.nCopies(count, 0)); 
  4.     while (true) { 
  5.         Thread.sleep(10); 
  6.  
  7.         // 隨機(jī)選擇一個(gè)進(jìn)度條,增加進(jìn)度 
  8.         List<Integer> unfinished = new LinkedList<>(); 
  9.         for (int i = 0; i < allProgress.size(); i++) { 
  10.             if (allProgress.get(i) < 100) { 
  11.                 unfinished.add(i); 
  12.             } 
  13.         } 
  14.         if (unfinished.isEmpty()) { 
  15.             break; 
  16.         } 
  17.         int index = unfinished.get(new Random().nextInt(unfinished.size())); 
  18.         allProgress.set(index, allProgress.get(index) + 1); // 進(jìn)度+1 
  19.  
  20.         // 繪制進(jìn)度條 
  21.         System.out.print("\u001b[1000D"); // 移動(dòng)到最左邊 
  22.         System.out.print("\u001b[" + count + "A"); // 往上移動(dòng) 
  23.         for (Integer progress : allProgress) { 
  24.             int width = progress / 4; 
  25.             String left = "[" + String.join("", Collections.nCopies(width, "#")); 
  26.             String right = String.join("", Collections.nCopies(25 - width, " ")) + "]"
  27.             System.out.println(left + right); 
  28.         } 
  29.     } 

在上述代碼中:

  • 我們首先執(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ì)長這樣: 

  1. import java.io.IOException; 
  2.  
  3. public class CommandLine{ 
  4.     public static void main(String[] args)throws IOException, InterruptedException { 
  5.         // 設(shè)置命令行為raw模式,否則會(huì)自動(dòng)解析方向鍵以及后退鍵,并且直到按下回車read方法才會(huì)返回 
  6.         String[] cmd = { "/bin/sh""-c""stty raw </dev/tty" }; 
  7.         Runtime.getRuntime() 
  8.                .exec(cmd) 
  9.                .waitFor(); 
  10.         while (true) { 
  11.             String input = ""
  12.             while (true) { 
  13.                 char ch = (char) System.in.read(); 
  14.                 if (ch == 3) { 
  15.                     // CTRL-C 
  16.                     return
  17.                 } 
  18.                 else if (ch >= 32 && ch <= 126) { 
  19.                     // 普通字符 
  20.                     input += ch; 
  21.                 } 
  22.                 else if (ch == 10 || ch == 13) { 
  23.                     // 回車 
  24.                     System.out.println(); 
  25.                     System.out.print("\u001b[1000D"); 
  26.                     System.out.println("echo: " + input); 
  27.                     input = ""
  28.                 } 
  29.  
  30.                 System.out.print("\u001b[1000D"); // 首先將光標(biāo)移動(dòng)到最左側(cè) 
  31.                 System.out.print(input); // 重新輸出input 
  32.                 System.out.flush(); 
  33.             } 
  34.         } 
  35.     } 

好的,讓我們來說明一下代碼中的關(guān)鍵點(diǎn):

  1. 首先最關(guān)鍵的是我們需要將我們的命令行設(shè)置為raw模式,這可以避免JVM幫我們解析方向鍵,回退鍵以及對用戶輸入進(jìn)行緩沖。大家可以試一下不設(shè)置raw模式然后看一下效果,就可以理解我說的話了。
  2. 通過 System.in.read() 方法獲取用戶輸入,然后對其ascii值進(jìn)行分析。
  3. 如果發(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碼的解析即可: 

  1. import java.io.IOException; 
  2.  
  3. public class CommandLine{ 
  4.     public static void main(String[] args)throws IOException, InterruptedException { 
  5.         // 設(shè)置命令行為raw模式,否則會(huì)自動(dòng)解析方向鍵以及后退鍵,并且直到按下回車read方法才會(huì)返回 
  6.         String[] cmd = { "/bin/sh""-c""stty raw </dev/tty" }; 
  7.         Runtime.getRuntime() 
  8.                .exec(cmd) 
  9.                .waitFor(); 
  10.         while (true) { 
  11.             String input = ""
  12.             int index = 0; 
  13.             while (true) { 
  14.                 char ch = (char) System.in.read(); 
  15.                 if (ch == 3) { 
  16.                     // CTRL-C 
  17.                     return
  18.                 } 
  19.                 else if (ch >= 32 && ch <= 126) { 
  20.                     // 普通字符 
  21.                     input = input.substring(0, index) + ch + input.substring(index, input.length()); 
  22.                     index++; 
  23.                 } 
  24.                 else if (ch == 10 || ch == 13) { 
  25.                     // 回車 
  26.                     System.out.println(); 
  27.                     System.out.print("\u001b[1000D"); 
  28.                     System.out.println("echo: " + input); 
  29.                     input = ""
  30.                     index = 0; 
  31.                 } 
  32.                 else if (ch == 27) { 
  33.                     // 左右方向鍵 
  34.                     char next1 = (char) System.in.read(); 
  35.                     char next2 = (char) System.in.read(); 
  36.                     if (next1 == 91) { 
  37.                         if (next2 == 68) { 
  38.                             // 左方向鍵 
  39.                             index = Math.max(0, index - 1); 
  40.                         } 
  41.                         else if (next2 == 67) { 
  42.                             // 右方向鍵 
  43.                             index = Math.min(input.length(), index + 1); 
  44.                         } 
  45.                     } 
  46.                 } 
  47.  
  48.                 System.out.print("\u001b[1000D"); // 將光標(biāo)移動(dòng)到最左側(cè) 
  49.                 System.out.print(input); 
  50.                 System.out.print("\u001b[1000D"); // 再次將光標(biāo)移動(dòng)到最左側(cè) 
  51.                 if (index > 0) { 
  52.                     System.out.print("\u001b[" + index + "C"); // 將光標(biāo)移動(dòng)到index處 
  53.                 } 
  54.                 System.out.flush(); 
  55.             } 
  56.         } 
  57.     } 

效果如下:

震驚!原來命令行還可以這么玩?!

It works!但是這個(gè)命令行還不支持刪除,我們無法通過 Backspace 鍵刪去敲錯(cuò)的字符。有了剛才的經(jīng)驗(yàn),實(shí)現(xiàn)刪除功能也十分簡單!

刪除

照著剛才的思路,我們可能會(huì)在處理用戶輸入的地方,加上如下的代碼: 

  1. else if (ch == 127) { 
  2.     // 刪除 
  3.     if (index > 0) { 
  4.         input = input.substring(0, index - 1) + input.substring(index, input.length()); 
  5.         index -= 1; 
  6.     } 

但是這段代碼存在點(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í)再輸出正確的字符。完整代碼如下: 

  1. import java.io.IOException; 
  2.  
  3. public class CommandLine{ 
  4.     public static void main(String[] args)throws IOException, InterruptedException { 
  5.         // 設(shè)置命令行為raw模式,否則會(huì)自動(dòng)解析方向鍵以及后退鍵,并且直到按下回車read方法才會(huì)返回 
  6.         String[] cmd = { "/bin/sh""-c""stty raw </dev/tty" }; 
  7.         Runtime.getRuntime() 
  8.                .exec(cmd) 
  9.                .waitFor(); 
  10.         while (true) { 
  11.             String input = ""
  12.             int index = 0; 
  13.             while (true) { 
  14.                 char ch = (char) System.in.read(); 
  15.                 if (ch == 3) { 
  16.                     // CTRL-C 
  17.                     return
  18.                 } 
  19.                 else if (ch >= 32 && ch <= 126) { 
  20.                     // 普通字符 
  21.                     input = input.substring(0, index) + ch + input.substring(index, input.length()); 
  22.                     index++; 
  23.                 } 
  24.                 else if (ch == 10 || ch == 13) { 
  25.                     // 回車 
  26.                     System.out.println(); 
  27.                     System.out.print("\u001b[1000D"); 
  28.                     System.out.println("echo: " + input); 
  29.                     input = ""
  30.                     index = 0; 
  31.                 } 
  32.                 else if (ch == 27) { 
  33.                     // 左右方向鍵 
  34.                     char next1 = (char) System.in.read(); 
  35.                     char next2 = (char) System.in.read(); 
  36.                     if (next1 == 91) { 
  37.                         if (next2 == 68) { 
  38.                             // 左方向鍵 
  39.                             index = Math.max(0, index - 1); 
  40.                         } 
  41.                         else if (next2 == 67) { 
  42.                             // 右方向鍵 
  43.                             index = Math.min(input.length(), index + 1); 
  44.                         } 
  45.                     } 
  46.                 } 
  47.                 else if (ch == 127) { 
  48.                     // 刪除 
  49.                     if (index > 0) { 
  50.                         input = input.substring(0, index - 1) + input.substring(index, input.length()); 
  51.                         index -= 1; 
  52.                     } 
  53.                 } 
  54.                 System.out.print("\u001b[1000D"); // 將光標(biāo)移動(dòng)到最左側(cè) 
  55.                 System.out.print("\u001b[0K"); // 清除光標(biāo)所在行的全部內(nèi)容 
  56.                 System.out.print(input); 
  57.                 System.out.print("\u001b[1000D"); // 再次將光標(biāo)移動(dòng)到最左側(cè) 
  58.                 if (index > 0) { 
  59.                     System.out.print("\u001b[" + index + "C"); // 將光標(biāo)移動(dòng)到index處 
  60.                 } 
  61.                 System.out.flush(); 
  62.             } 
  63.         } 
  64.     } 

讓我們來看一下效果: 

震驚!原來命令行還可以這么玩?!

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請參考 維基百科 。

責(zé)任編輯:未麗燕 來源: 夜有所思,日有所夢
相關(guān)推薦

2022-12-06 17:30:04

2016-12-02 20:43:28

Android

2018-10-28 17:54:00

分布式事務(wù)數(shù)據(jù)

2017-11-06 19:09:45

在線抓娃娃機(jī)

2024-03-12 08:44:56

WebWorkerTypeScript語法

2015-08-12 16:32:34

華為/物聯(lián)網(wǎng)

2022-07-29 16:50:30

網(wǎng)絡(luò)帶寬

2017-10-28 23:13:43

微服務(wù)架構(gòu)開發(fā)單體應(yīng)用

2023-10-11 08:16:42

客戶端服務(wù)器內(nèi)容

2020-07-21 18:54:21

Rust類型轉(zhuǎn)換語言

2016-09-29 17:48:32

騰訊云語音質(zhì)檢珍愛網(wǎng)

2023-03-01 11:35:45

2017-09-27 14:57:44

IOS 11Siri蘋果

2016-12-26 09:50:15

2010-06-21 15:51:29

Linux命令Google服務(wù)

2013-09-18 10:44:01

搜狗輸入法詞語

2021-02-07 08:13:18

@DateTimeFo@NumberFormSpring

2022-01-04 08:00:48

前端技術(shù)Esbuild

2020-05-09 16:45:56

ping命令Linux

2020-12-10 16:16:08

工具代碼開發(fā)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號