守护进程接收终端输入的一种变通性方法(二)
2014-11-13 08:59
232 查看
前言
本文作为《守护进程接收终端输入的一种变通性方法》的补充版,主要讨论不使用第三方库时,如何支持字符终端命令行的退格和历史记录。文中涉及的代码运行环境如下:一 退格键
术语“退格”(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 |
字符 | 8进制 | 10进制 | 16进制 |
BS '\b' (backspace) | 010 | 8 | 0x08 |
DEL | 177 | 127 | 0x7f |
若不做该修改,也可使用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。
本实现首先需要增加一组宏定义和数据结构,如下:
/* 方向控制键字符编码 */ //上移键:{0x1b, 0x5b, 0x41} //下移键:{0x1b, 0x5b, 0x42} //右移键:{0x1b, 0x5b, 0x43} //左移键:{0x1b, 0x5b, 0x44} #define LINUX_KEY1 0x1b #define LINUX_KEY2 0x5b #define LINUX_KEY_UP 0x41 #define LINUX_KEY_DN 0x42 #define LINUX_KEY_LT 0x43 #define LINUX_KEY_RT 0x44 #define LINUX_KEY_BS 0x08 /* 命令历史环形列表结构 */ typedef struct{ INT32U dwTail; //命令列表的尾端,即最后一条命令的下个位置 INT32U dwSelCmdIdx; //上下键翻转时当前选择的命令索引 CHAR szCmd[CMD_MAX_NUM][CMD_MAX_SIZE]; //历史命令 }T_CMD_HIST; //前条或后条命令索引 #define PREV_CMD_IDX(curIdx) ((curIdx+CMD_MAX_NUM-1)%CMD_MAX_NUM) #define NEXT_CMD_IDX(curIdx) ((curIdx+1)%CMD_MAX_NUM)
命令历史以环形列表存储,即表满时最新存入的命令会覆盖最老的命令。
然后定义全局的命令历史列表:
/* 命令历史列表 */ static T_CMD_HIST gCmdHist = {0, 0, {{0}}};
其判空函数为:
/* 判断命令历史列表是否为空 */ static BOOL IsCmdsEmpty(T_CMD_HIST *ptCmds) { return '\0' == gCmdHist.szCmd[0][0]; }
即判断缓存列表中首条命令首个字节是否为0,以提高判断效率(初次存入后永不为空)。
基于命令历史列表,改造GetChars()函数:
static INT32S GetChars(CHAR *pszBuf, INT32U dwBufSize) { BOOL bIsUpDn = FALSE; INT32U dwIdx = 0; INT32S dwChar = '\0'; while(dwIdx<dwBufSize && (dwChar=getchar())!=EOF && dwChar!='\n') { switch(dwChar) { case LINUX_KEY_BS: if(dwIdx>0) dwIdx--; break; case LINUX_KEY1: if((dwChar=getchar()) == EOF || dwChar != LINUX_KEY2) break; if((dwChar=getchar()) == EOF) break; switch(dwChar) { case LINUX_KEY_UP: if(!IsCmdsEmpty(&gCmdHist)) { gCmdHist.dwSelCmdIdx = PREV_CMD_IDX(gCmdHist.dwSelCmdIdx); dwIdx = MIN(strlen(gCmdHist.szCmd[gCmdHist.dwSelCmdIdx]), dwBufSize-1); strncpy(pszBuf, gCmdHist.szCmd[gCmdHist.dwSelCmdIdx], dwIdx); pszBuf[dwIdx] = '\0'; bIsUpDn = TRUE; } break; case LINUX_KEY_DN: if(!IsCmdsEmpty(&gCmdHist)) { gCmdHist.dwSelCmdIdx = NEXT_CMD_IDX(gCmdHist.dwSelCmdIdx); dwIdx = MIN(strlen(gCmdHist.szCmd[gCmdHist.dwSelCmdIdx]), dwBufSize-1); strncpy(pszBuf, gCmdHist.szCmd[gCmdHist.dwSelCmdIdx], dwIdx); pszBuf[dwIdx] = '\0'; bIsUpDn = TRUE; } break; default: break; } break; default: pszBuf[dwIdx++] = dwChar; //break; } } if(dwIdx==0 && dwChar==EOF) return -1; if(dwIdx==dwBufSize && dwChar!=EOF && dwChar!='\n') return -2; if(dwChar == '\n' && bIsUpDn == TRUE) {//若已点击上下键,则输出历史命令,然后等待用户继续输入(该输入只能是普通字符或回车) printf("%s", pszBuf); fflush(stdout); INT32S dwRet = GetChars(&pszBuf[dwIdx], dwBufSize-dwIdx); if(dwRet != -1 && dwRet != -2) dwIdx += dwRet; pszBuf[dwIdx] = '\0'; //剔除末尾的换行符 return (++dwIdx); } pszBuf[dwIdx] = '\0'; //剔除末尾的换行符 return (++dwIdx); }
改造后的GetChars()函数中,退格键会删除(但可能打印^H)字符。上下键可提取历史命令,但功能非常有限,且与Linux Shell略有不同。
这些不足(包括退格打印^H)主要由getchar()函数的回显和缓冲机制导致。该函数会等待用户按键,将其输入的字符回显到屏幕并存入标准I/O缓冲区。当用户键入回车后,getchar()从标准输入流中每次读入一个字符返回给用户。若用户在按回车之前输入多个字符,其他字符会保留在缓存区中,等待后续的getchar()调用读取。而后续调用将直接读取缓冲区中的字符,直至读完后才等待用户按键。由于getchar()函数会回显输入,故不可能在GetChars()函数内消除退格键和上下键的打印。
最后,补充AddHistory()函数的实现:
VOID AddHistory(CHAR *pszCmd) { INT32S dwCmdLen = strlen(pszCmd); if(dwCmdLen == 0 || dwCmdLen >= CMD_MAX_SIZE) return; //长度非法 INT32U dwCmdIdx; for(dwCmdIdx = 0; dwCmdIdx < CMD_MAX_NUM; dwCmdIdx++) { if(!strcmp(pszCmd, gCmdHist.szCmd[dwCmdIdx])) return; //命令相同 } strcpy(gCmdHist.szCmd[gCmdHist.dwTail], pszCmd); gCmdHist.dwTail = NEXT_CMD_IDX(gCmdHist.dwTail); gCmdHist.dwSelCmdIdx = gCmdHist.dwTail; }
基于上述实现的退格和命令历史效果如下(未重新映射退格键):
>>hhh pszCmdLine = hhh! >>iii pszCmdLine = iii! >>jjj pszCmdLine = jjj! >>kkkk^H //退格键+回车 pszCmdLine = kkk! >>^[[A //上移键+回车 kkklll //输出kkk,输入lll pszCmdLine = kkklll! >>^[[B //下移键+回车 iiimmm //输出iii,输入mmm pszCmdLine = iiimmm! >>
可见,按上下键时将回显其打印字符,必须键入回车后才在下行调出历史命令。
三 其他
若能关闭回显,则可部分规避上节getchar()函数所导致的问题。本节将实现getchar的非回显版本(类似getch)。该版本直接从键盘获取键值,而不等待用户按回车。即只要用户输入字符,函数就立刻返回用户输入的ASCII码。假定该函数名为Getch:
#include <termios.h> #include <unistd.h> int Getch(void) { struct termios tOldTerm, tNewTerm; tcgetattr(STDIN_FILENO, &tOldTerm); tNewTerm = tOldTerm; tNewTerm.c_lflag &= ~(ICANON | ECHO); tcsetattr(STDIN_FILENO, TCSANOW, &tNewTerm); int dwChar = getchar(); tcsetattr(STDIN_FILENO, TCSANOW, &tOldTerm); return dwChar; }
测试函数如下:
int main(void) { char cChar; while((cChar=Getch()) != '\n') { if(cChar == '\b') printf("\b \b"); else printf("%c", cChar); } printf("\nwxy\bz\n"); printf("wxy\b\n"); printf("wxy\b \b\n"); printf("wxy"); putchar('\b'); putchar(' '); putchar('\b'); putchar('\n'); printf("wxy"); putchar(0x7f); putchar(' '); putchar(0x7f); putchar('\n'); return 0; }
未重新映射退格键时,执行结果如下所示:
hello //输入helloo+退格 wxz wxy wx wx wxy
后五行输出用于展示退格键的实际效果。可见,退格仅使光标位置回退一格,并不删除该处字符,必须使用"\b \b"回退并覆盖字符。此外,退格时应使用'\b',而不要直接用0x7f。
最后,本文试图绕开readline、termcap和ncurses等库(Getch实现无需这些库的支持),但若有可能,借助这些库可更好地支持字符终端的退格和命令历史等功能。若考虑许可证问题,可选用libedit库(非GPL)代替readline库。
相关文章推荐
- 守护进程接收终端输入的一种变通性方法
- 一种较方便在Android中添加开机自启动的守护进程的方法
- linux 不回车直接读取一个字符的方法(termios结构的描述了终端的模式,在这段代码中我们改变了它,使得终端能够接收到键盘输入马上返回。)
- 一种以standalone方式启动erlang应用的方法(守护进程)
- 一种较方便在Android中添加开机自启动的守护进程的方法
- Linux 守护进程的编程方法
- 在控制台获得键盘输入的一种方法
- [转]客户端缓存某些重要用户输入数据的一种方法
- URL编码:在地栏直接输入参数带汉字的URL后,服务器端接收后出现乱码的解决方法
- Linux 守护进程的编程方法[转载]
- 将动态连接库注入到其他进程中的一种新方法
- 将动态连接库注入到其他进程中的一种新方法
- Linux 守护进程的编程方法
- 实现textBox只能输入数字的一种方法!
- Linux 守护进程的编程方法
- 一种注入进程,获得完全操控的方法之一[先贴过来,等下研究一下,看起来不错]
- 将动态连接库注入到其他进程中的一种新方法
- [转]如何禁止用户通过任务管理器终止进程的一种方法(Win2000/xp)
- [转]Linux 守护进程的编程方法
- [转载,并且格式化页面]Linux守护进程的编程方法