ANSI转义码

前言

最近期末考试考了一部分,间隙逛群的时候发现群友打的CatCTFMisc中的CatFlag有点意思,通过一个简单的cat flag操作就可以拿到flag,但是在vim010Editor下拿flag比较的困难,同时如果直接使用strings flag也是拿不到flag,作为一个签到题,可能确实签到(对我这种彩笔可能拿不到),同时作者也给出了出题原理

https://en.wikipedia.org/wiki/ANSI_escape_code

简单的去了解了一下,发现了一些比较好玩的东西,同时也顺便了解到一些题目中shell窗口是如何实现输出彩色字以及彩色背景等有关内容(XD),本文记录一下关于这个题涉及到的ANSI转义码中的一些小玩意

富文本

字体颜色

有没有觉得直接输出一个白色字符串有些多少单调且无聊?想不想输出彩色的字符?

最基本的ANSI转义码是涉及呈现文本的代码,可以让我们修改文字的颜色、背景或者是添加一些修饰等,我们以Python3的环境为例子:

1
print("Hello")

我们在控制台输入这个指令,可以看到字体是白色的:

image-20230104232231945

但是我们加入一个ANSI转义码\u001b[31m

1
print("\u001b[31mHello")

此时的输出是:

image-20230104232349539

可以看到我们的输出结果变成了红色,且下面一行也是红色,而这便是ANSI颜色的一种工作方式,当我们设置了某种颜色后,其便会一直存在,直到我们换了一种颜色或者打出重置代码

我们输入重置代码来进行恢复:

1
print("\u001b[0m")

image-20230104232612082

可以看到已经变回了原来的白色了。根据ANSI颜色的工作特性,我们一般都是讲要输入的字符串包裹在颜色代码以及重置代码之间,以此来控制我们所需要的彩色部分

1
print("\u001b[31mHello\u001b[0m")

image-20230104232815303

我们简单的了解了颜色的工作原理后我们列出一些颜色供给大家进行选择:

8 色彩

基本上所有的控制台都支持以下的颜色:

  • 黑色:\u001b[30m
  • 红色:\u001b[31m
  • 绿色:\u001b[32m
  • 黄色:\u001b[33m
  • 蓝色:\u001b[34m
  • 洋红色:\u001b[35m
  • 青色:\u001b[36m
  • 白色:\u001b[37m

我们将其都进行输出:

image-20230104233241429

16 色彩

对于大多数终端,除了上述 8 种基本的颜色集外,还支持更明亮的颜色:

  • 亮黑色:\u001b[30;1m
  • 亮红色:\u001b[31;1m
  • 亮绿色:\u001b[32;1m
  • 亮黄色:\u001b[33;1m
  • 亮蓝色:\u001b[34;1m
  • 明亮的洋红色:\u001b[35;1m
  • 亮青色:\u001b[36;1m
  • 亮白:\u001b[37;1m

其与 8 个基本的色彩的区别在于其代码中间增加了一个:;1

image-20230104233700947

256 色彩

一些终端支持256色扩展颜色集,其颜色代码形势如下:\u001b[38;5;${ID}m

我们换到VSCshell进行演示:

1
2
3
4
5
6
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(u"\u001b[0m")

image-20230104234048257

我们此处使用sys.stdout.write而不是print,这样我们可以在一行上打印多个

背景颜色

ANSI转义码还支持你设置对应的文本背景颜色,和设置字体颜色一样,其重置的方式代码仍然是\u001b[0m]

  • 背景黑色:\u001b[40m
  • 背景红色:\u001b[41m
  • 背景绿色:\u001b[42m
  • 背景黄色:\u001b[43m
  • 背景蓝色:\u001b[44m
  • 背景洋红色:\u001b[45m
  • 背景青色:\u001b[46m
  • 背景白色:\u001b[47m
1
2
3
4
import sys
for i in range(40, 48):
sys.stdout.write("\u001b[" + str(i) + "m"+" ")
print(u"\u001b[0m")

image-20230104235121754

亮色版本:

  • 背景亮黑色:\u001b[40;1m
  • 背景亮红色:\u001b[41;1m
  • 背景亮绿色:\u001b[42;1m
  • 背景亮黄色:\u001b[43;1m
  • 背景亮蓝色:\u001b[44;1m
  • 背景明亮洋红色:\u001b[45;1m
  • 背景亮青色:\u001b[46;1m
  • 背景亮白色:\u001b[47;1m
1
2
3
4
import sys
for i in range(40, 48):
sys.stdout.write(u"\u001b[" + str(i) + "m"+" ")
print(u"\u001b[0m")

对于 256 颜色也是一样的:

1
2
3
4
5
6
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")

image-20230104235619983

修饰

除了字体颜色以及背景可以提供修改,我们还可以加入粗体,下划线,反转等修饰

  • 粗体:\u001b[1m
  • 下划线:\u001b[4m (好像VSC控制台会吃掉这个下划线,但是控制台不会)
  • 反转:\u001b[7m
1
print("\u001b[1m BOLD \u001b[0m\u001b[4m Underline \u001b[0m\u001b[7m Reversed \u001b[0m")

image-20230104235902453

这几个也可以一起搭配前面所提到的修改字符颜色以及背景一起使用,此处就不再赘述

光标控制

顾名思义,我们可以使用ANSI转义码来控制光标的位置,以此来达到模仿输入的操作

其中基本的为:上下左右控制

  • 向上:\u001b[{n}A
  • 向下:\u001b[{n}B
  • 向右:\u001b[{n}C
  • 向左:\u001b[{n}D

其中{n}在使用过程中我们需要将其替换为对应的数字,代表我们想要移动的长度

需要注意的是,当我们的光标向左移动了 n 个后再进行输出时,会将后面的内容进行覆盖

在输出时,会先解析对应的ANSI转义码,将光标进行移动,之后在进行输出,借助这个特性我们可以写一个进度显示器,而不需要使用类似tqdm的库

1
2
3
4
5
6
7
8
9
10
import time, sys
def loading():
print("Loading...")
for i in range(0, 100):
time.sleep(0.1)
sys.stdout.write(u"\u001b[3D" + str(i + 1) + "%") # 3 可以是大于它的数字,我们仅仅只是需要将其移动到最前面将输出覆盖即可
sys.stdout.flush()
print()

loading()

或者是类似加载条的形式:

1
2
3
4
5
6
7
8
9
10
11
12
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("\u001b[1000D" + bar)
sys.stdout.flush()
print()

loading()

以上的进度条都没有任何的监控进度的完成,但是我们可以加入对应的监控项目,然后降至转化为真正的进度监控,或者结合上面提到的字体颜色以及背景的更改来设计更为花哨的进度条

对于下一行的操作ANSI转义码也提供了一些方法:

  • 下一行:将光标向下移动到行首\u001b[{n}E
  • 上一行:将光标向下移动到行首\u001b[{n}F

同时其也允许我们保存对应位置数据:

  • 保存位置:保存当前光标位置\u001b[{s}
  • 保存位置:将光标恢复到上次保存的位置\u001b[{u}

编写命令行

ANSI转义码的主要目的就是实现命令行,我们可以根据用户的输入来产生对应的输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import sys, tty

def command_line():
tty.setraw(sys.stdin) # 将我们的命令行设置成raw模式,避免shell帮我们解析方向键、回退键(Backspace)以及用户输入进行缓冲
while True:
input = ""
while True: # 循环来控制每一个字符
char = ord(sys.stdin.read(1)) # 每次只读入一个字符

if char == 3: # CTRL-C
return
elif 32 <= char <= 126: # 普通字符
input = input + chr(char)
elif char in {10, 13}: # 如果是回车时
sys.stdout.write("\u001b[1000D")
print("\necho something ... ", input)
input = ""

sys.stdout.write("\u001b[1000D") # 光标移动到最左侧
sys.stdout.write(input) # 重新输出 input
sys.stdout.flush()

需要注意的是上述代码中导入了tty库,我们使用这个库的时候需要有termios库,此库仅适用于Unix,因此上述代码在Windows端无法执行

因为我们采用了raw模式,不移动光标照顾护额打印会出现光标不会移动到行首的问题,我们我们再次输出一个\u001b[1000D确保其回到开头

删除

但是我们想要实现删除时,我们采用覆写的方式在某种意义上也算是完成了我们的需要,但是假如我们需要删掉一个字符呢?我们覆写模式还是需要将字符串后面的内容再次进行输出,稍微有点麻烦,对此ANSI转义码也提供了删除这一操作

  • 清除屏幕:清除屏幕

    1
    \u001b[{n}J
    • n=0从光标中清除,直到屏幕结束,
    • n=1从光标清除到屏幕开头
    • n=2清除整个屏幕
  • 清除行:清除当前行

    1
    \u001b[{n}K
    • n=0从光标清除到行尾
    • n=1从光标清除到行首
    • n=2清除整行

总结

我们简答的了解了一下ANSI转义码的使用,对于更多的指令大家可以参考下面的表格

name signature description
A Cursor Up (n=1) Move cursor up by n 【将光标向上移动n行】
B Cursor Down (n=1) Move cursor down by n 【将光标向下移动n行】
C Cursor Forward (n=1) Move cursor forward by n 【将光标向前移动n个字符】
D Cursor Back (n=1) Move cursor back by n 【将光标向后移动n个字符】
E Cursor Next Line (n=1) Move cursor to the beginning of the line n lines down【将光标向下移到到n行的行首】
F Cursor Previous Line (n=1) Move cursor to the beginning of the line n lines up【将光标向上移到到n行的行首】
G Cursor Horizontal Absolute (n=1) Move cursor to the the column nwithin the current row 【将光标移动到当前行中的第n列】
H Cursor Position (n=1, m=1) Move cursor to row n, column m, counting from the top left corner
J Erase in Display (n=0) Clear part of the screen. 0, 1, 2, and 3 have various specific functions
K Erase in Line (n=0) Clear part of the line. 0, 1, and 2 have various specific functions
S Scroll Up (n=1) Scroll window up by n lines 【将窗口向上滚动到n行】
T Scroll Down (n=1) Scroll window down by n lines 【将窗口向下滚动到n行】
s Save Cursor Position () Save current cursor position for use with u
u Restore Cursor Position () Set cursor back to position last saved by s
f (same as G)
m SGR (*) Set graphics mode. More below【见下一个表格】

关于SGR的作用我们在下面一个表格进行简单说明:

value name / description
0 Reset: turn off all attributes 【重置:关闭所有属性】
1 Bold (or bright, it’s up to the terminal and the user config to some extent) 【粗体】
3 Italic 【斜体】
4 Underline 【下划线】
30–37 Set text colour from the basic colour palette of 0–7 【从0-7的基本颜色板中设置文本颜色,即前景色】
38;5;n Set text colour to index n in a 256-colour palette(e.g. \x1b[38;5;34m)【前景色】
38;2;r;g;b Set text colour to an RGB value (e.g. \x1b[38;2;255;255;0m)【前景色】
40–47 Set background colour 【从0-7的基本颜色板中设置背景色】
48;5;n Set background colour to index n in a 256-colour palette 【背景色】
48;2;r;g;b Set background colour to an RGB value 【背景色】
90–97 Set text colour from the bright colour palette of 0–7
100–107 Set background colour from the bright colour palette of 0–7

其中有些没有列出来,感兴趣可以自己去看看Wiki

我们可以通过ANSI转义码来实现Shell端的游戏、命令行、Vim等功能,更多的小玩意可以靠大家的想象去实现与完成


ANSI转义码
https://equinox-shame.github.io/2023/01/05/ANSI转义码/
作者
梓曰
发布于
2023年1月5日
许可协议