u-boot-2012.10 shell模式命令自动补齐功能 源代码分析

        最近对u-boot的命令自动补齐功能产生了兴趣,想从源代码的层面看看它是如何实现的。之前接触最多的是u-boot-1.1.6,不过这个真的是很老的版本了。u-boot在之后的版本进行了很多的改进与优化,最显而易见的莫过于调整了自身的目录结构,1.1.6中为了支持各种体系结构的lib_*目录已不再零散的存放在根目录下。

正好手头有一份u-boot-2012.10的源代码,算是比较新的u-boot,就决定是你了。闲话少言,直逼主题。

u-boot的shell命令自动补齐功能通过CONFIG_AUTO_COMPLETE宏定义控制,README中对其有简短的介绍:

Enable auto completionof commands using TAB.

先来总览函数整体调用流程:

u-boot-2012.10 shell模式命令自动补齐功能 源代码分析_第1张图片

接下来看一个本文代码分析中会涉及到的一个比较重要的结构体:

/*

 * Monitor Command Table

 */

      

struct cmd_tbl_s {

              char        *name;          /* Command Name                   */

              int          maxargs;       /* maximum number of arguments  */

              int          repeatable;    /* autorepeat allowed?              */

                                          /* Implementation function      */

              int          (*cmd)(struct cmd_tbl_s *, int, int, char * const []);

              char        *usage;          /* Usage message (short)    */

#ifdef     CONFIG_SYS_LONGHELP

              char        *help;            /* Help  message (long)     */

#endif

#ifdef CONFIG_AUTO_COMPLETE

              /* do auto completion on the arguments */

              int          (*complete)(int argc, char * const argv[], char last_char, int maxv, char *cmdv[]);

#endif

};

include/command.h的46行,每个u-boot命令都对应一个该结构体,本文会涉及到该结构体的name和complete成员。name指向命令名,complete指向该命令的自动补齐函数,每个u-boot命令都可以有自己的命令自动补齐函数。

 

当在u-boot的shell模式下按下TAB键出现命令自动补齐,所以代码的切入点应在u-boot获取用户输入部分。common/main.c文件main_loop函数中,当u-boot做完了一系列平台无关的初始化工作后,424行通过readline函数读取用户输入。我们的旅程就从这里开始。

① common/main.c             918行

int readline (constchar *const prompt)

prompt:u-boot的shell模式命令提示符,传入CONFIG_SYS_PROMPT宏定义的值,各平台可根据喜好定义自己的样式。

       初始化控制台缓冲区console_buffer、调用readline_into_buffer函数进行其他的工作。

--------------------------------------------------------------------------------------------------------------------------------------

console_buffer[0] = '\0';

       该函数最主要的工作就是初始化控制台缓冲区console_buffer(之前使用u-boot均是使用PC的超级终端通过串口与之交互,在此默认为此种方式,故而控制台缓冲区即为串口缓冲区),该缓冲区定义于65行(char console_buffer[CONFIG_SYS_CBSIZE +1];  /* console I/O buffer */),不同平台可以通过CONFIG_SYS_CBSIZE宏定义定义它的大小。

return readline_into_buffer(prompt, console_buffer, 0);

       之后readline函数调用readline_into_buffer函数进行接下来的工作。

② common/main.c             930行

int readline_into_buffer(constchar *const prompt, char *buffer, int timeout)

prompt:u-boot的shell模式命令提示符。

buffer:串口缓冲区console_buffer。

timeout:本文的代码分析中未涉及到该参数。

       获得用户的输入,如果为特殊字符则进行相应的处理,否则将字符存入缓冲区并将之打印到终端。

--------------------------------------------------------------------------------------------------------------------------------------

/*print prompt */

if(prompt) {

       plen = strlen (prompt);

       puts (prompt);

}

col = plen;

       打印u-boot的shell模式命令提示符。plen和col是两个int变量,分别存储命令提示符长度以及当前光标所在列数。

c = getc();

       用户输入的每个字符都会存储在变量c中,之后会通过switch判断是否为特殊字符。

