FW: 使用ANSI轉義碼實現一個終端命令列介面
習慣於使用Linux的人,時常需要在終端命令列工作,預設的黑白介面看的蒼白而單調。實際上我們可以美化它的顯示,之前蟲蟲有很多文章中曾介紹過很多這樣的工具和小APP,大家可以我的參考歷史文章參考學習。除了這些工具外有沒有其他辦法美化命令列呢,還有如何在我們自己的指令碼中呈現色彩化的顯示呢?本文蟲蟲就來教你實現這些,包括文字著色,自由的定位移動游標(向上,向下,向左或向右),清屏重顯示(實現動態進度條,ASCII動畫)。這其實都是使用ANSI標準轉義碼程式設計實現的,今天蟲蟲就給大家來說明ANSI轉義碼的使用方法,最後還使用Python語言實現一個簡單命令列介面。
顏色
在*nix體系下數程式與終端互動的方式是通過ANSI轉義碼。這些轉義碼是作為程式可列印的特殊程式碼,以便擴充套件終端的顯示。當然由於相容性問題,各種終端對ANSI轉義碼支援也有差異,但是基本上在Linux(Windows可能解析有問題)下基本的ANSI轉義程式碼還相容的存在。首先舉個例子"Hello,Chongchong"開始:
最基本的ANSI轉義碼對文字進行渲染的程式碼。它允許我們給要列印的文字新增顏色、背景顏色或其他裝飾:
顏色
最基本操作是對文字著色。ANSI顏色轉義碼:
紅色:\u001b[31m
重置:\u001b[0m
這個\u001b字首是大多數ANSI顏色轉義碼的開頭。大多數程式語言都支援用這種語法來表示特殊字元,比如Java,Perl,Python和Javascript等都支援\u001b語法。
例如,這裡列印字串"Hello World",但是紅色:
print("\u001b[31mHello ,chongchong\n")
上面顯示了列印了紅色的Hello chongchong,連提示符都變成紅色的了。事實上,此後再輸入的程式碼都將顯示為紅色。這就是Ansi顏色轉義碼渲染的工作方式,設定列印特殊的顏色程式碼後,設定就會一直生生效,除非再設定顏色程式碼或者用Reset的程式碼來恢復到預設顏色。比如我們列印下Reset的程式碼:
可以看到提示符號恢復到了回白色。在我們的程式碼中如果我們設定過顏色程式碼,一定要接著最後用Rest恢復初始環境,對上面的例子我們改造一下:
8顏色
上面我講了顏色程式碼使用和恢復,終端共支援8種(程式碼0,30-37)不同的顏色,分別為:
重置:\u001b[0m
黑色:\u001b[30m
紅色:\u001b[31m
綠色:\u001b[32m
黃色:\u001b[33m
藍色:\u001b[34m
洋紅:\u001b[35m
青色:\u001b[36m
白特:\u001b[37m
我們用上面的顏色列印下字母:
16色
大多數終端除了基本的8種顏色外,還支援"明亮"顏色。這些也都有自己的轉義碼,修飾正常的顏色,但在程式碼中額外要加一個;1
亮黑: \u001b[30;1m
亮紅: \u001b[31;1m
亮綠: \u001b[32;1m
亮黃: \u001b[33;1m
亮藍: \u001b[34;1m
亮洋紅: \u001b[35;1m
亮青: \u001b[36;1m
亮白: \u001b[37;1m
Reset: \u001b[0m
我們可以打印出這些鮮豔的顏色並看到它們的效果:
可以看到它們確實比基本的8種顏色更亮。黑色A現在足夠亮,可以在黑色背景上成了灰色可見了,白色的H現在比預設的文字顏色更亮。
256色
在16種色的基礎上,有些些終端支援256色的擴充套件的,265中顏色的轉義碼格式為:
\u001b[38;5;${ID}m
下面我們寫一個程式列印所有者256種顏色,為了顯示方便,我們使用jupyer notebook在瀏覽器上顯示:
import sys
for i in range(0, 16):
for j in range(0, 16):
code = str(i * 16 + j)
sys.stdout.write(u"\u001b[38;5;" + code + "m " + code.ljust(4))
print("\u001b[0m")
其中,也支援用jupyer notebook在瀏覽器上顯示:
程式碼中,我們使用sys.stdout.write,這樣就可以在同一行上列印多個顏色,
背景顏色
ANSI轉義碼也支援設定文字背景的顏色。
例如,8種背景顏色對應的程式碼為(40-47):
黑色背景: \u001b[40m
紅色背景: \u001b[41m
綠色背景: \u001b[42m
黃色背景: \u001b[43m
藍色背景: \u001b[44m
洋紅背景: \u001b[45m
青色背景: \u001b[46m
黑色背景: \u001b[47m
也支援亮色版本的背景色:
亮黑色背景: \u001b[40;1m
亮紅色背景: \u001b[41;1m
亮綠色背景: \u001b[42;1m
亮黃色背景: \u001b[43;1m
亮藍色背景: \u001b[44;1m
亮洋紅背景: \u001b[45;1m
亮青色背景: \u001b[46;1m
亮白色背景: \u001b[47;1m
Reset的內碼表一樣都為: \u001b[0m也一樣
我們可以將它們打印出來並看到它們有效:
注意,背景顏色的亮色版不改變背景,而是增加前景文字的亮度,所以顯示效果不是很直觀。
256色背景
也用一個小指令碼展示:
import sys
for i in range(0, 16):
for j in range(0, 16):
code = str(i * 16 + j)
sys.stdout.write("\u001b[48;5;" + code + "m " + code.ljust(4))
print("\u001b[0m")
Jupyter顯示:
格式修飾符
除了顏色和背景顏色,ANSI轉義碼還支援一些文字格式修飾符
粗體:\u001b[1m
下劃線:\u001b[4m
反色:\u001b[7m
每一個都可以單獨或者組合使用,或者於顏色符組合使用,看下面的例子:
游標導航
游標移動的ANSI轉義碼更復雜,它允許我們在終端窗移動游標,或者清除部分視窗。其中最基本的是向上,向下,向左或向右的游標移動:
↑: \u001b[{n}A
↓: \u001b[{n}B
→: \u001b[{n}C
←: \u001b[{n}D
為了演示需要,我們新增一個time.sleep(10),以便觀察顯示效果。
import time
print("Hello I Am Chongchong"); time.sleep(10
進度百分比:
游標導航ANSI轉義碼可以利用來做的最簡單的事情就是進度條:
import time, sys
def loading():
print("Loading...")
for i in range(0, 100):
time.sleep(0.1)
sys.stdout.write("\u001b[1000D" + str(i + 1) + "%")
sys.stdout.flush()
print()
loading()
ASCII進度條
上面我們用ANSI轉義碼來控制終端,建立一個顯示進度百分比,我們對其進行一下美化,比如顯示一個完成進度條:
import time, sys
def loading():
print "Loading..."
for i in range(0, 100):
time.sleep(0.1)
width = (i + 1) / 4
bar = "[" + "#" * width + " " * (25 - width) + "]"
sys.stdout.write(u"\u001b[1000D" + bar)
sys.stdout.flush()
loading()
程式碼原理:利用迴圈迭代,刪除整行顯示,然後重新繪製一個更長的顯示,從而實現一個動態ASCII進度條。效果如下:
利用向上和向下游標程式碼移動,我們甚至可以一次性繪多個進度條,比如下面程式碼我們並行三個進度條顯示:
import time, sys, random
def loading(count):
all_progress = [0] * count
sys.stdout.write("\n" * count)
while any(x < 100 for x in all_progress):
time.sleep(0.01)
unfinished = [(i, v) for (i, v) in enumerate(all_progress) if v < 100]
index, _ = random.choice(unfinished)
all_progress[index] += 1
sys.stdout.write("\u001b[1000D")
sys.stdout.write("\u001b[" + str(count) + "A")
for progress in all_progress:
width = progress / 4
print("[" + "#" * width + " " * (25 - width) + "]")
loading()
程式碼原理:
為了確保有足夠的空間來繪製進度條,我們通過在函式啟動時寫入"\n" * count來完成的。該程式碼會建立一列新行,使終端滾動,確保在終端底部有準確的空白行,以便呈現進度條;使用all_progress陣列模擬正在進行的多項操作,並使該陣列被隨機填充使用Up ANSI程式碼每次移動游標計數行,這樣我們就可以每行列印一個計數進度條。
編寫命令列介面
使用ANSI轉義碼可以做的更有意義事情之一就是實現一個命令列介面。Bash,Python,Ruby都有自己的內建命令列,編輯文字,提交命令,解析執行。上面我們學習了,如何使用ANSI,本部分我們利用這些命轉義碼實現自己的命令列介面。
使用者介面
命令介面最重要的一部分就是使用者介面,負責接受使用者輸入。這可以使用以下程式碼完成:
import sys, tty
def command_line():
tty.setraw(sys.stdin)
while True:
char = sys.stdin.read(1)
if ord(char) == 3:
break;
print(ord(char))
sys.stdout.write(u"\u001b[1000D")
上面程式碼中,我們使用setraw來確保我們的原始字元輸入直接被程式接收,然後讀取並顯示我們的按鍵的ASCII碼,如果按下3(CTRL-C的程式碼)。由於我們已開啟tty.setraw列印,不會進行游標重置,最後我們執行向左移動\u001b[1000D移動游標到最左。顯示效果如下:
基本命令列
基於上面的基本介面,我們來實現一個原始命令列介面,用來回顯使用者鍵入的內容,我們約定:
當用戶按下可列印字元時,直接打印出來
當用戶按Enter鍵時,打印出使用者輸入,換行輸入。
當用戶按Backspace鍵時,刪除游標所在的一個字元
當用戶按下箭頭鍵時,使用ANSI轉義碼向左或向右移動游標。
首先,讓我們首先實現前兩個功能,程式碼如下:
import sys, tty
def command_line():
tty.setraw(sys.stdin)
while True:
input = ""
while True:
char = ord(sys.stdin.read(1))
if char == 3:
return
elif 32 <= char <= 126:
input = input + chr(char)
elif char in {10, 13}:
sys.stdout.write(u"\u001b[1000D")
print("\nechoing..."), input
input = ""
sys.stdout.write(u"\u001b[1000D")
sys.stdout.write(input)
sys.stdout.flush()
顯示效果如下:
游標導航
下一步是讓使用者使用箭頭鍵移動游標。鍵盤上左右箭頭鍵對應於字元碼27 91 68和27 91 67的序列,所以我們可以對輸入程式碼檢查並對應移動游標
程式碼原理:
通過維護一個索引變數,保留一個單獨的索引,該索引不一定在輸入的末尾,當用戶輸入一個字元時,將其拼接到輸入的正確位置。
檢查char == 27,然後檢查接下來的兩個字元來識別左右箭頭鍵,並遞增/遞減游標的索引。
寫入輸入後,手動將游標一直向左移動,並向右移動與游標索引對應的正確字元數。效果如下:
後續根據需要,可以增加Home和End(^和$)功能,只需通過類似方法新增程式碼即可,我們不在多贅述。
刪除功能
我們還要實現刪除功能:使用Backspace將會刪除游標前字元,並將游標向左移動一位。為了實現效果我們還得用一個ANSI轉義碼實現各種清屏工作:
清除螢幕:
\u001b[{n}J清除螢幕
n = 0從游標清除到螢幕結束,
n = 1從游標到螢幕的開頭清除
n = 2清除整個螢幕
清除行:
\u001b[{n}K清除當前行
n = 0從游標到行尾清除
n = 1從游標到行首開始清除
n = 2清除整行
下面的程式碼:
sys.stdout.write(u"\u001b[0K")
清除游標位置到行末的所有字元。
增加刪除功能後的程式碼:
效果如下:
工作原理如下:
游標移動到行的開頭sys.stdout.write(u"\u001b[1000D")
清除行sys.stdout.write(u"\u001b[0K")
當前輸入寫入sys.stdout.write(輸入)
游標移動到正確索引的位置sys.stdout.write(u"\u001b[" + str(index) + "C")
通常,使用這些程式碼時候,都會在呼叫.flush()時生效。
最終我們實現了了一個最小規格的命令列介面,使用sys.stdin.read和sys.stdout.write實現讀寫,用ANSI轉義碼控制終端。
語法高亮
截止目前,我們已經嘗試使用ANSI轉義符顯示顏色,游標導航實現進度條,並實現了一個原始的命令列介面。最後我們給我們的命令列介面增加一個功能,對其中程式碼實現語法高亮。
基於我們以後的程式碼,實現語法高亮就像在輸入字串上呼叫一個syntax_highlight函式一樣簡單,
sys.stdout.write(syntax_highlight(input))
為了演示我將使用一個虛擬語法高亮顯示器來突出顯示尾隨空格。
def syntax_highlight(input):
stripped = input.rstrip()
return stripped + u"\u001b[41m" + " " * (len(input) - len(stripped)) + u"\u001b[0m"
這就是一個最簡單的例項,為了支援更強大的功能,可以利用Pygments的類庫來替換上面syntax_highlight函式,這樣就可以實現任何程式語言的真正的語法高亮。
結論
這種與命令列程式的"豐富"互動是大多數傳統命令列程式和庫所缺乏的。在充分了解ANSI轉義碼的基礎上實現自己的富終端明亮行介面並不像想象的那麼難。