最近对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.
先来总览函数整体调用流程:
接下来看一个本文代码分析中会涉及到的一个比较重要的结构体:
/*
* 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命令自动补齐功能的一个整体的实现框架,我们的旅程也在此暂告一段落。