“命令终端”的实现2-字符读取及按键控制

在上篇文章中已经完成了必要的前期准备:各种与“终端”相关的接口已经完成。本文主要内容是接收“终端”的字符并存储在缓冲区中,为下一步作好准备。同时也涉及到“终端”按键的处理,以弥补约2年前定的一篇文章的不足。

一、测试效果

先看一下测试函数,如下:

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
int readline_test(void)
{
static char lastcommand[CB_SIZE] = {0};
int len;

while (1)
{
len = readline(PROMPT, lastcommand);
if (len > 0)
{
if (len > CB_SIZE)
{
myprintf("command line too large.\n");
break;
}
}
else if (len == 0)
{
// do nothing
}
if (len == -1)
{
myputs("\n");
}
else
{
myprintf("YOUR INPUT: %s\n", lastcommand);
}
}
return 0;
}

效果如下:

1
2
3
4
5
6
7
NotAShell> hello world!
YOUR INPUT: hello world!
NotAShell> help
YOUR INPUT: help
NotAShell> clear
YOUR INPUT: clear
NotAShell>

二、实现

下面逐一介绍其实现。

2.1 函数声明及宏定义相关:

1
2
3
4
5
6
7
8
9
#define PROMPT          "NotAShell> "
#define CB_SIZE 256 /* console buffer size */
#define MAX_CMDBUF_SIZE CB_SIZE

/* history command */
#define HIST_MAX 10
#define HIST_SIZE MAX_CMDBUF_SIZE

int readline (const char *const prompt, char* line_buf);

这里定义了“终端”提示符,缓冲区大小,保存的历史命令最大数,等等。

2.2 ASCII码定义及光标控制相关:

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
#define CTL_CH(c)           ((c) - 'a' + 1)

#define CTL_BACKSPACE ('\b')
#define DEL ((char)255)
#define DEL7 ((char)127)
#define BACKSPACE ((char)8)
#define CREAD_HIST_CHAR ('!')

#define putnstr(str,n) do { \
getcmd_printf ("%.*s", (int)n, str);\
} while (0)

// 将光标移到命令行首
#define BEGINNING_OF_LINE() { \
while (num) { \
getcmd_putch(CTL_BACKSPACE); \
num--; \
} \
}

// 删除当前光标到行尾所有字符
#define ERASE_TO_EOL() { \
if (num < eol_num) { \
getcmd_printf("%*s", (int)(eol_num - num), ""); \
do { \
getcmd_putch(CTL_BACKSPACE); \
} while (--eol_num > num); \
} \
}

// 将光标移到命令行尾
#define REFRESH_TO_EOL() { \
if (num < eol_num) { \
wlen = eol_num - num; \
putnstr(buf + num, wlen); \
num = eol_num; \
} \
}

上面最重要的一个宏定义是CTL_CH,它用于判断用户输入的按键组合,代码中处理的按键有:

1
2
3
4
5
6
7
8
9
10
11
12
0)、上下左右方向键
1)、c-a: 移到命令行首
2)、c-e: 移到命令行尾
3)、c-f: 前移一字符
4)、c-b: 后移一字符
5)、c-x/c-u: 删除整行
6)、c-d: 删除光标当前字符
7)、c-k: 删除当前光标至行尾字符
8)、c-p: 前一条历史命令
9)、c-n: 后一条历史命令
10)、HOME/END: 移至行首/行尾
11)、Delete:Windows-删除当前光标字符 Linux-删除光标前字符

很多快捷键与GNU Readline库相同。实际中,Linux有很多工具、软件都使用了Readline库,像bash shell,因此,你可以在bash shell使用c-a将光标移到行首,c-e移到行尾——这一点在一个长命令中尤其有用。更多关于Readline库的快捷键,请在搜索引擎中使用关键字“Readline shortcuts”进行搜索。
上面提到的c-a等就是组合按键,c表示Ctrl键,c-a表示同时按Ctrl键和a键,从ASCII码表(请搜索多一些ASCII码表,最好用google,或者搜索笔者写的文章《u-boot移植随笔:u-boot shell与ASCII码)中可以知道,c-a的ASCII码是1,即:CTL_CH(‘a’) = ‘a’ - ‘a’ + 1。

3.3 处理普通字符

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
static void cread_add_char(char ichar, int insert, unsigned long *num,
unsigned long *eol_num, char *buf, unsigned long len)
{
unsigned long wlen;

/* room ??? */
if (insert || *num == *eol_num)
{
if (*eol_num > len - 1)
{
getcmd_cbeep();
return;
}
(*eol_num)++;
}

if (insert)
{
wlen = *eol_num - *num;
if (wlen > 1)
{
memmove(&buf[*num+1], &buf[*num], wlen-1);
}

buf[*num] = ichar;
putnstr(buf + *num, wlen);
(*num)++;
while (--wlen)
{
getcmd_putch(CTL_BACKSPACE);
}
}
else
{
/* echo the character */
wlen = 1;
buf[*num] = ichar;
putnstr(buf + *num, wlen);
(*num)++;
}
}

static void cread_add_str(char *str, int strsize, int insert, unsigned long *num,
unsigned long *eol_num, char *buf, unsigned long len)
{
while (strsize--)
{
cread_add_char(*str, insert, num, eol_num, buf, len);
str++;
}
}

4.4 关键处理函数

这部分代码比较长,所以先说关键代码。
处理按键,包括方向键及HOME、END等,Windows及Linux按键如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Windows: 
up: 0xe0 'H'
down: 0xe0 'P'
right: 0xe0 'M'
left: 0xe0 'K'

