终端调测命令易用性的改进
背景
某平台向上层应用模块提供调测命令支持,其接口结构定义如下:
1 #define MAX_CMDMSG_LEN 32 2 #define MAX_ARG_NUM 7 3 #define MAX_USERARG_NUM (MAX_ARG_NUM - 1) 4 5 typedef struct{ 6 INT32U arg_counts; /* 二级进程调试命令带的参数个数 */ 7 CHAR arg[MAX_USERARG_NUM][MAX_CMDMSG_LEN]; /* 调试命令的参数 */ 8 }CMD_USER_ARG; 9 10 /* 用户处理命令函数*/ 11 typedef INT32S (*CmdUserProcFun)(CMD_USER_ARG *pCmdUserArg); 12 13 /* 应用模块处理命令数据结构 */ 14 typedef struct { 15 INT32U dwArgCounts; /* 该命令实际带的参数 */ 16 INT32U dwCheckArgCounts; /* 统一比较的参数 */ 17 CHAR *CmdStr[MAX_USERARG_NUM]; 18 CmdUserProcFun procfun; 19 }CMD_USER_PROC;
某应用模块内部定义需要的命令列表,如下:
1 static CMD_USER_PROC gOmciDebugMap[] = { 2 {1, 1, {"help",0,0,0,0,0}, (CmdUserProcFun)OmciDebugHelp}, 3 {1, 1, {"showmectrl",0,0,0,0,0}, (CmdUserProcFun)ShowMeCtrl}, 4 {2, 1, {"showmeinfo",0,0,0,0,0}, (CmdUserProcFun)ShowMeInfo}, 5 {1, 1, {"showmeminfo",0,0,0,0,0}, (CmdUserProcFun)ShowMemInfo}, 6 {1, 1, {"showuniinfo",0,0,0,0,0}, (CmdUserProcFun)ShowUniInfo}, 7 {3, 1, {"setmectrl",0,0,0,0,0}, (CmdUserProcFun)SetMeCtrl}, 8 {2, 2, {"setmectrl","help",0,0,0,0}, (CmdUserProcFun)SetMeCtrlHelp},//注意此行! 9 {3, 1, {"log",0,0,0,0,0}, (CmdUserProcFun)SetOmciLog}, 10 {4, 1, {"testbatch",0,0,0,0,0}, (CmdUserProcFun)TestBatch}, 11 {1, 1, {"testendian",0,0,0,0,0}, (CmdUserProcFun)TestEndianOper} 12 };
对应的调测命令形如“sendcmd [pid] [module] [args...]”。本模块将解析处理args部分的参数。
用户在终端(如串口)输入的模块调测命令,经平台捕获后转发给该模块。模块接收到平台发来的调测命令消息后,调用如下函数进行解析:
1 VOID OmciDebugMsgProc(INT16U wEvent, VOID *lpMsg, INT16U wMsgLen) 2 { 3 INT32U dwDbgMapIdx = 0, dwChkArgIdx = 0; 4 CMD_USER_ARG *ptCmdUserArg = (CMD_USER_ARG *)lpMsg; 5 6 if((wMsgLen != sizeof(CMD_USER_ARG)) || (0 == ptCmdUserArg->arg_counts)) 7 { 8 OmciDebugHelp(ptCmdUserArg); 9 return ; 10 } 11 12 for(dwDbgMapIdx = 0; dwDbgMapIdx < (sizeof(gOmciDebugMap)/sizeof(CMD_USER_PROC)); dwDbgMapIdx++) 13 { 14 if(gOmciDebugMap[dwDbgMapIdx].dwArgCounts != ptCmdUserArg->arg_counts) 15 continue; 16 17 for(dwChkArgIdx = 0; dwChkArgIdx < gOmciDebugMap[dwDbgMapIdx].dwCheckArgCounts; dwChkArgIdx++) 18 { 19 if(0 != strcmp(ptCmdUserArg->arg[dwChkArgIdx], gOmciDebugMap[dwDbgMapIdx].CmdStr[dwChkArgIdx])) 20 { //若任一待检查命令参数不匹配,则认为整体不匹配 21 break; 22 } 23 } 24 25 if(dwChkArgIdx == gOmciDebugMap[dwDbgMapIdx].dwCheckArgCounts) 26 { 27 gOmciDebugMap[dwDbgMapIdx].procfun(ptCmdUserArg); //调用相应的调试函数 28 return ; 29 } 30 } 31 32 OmciDebugHelp(ptCmdUserArg); //若未找到对应调试函数,则输出帮助信息 33 }
可见,接收到的的命令必须满足以下条件才能正确解析:
- 命令携带的参数数目与命令列表中某条目预期一致
- 命令中待检查的参数与命令列表中某条目精确匹配(包括大小写)
当命令解析失败时,调用全局帮助函数提示用户:
1 INT32 OmciDebugHelp(CMD_USER_ARG * pCmdUserArg) 2 { 3 INT32U dwDbgMapIdx = 0, dwChkArgIdx = 0; 4 CHAR szDbgBuf[MAX_CMDMSG_LEN] = {0}; 5 6 for(dwDbgMapIdx = 0; dwDbgMapIdx < (sizeof(gOmciDebugMap)/sizeof(CMD_USER_PROC)); dwDbgMapIdx++) 7 { 8 memset(szDbgBuf, 0, sizeof(szDbgBuf)); 9 for(dwChkArgIdx = 0; dwChkArgIdx < gOmciDebugMap[dwDbgMapIdx].dwCheckArgCounts; dwChkArgIdx++) 10 { 11 sprintf(szDbgBuf, "%s %s", szDbgBuf, gOmciDebugMap[dwDbgMapIdx].CmdStr[dwChkArgIdx]); 12 } 13 printf("sendcmd 132 omcidebug %s\n\r", szDbgBuf); 14 } 15 return 0; 16 }
因此,当用户输入错误的命令时,终端将呈现如下的“救命稻草” :
1 sendcmd 132 omcidebug help 2 sendcmd 132 omcidebug showmectrl 3 sendcmd 132 omcidebug showmeinfo 4 sendcmd 132 omcidebug showmeminfo 5 sendcmd 132 omcidebug showuniinfo 6 sendcmd 132 omcidebug setmectrl 7 sendcmd 132 omcidebug setmectrl help 8 sendcmd 132 omcidebug log 9 sendcmd 132 omcidebug testbatch 10 sendcmd 132 omcidebug testendian
问题
问题是,这根稻草真的能“救命”吗?设想:
1) 用户A从未接触过该模块调测命令,试探性地输入“sendcmd 132 omcidebug help”(简记为“help”命令,后面类似)。
令人惊讶的是,除了“setmectrl help”命令似乎可以进一步提供些有用信息外,其他的命令只有一个不太友好的组合字符串(“testendian”何解?)……
2) 用户B对命令提示的组合字符串含义有所领悟,愉快地输入“showmeinfo”命令——What?终端又吐出一根稻草!
3) 用户C曾亲睹旁人输入该模块调测命令,知道某些命令只提示了部分参数。于是自信地输入绝无隐藏参数的“showuninfo”命令,但是——
为什么终端还是吐出一根稻草?!Are u kidding???
至此,用户A、B、C全部败下阵来,内牛满面。所幸,因新手故,A还不至于显得太狼狈。
改进
经过缜密的调查,发现问题主要体现为:
- 缺乏具体命令的帮助信息
- 隐藏的参数未加以提示
- 小写的组合字符串容易误读(用户C承认因老眼昏花将“showuniinfo”错看为“showuninfo”)
于是乎,接下来对症下药。
首先,将模块内部定义的命令列表稍加改造,如下:
1 //调测命令表项宏,用于简化书写 2 #define DEBUG_CMD_ENTRY(argNum, cmdStr, dbgFunc) {argNum, 1, {cmdStr}, dbgFunc} 3 #define DEBUG_CMD_ENTRY_END {0, 0, {NULL}, NULL} 4 5 //调测命令表 6 static CMD_USER_PROC gOmciDebugMap[] = { 7 DEBUG_CMD_ENTRY(1, "Help", OmciDebugHelp), 8 9 DEBUG_CMD_ENTRY(1, "ShowMeCtrl", ShowMeCtrl), 10 DEBUG_CMD_ENTRY(2, "ShowMeInfo", ShowMeInfo), 11 DEBUG_CMD_ENTRY(1, "ShowMemInfo", ShowMemInfo), 12 DEBUG_CMD_ENTRY(1, "ShowUniInfo", ShowUniInfo), 13 DEBUG_CMD_ENTRY(3, "SetMeCtrl", SetMeCtrl), 14 DEBUG_CMD_ENTRY(3, "Log", SetOmciLog), 15 DEBUG_CMD_ENTRY(4, "TestBatch", TestBatch), 16 DEBUG_CMD_ENTRY(1, "TestEndian", TestEndianOper), 17 18 DEBUG_CMD_ENTRY_END 19 }; 20 const INT32U OMCI_DEBUG_MAP_NUM = (INT32U)(sizeof(gOmciDebugMap)/sizeof(CMD_USER_PROC) - 1/*End*/);
这里将“setmectrl help”与“setmectrl”命令合为一体,即命令帮助信息置于SetMeCtrl()函数内部*,不另设help函数。待检查参数定为单个字符串,且为PascalCase格式以增强可读性(“ShowUniInfo”比“showuniinfo”更易识别)。
注*:若在全局帮助函数里提供各命令的详细帮助,则不利于代码的清晰化。
接着,命令解析函数改为:
1 /****************************************************************************** 2 * 函数名称: IsUserNeedHelp 3 * 功能说明: 判断用户是否需要调试帮助 4 若首个待检查命令参数后紧跟"?"、"help"等字符串(可扩充), 5 则认为用户期望查看调试命令内部帮助信息。 6 * 输入参数: VOID *pvDbgArg : 首个待检查命令参数指针 7 * 输出参数: NA 8 * 返回值 : BOOL 9 ******************************************************************************/ 10 static BOOL IsUserNeedHelp(VOID *pvDbgArg) 11 { 12 return ((0 == strcmp(pvDbgArg, "?")) || (0 == strcmp(pvDbgArg, "help"))); 13 } 14 15 16 VOID OmciDebugMsgProc(INT16U wEvent, VOID *lpMsg, INT16U wMsgLen) 17 { 18 INT32U dwDbgMapIdx = 0, dwChkArgIdx = 0; 19 CMD_USER_ARG *ptCmdUserArg = (CMD_USER_ARG *)lpMsg; 20 21 if((wMsgLen != sizeof(CMD_USER_ARG)) || (0 == ptCmdUserArg->arg_counts)) 22 { 23 OmciDebugHelp(ptCmdUserArg); 24 return ; 25 } 26 27 for(dwDbgMapIdx = 0; dwDbgMapIdx < OMCI_DEBUG_MAP_NUM; dwDbgMapIdx++) 28 { 29 if((gOmciDebugMap[dwDbgMapIdx].dwArgCounts != ptCmdUserArg->arg_counts) && 30 (0 == IsUserNeedHelp(ptCmdUserArg->arg[1]))) 31 { //尽可能提供机会,以触发相应调试函数内部帮助信息 32 continue; 33 } 34 35 for(dwChkArgIdx = 0; dwChkArgIdx < gOmciDebugMap[dwDbgMapIdx].dwCheckArgCounts; dwChkArgIdx++) 36 { 37 if(0 != strcasecmp(ptCmdUserArg->arg[dwChkArgIdx], gOmciDebugMap[dwDbgMapIdx].CmdStr[dwChkArgIdx])) 38 { //若任一待检查命令参数不匹配(不区分大小写),则认为整体不匹配 39 break; 40 } 41 } 42 43 if(dwChkArgIdx == gOmciDebugMap[dwDbgMapIdx].dwCheckArgCounts) 44 { 45 gOmciDebugMap[dwDbgMapIdx].procfun(ptCmdUserArg); //调用相应的调试函数 46 return ; 47 } 48 } 49 50 OmciDebugHelp(ptCmdUserArg); //若未找到对应调试函数,则输出帮助信息 51 }
这样,用户在提示的参数后输入“?”或“help”,会显示命令的具体帮助信息。同时,因为不区分大小写**,用户可根据习惯输入全大写、全小写或PascalCase格式的参数。
注**:又不是账号密码,完全没必要区分大小写嘛~
然后,全局帮助函数也稍作修改:
1 INT32 OmciDebugHelp(CMD_USER_ARG *pCmdUserArg) 2 { 3 INT32U dwDbgMapIdx = 0, dwChkArgIdx = 0; 4 CHAR szDbgBuf[MAX_CMDMSG_LEN] = {0}; 5 6 printf("\n<Usage>: %s [Args...]. e.g.\n", pszOmciDbgHead); 7 for(dwDbgMapIdx = 0; dwDbgMapIdx < OMCI_DEBUG_MAP_NUM; dwDbgMapIdx++) 8 { 9 memset(szDbgBuf, 0, sizeof(szDbgBuf)); 10 for(dwChkArgIdx = 0; dwChkArgIdx < gOmciDebugMap[dwDbgMapIdx].dwCheckArgCounts; dwChkArgIdx++) 11 { 12 sprintf(szDbgBuf, "%s %s", szDbgBuf, gOmciDebugMap[dwDbgMapIdx].CmdStr[dwChkArgIdx]); 13 } 14 printf("%s %s%s\n", pszOmciDbgHead, szDbgBuf, 15 (gOmciDebugMap[dwDbgMapIdx].dwArgCounts>1) ? " ..." : ""); 16 } 17 18 printf("<Note> [Args...] are case-insensative(i.e.ShowPwSrvInfo=showpwsrvinfo)!\n"); 19 printf(" Press '?' or 'help' after the first [Arg] to get detailed usage.\n\n"); 20 21 return 0; 22 }
经过改造后,终端呈现的帮助提示如下所示:
1 <Usage>: sendcmd 132 omcidebug [Args...]. e.g. 2 sendcmd 132 omcidebug Help 3 sendcmd 132 omcidebug ShowMeCtrl 4 sendcmd 132 omcidebug ShowMeInfo ... 5 sendcmd 132 omcidebug ShowMemInfo 6 sendcmd 132 omcidebug ShowUniInfo 7 sendcmd 132 omcidebug SetMeCtrl ... 8 sendcmd 132 omcidebug Log ... 9 sendcmd 132 omcidebug TestBatch ... 10 sendcmd 132 omcidebug TestEndian 11 <Note> [Args...] are case-insensative(i.e.ShowPwSrvInfo=showpwsrvinfo)! 12 Press '?' or 'help' after the first [Arg] to get detailed usage.
用户看到命令参数后携带“...”,就知道该命令有隐藏参数,再输入“?”或“help”即可获得具体的帮助信息。
以“SetMeCtrl”命令为例,其处理函数实现如下:
1 static INT32 SetMeCtrl(CMD_USER_ARG *pCmdUserArg) 2 { 3 if(IsUserNeedHelp(pCmdUserArg->arg[1]) || 4 IsUserNeedHelp(pCmdUserArg->arg[2])) 5 { 6 printf("\n<Usage>: %s SetMeCtrl [MeClass:Decimal][AutoCreated:BOOL]\n", pszOmciDbgHead); 7 printf(" Set MeClass=0 to Clear Me Create Ctrl File...\n\n"); 8 return 0; 9 } 10 11 INT32U dwMeClass = 0; 12 INT32U dwAutoCreated = OMCI_TRUE; 13 StrToULong((CHAR *)pCmdUserArg->arg[1], &dwMeClass, 0); 14 StrToULong((CHAR *)pCmdUserArg->arg[2], &dwAutoCreated, 0); 15 16 dwAutoCreated = (OMCI_TRUE == dwAutoCreated); 17 OmciSetMeCreateCtrl((INT16U)dwMeClass, (BOOL)dwAutoCreated); 18 printf("\n"); 19 20 OaSaveDbConfig(); 21 return 0; 22 }
至此,用户A、B或C可以一起愉快地“玩耍”了:
1 sendcmd 132 omcidebug SetMeCtrl ? 2 <Usage>: sendcmd 132 omcidebug SetMeCtrl [MeClass:Decimal][AutoCreated:BOOL] 3 Set MeClass=0 to Clear Me Create Ctrl File...