default:

       /*

        *Must be a normal character then

        */

       if (n < CONFIG_SYS_CBSIZE-2) {

              if (c == '\t') {  /* expand TABs */

#ifdef CONFIG_AUTO_COMPLETE

                     /* if auto completion triggered justcontinue */

                     *p = '\0';

                     if(cmd_auto_complete(prompt, console_buffer, &n, &col)) {

                            p = p_buf + n;       /* reset */

                            continue;

                     }

#endif

                     puts(tab_seq+(col&07));

                     col += 8 - (col&07);

              } else {

                     ++col;            /* echo input         */

                     putc (c);

              }

              *p++ = c;

              ++n;

       } else {                  /*Buffer full          */

              putc ('\a');

       }

找到亮点了,if(c == '\t')。当用户按下TAB键后就会进入这里,TAB被认为是普通字符中的特殊字符 +_+。紧接着就看到了CONFIG_AUTO_COMPLETE条件编译,那么自动补齐功能的实现就在这里,继续深入。这里的p实际上指向缓冲区中即将存放字符的位置,而这里将该位置赋值为'\0'是为了在接下来的cmd_auto_complete函数中寻找last_char用。现在进入cmd_auto_complete函数。

③ common/command.c     334行

intcmd_auto_complete(const char *const prompt, char *buf, int *np, int *colp)

prompt:u-boot的shell模式命令提示符。

buffer:串口缓冲区console_buffer。

np:缓冲区索引值指针(即将存放用户输入的位置)。

colp:当前光标位置指针。

       调用complete_cmdv函数搜索自动补齐的匹配命令、根据匹配命令个数以及搜索到的匹配命令实现自动补齐功能。

函数返回命令自动匹配是否成功(0:未成功,1:成功)。

--------------------------------------------------------------------------------------------------------------------------------------

if(strcmp(prompt, CONFIG_SYS_PROMPT) != 0)

       return 0;  /*not in normal console */

       首先查看当前是否处于shell模式。

cnt = strlen(buf);

if(cnt >= 1)

       last_char = buf[cnt - 1];

else

       last_char = '\0';

       last_char存储用户在按下TAB键前输入的最后一个字符。因为用到了strlen函数,所以之前需要将*p = '\0'。

/*copy to secondary buffer which will be affected */

strcpy(tmp_buf,buf);

       将缓冲区拷贝一份,对拷贝的缓冲区进行操作。

/*separate into argv */

argc = make_argv(tmp_buf, sizeof(argv)/sizeof(argv[0]), argv);

       make_argv函数过滤掉用户输入的各参数(有可能未输入任何参数)周围多余的空格(或是\t),并将各参数在缓冲区中的首地址分别存放到argv指针数组(其在cmd_auto_complete函数中定义如下“char*argv[CONFIG_SYS_MAXARGS + 1];             /*NULL terminated     */”,各平台可通过定义CONFIG_SYS_MAXARGS宏的值设置shell命令最大参数个数)中,数组最后一个元素赋值为NULL,最后函数返回参数个数存入argc变量中。

/*do the completion and return the possible completions */

i= complete_cmdv(argc, argv, last_char, sizeof(cmdv)/sizeof(cmdv[0]), cmdv);

       complete_cmdv函数将会返回与用户输入命令匹配或前半部分匹配的命令个数,并将这些命令的首地址依次存入cmdv指针数组中,该指针数组的定义为“char *cmdv[20];”,接下来进入complete_cmdv函数。

④common/command.c     182行

static intcomplete_cmdv(int argc, char * const argv[], char last_char, int maxv, char*cmdv[])

argc:shell命令参数个数。

argv:shell命令各参数在缓冲区中的首地址。

last_char:用户在按下TAB键前输入的最后一个字符。

maxv:cmdv指针数组的大小,根据之前的定义,此处应为20。

cmdv:该指针数组用于存储匹配命令的首地址。

根据用户按下TAB键时已输入的命令个数以及在TAB前输入的最后一个字符,分为3种情况进行匹配命令的搜索。

函数正常情况下返回匹配命令的个数。

--------------------------------------------------------------------------------------------------------------------------------------

cmd_tbl_t*cmdtp;

       在函数开始便定义了一个cmd_tbl_t指针,它定义在include/command.h的62行,实际上是struct cmd_tbl_s的重命名。不记得这个结构体?到文章的开始处看看。