HOME: 0xe0 'G'
END: 0xe0 'O'(大写字母O)
Delete: 0xe0 'S'
Linux:
up: 0x1b 0x5b 0x41 ^[[A
down: 0x1b 0x5b 0x42 ^[[B
right: 0x1b 0x5b 0x43 ^[[C
left: 0x1b 0x5b 0x44 ^[[D

HOME: ?
END: ?
Delete: 0x7f

处理代码如下:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
static int cread_line(const char *const prompt, char *buf, unsigned int *len)
{
unsigned long num = 0;
unsigned long eol_num = 0;
unsigned long wlen;
unsigned char ichar;
int insert = 1;
int esc_len = 0;
char esc_save[8];
int init_len = (int)strlen(buf);
int cur_num = 0;

if (init_len)
cread_add_str(buf, init_len, 1, &num, &eol_num, buf, *len);

while (1)
{
ichar = getcmd_getch();

if ((ichar == '\n') || (ichar == '\r'))
{
getcmd_putch('\n');
break;
}

/*
* handle Windows arrow key, etc.
* note: arrow keys have two char, the first is 0xe0
*/
if (ichar == 0xe0)
{
ichar = getcmd_getch();
switch(ichar)
{
case 'H': /* up arrow */
ichar = CTL_CH('p');
break;
case 'P': /* down arrow */
ichar = CTL_CH('n');
break;
case 'K': /* left arrow */
ichar = CTL_CH('b');
break;
case 'M': /* right arrow */
ichar = CTL_CH('f');
break;
case 'G': /* Home */
ichar = CTL_CH('a');
break;
case 'O': /* End */
ichar = CTL_CH('e');
break;
case 'S': /* Delete */
ichar = CTL_CH('d');
break;
default:
break;
}
}

/*
* handle standard linux xterm esc sequences for arrow key, etc.
*/
if (esc_len != 0)
{
if (esc_len == 1)
{
if (ichar == '[')
{
esc_save[esc_len] = ichar;
esc_len = 2;
}
else
{
cread_add_str(esc_save, esc_len, insert,
&num, &eol_num, buf, *len);
esc_len = 0;
}
continue;
}

switch (ichar)
{
case 'D': /* <- key */
ichar = CTL_CH('b');
esc_len = 0;
break;
case 'C': /* -> key */
ichar = CTL_CH('f');
esc_len = 0;
break; /* pass off to ^F handler */
case 'H': /* Home key */
ichar = CTL_CH('a');
esc_len = 0;
break; /* pass off to ^A handler */
case 'A': /* up arrow */
ichar = CTL_CH('p');
esc_len = 0;
break; /* pass off to ^P handler */
case 'B': /* down arrow */
ichar = CTL_CH('n');
esc_len = 0;
break; /* pass off to ^N handler */
default:
esc_save[esc_len++] = ichar;
cread_add_str(esc_save, esc_len, insert,
&num, &eol_num, buf, *len);
esc_len = 0;
continue;
}
}
/* End of linux arrow key */

switch (ichar)
{
/* linux esc sequences */
case 0x1b:
if (esc_len == 0)
{
esc_save[esc_len] = ichar;
esc_len = 1;
}
else
{
getcmd_puts("impossible condition #876\n");
esc_len = 0;
}
break;

case CTL_CH('a'):
BEGINNING_OF_LINE();
break;
case CTL_CH('c'): /* ^C - break */
*buf = '\0'; /* discard input */
return (-1);
case CTL_CH('f'):
if (num < eol_num)
{
getcmd_putch(buf[num]);
num++;
}
break;
case CTL_CH('b'):
if (num)
{
getcmd_putch(CTL_BACKSPACE);
num--;
}
break;
case CTL_CH('d'):
if (num < eol_num)
{
wlen = eol_num - num - 1;
if (wlen)
{
memmove(&buf[num], &buf[num+1], wlen);
putnstr(buf + num, wlen);
}

getcmd_putch(' ');
do {
getcmd_putch(CTL_BACKSPACE);
} while (wlen--);
eol_num--;
}
break;
case CTL_CH('k'):
ERASE_TO_EOL();
break;
case CTL_CH('e'):
REFRESH_TO_EOL();
break;
case CTL_CH('o'):
insert = !insert;
break;
case CTL_CH('x'):
case CTL_CH('u'):
BEGINNING_OF_LINE();
ERASE_TO_EOL();
break;
case DEL:
case DEL7:
case BACKSPACE:
if (num)
{
wlen = eol_num - num;
num--;
memmove(&buf[num], &buf[num+1], wlen);
getcmd_putch(CTL_BACKSPACE);
putnstr(buf + num, wlen);
getcmd_putch(' ');
do {
getcmd_putch(CTL_BACKSPACE);
} while (wlen--);
eol_num--;
}
break;
case CTL_CH('p'):
case CTL_CH('n'):
{
char * hline;

esc_len = 0;

if (ichar == CTL_CH('p'))
hline = hist_prev();
else
hline = hist_next();

if (!hline)
{
getcmd_cbeep();
continue;
}

/* nuke the current line */
/* first, go home */
BEGINNING_OF_LINE();

/* erase to end of line */
ERASE_TO_EOL();

/* copy new line into place and display */
strcpy(buf, hline);
eol_num = (unsigned long)strlen(buf);
REFRESH_TO_EOL();
continue;
}

default:
cread_add_char(ichar, insert, &num, &eol_num, buf, *len);
break;
}
}

*len = eol_num;
buf[eol_num] = '\0'; /* lose the newline */

if (buf[0] && buf[0] != CREAD_HIST_CHAR)
cread_add_to_hist(buf);
hist_cur = hist_add_idx;

return 0;
}

完整处理代码请参考工程代码,或者参考uboot代码。
现在已经可以读取到了用户输入的字符串了,下一步,也是最后一步,就是执行这些命令(当然,不是支持的命令会提示出错信息)。

李迟 2020.9.30