守护进程接收终端输入的一种变通性方法(二)

前言

本文作为《守护进程接收终端输入的一种变通性方法》的补充版,主要讨论不使用第三方库时,如何支持字符终端命令行的退格和历史记录。文中涉及的代码运行环境如下:

一  退格键

术语“退格”(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库。

时间: 2024-10-05 20:20:32

守护进程接收终端输入的一种变通性方法(二)的相关文章

守护进程接收终端输入的一种变通性方法

说明 本文主要介绍某嵌入式产品中DSL用户态驱动模块作为守护进程时,如何接收终端输入的变通性方法. 出于信息安全考虑,文中涉及系统方面的接口函数未给出实现细节,但不影响表述的完整性. 相关性文章参见<GNU Readline库函数的应用示例>. 一  背景知识 init进程(如Busybox init)是嵌入式系统内核自举时启动的第一个也是惟一的用户进程.init进程是后续所有其他进程的父进程(其进程ID为1),在系统运行期间以守护进程的形式一直存在.它主要负责启动各运行层次特定的系统服务(如

while持续输入的几种常用使用方法

while(scanf("%d,&n")!=EOF) 如果n被成功读入,则返回值为1, 如果n未被成功读入,则返回值为0, 如果遇到错误或遇到end of file,返回值为EOF. 那么什么时候返回EOF呢,简单来说在Windows下按住Ctrl+Z,在Mac下按住Ctrl+D,作为结束流的信号. 当然你有一些特殊的要求,比如我想让n=0时结束. while(scanf("%d",&n)!=EOF) { if(n==0) break; } 当然我将

从进程组、会话、终端的概念深入理解守护进程

一.写在前面 「守护进程」是 Linux 的一种长期运行的后台服务进程,也有人称它为「精灵进程」.我们常见的 httpd.named.sshd 等服务都是以守护进程 Daemon 方式运行的,通常服务名称以字母d结尾,也就是 Daemon 第一个字母.与普通进程相比它大概有如下特点: 无需控制终端(不需要与用户交互) 在后台运行 生命周期比较长,一般是随系统启动和关闭 二.守护进程必要性 为什么要设置为守护进程,普通进程不可以吗? 当我们在命令行提示符后输入类似./helloworld程序时,在

守护进程 python

守护进程(类似Windows的服务):通常被定义为一个后台进程,而且它不属于任何一个终端会话(terminal session).许多系统服务由守护程序实施:如网络服务,打印等. 下面介绍下守护进程的基本编码过程以及python的对应实现: 1. 调用 fork 并使父进程退出(exit)( 若父进程退出,子进程尚未结束,则子进程会被init进程领养,也就是说init进程将成为该子进程的父进程).这一步骤的目的在于.首先,如果守护进程是通过一个简单的shell命令建立的,那么在父进程终止的时候s

ASP.ENT Core Linux 下 为 donet创建守护进程(转载)

原文地址:http://www.cnblogs.com/savorboard/p/dotnetcore-supervisor.html 前言 在上篇文章中介绍了如何在 Docker 容器中部署我们的 asp.net core 应用程序,本篇主要是怎么样为我们在 Linux 或者 macOs 中部署的 dotnet 程序创建一个守护进程,来保证我们的程序在异常或者是电脑重启的时候仍然能够正常访问. 如果你以后用准备使用 asp.net core来开发项目的话,程序并且部署到 Linux 上的话,那

《Unix环境高级编程》读书笔记 第13章-守护进程

1. 引言 守护进程是生存期长的一种进程.它们常常在系统引导装入时启动,仅在系统关闭时才终止.它们没有控制终端,在后台运行. 本章说明守护进程结构.如何编写守护进程程序.守护进程如何报告出错情况. 2. 守护进程的特征 基于BSD的系统下执行:ps -axj -a 显示由其他用户所拥有的进程的状态:-x 显示没有控制终端的进程状态:-j 显示与作业有关的信息 基于System V的系统下执行:ps -efj Linux下执行以上两个命令输出一致 常见的守护进程: kswapd,内存换页守护进程.

守护进程详细总结

1. 守护进程的概念: 守护进程(Daemon)是一种运行在后台的一种特殊的进程,它独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件.由于在Linux中,每个系统与用户进行交流的界面成为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端被称为这些进程的控制终端,当控制终端被关闭的时候,相应的进程都会自动关闭.但是守护进程却能突破这种限制,它脱离于终端并且在后台运行,并且它脱离终端的目的是为了避免进程在运行的过程中的信息在任何终端中显示并且进程也不会被任何终端所产生的终端

守护线程、守护进程

1. 几点认识: java中有两类线程:user thread(用户线程),daemon thread(守护线程) 守护线程为其他线程的运行提供服务,例如GC线程(垃圾回收线程),内存管理线程. 虚拟机判断程序执行结束的标准时不考虑守护线程:如果user thread全部撤离,daemon thread因为无服务对象,所以虚拟机也就退出了. public final void setDaemon(boolean on) :用户自行设定守护线程 是JVM模拟了操作系统中的“守护进程”而定义出的一种

守护进程详解及创建,daemon()使用

一,守护进程概述 Linux Daemon(守护进程)是运行在后台的一种特殊进程.它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件.它不需要用户输入就能运行而 且提供某种服务,不是对整个系统就是对某个用户程序提供服务.Linux系统的大多数服务器就是通过守护进程实现的.常见的守护进程包括系统日志进程 syslogd. web服务器httpd.邮件服务器sendmail和数据库服务器mysqld等. 守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行.守护进