if(argc == 0) {

       /* output full list of commands */

       for (cmdtp = &__u_boot_cmd_start;cmdtp != &__u_boot_cmd_end; cmdtp++) {

              if (n_found >= maxv - 2) {

                     cmdv[n_found++] ="...";

                     break;

              }

              cmdv[n_found++] = cmdtp->name;

       }

       cmdv[n_found] = NULL;

       retur n n_found;

}

       第一种情况,未输入任何命令时直接按下TAB键,此时应当列出所有u-boot命令(此时可理解为所有命令均匹配)。这个__u_boot_cmd_start和__u_boot_cmd_end是定义在各平台的u-boot.lds文件中,用于标明u-boot命令域,它们分别代表命令域的起始和结束,在它们之间依次存放着每个u-boot命令所对应的cmd_tbl_s结构体。所以这个for循环的作用就是遍历u-boot命令域中每个命令的cmd_tbl_s结构体,并将每个命令名字的首地址存入cmdv指针数组中,该数组最后一个元素为NULL。在for循环中还可以看到,如果命令过多(多余20 – 2个),则其他命令的名字均以“...”代替。最后返回匹配命令的个数。

/*more than one arg or one but the start of the next */

if(argc > 1 || (last_char == '\0' || isblank(last_char))) {

       cmdtp = find_cmd(argv[0]);

       if (cmdtp == NULL || cmdtp->complete== NULL) {

              cmdv[0] = NULL;

              return 0;

       }

       return (*cmdtp->complete)(argc, argv,last_char, maxv, cmdv);

}

       第二种情况,输入了多个命令,或者正在准备输入第二个命令(last_char为\0、空格或者\t时)时按下TAB键,此时应当根据第一个命令补全其余命令,即执行第一个命令(因为它是主命令)的complete函数。但在这之前,需要使用find_cmd函数判断主命令是否唯一存在。之后会对该函数进行分析,最终的结果是其返回唯一匹配主命令的cmd_tbl_s结构体的地址,否则返回NULL。如果执行了complete函数,则函数返回匹配命令个数,同时将匹配命令名字(此时是针对于主命令的匹配命令)的首地址存入cmdv指针数组中。

cmd= argv[0];

/*

 * Some commands allow length modifiers (like"cp.b");

 * compare command name only until first dot.

 */

p= strchr(cmd, '.');

if(p == NULL)

       len = strlen(cmd);

else

       len = p - cmd;

 

/*return the partial matches */

for(cmdtp = &__u_boot_cmd_start; cmdtp != &__u_boot_cmd_end; cmdtp++) {

 

       clen = strlen(cmdtp->name);

       if (clen < len)

              continue;

 

       if (memcmp(cmd, cmdtp->name, len) !=0)

              continue;

 

       /* too many! */

       if (n_found >= maxv - 2) {

              cmdv[n_found++] = "...";

              break;

       }

 

       cmdv[n_found++] = cmdtp->name;

}

 

cmdv[n_found] = NULL;

return n_found;

       第三种情况,当正在输入第一个命令时(last_char不是\0、空格或者\t)按下TAB键,此时应当补全该命令。此种情况与find_cmd函数的工作方式非常类似,只不过此处当命令域中命令名长度(clen)比用户输入的命令长度(len)短时,不再进行命令匹配比较,此处是个优化。最后结果依旧是将匹配命令名字首地址放入cmdv指针数组中,并且返回匹配命令个数。

⑤ common/command.c     138行     104行

cmd_tbl_t *find_cmd(const char *cmd)

cmd:用户输入的主命令。

       该函数只是计算出u-boot命令域的长度,之后传递给find_cmd_tbl函数,主要的命令匹配工作是由find_cmd_tbl函数完成的。

       函数返回find_cmd_tbl函数的返回值。

cmd_tbl_t *find_cmd_tbl(const char *cmd, cmd_tbl_t *table, int table_len)

cmd:用户输入的主命令。

table:u-boot命令域起始地址。

table_len:u-boot命令域长度。

       该函数完成命令的匹配工作。

       函数返回唯一匹配的命令在命令域中的地址,如果有多个匹配命令则返回NULL。

--------------------------------------------------------------------------------------------------------------------------------------

