下文转载自:https://baijiahao.baidu.com/s?id=1630601858000127124&wfr=spider&for=pc
习惯于使用Linux的人,时常需要在终端命令行工作,默认的黑白界面看的苍白而单调。实际上我们可以美化它的显示,之前虫虫有很多文章中曾介绍过很多这样的工具和小APP,大家可以我的参考历史文章参考学习。除了这些工具外有没有其他办法美化命令行呢,还有如何在我们自己的脚本中呈现色彩化的显示呢?本文虫虫就来教你实现这些,包括文字着色,自由的定位移动光标(向上,向下,向左或向右),清屏重显示(实现动态进度条,ASCII动画)。这其实都是使用ANSI标准转义码编程实现的,今天虫虫就给大家来说明ANSI转义码的使用方法,最后还使用Python语言实现一个简单命令行界面。
颜色
在*nix体系下数程序与终端交互的方式是通过ANSI转义码。这些转义码是作为程序可打印的特殊代码,以便扩展终端的显示。当然由于兼容性问题,各种终端对ANSI转义码支持也有差异,但是基本上在Linux(Windows可能解析有问题)下基本的ANSI转义代码还兼容的存在。首先举个例子"Hello,Chongchong"开始:
![](https://pics6.baidu.com/feed/f636afc379310a558f5be8c9a562eaad822610be.jpeg?token=ace215904fb8cec4aef63364826bf15f&s=4542BB42C9ECBF704CD914020000F0C1)
最基本的ANSI转义码对文本进行渲染的代码。它允许我们给要打印的文本添加颜色、背景颜色或其他装饰:
颜色
最基本操作是对文本着色。ANSI颜色转义码:
红色:u001b[31m
重置:u001b[0m
这个u001b前缀是大多数ANSI颜色转义码的开头。大多数编程语言都支持用这种语法来表示特殊字符,比如Java,Perl,Python和Javascript等都支持u001b语法。
例如,这里打印字符串"Hello World",但是红色:
print("u001b[31mHello ,chongchong ")
![](https://pics1.baidu.com/feed/5fdf8db1cb13495419e3084444693b5cd1094a38.jpeg?token=c960064fe5c224efc3d1dc1275ac7dbb&s=8D45834A6FAEBF701CDDFD060000E0C3)
上面显示了打印了红色的Hello chongchong,连提示符都变成红色的了。事实上,此后再输入的代码都将显示为红色。这就是Ansi颜色转义码渲染的工作方式,设置打印特殊的颜色代码后,设置就会一直生生效,除非再设置颜色代码或者用Reset的代码来恢复到默认颜色。比如我们打印下Reset的代码:
![](https://pics2.baidu.com/feed/4610b912c8fcc3ce3381dcd483627f8cd53f20bb.jpeg?token=fa09d97f90497a7fc5d11a60472f3f69&s=D165D34A6FA3BB7454C8FC060200A0C3)
可以看到提示符号恢复到了回白色。在我们的代码中如果我们设置过颜色代码,一定要接着最后用Rest恢复初始环境,对上面的例子我们改造一下:
![](https://pics1.baidu.com/feed/77c6a7efce1b9d167c13fe09e0f91d8b8d5464a9.jpeg?token=860e7a972524e302c7e846480f2f13b8)
8颜色
上面我讲了颜色代码使用和恢复,终端共支持8种(代码0,30-37)不同的颜色,分别为:
重置:u001b[0m
黑色:u001b[30m
红色:u001b[31m
绿色:u001b[32m
黄色:u001b[33m
蓝色:u001b[34m
洋红:u001b[35m
青色:u001b[36m
白特:u001b[37m
我们用上面的颜色打印下字母:
![](https://pics2.baidu.com/feed/9213b07eca806538943c85a784fa0840ac34825f.jpeg?token=5a4bd9b39bc35b988be16d343fe09140)
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
我们可以打印出这些鲜艳的颜色并看到它们的效果:
![](https://pics6.baidu.com/feed/e4dde71190ef76c692d555318f3154feaf516754.jpeg?token=5844395e7664c2a544cdc3bbb665747e&s=45703BC2CDEDB3704E5DD00C0000A0C3)
可以看到它们确实比基本的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")
![](https://pics6.baidu.com/feed/5366d0160924ab18b405d57127dd4fc97b890b0b.jpeg?token=fd4ae274786e9273277245bb539a1a40&s=195133CAD7B58A7954E5750D030010C1)
其中,也支持用jupyer notebook在浏览器上显示:
![](https://pics7.baidu.com/feed/a8ec8a13632762d025500e94b2cba1fe513dc65f.jpeg?token=c0219c62d508a035cf68c2fd5b6278e5&s=C6D2E13287B349224E6504DF000010B2)
代码中,我们使用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也一样
我们可以将它们打印出来并看到它们有效:
![](https://pics1.baidu.com/feed/4afbfbedab64034f7fa7c471bde4d0350a551d0d.jpeg?token=356ffc76767ebc7353c1fa4b7fd6f948&s=11E543A2CBACB7700E5DD41B0000E0C1)
注意,背景颜色的亮色版不改变背景,而是增加前景文本的亮度,所以显示效果不是很直观。
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")
![](https://pics1.baidu.com/feed/a2cc7cd98d1001e9bf311b7baa29d2e854e79764.jpeg?token=9a714727de25bb99b48fd5eb3e179949&s=1A37B2AA5ABD000B54A7214D0300D0F0)
Jupyter显示:
![](https://pics1.baidu.com/feed/71cf3bc79f3df8dcc470489bdf36db8f47102812.jpeg?token=abb6b457da5de1d9918f10f8c67b8577&s=80A7A0BABCA5C30BEDCD2CD4030080A2)
格式修饰符
除了颜色和背景颜色,ANSI转义码还支持一些文本格式修饰符
粗体:u001b[1m
下划线:u001b[4m
反色:u001b[7m
每一个都可以单独或者组合使用,或者于颜色符组合使用,看下面的例子:
![](https://pics3.baidu.com/feed/9213b07eca806538f03c219c84fa0840ac348213.jpeg?token=1761ddf386805f18227948194bfe1e97)
光标导航
光标移动的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()
![](https://pic.rmb.bdstatic.com/809c37b3943a994a5be818240948c5412081.gif)
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进度条。效果如下:
![](https://pic.rmb.bdstatic.com/880414449205b4345a23b9fe2a65bfed3603.gif)
利用向上和向下光标代码移动,我们甚至可以一次性绘多个进度条,比如下面代码我们并行三个进度条显示:
import time, sys, random
def loading(count):
all_progress = [0] * count
sys.stdout.write(" " * 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()
![](https://pic.rmb.bdstatic.com/d86b99016c446d4b389fe42ea4c4474b7933.gif)
代码原理:
为了确保有足够的空间来绘制进度条,我们通过在函数启动时写入" " * 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移动光标到最左。显示效果如下:
![](https://pics4.baidu.com/feed/3b87e950352ac65cff1fe5dfe9d51b1593138a34.jpeg?token=480d7f6f90464fc5d79568f13f562e83&s=2D50AB4253B4966E0C51BC070000E0C2)
基本命令行
基于上面的基本接口,我们来实现一个原始命令行界面,用来回显用户键入的内容,我们约定:
当用户按下可打印字符时,直接打印出来
当用户按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(" echoing..."), input
input = ""
sys.stdout.write(u"u001b[1000D")
sys.stdout.write(input)
sys.stdout.flush()
显示效果如下:
![](https://pic.rmb.bdstatic.com/1fe320ac399d44a75b97636f5bedb4585156.gif)
光标导航
下一步是让用户使用箭头键移动光标。键盘上左右箭头键对应于字符码27 91 68和27 91 67的序列,所以我们可以对输入代码检查并对应移动光标
![](https://pics3.baidu.com/feed/bf096b63f6246b60f4ac1293f9dfb348500fa267.jpeg?token=6fd97e55d4ccb6c1e00574acd8818e17&s=0D43BB424BE4B37E5C55540F0000E0C1)
代码原理:
通过维护一个索引变量,保留一个单独的索引,该索引不一定在输入的末尾,当用户输入一个字符时,将其拼接到输入的正确位置。
检查char == 27,然后检查接下来的两个字符来识别左右箭头键,并递增/递减光标的索引。
写入输入后,手动将光标一直向左移动,并向右移动与光标索引对应的正确字符数。效果如下:
![](https://pic.rmb.bdstatic.com/f30726d23b2f08622d86e5f7c37b2d4c6414.gif)
后续根据需要,可以增加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")
清除光标位置到行末的所有字符。
增加删除功能后的代码:
![](https://pics3.baidu.com/feed/77094b36acaf2edd09596c0c9937a8ed38019352.jpeg?token=b429ad9fd9d911ad58dbd6a52022b47c&s=0D43AB424BA4B37E5C51DC0F0000E0C1)
效果如下:
![](https://pic.rmb.bdstatic.com/a02c6b020535f768cbc22ebbdc6b8bda7439.gif)
工作原理如下:
光标移动到行的开头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"
![](https://pic.rmb.bdstatic.com/56a824c1be854c7c3142fe6374ddcec95412.gif)
这就是一个最简单的实例,为了支持更强大的功能,可以利用Pygments的类库来替换上面syntax_highlight函数,这样就可以实现任何编程语言的真正的语法高亮。
结论
这种与命令行程序的"丰富"交互是大多数传统命令行程序和库所缺乏的。在充分了解ANSI转义码的基础上实现自己的富终端明亮行界面并不像想象的那么难。