0x00 引言

虽然命令行程序使用文字来展示,但在使用得当的情况下也可以有着独特且不输于图形界面程序的交互与使用体验。
而实现这种效果就需要用到ANSI转义代码。本文章从自定义样式输出开始介绍ANSI转义代码的用法与应用。

0x01 什么是ANSI

ANSI转义代码(ANSI Escape Code,也被称为 VT100。以下为了方便简称为 ANSI
是一种在文本终端中控制格式、颜色和其他输出选项的代码。这些代码允许开发者在终端中创建丰富多彩的文本界面,提高用户体验。

通过使用ANSI,可以摆脱传统CLI程序的逐行输出纯文本的交互模式,构建动态与彩色的命令行界面。

设置文本颜色和样式是ANSI标准中定义的一个功能,本篇文章中我们先来了解这一部分的功能。

0x02 认识ANSI控制序列

要使用ANSI,我们只需要向输出正常文字一样像控制台额外输出,或在原来的文字前追加一串特殊字符即可。
比如说这样:

1
\033[4;34mHello,\033[0;3;32mWorld!

输出以上的文字,在控制台的效果为带下划线蓝色的 Hello, 和斜体绿色的 World!

看不懂是正常的,我当时初见这玩意的时候也是很懵逼。但实际上,这个控制序列实际上并不难理解。

以上的示例中,去除实际输出的文字,还剩下两个部分,这就是两个控制序列。
它们分别是 \033[4;34m\033[0;3;32m 。我们先来看第一个,它可以分为以下的三个部分:

前缀:\033[

这是ANSI转义序列的开始标识。任何一个ANSI转义序列都由 \033[ 开头。

\033 是ASCII码为27的字符 ESC 的转义。你也可以使用 \x1b\e 代替 \033

中间:4;34

这是使用分号分割的参数列表,它决定了这个转义序列具体的功能的参数。

在这个例子中,4 代表了下划线,34 代表了蓝色,且是按照从左到右的顺序依次生效的。

后缀:m

这是转义序列的功能标识,使用一个字符表示。它决定了这个转义序列的具体功能。

m 字符代表的功能是 Select Graphic Rendition (SGR),即设定渲染样式,可以设置文字的颜色和各种样式。

所以说,这整个转义序列的作用即为 使用设定样式功能,设置样式4,再设置样式34
这相当于在告诉终端:为后面的文字添加下划线,再设置为蓝色

0x03 样式的叠加和重置

值得注意的是,使用ANSI设置的样式默认是对其后面所有文字生效的。如果要重新设置样式,就需要重置样式。

重置样式非常简单,只需要使用 0 作为参数即可。你可以理解为 0 代表了 重置所有样式 的样式。

在上面的例子的第二部分中,我们使用了 \033[0;3;32m 转义序列,
可以看到在样式参数列表开头有 0 参数,用来重置前面设置的下划线和蓝色样式。

所以,这个转义序列的作用即为 使用设定样式功能,设置样式0,再设置样式3,再设置样式32
这相当于在告诉终端:先重置前面设置的所有参数,再设置为斜体,最后设置为绿色

实际上,单独使用 \033[m 实际上和 \033[0m 在大多数终端中是完全等效的,
不过明确写上 0 会让代码的语义更为严谨。

0x04 构建ANSI控制序列

知道了上面的知识,我们就可以开始使用ANSI实现自己想要的样式输出了。

只需要查表获取想要的样式对应的参数,再按照顺序使用 ; 拼接,最后在前面加 \033[,在后面加 m 即可。

以下是常用的样式和颜色表,完整列表可以在维基百科
查询。

0x05 常用样式表

参数 作用 注释
0 重置所有样式
1 设置为粗体或深色 具体样式取决于具体环境
2 设置为细体或浅色 具体样式取决于具体环境
3 设置为斜体 某些环境下可能不支持
4 添加下划线
5 设置为慢速闪烁
6 设置为快速闪烁 某些环境下可能不支持
7 调换文字前后景颜色
8 隐藏文字

0x06 常用颜色表

参数 作用 注释
30-37 设置文字颜色为基础颜色的某一个 从30-37分别为:黑,红,绿,黄,蓝,品红,青,白
40-47 设置文字背景颜色为基础颜色的某一个 从40-47分别如上
38;5;n 设置文字颜色为256色表的第n个 色表见下
48;5;n 设置文字背景颜色为256色表的第n个 色表见下
38;2;r;g;b 设置文字颜色为rgb
48;2;r;g;b 设置文字背景颜色为rgb

256色表

256

0x07 示例

以下是一个使用ANSI实现彩色输出的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void main() {
// 示例 1:我们在 0x02 中提到的例子(别忘了在末尾使用 \033[0m 重置,以免影响控制台后续输出)
System.out.println("\033[4;34mHello,\033[0;3;32mWorld!\033[0m");

// 示例 2:基础样式与颜色叠加(粗体 + 白底 + 红字)
System.out.println("\033[1;47;31m [ERROR] 发生了一个严重的错误! \033[0m");

// 示例 3:256色模式(前景色设为 208 号橙色,并带下划线)
System.out.println("\033[4;38;5;208m这是256色表中的橙色带有下划线的文字\033[0m");

// 示例 4:RGB 真彩色模式(背景色为暗灰 rgb(40,40,40),前景色为青绿 rgb(0,255,150))
System.out.println("\033[48;2;40;40;40;38;2;0;255;150m 这是使用 RGB 真彩色的自定义高亮文本 \033[0m");

// 示例 5:验证样式是否已重置
System.out.println("因为上面都规范地使用了 \\033[0m,所以这行是普通的默认文本。");
}

实际上,在部分语言中,可能对于样式输出有更为友好的封装库(例如Java中的Jansi,Python中的Colorama等),
这些库在底层也是使用ANSI转义序列实现的,但提供了更为简洁和易用的接口来设置样式和颜色。

0x08 总结

通过在输出字符串中嵌入这些简单的转义序列,我们就可以打破传统黑白终端的限制,
为命令行程序赋予更直观的信息层级(例如用红色高亮错误日志,用绿色高亮成功提示)和丰富的美感。

在使用ANSI的时候,有几点需要特别注意:

  1. 首先要始终记得重置样式。在每段彩色文本输出完毕后,务必养成加上 \033[0m 的好习惯。如果不重置,设置的颜色和样式可能会污染后续输出。
  2. 其次是兼容性考量。绝大多数现代终端都已完美支持 ANSI 和 RGB 真彩色。
    但在较老的 Windows 系统中可能无法正常解析,会输出乱码字符。 这种情况下直接调用成熟的库(如 Jansi)会是更好的选择。

虽然这篇文章主要介绍ANSI的样式输出功能,但ANSI的功能远不止于此。
它还可以实现光标控制、屏幕清除、输入控制等功能,这些功能可以帮助我们构建更为复杂和动态的命令行界面。
在(有可能的)后续文章中,我们将继续介绍这些功能,并给出一些实用的示例。

END.

0xFF 引言

最近入手了个路由器,用来接校园网的宽带(神秘校园网只允许一个设备同时接入..)
正当我在想Wifi名的时候,突然想整个活,于是便设置成了Ciallo~ (∠・ω )⌒☆
密码也是喜闻乐见的0d000721

但设置好后突然感觉不对:这不是相当于公开了我花钱办的宽带了吗🤔于是便想改

但最后思来想去还是不想放弃这个Wifi名称,于是便设法实现了使用两个不同密码
连接会有不同响应的Wifi。具体来说,同一个名为Ciallo~ (∠・ω )⌒☆的网络,
使用0d000721连接就只会弹一个带图片的网址,
而使用另外一个密码连接才能正常上网。

0x00 准备工作

想要实现上述效果,需要:

  • 一台已刷OpenWrt的路由器(关于路由器的选购与刷机这里不赘述)
  • 基础Shell操作知识
  • 正常的网络连接

注意:

  • 本教程使用的OpenWrt版本为25.XX,不建议在更早的版本上使用本教程,
    ,因为不同版本间的差异较大,可能会无法使用或造成故障。
  • 搞机有风险,操作需谨慎🥹

0x01 连接网络

如果你的路由器已经连接到网络,可以跳过本节。

打开OpenWrt的管理页面,选择网络>无线。这里便是管理你设备的无线网络的地方。

这里我们使用客户端模式连接到网络并做中继。
点击任意radio扫描按钮打开网络扫描,在弹出的框中
选择要连接的网络点击加入网络,之后在WPA 密钥中输入
密码,点击提交,再点击保存,最后点击最下面的保存并应用
等待设置更新。

这里不同的radio可能分别代表的是2.4GHz和5GHz的天线,具体取决于你自己的设备。
图中以802.11ax/b/g/n结尾的为2.4GHz天线,以802.11ac/ax/n结尾的为5GHz天线。

0x02 创建Wifi

接着我们创建需要使用不同密码连接的Wifi。

点击任意radio添加按钮打开网络扫描,在弹出的框中翻到最下面,
ESSID框中输入要使用的Wifi名称。

切换到无线安全,将加密改为WPA2-PSK,之后在密钥中输入真实的密码(之后能够正常联网的密码)

点击保存,最后点击最下面的保存并应用,等待设置更新。

0x03 升级dnsmasq-full

首先由于我使用的是OpenNDS来做弹出的页面,所以需要将dnsmasq包升级为完整版。

在Shell中输入以下命令:

1
apk update && apk add dnsmasq-full

看到形如OK: XXX MiB in XXX packages的结果即可。

0x04 配置Multi-PSK

使用Shell编辑/etc/config/wireless

关于Shell中的文本编辑器Vim的操作,请详见此文章

以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
config wifi-device 'radio0'
......

config wifi-iface 'default_radio0'
......

config wifi-device 'radio1'
......

config wifi-iface 'wifinet3'
option device 'radio1'
option network 'lan'
option mode 'ap'
option ssid 'hyw'
option encryption 'psk2'
option key 'ThisIsTheTruePassword'

config wifi-iface 'wifinet2'
option device 'radio1'
option network 'wwan'
option ssid 'ExampleWifi'
......

可以看到,在config wifi-iface 'default_radio1'配置块中有我们刚刚创建的Wifi,
config wifi-iface 'wifinet2'配置块中有我们刚刚连接的Wifi。

接下来对配置文件做以下更改:

1.启用MultiPSK

在创建的Wifi的配置块中加入option multi_psk '1'
之后你的配置应该像这样:

1
2
3
4
5
6
7
8
config wifi-iface 'wifinet3'
option device 'radio1'
option network 'lan'
option mode 'ap'
option ssid 'hyw'
option encryption 'psk2'
option key 'ThisIsTheTruePassword'
option multi_psk '1'

2.添加wifi-vlan

在文件底部添加以下配置:

1
2
3
4
5
config wifi-vlan
option name 'g_v'
option network 'guest'
option vid '10'
option iface 'wifinet3'

注意:

  • 这里的wifinet3为我们创建的wifi的wifi-iface的名字,见上面的示例配置。
  • 这里的name可以任取但是不能太长,具体限制在不同设备上
    可能不同,但建议只使用3~4个字符。
  • vid10networkguest均可自定义,不过之后需要保证配置能够对的上。

3.添加wifi-station

继续在文件底部添加以下配置:

1
2
3
4
config wifi-station
option key '0d000721'
option vid '10'
option iface 'wifinet3'

注意:

  • 这里的key即为假密码,即无法正常上网的密码。
  • 这里的vidiface要和上面匹配。

编辑完成,保存文件进行下一步。

0x05 添加设备与接口

打开OpenWrt管理面板,进入网络>接口,切换到设备页面,点击最下面的添加设备配置

在弹出的窗口中,将设备类型选为网桥设备,
设备名称设为br-guest(可以自己改),选中允许启动空网桥,然后点击保存

接着切换回接口页面,点击下面的添加新接口
将名称设置为guest(注意和上面的匹配),将协议设置为静态地址,将设备选为br-guest(和上面创建的匹配),然后点击创建接口

接着在弹出的窗口中,将IPv4 地址设置为192.168.72.1(可以任意设置,但要和你正常使用的主网络不同),将IPv4 子网掩码设置为255.255.255.0

然后点击上面的防火墙设置,点击创建/分配防火墙区域,在最下面的自定义框中输入guest_zone(可以自己起名)然后按回车。

最后点击上面的DHCP 服务器,然后点击配置 DHCP 服务器按钮,不用变更配置。

最后点击保存关闭配置窗口,点击最下面的保存并应用,等待设置更新。

0x06 安装OpenNDS

OpenNDS的作用本来一般是提供上网认证界面,但这里我们使用它来做弹出图片的效果。

打开Shell,输入以下命令:

1
apk update && apk add opennds

之后编辑/etc/config/opennds,找到option gatewayinterface
(我设备上是第308行,可以使用查找功能查找),
将其取消注释并把后面的值换成上面配置的设备的名称。

更换之后配置应该如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
...

# GateWayInterface
# Default br-lan
# Use this option to set the device opennds will bind to.
# The value may be an interface section in /etc/config/network or adevice name such as br-lan.
# The selected interface must be allocated an IPv4 address.
# In OpenWrt this is normally br-lan, in generic Linux it might besomething else.
#
option gatewayinterface 'br-guest'
##################################################################

...

保存文件,进行下一步。

0x07 配置弹出页面

编辑/usr/lib/opennds/theme_click-to-continue-basic.sh
将里面的内容全部删除,替换成下面的:

1
2
3
#!/bin/sh
cat /etc/opennds/htdocs/splash.html
exit 0

之后保存文件。

然后编辑/etc/opennds/htdocs/splash.html(即上面配置的文件),将原有内容(如果有)全部删除,替换成你想要的内容。

在使用假密码登录时,OpenNDS会拦截链接并把/etc/opennds/htdocs/splash.html里面的HTML提供给登录者作为登录界面,所以这里可以自由发挥。

值得注意的是,由于使用假密码连接的时候没有网络连接,所以如果想要插入图片的话不能使用图床,因为无法访问。这里我为了方便直接使用了Base64硬编码图片到splash.html里。

另外,由于存在缓冲区大小限制,splash.html不能太长,所以如果要使用Base64添加图片的话需要对图片进行充分压缩。

以下是我的配置:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<head>
<title>Ciallo~ (∠・ω )⌒☆</title>
</head>
<body>
<h1>Ciallo~ (∠・ω )⌒☆</h1><br>
<img width="300px" id="ciallo" alt="ciallo" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAgCAIAAADvz6...(太长了省略)" />
</body>
</html>

保存文件,至此配置已经全部完成。

0x08 重启服务

使用以下命令重启服务:

1
2
3
4
/etc/init.d/network restart
wifi reload
/etc/init.d/dnsmasq restart
/etc/init.d/opennds restart

之后应该就可以看到创建的Ciallo~ (∠・ω )⌒☆Wifi了。

0x09 遇到问题?

由于本人之前也没有接触过OpenWrt,在尝试实现这个效果的过程中也是在摸索,
所以如果各位遇到了一些问题我可能无法进行有效的解答。不过这里有些我
遇到过的问题,供大家参考。

在配置完OpenNDS并重启后无法访问LuCI?

这一般是因为OpenNDS没有被正确配置(即gatewayinterface没有与前面
创建的设备匹配),导致它将所有网络连接全部拦截。

遇到这种情况时,使用Shell登录,并执行/etc/init.d/opennds stop
来停止OpenNDS,之后应该就能恢复LuCI访问。
为了解决问题,请参照上面的步骤正确配置之后,再运行/etc/init.d/opennds start
恢复OpenNDS,看看是否正常工作。

重启服务后找不到添加的Wifi?

这可能是因为你的wifi-vlan块中的name过长,
请参照上面的步骤将其改短后再执行wifi reload看看。

END.

0x00 引言

本部分没有任何干货,可以直接跳过

在我刚学习写代码的时候,经常做一些命令行的小程序玩(因为GUI还不会做()),
虽然能够实现我想要的功能,但这极致简约风格的黑底白字我是实在看得不得劲。
再加上之后跑各种程序的时候又见识到了各种大佬的命令行程序,才知道CLI程序
做的不好看原来触及到的是我的上限而非终端的上限😭

我做的:
有点弱
有点弱
别人做的:
Description 2
!?强强?!

于是我就去了解了一下CLI程序到底该怎么做得美观,自己也写了一些好看一点的小程序,
便于此留下学习经验供各位参考。

本文章作为CLI程序美化系列(可能会存在)的第一篇文章,从最基础的\r开始介绍,
初步实现行刷新功能。在之后的文章中,我会介绍使用ANSI实现彩色、样式输出和
全屏渲染控制,和使用JLine读取键盘输入实现交互的方法。

0x01 \r 是什么

在大部分情况下,\r指的是回车。这里的回车换行不同,前者是将光标移动到行首,后者是将光标移动到下一行。而我们知道,当光标位于一行的行首时,再输出文字,就可以将已经输出的文字覆盖掉。如果根据程序运行情况实时覆盖,就能起到动态进度条、状态栏等的效果。

0x02 \r 的简单使用

要使用\r的话,直接输出即可。下面是一个例子:

1
2
3
4
System.out.print("正在加载中..."); // 不能使用println()
// Doing something ...
Thread.sleep(2000);
System.out.print("\r加载完成! "); // 加空格补齐长度

这里有几点需要注意:

首先部分IDE内置的运行调试窗口可能不支持光标移动的操作,所以可能需要进行
额外设置,或是手动编译运行。

其次在输出待覆盖文字时不能换行,即不能调用println()或者加\n
否则光标就会移动到下一行,而无法覆盖上一行的文字。

最后如果要覆盖的文字的长度比被覆盖的文字短,则不会被完全覆盖,所以必要
时需要使用空格补齐

0x03 使用\r制作带进度条的下载器

以下程序实现了一个命令行下载器,并将下载进度动态更新至进度条。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Scanner;

public class CLIDownloader {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);

System.out.print("请输入下载地址: ");
String fileURL = scanner.nextLine();
System.out.print("请输入保存的文件名: ");
String saveFileName = scanner.nextLine();
System.out.println("连接中,准备开始下载...");

try {
URL url = new URL(fileURL);
URLConnection conn = url.openConnection();
long totalBytes = conn.getContentLengthLong();

InputStream in = conn.getInputStream();
FileOutputStream out = new FileOutputStream(saveFileName);

byte[] buffer = new byte[64 * 1024];
int bytesRead;
long downloadedBytes = 0;

while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
downloadedBytes += bytesRead;
// 借助 \r 打印动态进度条
if (totalBytes > 0) {
int percent = (int) (downloadedBytes * 100 / totalBytes);
StringBuilder bar = new StringBuilder("[");
for (int i = 0; i < 50; i++) {
bar.append(i < percent / 2 ? "=" : " ");
}
bar.append("]");
// \r 让光标回到行首,实现覆盖刷新
System.out.print("\r" + bar + " " + percent + "%");
}
}

in.close();
out.close();
System.out.println("\n下载完成!文件已保存为: " + saveFileName);
} catch (Exception e) {
System.out.println("\n下载出错: " + e.getMessage());
e.printStackTrace();
} finally {
scanner.close();
}
}
}

虽然本文章使用Java进行演示、但大部分编程语言理论上都支持使用\r进行光标移动操作🤔

运行演示:

demo

0x04 总结

相比于其他美化控制台程序的方法来说,使用\r是最简单直接的一种。
虽然只能实现单行文本的刷新,但已经能够满足大部分轻量程序的要求。
而如果想要进一步美化CLI程序(颜色输出、全屏渲染控制、接收键盘控制等)
则需要使用更高级的一些方法,例如ANSIJLine库(Java),之后可能
会写一些教程qw

其实当程序使用上述技术后,就从CLI(Command Line Interface)
程序进化为TUI(Text User Interface)程序了😋

0%