在日常开发中,相信很多小伙伴好奇过以下一些问题,尤其是经常使用命令行工具的…
- 为什么 AndroidStudio/IDEA 能打印各种颜色的日志
- 各种 CLI 在执行时,底下的进度条是怎么实现的
- Vim 在 Terminal 中是如何做到清屏和恢复屏幕的
- StartAgent 运维平台的 Web Terminal 是怎么做到跟 Terminal 类似的
- SpringBoot 启动 Logo 是怎么打印出来的
最近在建设云真机平台时,为了便于对 宿主机服务器 & Android设备 的控制,增加了Web Terminal 的功能,通过终端指令控制设备,包含 Web SSH、Android 远程 Shell等,实现过程还蛮有趣的,所以也重新整理了一下 Terminal 的一些细节原理。
终端(Terminal)
通常来讲,操作系统分为两部分,内核 和 用户交互界面。
内核
支持各种命令,负责操作系统底层复杂的操作
用户交互界面
展示在屏幕的交互界面,例如各种应用程序窗口
终端
负责输入输出,为内核和用户交互界面搭桥,我们通常说的终端(Terminal),是指处理终端指令的应用程序,比如 Mac的Terminal、Windows的cmd.exe,实际上是终端命令行界面,在图形界面普及之前使用最广泛,通常不支持鼠标操作,用户通过键盘输入指令,Shell翻译并交给系统执行。正因为通过终端,可以轻易的和内核通信,即便Linux系统无图形界面,也不妨碍它成为优秀的操作系统。
Shell
Shell常常运行在终端中,是操作系统内核和用户交互的接口程序,可以理解为命令行解释器。例如 Bash、Zsh 都属于 Shell。Linux、Mac早期版本自带的 Shell 就是 bash,正常情况下 Terminal 输入的指令,是交给 bash 执行。
那为什么 Terminal 可以执行 ll 指令,但是 bash 却执行不了呢?原因是 ll 并不是一个标准的 bash 指令,而是 Terminal 在 bash 之上做的扩展,定义了一个别名(alias),实际上相当于执行 ls -l ,所以我们也可以在 bash 里定义一个别名。
macOS 从 Catalina 版开始,使用 Zsh 作为默认 Shell,Zsh 比 bash 有更好的交互体验和兼容性,比如从上图也可以看出来,Zsh 可以设置丰富多彩的颜色。
控制字符&转义序列
控制字符
在 ASCII 码表中,前32个字符编码是不能用于打印的,而是用于控制终端行为,这些字符被称为控制字符。如下所示:
例如我们常用的控制字符有:
1 | \n(换行):将光标移动到下一行的开头。 |
从图上可以看出,例如Tab 键的 ASCII 码为 9 ,可以用 \t 表示;Enter键的 ASCII 码为 10,可以用 \n 表示。
转义字符
转义字符是以反斜杠(\)开头的字符序列,它们表示一些特殊的字符。转义字符可以用于在字符串中插入一些特殊的字符,例如引号或换行符。
以下是一些常见的转义字符及其作用:
1 | \'(单引号):用于在字符串中插入一个单引号。 |
ANSI 转义序列
ANSI 转义序列是由 ESC (ASCII 码表中的 27)开头的一些字符序列,用于表示一些特殊的字符或控制序列。转义序列通常用于在终端中显示一些特殊的效果,例如颜色、光标位置等。
转义序列通常以 \033[……m 开头,以 \033[0m 结尾。
例如我们通过 Python 打印一些内容:
1 | print("\033[31;1;4mHello\033[0m") |
或者通过 Java 在控制台打印一些内容:
1 | System.out.println("\u001b[30mHello\u001b[0m"); |
或者通过 JS 在浏览器控制台里打印一些内容:
1 | console.log("\u001b[91mHello\u001b[0m") |
或者直接在终端通过 Shell 脚本打印:
1 | echo "\u001b[91mHello\u001b[0m" |
样式说明
\033、\x1b、\u001b 三种是一样的,分别代表 8进制、16进制、unicode 16进制 的 27,也就是对应上面提到的 ESC 的 ASCII 码。
左边部分的31代表红色 1代表粗体 4代表下划线
右边部分的0代表清空第一部分的所有效果,如果不清空,则样式会一直延续,如下所示:
当然也可为文字添加背景色,有以下两种形式:
1 | \u001b[前景色代码;背景色代码m |
例如:
1 | # \u001b[黑字;绿底m |
效果如下所示:
3/4位颜色表示法
起初ANSI只支持3位表示法,也就是只有 2^3=8 种。
0
7:设置字体37:设置前景色
30
40~47:设置背景色
后来为了实现更明亮的字体,扩展到4位,有 2^4=16 种。
0
7:设置字体37 90
3097:设置前景色47 100~107:设置背景色
40
1 | print("\u001b[91mHello\u001b[0m") |
每一种终端对颜色代码的实现效果上有一些差异。颜色代码对照表如下所示:
8位颜色表示法
随着显卡支持256色查找表,相应的转义序列也增加到 2^8=256 种。
0
15: 标准颜色 & 高强度色231: 216种 RGB 颜色
16
232~255: 24种灰度色
1 | print("\u001b[48;5;177mHello\u001b[0m") |
8位颜色对照表如下所示:
24位颜色表示法
后来”真彩色“显卡普及,支持24位颜色,转义序列也增加到 2^24=16777216 种,也就是我们常用的 RGB 颜色。
但是 Mac 的终端目前不支持,浏览器控制台支持。
1 | console.log("\u001b[38;2;255;0;0mHello\u001b[0m") |
光标移动
前面提到 ANSI 转义序列,只是控制了颜色,实际上转义序列还有其他许多能力,例如光标移动,转义序列如下:
上: \u001b[{n}A
下: \u001b[{n}B
右: \u001b[{n}C
左: \u001b[{n}D
注:{n} 表示移动n个字符
通常来讲,我们打印内容时如果不做特殊处理,光标会自动移动到最后,例如:
那如果想在打印进入的时候,不断刷新前面的进度数字呢?那就需要依靠光标移动,例如:
1 | import time, sys |
打印 1%~100% 进度的时候,通过 sys.stdout.write
标准输出,打印在同一行,在每次打印进度之前,通过 \u001b[1000D 指令,把光标移动到行首,之后打印的值会覆盖前面的。但这里有其实个细节,比如光标移动到行首并打印3个字符,就只会覆盖前三个字符,假设原来这一行里面有4个字符,那么第4个字符就不会被清除。不过这里 1-100 刚好是字符数越变越大,所以也没问题。
进度条光展示数字显然不够骚气,平时各种 CLI 命令行的进度条是怎么实现的呢?
1 | import time, sys |
每次根据当前进度,光标移到行首,覆盖打印对应的小方块,其余部分打印空格。
很多时候,CLI 背后可能在同时执行多个任务,那么通过光标移动也可以实现同时打印多个进度条,例如:
1 | import time, sys, random |
自定义命令行工具
如何自定义一个命令行工具?核心就是 接收输入数据->交给系统执行->打印输出->继续接收输入数据…..
前面提到,当键盘摁下的键属于控制字符时(ASCII码 < 32),例如 回车、删除、方向键、Tab键,不做特殊处理是无法识别成对应操作的,例如:
1 | import sys, tty |
在接收输入的时候,只处理了 CTRL+C 和 ENTER 键,当我们摁下方向键时,上下左右 就会变成 [A[B[D[C,这样就无法实现光标移动,输入数据插入到前面的效果。
解析方向键&删除键
从输入的字符里面,解析是否是方向键或删除键(这里没有处理 上键和下键,一般对应获取历史输入的命令)
1 | import sys, tty |
可能涉及的其他常用指令:
- 清除屏幕:\u001b[{n}J
- n=0:清除光标到屏幕末尾的所有字符。
- n=1:清除屏幕开头到光标的所有字符。
- n=2:清除整个屏幕的字符。
- 清除行:\u001b[{n}K
- n=0:清除光标到当前行末所有的字符。
- n=1:清除当前行到光标的所有字符。
- n=2:清除当前行。
- 光标按行向下移动:\u001b[{n}E 将光标向下移动n行并且将光标移至行首。
- 光标按行向上移动:\u001b[{n}F 将光标向上移动n行并且将光标移至行首。
- 设置光标所在列:\u001b[{n}G 将光标移至当前行的第n列。
- 设置光标所在位置:\u001b[{n};{m}H 将光标移至第n行m列。
Vim 清屏实现原理
保存/恢复 Terminal屏幕:通过 smcup、rmcup 控制码
smcup (Start Cursor Position) 用于启用终端屏幕上的复杂图形和窗口环境。当 smcup 被发送到终端时,终端会将当前屏幕内容保存到缓冲区中,并进入图形环境。
rmcup (Reset Cursor Position) 用于禁用终端屏幕上的复杂图形和窗口环境。当 rmcup 被发送到终端时,终端会从缓冲区中恢复上一个屏幕状态,结束图形环境并返回到普通终端模式。
以下通过 Shell 脚本模拟一个类似 Vim 清屏->输入数据->恢复屏幕 的过程
1 | !/bin/bash |
Tab 键自动补全原理
在 Terminal 中,Tab 键可以用于自动补全命令和文件名。当用户在 Terminal 中输入部分命令或文件名并按下 Tab 键时,终端程序会根据当前所在目录以及用户输入的内容,搜索与之匹配的命令和文件名,并在屏幕上显示匹配的结果。
摁下Tab键之后,如果有多个匹配项,继续摁Tab可以实现在这些匹配项里面顺序选择。里面实际上也是不断修改输入的内容,调整光标位置,当前选中的项加上背景色。
SpringBoot 启动广告如何实现
这个跟 Terminal 关系不大,其实就是一种艺术字符号转换。
在线生成链接,里面有很多花里胡哨的字体:https://tooltt.com/art-ascii/
生成后复制下来保存到文件(因为有些艺术字包含特殊字符 直接放到代码里会报错)里,读取并打印到控制台。也可通过前面提到的 ANSI 为其加上字体颜色。
1 | import os.path |