命令行工具在输出时,如果是简单的进度更新,可以使用 \r
和 \b
来达成刷新行的效果,但如果要更复杂些的如字体颜色、背景颜色、光标位置移动等功能,那就需要使用 ANSI 转移序列了。
已有不少成熟的命令行工具库,如 Readline、JLine 和 Python Prompt Toolkit,基于这些库创造了如 mycli 和 ipython 等好用的工具。
ANSI 转义序列有比较悠久的历史,不同平台支持的功能不完全一致,这里学习到的是比较简单常用的,包括字体颜色、背景色和其它装饰的富文本和光标操作。
\u001b
即 ESC 的 ASCII 码,\u001b[0m
是清除之前的设定。
适用于 *nix 的系统。
富文本
前景色
8 色
\u001b[?m
,其中 ? ∈ [30, 37]
。
- 黑(black):
\u001b[30m
- 红(red):
\u001b[31m
- 绿(green):
\u001b[32m
- 黄(yellow):
\u001b[33m
- 蓝(blue):
\u001b[34m
- 品红(magenta):
\u001b[35m
- 蓝绿(cyan):
\u001b[36m
- 白(white):
\u001b[37m
- 重置(reset) :
\u001b[0m
16 色
在 8 色的基础上对字体加粗,颜色加亮,得到另外 8 种,加起来就是 16 色。
\u001b[?;1m
,其中 ? ∈ [30, 37]
。
- 亮黑(black):
\u001b[30;1m
- 亮红(red):
\u001b[31;1m
- 亮绿(green):
\u001b[32;1m
- 亮黄(yellow):
\u001b[33;1m
- 亮蓝(blue):
\u001b[34;1m
- 亮品红(magenta):
\u001b[35;1m
- 亮蓝绿(cyan):
\u001b[36;1m
- 亮白(white):
\u001b[37;1m
const out = process.stdout;
function colors8(pre, post, startCode = 30) {
const codePointA = 'A'.codePointAt(0);
let i = 0;
while (i < 8) {
const colorCode = startCode + i;
const char = String.fromCodePoint(codePointA + i);
out.write(`{pre}{colorCode}{post}{char} `);
i++;
}
console.log('\u001b[0m');
}
function fgColors8() {
colors8('\u001b[', 'm');
}
function fgColors8Bright() {
colors8('\u001b[', ';1m');
}
fgColors8();
fgColors8Bright();
256 色
\u001b[38;5;?m
,其中 ? ∈ [0, 255]
function colors256(pre, post) {
let i = 0;
while (i < 16) {
let j = 0;
while (j < 16) {
const colorCode = i * 16 + j;
const text = `{colorCode}`.padEnd(4);
out.write(`{pre}{colorCode}{post}${text}`);
j++;
}
console.log('\u001b[0m');
i++;
}
}
function fgColors256() {
colors256('\u001b[38;5;', 'm');
}
fgColors256();
背景色
背景色和前景色的方案一致,只是 code 不同而已。
8 色
\u001b[?m
,其中 ? ∈ [40, 47]
。
- 黑(black):
\u001b[40m
- 红(red):
\u001b[41m
- 绿(green):
\u001b[42m
- 黄(yellow):
\u001b[43m
- 蓝(blue):
\u001b[44m
- 品红(magenta):
\u001b[45m
- 蓝绿(cyan):
\u001b[46m
- 白(white):
\u001b[47m
16 色
\u001b[?;1m
,其中 ? ∈ [40, 47]
。
- 亮黑(black):
\u001b[40;1m
- 亮红(red):
\u001b[41;1m
- 亮绿(green):
\u001b[42;1m
- 亮黄(yellow):
\u001b[43;1m
- 亮蓝(blue):
\u001b[44;1m
- 亮品红(magenta):
\u001b[45;1m
- 亮蓝绿(cyan):
\u001b[46;1m
- 亮白(white):
\u001b[47;1m
function bgColors8() {
colors8('\u001b[', 'm', 40);
}
function bgColors8Bright() {
colors8('\u001b[', ';1m', 40);
}
bgColors8();
bgColors8Bright();
256 色
\u001b[48;5;?m
,其中 ? ∈ [0, 255]
function bgColors256() {
colors256('\u001b[48;5;', 'm');
}
bgColors256();
装饰
- 加粗加亮:
\u001b[1m
- 降低亮度:
\u001b[2m
- 斜体:
\u001b[3m
- 下划线:
\u001b[4m
- 反色:
\u001b[7m
可以单独使用,也可以组合使用(如 \u001b[1m\u001b[4m\u001b[7m
表示加粗、下划线和反色)。
还可以和颜色结合一起使用,如 \u001b[1m\u001b[4m\u001b[44m\u001b[31m Blue Background Red Color Bold Underline
。
function decorations() {
const codes = [
[1, 'High'],
[2, 'Low'],
[3, 'Italic'],
[4, 'Underline'],
[7, 'Reverse']
];
for (let c of codes) {
out.write(`\u001b[{c[0]}m{c[1]} \u001b[0m`);
}
console.log();
console.log('\u001b[1m\u001b[4m\u001b[44m\u001b[31mBlue Background Red Color Bold Underline\u001b[0m');
}
decorations();
光标操作
- 上:
\u001b[{n}A
,光标上移n
行 - 下:
\u001b[{n}B
,光标下移n
行 - 右:
\u001b[{n}C
,光标右移n
个位置 - 左:
\u001b[{n}D
,光标左移n
个位置 - 下几行行首:
\u001b[{n}E
- 上几行行首:
\u001b[{n}F
- 指定列:
\u001b[{n}G
- 指定位置:
\u001b[{n};{m}H
,移动光标到n
行m
列 - 清屏:
\u001b[{n}J
n=0
,当前光标到屏末n=1
,当前光标到屏首n=2
,整屏
- 清行:
\u001b[{n}K
n=0
,当前光标到行末n=1
,当前光标到行首n=2
,整行
function sleep(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
async function progressIndicator() {
console.log('Loading...');
let i = 0;
while(i <= 100) {
await sleep(10);
out.write(`\u001b[1000D${i}%`);
i++;
}
// 清除进度信息(上两行)然后输出 Done!
console.log('\u001b[2F\u001b[0JDone!');
}
(async () => {
await progressIndicator();
})();
async function progressBars(count = 1) {
const list = [];
let i = 0;
while (i++ < count) {
list[i] = 0;
}
// 占位提供空间
out.write(list.map(i => '').join('\n'));
while(list.some(i => i< 100)) {
await sleep(10);
const unfinished = list.reduce((p, c, i) => {
if (c < 100) p.push(i);
return p;
}, []);
const randomIndex = unfinished[Math.floor(Math.random() * unfinished.length)];
list[randomIndex] += 1;
out.write('\u001b[1000D');
out.write('\u001b[' + count + 'A');
list.forEach(p => {
const width = Math.floor(p / 4);
console.log('[' + '#'.repeat(width) + ' '.repeat(25-width) + ']');
});
}
console.log(`\u001b[1000D\u001b[${count}A\u001b[0JDone!`);
}
(async () => {
console.log('Single progress');
await progressBars();
console.log();
console.log('Multiple progress');
await progressBars(4);
})();