处理命令行参数是指向指针的指针的另一个用武之地。有些操作系统,包括UNIX和MS-DOS,让用户在命令行中编写参数来启动一个程序的执行。这些参数被传递给程序,程序按照它认为合适的任何方式对它们进行处理。
13.4.1 传递命令行参数
这些参数如何传递给程序呢?C程序的main函数具有两个形参。第1个通常称为argc,它表示命令行参数的数目。第2个通常称为argv,它指向一组参数值。由于参数的数目并没有内在的限制,因此argv指向这组参数值(从本质上说是一个数组)的第1个元素。这些元素的每一个都是指向一个参数文本的指针。如果程序需要访问命令行参数,main函数在声明时就要加上这些参数:
int main(int argc, char **argv)
注意,这两个参数通常取名为argc和argv,但它们并无神奇之处。如果你喜欢,也可以把它们称为fred和ginger,只不过程序的可读性会差一点。
下图显示了下面这条命令行是如何进行传递的。
$ cc –c –o main.c insert.c –o test
注意指针数组:这个数组的每个元素都是一个字符指针,数组的末尾是一个NULL指针。argc的值和这个NULL值都用于确定实际传递了多少个参数。argv指向数组的第1个元素,这就是它被声明为一个指向字符的指针的指针的原因。
最后一个需要注意的地方是第1个参数就是程序的名称。把程序名作为参数传递有什么用意呢?程序显然知道自己的名字,通常这个参数是被忽略的。不过,如果程序通常采用几组不同的选项进行启动,此时这个参数就有用武之地了。UNIX中用于列出一个目录中所有文件的ls命令就是一个这样的程序。在许多UNIX系统中,这个命令具有几个不同的名字。当它以名字ls启动时,它将产生一个文件的简单列表;如果它以名字l启动,就产生一个多列的简单列表;如果它以名字ll启动,就产生一个文件的详细列表。程序对第1个参数进行检查,确定它是由哪个名字启动的,从而根据这个名字选择启动选项。
在有些系统中,参数字符串是挨个存储的。这样当把指向第1个参数的指针向后移动,越过第一个参数的尾部时,就到达了第2个参数的起始位置。但是,这种排列方式是由编译器定义的,所以不能依赖它。为了寻找一个参数的起始位置,应该使用数组中合适的指针。
程序是如何访问这些参数的呢?程序13.2是一个非常简单的例子,它简单地打印出它的所有参数(除了程序名)——非常像UNIX的echo命令。
/* ** 一个打印其命令行参数的程序。 */ #include <stdio.h> #include <stdlib.h> int main( int argc, char **argv ) {/*** 打印参数,直到遇到NULL指针(未使用argc)。程序名被跳过。*/while( *++argv != NULL )printf( "%s\n", *argv );return EXIT_SUCCESS; }
程序13.2 打印命令行参数 echo.c
while循环增加argc的值,然后检查*argv,看看是否到达了参数列表的尾部,方法是把每个参数都与表示列表末尾的NULL指针进行比较。如果还存在另外的参数,循环体就执行,打印出这个参数。在循环一开始就增加argc的值,程序名就被自动跳过了。
printf函数的格式字符串中的%s格式码要求参数是一个指向字符的指针。printf假定该字符是一个以NUL字节结尾的字符串的第一个字符。对argv参数使用间接访问操作产生它所指向的值,也就是一个指向字符的指针——这正是格式所要求的。
13.4.2 处理命令行参数
让我们编写一个程序,用一种更加现实的方式处理命令行参数。这个程序将处理一种非常常见的形式——文件名参数前面的选项参数。在程序名的后面,可能有零个或多个选项,后面跟随零个或多个文件名,像下面这样:
prog –a –b –c name1 name2 name3
每个选项都以一条横杠开头,后面是一个字母,用于在几个可能的选项中标明程序所需的一个。每个文件名以某种方式进行处理。如果命令行中没有文件名,就对标准输入进行处理。
为了让这些例子更为通用,我们的程序设置了一些变量,记录程序所找到的选项。一个现实程序的其他部分可能会测试这些变量,用于确定命令所请求的处理方式。在一个现实的程序中,如果程序发现它的命令行参数有一个选项,其对应的处理过程可能也会执行。
下面的程序13.3和程序13.2颇为相似,因为它包含了一个循环,用于检查所有的参数。它们的主要区别在于我们现在必须区分选项参数和文件名参数。当循环到达并非以横杠开头的参数时就结束。第2个循环用于处理文件名。
/* ** 处理命令行参数。 */ #include <stdio.h> #define TRUE 1 /* ** 执行实际任务的函数的原型。 */ void process_standard_input( void ); void process_file( char *file_name ); /* ** 选项标志,缺省初始化为FALSE。 */ int option_a, option_b /* etc. */ ; void main( int argc, char **argv ) {/*** 处理选项参数:跳到下一个参数,并检查它是否以一个横杠开头。*/while( *++argv != NULL && **argv == '-' ){/*** 检查横杠后面的字母。*/switch( *++*argv ){case 'a':option_a = TRUE;break;case 'b':option_b = TRUE;break;/* etc. */}}/*** 处理文件名参数。*/if( *argv == NULL )process_standard_input();else {do {process_file( *argv );} while( *++argv != NULL );} }
程序13.3 处理命令行参数 cmd_line.c
注意,在程序13.3的while循环中,增加了下面这个测试:
**argv == '-'
双重间接访问操作访问参数的第1个字符,如图13.2所示。如果这个字符不是一个横杠,那就表示不再有其他的选项,循环终止。注意,在测试**argv之前先测试*argv是非常重要的。如果*argv为NULL,那么**argv中的第2个间接访问就是非法的。
switch语句中的*++*argv表达式以前曾见到过。第1个间接访问操作访问argv所指的位置,然后这个位置执行自增操作。最后1个间接访问操作根据自增后的指针进行访问,如图13.3所示。switch语句根据找到的选项字母设置一个变量,while循环中的++操作符使argv指向下一个参数,用于循环的下一次迭代。
当不再存在其他选项时,程序就处理文件名。如果argv指向NULL指针,命令行参数中就没有其他内容了,程序就处理标准输入;否则,程序就逐个处理文件名。这个程序的函数调用较为通用,它们并未显示一个现实程序可能执行的任何实际工作。然而,这个设计方式是非常好的。main程序处理参数,这样执行处理过程的函数就无须担心怎样对选项进行解析或者怎样挨个访问文件名。
有些程序允许用户在一个参数中放入多个选项字母,像下面这样:
prog –abc name1 name2 name3
一开始我们可能会觉得这个改动会使程序变得复杂,但实际上它很容易进行处理。每个参数都可能包含多个选项,所以我们使用另一个循环来处理它们。这个循环在遇到参数末尾的NUL字节时应该结束。
程序13.3中的switch语句由下面的代码段代替:
while((opt= *++*argv) != '\0'){switch(opt){case 'a':option_a = TRUE;break;/*etc*/}}
循环中的测试使参数指针移动到横杠后的那个位置,并复制一份位于那里的字符。如果这个字符并非NUL字节,那么就像前面一样使用switch语句来设置合适的变量。注意,选项字符被保存到局部变量opt中,这可以避免在switch语句中对**argv进行求值。
注意,使用这种方式时,命令行参数可能只能处理一次,因为指向参数的指针在内层的循环中被破坏。如果必须多次处理参数,则当挨个访问列表时,对每个需要增值的指针都复制一份。在处理选项时还存在其他的可能性。例如,选项可能是一个单词而不是单个字母,或者可能有一些值与某些选项联系在一起,如下面的例子所示:
cc -o prog prog.c
本章的其中一个问题就是对这个思路的扩展。