/*

 * Some commands allow length modifiers (like"cp.b");

 * compare command name only until first dot.

 */

len= ((p = strchr(cmd, '.')) == NULL) ? strlen (cmd) : (p - cmd);

       有的命令形如“cp.b”,如果是这种命令,则需要得到点之前的命令长度,否则len直接得到命令长度。

for(cmdtp = table; cmdtp != table + table_len; cmdtp++) {

       if (strncmp (cmd, cmdtp->name, len) ==0) {

              if (len == strlen(cmdtp->name))

                     return cmdtp; /* full match */

 

              cmdtp_temp = cmdtp;  /* abbreviated command ? */

              n_found++;

       }

}

if(n_found == 1) {                /* exactlyone match */

       return cmdtp_temp;

}

 

returnNULL;   /* not found or ambiguous command*/

       for循环遍历命令域,如果找到完全匹配的命令,或是找到唯一的前半部分匹配命令,则都会返回该命令cmd_tbl_s结构体的地址。如果找到多个前半部分匹配命令,则不知道该调用哪个命令的complete函数,所以认定为未找到匹配命令,返回NULL。

⑥ 回到cmd_auto_complete()

       现在cmdv中得到了所有匹配的命令,i也存储了匹配命令的个数。

--------------------------------------------------------------------------------------------------------------------------------------

/*no match; bell and out */

if(i == 0) {

       if (argc > 1)    /* allow tab for non command */

              return 0;

       putc('\a');

       return 1;

}

       如果用户输入了多个命令且未找到匹配命令,则认定为匹配失败。但如果用户只输入了一个参数(未输入参数情况下,只有u-boot没有任何命令时才会匹配失败,这个,钻牛角尖了+_+)并且未找到匹配命令,则系统响铃并返回匹配成功。

s= NULL;

len= 0;

sep= NULL;

seplen= 0;

if(i == 1) { /* one match; perfect */

       k = strlen(argv[argc - 1]);

       s = cmdv[0] + k;

       len = strlen(s);

       sep = " ";

       seplen = 1;

}else if (i > 1 && (j = find_common_prefix(cmdv)) != 0) {  /* more */

       k = strlen(argv[argc - 1]);

       j -= k;

       if (j > 0) {

              s = cmdv[0] + k;

              len = j;

       }

}

       如果正好有一个匹配命令,则s得到需要补齐的第一个字符的位置,len得到需要补齐的总字符长度。由于是补齐一个命令,所以在补齐后需要多输出一个空格(多输出一个seq,方便用户输入接下来的命令,人性化★_★)。如果有多个命令匹配并且它们有公有前缀(find_common_prefix函数会找出匹配命令公有前缀的长度,之后会对该函数进行分析),则j得到最后一个输入的命令长度与匹配命令公有前缀的长度差(也就是需要自动补齐几个字符)。如果确实有需要补齐的字符,则同样s得到需要补齐的第一个字符的位置,len得到需要补齐字符的总长度。

if(s != NULL) {

       k = len + seplen;

       /* make sure it fits */

       if (n + k >= CONFIG_SYS_CBSIZE - 2) {

              putc('\a');

              return 1;

       }

 

       t = buf + cnt;

       for (i = 0; i < len; i++)

              *t++ = *s++;

       if (sep != NULL)

              for (i = 0; i < seplen; i++)

                     *t++ = sep[i];

       *t = '\0';

       n += k;

       col += k;

       puts(t - k);

       if (sep == NULL)

              putc('\a');

       *np = n;

       *colp = col;

}else {

       print_argv(NULL, "  ", " ", 78, cmdv);

 

       puts(prompt);

       puts(buf);

}

return1;

       代码走到这里,当s为NULL说明有多个匹配命令,但它们之间没有公有前缀。此时会调用print_argv函数,将匹配的命令都打印出来,之后重新打印命令提示符以及已输入的命令,此函数会在之后进行分析。那么当s确实指向了需要补齐的第一个字符的位置时,k得到需要补齐的字符串长度,n是传进来的np的值,也就是缓冲区索引位置,用n+k来判断即将补齐的字符串是否使缓冲区溢出。t得到缓冲区放入下一个字符的地址,之后通过for循环依次将需要补齐的字符放入缓冲区中,之后再按需要放入一个空格(只有一个匹配命令,补齐命令后就需要这个空格)。之后puts(t - k)打印出补齐的字符串,然后更新缓冲区索引以及当前光标位置索引。

