前言
本文作为《守护进程接收终端输入的一种变通性方法》的补充版,主要讨论不使用第三方库时,如何支持字符终端命令行的退格和历史记录。文中涉及的代码运行环境如下:
一 退格键
术语“退格”(BS,BackSpace)本意指删除光标左侧的一个字符。最初的打字机中,退格键将机架(carriage)回退一个位置;而在现代计算机系统中,退格键将显示器光标左移一个位置,并删除该处的字符,然后将该处之后的文字左移一个位置。
删除(DEL,Delete)键可追溯到计算机使用打孔磁带的年代。当时,纠正一个字符打孔错误的唯一办法是在磁带上将额外的比特位置打孔。所有比特位被打孔的字符被视为已删除,其对应ASCII控制码DEL(0x7f)。要删除最后输入的字符,必须先点击BS键回移一个位置,再点击DEL键删除该字符。在较新的计算机中,对BS键或DEL键的一次点击将同时完成回移和删除。
在现代计算机系统中,退格键常被映射到DEL符(0x7f),但保留退格键删除光标之前字符的功能。
在计算机终端上点击退格键(如[<—])或Ctrl+H组合键会产生ASCII控制码BS。若终端未将退格键映射为回移光标并删除字符的功能,则点击退格键时显示^H符号。即使终端将退格键解释为删除前置字符,接收文本的系统也不一定如此。这样,屏幕将显示未删除的文本,并包含可见的删除码。如:
You want to delete^H^H^H |
在Linux系统中,通过‘man ascii‘命令可查看ASCII码字符集。如:
字符 |
8进制 |
10进制 |
16进制 |
BS ‘\b‘ (backspace) |
010 |
8 |
0x08 |
DEL |
177 |
127 |
0x7f |
使用SecureCRT终端时,可重新设置码值映射关系。例如,若想使退格键实现回移和删除(而非打印^H),可在Options->Session Options->Terminal->Emulation->Mapped Keys页的Other mappings选框中,勾选“退格发送删除”,即:
若不做该修改,也可使用Ctrl+退格组合键达到相同的效果。
注意,SecureCRT在VT100模式下时,删除键和退格键均删除光标前面的字符。若要删除光标处(向后删除)的字符,需在Emulation页的Terminal类型中选择Linux或Xterm模式。
其他终端下删除键和退格键的适配可参考《Consistent BackSpace and Delete Configuration》。
二 命令历史
命令历史模拟Linux Shell风格,即保存已输入的命令行,用户通过上下键调取前条或后条命令(不同于Shell的history命令)。
上移键和下移键对应由转义符开头的三个ASCII码。上移键对应{0x1b, 0x5b, 0x41},其打印形式为^[[A;下移键对应{0x1b, 0x5b, 0x42},其打印形式为^[[B。
本实现首先需要增加一组宏定义和数据结构,如下:
1 /* 方向控制键字符编码 */ 2 //上移键:{0x1b, 0x5b, 0x41} 3 //下移键:{0x1b, 0x5b, 0x42} 4 //右移键:{0x1b, 0x5b, 0x43} 5 //左移键:{0x1b, 0x5b, 0x44} 6 #define LINUX_KEY1 0x1b 7 #define LINUX_KEY2 0x5b 8 #define LINUX_KEY_UP 0x41 9 #define LINUX_KEY_DN 0x42 10 #define LINUX_KEY_LT 0x43 11 #define LINUX_KEY_RT 0x44 12 #define LINUX_KEY_BS 0x08 13 14 /* 命令历史环形列表结构 */ 15 typedef struct{ 16 INT32U dwTail; //命令列表的尾端,即最后一条命令的下个位置 17 INT32U dwSelCmdIdx; //上下键翻转时当前选择的命令索引 18 CHAR szCmd[CMD_MAX_NUM][CMD_MAX_SIZE]; //历史命令 19 }T_CMD_HIST; 20 21 //前条或后条命令索引 22 #define PREV_CMD_IDX(curIdx) ((curIdx+CMD_MAX_NUM-1)%CMD_MAX_NUM) 23 #define NEXT_CMD_IDX(curIdx) ((curIdx+1)%CMD_MAX_NUM)
命令历史以环形列表存储,即表满时最新存入的命令会覆盖最老的命令。
然后定义全局的命令历史列表:
1 /* 命令历史列表 */ 2 static T_CMD_HIST gCmdHist = {0, 0, {{0}}};
其判空函数为:
1 /* 判断命令历史列表是否为空 */ 2 static BOOL IsCmdsEmpty(T_CMD_HIST *ptCmds) 3 { 4 return ‘\0‘ == gCmdHist.szCmd[0][0]; 5 }
即判断缓存列表中首条命令首个字节是否为0,以提高判断效率(初次存入后永不为空)。
基于命令历史列表,改造GetChars()函数:
1 static INT32S GetChars(CHAR *pszBuf, INT32U dwBufSize) 2 { 3 BOOL bIsUpDn = FALSE; 4 INT32U dwIdx = 0; 5 INT32S dwChar = ‘\0‘; 6 while(dwIdx<dwBufSize && (dwChar=getchar())!=EOF && dwChar!=‘\n‘) 7 { 8 switch(dwChar) 9 { 10 case LINUX_KEY_BS: 11 if(dwIdx>0) 12 dwIdx--; 13 break; 14 15 case LINUX_KEY1: 16 if((dwChar=getchar()) == EOF || dwChar != LINUX_KEY2) 17 break; 18 if((dwChar=getchar()) == EOF) 19 break; 20 21 switch(dwChar) 22 { 23 case LINUX_KEY_UP: 24 if(!IsCmdsEmpty(&gCmdHist)) 25 { 26 gCmdHist.dwSelCmdIdx = PREV_CMD_IDX(gCmdHist.dwSelCmdIdx); 27 dwIdx = MIN(strlen(gCmdHist.szCmd[gCmdHist.dwSelCmdIdx]), dwBufSize-1); 28 strncpy(pszBuf, gCmdHist.szCmd[gCmdHist.dwSelCmdIdx], dwIdx); 29 pszBuf[dwIdx] = ‘\0‘; 30 bIsUpDn = TRUE; 31 } 32 break; 33 34 case LINUX_KEY_DN: 35 if(!IsCmdsEmpty(&gCmdHist)) 36 { 37 gCmdHist.dwSelCmdIdx = NEXT_CMD_IDX(gCmdHist.dwSelCmdIdx); 38 dwIdx = MIN(strlen(gCmdHist.szCmd[gCmdHist.dwSelCmdIdx]), dwBufSize-1); 39 strncpy(pszBuf, gCmdHist.szCmd[gCmdHist.dwSelCmdIdx], dwIdx); 40 pszBuf[dwIdx] = ‘\0‘; 41 bIsUpDn = TRUE; 42 } 43 break; 44 45 default: 46 break; 47 } 48 break; 49 50 default: 51 pszBuf[dwIdx++] = dwChar; 52 //break; 53 } 54 } 55 56 if(dwIdx==0 && dwChar==EOF) 57 return -1; 58 59 if(dwIdx==dwBufSize && dwChar!=EOF && dwChar!=‘\n‘) 60 return -2; 61 62 if(dwChar == ‘\n‘ && bIsUpDn == TRUE) 63 {//若已点击上下键,则输出历史命令,然后等待用户继续输入(该输入只能是普通字符或回车) 64 printf("%s", pszBuf); 65 fflush(stdout); 66 INT32S dwRet = GetChars(&pszBuf[dwIdx], dwBufSize-dwIdx); 67 if(dwRet != -1 && dwRet != -2) 68 dwIdx += dwRet; 69 pszBuf[dwIdx] = ‘\0‘; //剔除末尾的换行符 70 return (++dwIdx); 71 } 72 73 pszBuf[dwIdx] = ‘\0‘; //剔除末尾的换行符 74 return (++dwIdx); 75 }
改造后的GetChars()函数中,退格键会删除(但可能打印^H)字符。上下键可提取历史命令,但功能非常有限,且与Linux Shell略有不同。
这些不足(包括退格打印^H)主要由getchar()函数的回显和缓冲机制导致。该函数会等待用户按键,将其输入的字符回显到屏幕并存入标准I/O缓冲区。当用户键入回车后,getchar()从标准输入流中每次读入一个字符返回给用户。若用户在按回车之前输入多个字符,其他字符会保留在缓存区中,等待后续的getchar()调用读取。而后续调用将直接读取缓冲区中的字符,直至读完后才等待用户按键。由于getchar()函数会回显输入,故不可能在GetChars()函数内消除退格键和上下键的打印。
最后,补充AddHistory()函数的实现:
1 VOID AddHistory(CHAR *pszCmd) 2 { 3 INT32S dwCmdLen = strlen(pszCmd); 4 if(dwCmdLen == 0 || dwCmdLen >= CMD_MAX_SIZE) 5 return; //长度非法 6 7 INT32U dwCmdIdx; 8 for(dwCmdIdx = 0; dwCmdIdx < CMD_MAX_NUM; dwCmdIdx++) 9 { 10 if(!strcmp(pszCmd, gCmdHist.szCmd[dwCmdIdx])) 11 return; //命令相同 12 } 13 14 strcpy(gCmdHist.szCmd[gCmdHist.dwTail], pszCmd); 15 gCmdHist.dwTail = NEXT_CMD_IDX(gCmdHist.dwTail); 16 gCmdHist.dwSelCmdIdx = gCmdHist.dwTail; 17 }
基于上述实现的退格和命令历史效果如下(未重新映射退格键):
1 >>hhh 2 pszCmdLine = hhh! 3 >>iii 4 pszCmdLine = iii! 5 >>jjj 6 pszCmdLine = jjj! 7 >>kkkk^H //退格键+回车 8 pszCmdLine = kkk! 9 >>^[[A //上移键+回车 10 kkklll //输出kkk,输入lll 11 pszCmdLine = kkklll! 12 >>^[[B //下移键+回车 13 iiimmm //输出iii,输入mmm 14 pszCmdLine = iiimmm! 15 >>
可见,按上下键时将回显其打印字符,必须键入回车后才在下行调出历史命令。
三 其他
若能关闭回显,则可部分规避上节getchar()函数所导致的问题。
本节将实现getchar的非回显版本(类似getch)。该版本直接从键盘获取键值,而不等待用户按回车。即只要用户输入字符,函数就立刻返回用户输入的ASCII码。假定该函数名为Getch:
1 #include <termios.h> 2 #include <unistd.h> 3 int Getch(void) 4 { 5 struct termios tOldTerm, tNewTerm; 6 tcgetattr(STDIN_FILENO, &tOldTerm); 7 tNewTerm = tOldTerm; 8 tNewTerm.c_lflag &= ~(ICANON | ECHO); 9 tcsetattr(STDIN_FILENO, TCSANOW, &tNewTerm); 10 int dwChar = getchar(); 11 tcsetattr(STDIN_FILENO, TCSANOW, &tOldTerm); 12 return dwChar; 13 }
测试函数如下:
1 int main(void) 2 { 3 char cChar; 4 while((cChar=Getch()) != ‘\n‘) 5 { 6 if(cChar == ‘\b‘) 7 printf("\b \b"); 8 else 9 printf("%c", cChar); 10 } 11 12 printf("\nwxy\bz\n"); 13 printf("wxy\b\n"); 14 printf("wxy\b \b\n"); 15 printf("wxy"); putchar(‘\b‘); putchar(‘ ‘); putchar(‘\b‘); putchar(‘\n‘); 16 printf("wxy"); putchar(0x7f); putchar(‘ ‘); putchar(0x7f); putchar(‘\n‘); 17 return 0; 18 }
未重新映射退格键时,执行结果如下所示:
1 hello //输入helloo+退格 2 wxz 3 wxy 4 wx 5 wx 6 wxy
后五行输出用于展示退格键的实际效果。可见,退格仅使光标位置回退一格,并不删除该处字符,必须使用"\b \b"回退并覆盖字符。此外,退格时应使用‘\b‘,而不要直接用0x7f。
最后,本文试图绕开readline、termcap和ncurses等库(Getch实现无需这些库的支持),但若有可能,借助这些库可更好地支持字符终端的退格和命令历史等功能。若考虑许可证问题,可选用libedit库(非GPL)代替readline库。