Skip to content

Commit d226fbe

Browse files
committed
add: 控制字符解析
1 parent d71cbd8 commit d226fbe

File tree

2 files changed

+247
-0
lines changed

2 files changed

+247
-0
lines changed
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace LLCOM.Models;
5+
6+
public enum TerminalCommand
7+
{
8+
None, //没匹配上任何命令
9+
10+
Bs, //退格 0x08
11+
Ht, //水平制表符 0x09
12+
Lf, //换行 0x0A
13+
Cr, //回车 0x0D
14+
15+
Hide, //隐藏光标 \x1b[?25l
16+
Show, //显示光标 \x1b[?25h
17+
18+
ClearLineEnd, //清除光标到行尾 \x1b[K
19+
ClearLineStart, //清除光标到行首 \x1b[1K
20+
ClearLine, //清除当前行 \x1b[2K
21+
22+
ClearScreenEnd, //清除光标到屏幕末尾 \x1b[J
23+
ClearScreenStart, //清除光标到屏幕开头 \x1b[1J
24+
ClearScreen, //清除屏幕 \x1b[2J
25+
26+
MoveCursorUp, //光标上移 \x1b[{n}A
27+
MoveCursorDown, //光标下移 \x1b[{n}B
28+
MoveCursorRight, //光标右移 \x1b[{n}C
29+
MoveCursorLeft, //光标左移 \x1b[{n}D
30+
ResetCursor, //光标移动到左上角 \x1b[H
31+
MoveCursorTo, //光标移动到指定位置 \x1b[{n};{m}H
32+
SaveCursor, //保存光标位置 \x1b[s
33+
RestoreCursor, //恢复光标位置 \x1b[u
34+
35+
ResetStyle, //重置样式 \x1b[m
36+
Bold, //加粗 \x1b[1m
37+
Underline, //下划线 \x1b[4m
38+
Reverse, //反转颜色 \x1b[7m
39+
ForegroundColor, //前景色 \x1b[3{n}m
40+
BackgroundColor, //背景色 \x1b[4{n}m
41+
}
42+
43+
public class TerminalCommandCheck
44+
{
45+
/// <summary>
46+
/// 分析给定的字符切片,判断是否为终端命令
47+
/// </summary>
48+
/// <param name="slice">切片,函数将会判断开头</param>
49+
/// <returns></returns>
50+
public static ((TerminalCommand, (int, int)), int) Do(ReadOnlySpan<char> slice)
51+
{
52+
if(slice.Length == 0)
53+
return ((TerminalCommand.None,(0,0)), 0);
54+
//先判断下是否为单字符命令
55+
var singleCmd = slice[0] switch
56+
{
57+
'\b' => TerminalCommand.Bs, //退格
58+
'\t' => TerminalCommand.Ht, //水平制表符
59+
'\n' => TerminalCommand.Lf, //换行
60+
'\r' => TerminalCommand.Cr, //回车
61+
_ => TerminalCommand.None
62+
};
63+
if (singleCmd != TerminalCommand.None)
64+
{
65+
return ((singleCmd, (0,0)), 1);
66+
}
67+
//判断是否为转义字符
68+
if(slice[0] != '\x1b' || slice[1] != '[' || slice.Length < 3)
69+
{
70+
return ((TerminalCommand.None, (0,0)), 0);
71+
}
72+
//看看是否为显示/隐藏光标
73+
if (slice.Length >= 6 && slice[2] == '?' && slice[3] == '2' && slice[4] == '5')
74+
{
75+
if (slice[5] == 'l')
76+
return ((TerminalCommand.Hide, (25,0)), 6);
77+
if (slice[5] == 'h')
78+
return ((TerminalCommand.Show, (25,0)), 6);
79+
}
80+
//其他命令就按正常格式分析
81+
//\x1b[{数字}{字母} 数字可能是2个字符也可能不存在
82+
int code = 0;
83+
char cmd = '\0';
84+
int i = 2; //从第三个字符开始分析,最多分析到第四个字符
85+
while (i < slice.Length && i < 5 && char.IsDigit(slice[i]))
86+
{
87+
code = code * 10 + (slice[i] - '0'); //将数字字符转换为数字
88+
i++;
89+
}
90+
if (i < slice.Length)
91+
{
92+
cmd = slice[i];
93+
i++;
94+
}
95+
if(cmd == '\0')
96+
{
97+
return ((TerminalCommand.None,(0,0)), 0); //没有命令
98+
}
99+
//根据命令字符返回对应的命令
100+
switch (cmd)
101+
{
102+
case 'K': //清除行
103+
var kr = code switch
104+
{
105+
2 => TerminalCommand.ClearLineEnd, //清除光标到行尾
106+
3 => TerminalCommand.ClearLineStart, //清除光标到行首
107+
4 => TerminalCommand.ClearLine, //清除当前行
108+
_ => TerminalCommand.None //不匹配
109+
};
110+
if(kr != TerminalCommand.None)
111+
return ((kr, (code,0)), i);
112+
break;
113+
case 'J': //清除屏幕
114+
var jr = code switch
115+
{
116+
0 => TerminalCommand.ClearScreenEnd, //清除光标到屏幕末尾
117+
1 => TerminalCommand.ClearScreenStart, //清除光标到屏幕开头
118+
2 => TerminalCommand.ClearScreen, //清除屏幕
119+
_ => TerminalCommand.None //不匹配
120+
};
121+
if(jr != TerminalCommand.None)
122+
return ((jr, (code,0)), i);
123+
break;
124+
case 'A': //光标上移
125+
if (code > 0)
126+
return ((TerminalCommand.MoveCursorUp, (code,0)), i);
127+
break;
128+
case 'B': //光标下移
129+
if (code > 0)
130+
return ((TerminalCommand.MoveCursorDown, (code,0)), i);
131+
break;
132+
case 'C': //光标右移
133+
if (code > 0)
134+
return ((TerminalCommand.MoveCursorRight, (code,0)), i);
135+
break;
136+
case 'D': //光标左移
137+
if (code > 0)
138+
return ((TerminalCommand.MoveCursorLeft, (code,0)), i);
139+
break;
140+
case 'H': //光标移动到指定位置
141+
return ((TerminalCommand.ResetCursor, (code,0)), i);
142+
case 's': //保存光标位置
143+
return ((TerminalCommand.SaveCursor, (0,0)), i);
144+
case 'u': //恢复光标位置
145+
return ((TerminalCommand.RestoreCursor, (0,0)), i);
146+
case 'm': //样式
147+
var mr = code switch
148+
{
149+
0 => TerminalCommand.ResetStyle, //重置样式
150+
1 => TerminalCommand.Bold, //加粗
151+
4 => TerminalCommand.Underline, //下划线
152+
7 => TerminalCommand.Reverse, //反转颜色
153+
_ when code >= 30 && code <= 37 => TerminalCommand.ForegroundColor, //前景色
154+
_ when code >= 40 && code <= 47 => TerminalCommand.BackgroundColor, //背景色
155+
_ => TerminalCommand.None //不匹配
156+
};
157+
if (mr != TerminalCommand.None)
158+
{
159+
return ((mr, (code,0)), i);
160+
}
161+
break;
162+
}
163+
//检查是不是匹配\x1b[{n};{m}H
164+
if(cmd != ';')
165+
return ((TerminalCommand.None,(0,0)), 0);
166+
//如果是分号,说明可能是光标移动到指定位置
167+
//需要检查后面的数字
168+
int col = code, row = 0;
169+
while (i < slice.Length && char.IsDigit(slice[i]))
170+
{
171+
row = row * 10 + (slice[i] - '0'); //将数字字符转换为数字
172+
i++;
173+
}
174+
if (i < slice.Length && slice[i] == 'H')
175+
{
176+
return ((TerminalCommand.MoveCursorTo, (col, row)), i + 1);
177+
}
178+
return ((TerminalCommand.None,(0,0)), 0);
179+
}
180+
181+
/// <summary>
182+
/// 分析给定的字符数组,判断是否为终端命令
183+
/// </summary>
184+
/// <param name="arr">数组</param>
185+
/// <param name="offset">从哪里开始</param>
186+
/// <param name="length">判断的长度,留空则为剩余全长</param>
187+
/// <returns></returns>
188+
public static ((TerminalCommand, (int,int)),int) Do(char[] arr, int offset, int? length = null)
189+
{
190+
Span<char> slice = arr.AsSpan(offset, length ?? arr.Length - offset);
191+
return Do(slice);
192+
}
193+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
namespace llcomTest;
2+
using LLCOM.Models;
3+
4+
[TestClass]
5+
public class TerminalCommandTest
6+
{
7+
[TestMethod]
8+
public void TestSingle()
9+
{
10+
var testCases = new (char, TerminalCommand)[]
11+
{
12+
('\b', TerminalCommand.Bs),
13+
('\t', TerminalCommand.Ht),
14+
('\n', TerminalCommand.Lf),
15+
('\r', TerminalCommand.Cr)
16+
};
17+
18+
foreach (var (input, expected) in testCases)
19+
{
20+
var result = TerminalCommandCheck.Do([input,'=','=','=','=','=','=']);
21+
Assert.AreEqual(expected, result.Item1.Item1);
22+
}
23+
}
24+
25+
[TestMethod]
26+
public void TestMultiple()
27+
{
28+
var testCases = new (string, TerminalCommand, (int, int), int)[]
29+
{
30+
("\x1b[?25l", TerminalCommand.Hide, (25, 0), 6),
31+
("\x1b[?25h", TerminalCommand.Show, (25, 0), 6),
32+
("\x1b[2J", TerminalCommand.ClearScreen, (2, 0), 4),
33+
("\x1b[H", TerminalCommand.ResetCursor, (0, 0), 3),
34+
("\x1b[10;20H", TerminalCommand.MoveCursorTo, (10, 20), 8),
35+
("\x1b[A", TerminalCommand.None, (0, 0), 0),
36+
("\x1b[0A", TerminalCommand.None, (0, 0), 0),
37+
("\x1b[2A", TerminalCommand.MoveCursorUp, (2, 0), 4),
38+
("\x1b[10A", TerminalCommand.MoveCursorUp, (10, 0), 5),
39+
("\x1b[m", TerminalCommand.ResetStyle, (0, 0), 3),
40+
("\x1b[0m", TerminalCommand.ResetStyle, (0, 0), 4),
41+
("\x1b[7m", TerminalCommand.Reverse, (7, 0), 4),
42+
("\x1b[31m", TerminalCommand.ForegroundColor, (31, 0), 5),
43+
("\x1b[43m", TerminalCommand.BackgroundColor, (43, 0), 5),
44+
};
45+
46+
foreach (var (input, expectedCmd, expectedPos, expectedLength) in testCases)
47+
{
48+
var result = TerminalCommandCheck.Do((input+"==========").AsSpan());
49+
Assert.AreEqual(expectedCmd, result.Item1.Item1);
50+
Assert.AreEqual(expectedPos, result.Item1.Item2);
51+
Assert.AreEqual(expectedLength, result.Item2);
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)