1. 程式人生 > >FW: 使用ANSI轉義碼實現一個終端命令列介面

FW: 使用ANSI轉義碼實現一個終端命令列介面

釋出時間:04-1217:58

習慣於使用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()

print

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轉義碼的基礎上實現自己的富終端明亮行介面並不像想象的那麼難。