⑦ common/command.c     310行

static int find_common_prefix(char* const argv[])

argv:找到的各匹配命令。

       该函数查看各匹配命令的公有前缀。

       函数返回所有匹配命令公有前缀的长度。

--------------------------------------------------------------------------------------------------------------------------------------

/*begin with max */

anchor= *argv++;

len= strlen(anchor);

while((t = *argv++) != NULL) {

       s = anchor;

       for (i = 0; i < len; i++, t++, s++) {

              if (*t != *s)

                     break;

       }

       len = s - anchor;

}

returnlen;

       s指向第一个匹配命令,而t指向随后的匹配命令。从命令的第一个字符开始,比较命令之间公有前缀的长度,每次比较后都由len都记录该长度。这样将随后的每个命令均与第一个命令比较,len就能得到所有命令的公有前缀长度。

⑧ common/command.c     283行

static void print_argv(const char *banner, const char *leader, const char *sep, intlinemax, char * const argv[])

banner:在输出所有匹配命令前输出的提示信息,此处传入的值为NULL。

leader:两个空格。如果匹配命令过多,则会多行显示,在每行的开始处均会输出这两个空格。

sep:一个空格。匹配命令之间用空格隔开。

linemax:默认的显示终端宽度。此处传入值为78,即默认显示终端每行可以显示78个字符。

argv:各匹配命令。

       该函数将所有匹配命令格式化输出。

--------------------------------------------------------------------------------------------------------------------------------------

if(banner) {

       puts("\n");

       puts(banner);

}

       在输出所有匹配命令之前,如果有需要,则输出提示信息。

i= linemax;    /* force leader and newline*/

while(*argv != NULL) {

       len = strlen(*argv) + sl;

       if (i + len >= linemax) {

              puts("\n");

              if (leader)

                     puts(leader);

              i = ll - sl;

       } else if (sep)

              puts(sep);

       puts(*argv++);

       i += len;

}

printf("\n");

       当将要输出的字符串在显示终端的当前行放不下时便会另起新行,同时匹配命令也需要在新的一行开始输出。i存储的是当前光标所在列数,所以此处将i = linemax,起新的一行准备输出匹配命令。len存储的是匹配命令+一个空格的长度,之后的工作就是依次将匹配命令输出,之间用空格隔开,行满起新行,但要在行首输出两个空格。

⑨ 回到readline_into_buffer()

       终于又回到了这梦开始的地方。此时n和col的值已被更新,还记得他们代表什么吗?回到③看看。

--------------------------------------------------------------------------------------------------------------------------------------

if(cmd_auto_complete(prompt, console_buffer, &n, &col)) {

       p = p_buf + n;       /* reset */

       continue;

}

      根据cmd_auto_complete的返回值,如果返回1(只有一种情况不返回1,接下来会说到),则需要更新p的值。p_buf指向console_buffer,而n是更新后的缓冲区索引值,所以p最后得到缓冲区中即将存放字符的位置。接下来的continue会回到971行的for(;;),重新开始获取用户输入。

puts(tab_seq+(col&07));

col+= 8 - (col&07);

...

*p++= c;

++n;

在⑥的一开始就提到,如果用户输入了多个命令且未找到匹配命令,那么这里cmd_auto_complete返回0,此时会认定为输出这个\t。输出\t则会将光标移动到下一个制表符的位置,而制表符之间的宽度一般被认定为8个空格。这里有很有意思的实现,col&07得到了当前光标位置与8的余数,那么也就是得到了当前光标距前一个制表符多远。tab_seq是一个存有8个空格的字符数组,所以tab_seq+(col&07)正好得到光标移到下一个制表符需要输出几个空格。之后更新当前光标位置,将\t存入缓冲区,更新n的值。最后依旧回到971行的for(;;),重新开始获取用户输入。

至此,文中分析了u-boot命令自动补齐功能的一个整体的实现框架,我们的旅程也在此暂告一段落。

你可能感兴趣的:(u-boot-2012.10 shell模式命令自动补齐功能 源代码分析)