1. 概述
gcc helloworld.c -o helloworld
这条命令,大家想必都非常熟悉了,就是使用gcc把c语言源代码helloworld.c编译为可执行程序helloworld。有没有想过,gcc这个命令内部是如何识别到我们传递给他的源文件路径以及-o参数和目标文件路径的呢?带着这个问题,我们开始今天的注意。
在C语言编程中,命令行参数是一种强大的工具,可以极大地提升程序的运行灵活性。通过命令行参数,用户可以轻松地为程序的每次运行提供不同的输入数据、配置选项或其他必要的运行参数,而无需修改程序源代码。
本文将探讨如何使用C语言处理命令行参数,并通过这种方式增加程序的灵活性和实用性。
2. 如何传参给程序
假设让你用C语言写个代码,功能是用来读取并显示某文件的内容,那我们会怎么写代码呢?最初级的写法是不是就是用fopen()函数打开这个文件,用fread()函数读取文件内容,然后用printf()函数把读到的内容打印出来,最后再调用fclose()关闭文件。这个做法有没有问题?没问题。
如果现在让你再显示另外一个文件的内容怎么办?So Easy,修改代码里传递给fopen()函数的路径,重新编译运行。有没有问题?完美,也没问题。
好,需求又变了,现在给你1000个文件,让你显示它们的内容怎么办?修改1000遍代码,编译1000个程序,运行1000次吗?有没有问题?问题来了,改代码要改到吐血了,简直不是人干的活。。。
在软件编程这个行业里做久了,会发现当某一段时间,你在做某个重复的工作时,一定是有办法去优化它的,不管是流程梳理还是制作工具辅助提效还是使用更先进高效的方法。
在C语言中,可以通过int main(int argc, char *argv[])函数的参数来访问命令行参数。main()函数通常有两个参数:argc和argv。我们在给程序运行添加的参数就是通过argc和argv的组合传递给main()函数的。其中:
argc是一个整数,表示命令行参数的数量(包括运行的程序本身)argv是一个指向字符串数组的指针,每个字符串代表的就是一个命令行参数。argv[0]是程序的名称,argv[1]、argv[2]一直到argv[argc - 1]等就是程序的所有参数。
//文件名:test_arg.c
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("当前运行的程序: %s\n", argv[0]);
printf("参数个数(包含程序本身)共计%d个\n", argc);
printf("参数列表:\n");
for (int i = 1; i < argc; i++) {
printf(" 第%d个: %s\n", i, argv[i]);
}
return 0;
}
如下图所示,我们可以运行上述代码来获取所有参数信息:
- 当没有参数时,
argc为1,argv[0]就是程序运行的路径 - 有参数,程序使用相对路径时,可以得到所有参数及其个数,此时
argv[0]就是程序的相对路径。 - 有参数,程序使用绝对路径时,可以得到所有参数及其个数,此时
argv[0]就是程序的绝对路径。
3. 手动解析命令行参数
我们能够访问命令行参数之后,下一步就是处理它们了。
我们摘录一段跨平台多媒体开发库SDL的demo程序中解析命令行参数的代码,看看开源软件是如何解析它们的。效果比较好;
已知这段程序的命令行参数方式:app [-width N] [-height N] [-bpp N] [-warp] [-hw] [-fullscreen],其解析源码如下所示:
int main(int argc, char *argv[])
{
。。。
int videoflags = SDL_SWSURFACE;
int video_bpp = 16;
for(i = 1; argv[i]; ++i) {
if( strcmp(argv[i], "-bpp") == 0){
video_bpp = atoi(argv[++i]);
if(video_bpp<=8){
video_bpp=16;
fprintf(stderr, "forced 16 bpp mode\n");
}
}
else if(strcmp(argv[i], "-hw") == 0){
videoflags |= SDL_HWSURFACE;
}
else if(strcmp(argv[i], "-warp") == 0){
videoflags |= SDL_HWPALETTE;
}
else if(strcmp(argv[i], "-width") == 0 && argv[i+1]){
w = atoi(argv[++i]);
}
else if(strcmp(argv[i], "-height") == 0 && argv[i+1]){
h = atoi(argv[++i]);
}
else if(strcmp(argv[i], "-resize") == 0){
videoflags |= SDL_RESIZABLE;
}
else if(strcmp(argv[i], "-noframe") == 0){
videoflags |= SDL_NOFRAME;
}
else if(strcmp(argv[i], "-fullscreen") == 0){
videoflags |= SDL_FULLSCREEN;
}
else {
fprintf(stderr,
"Usage: %s [-width N] [-height N] [-bpp N] [-warp] [-hw] [-fullscreen]\n",
argv[0]);
quit(1);
}
}
}
上面这段代码演示如何通过for循环来遍历命令行参数并进行解析的全过程,而且不用考虑命令行的参数顺序。通过这种方式,你可以轻松地扩展程序的功能,而无需修改源代码。大家可以收藏保存,有相关需求时拿过来修修改改就可以用了。
4. 使用getopt类库函数来解析
在C语言中,处理命令行参数,除了直接使用main函数的argc和argv参数外,还可以使用C库函数中的getopt()和getopt_long()和getopt_long_only()函数来解析,这些C库函数提供了更加灵活的参数解析方法。
4.1 getopt解析短选项
4.1.1 函数声明
#include <unistd.h>
int getopt(int argc, char * const argv[], char *optstring);
extern char *optarg;
extern int optind, opterr, optopt;
4.1.2 参数解析
argc:命令行参数的数量,通常是main函数的第一个参数。argv:命令行参数的数组,通常是main函数的第二个参数。optstring:一个字符串,包含了所有有效选项字符的列表。如果某个选项后面需要跟一个额外的参数值,那么在optstring中该选项字符后面应该加上一个冒号:,否则也不需要。假设某命令参数:-v -f filename -n num,-v后面不需要跟额外参数,-f后面跟一个额外的参数filename,-n后面跟一个额外的参数num,那么此时optstring就是vf:n:。
getopt函数的工作原理是遍历argv数组,检查每个参数是否是一个选项。如果是选项,则根据optstring来确定如何处理该选项。
4.1.3 返回值
- 如果成功解析一个选项,
getopt()返回该选项字符。- 如果遇到一个非选项参数(即不以 - 开头的参数),
getopt()返回-1,并且该参数之后的参数可以通过argv数组直接访问。- 如果遇到一个未知选项,
getopt()返回?,并且全局变量optopt被设置为该未知选项字符。- 如果遇到一个需要参数的选项,但后面没有跟着参数值,
getopt()会返回:,并且全局变量optopt被设置为该选项字符。
4.1.4 涉及的全局变量
optarg:当getopt遇到一个需要参数的选项时,optarg会被设置为该选项的参数值。如果选项不需要参数,或者遇到未知选项,optarg的值是不确定的。optind:这是一个索引,指向argv数组中下一个要处理的元素。通常,在getopt()返回-1之后,你可以使用optind来获取非选项参数。optopt:当getopt()遇到一个未知选项时,optopt会被设置为该未知选项字符。这可以用于错误处理,以确定是哪个选项导致了问题。
4.1.5 示例代码
下面是一个使用 getopt 的简单例子:
//文件名:test_getopt.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int opt;
int verbose = 0;
char *filename = NULL;
int num = 0;
while ((opt = getopt(argc, argv, "vf:n:")) != -1) {
switch (opt) {
case 'v':
verbose = 1;
break;
case 'f':
filename = optarg;
break;
case 'n':
num= atoi(optarg);
break;
case ':':
fprintf(stderr, "Option -%c requires an argument.\n", optopt);
exit(EXIT_FAILURE);
case '?':
fprintf(stderr, "Unknown option `-%c'.\n", optopt);
exit(EXIT_FAILURE);
default:
abort();
}
}
// optind 指向下一个要处理的 argv 元素,此处可以将所有未处理的(非选项)参数遍历打印出来
if (optind < argc) {
printf("Non-option arguments:\n");
while (optind < argc) {
printf("argv[%d] = %s\n", optind, argv[optind++]);
}
}
if (verbose) {
printf("Verbose mode is on.\n");
}
if (filename) {
printf("File to process: %s\n", filename);
}
if (num > 0) {
printf("Num: %d\n", num);
}
return 0;
}
将test_getopt.c编译为可执行程序test_getopt,然后分不同情况运行查看效果:
- 不带任何参数
$ ./test_getopt
- 所有参数都是有效(可识别)参数:-v -f ./config.log -n 2
$ ./test_getopt -v -f ./config.log -n 2
Verbose mode is on.
File to process: ./config.log
Num: 2
- 存在未知选项参数:-h haha
$ ./test_getopt -v -f ./config.log -n 2 -h haha
./test_getopt: invalid option -- 'h'
Unknown option `-h'.
- 需要参数的选项,但后面没有跟着参数值:-n
$ ./test_getopt -v -f ./config.log -n
./test_getopt: option requires an argument -- 'n'
Unknown option `-n'.
- 存在非选项(即不以-开头的)参数:in val id
$ ./test_getopt in -v val -f ./config.log -n 2 id
Non-option arguments:
argv[7] = in
argv[8] = val
argv[9] = id
Verbose mode is on.
File to process: ./config.log
Num: 2
4.2 getopt_long解析长参数
完善中
4.3 getopt_long_only解析参数
完善中
5. 思考题
- 已知,Linux中
ls -l -a和ls -la的效果是一样的。经过前面的学习,我们已经知道了如何解析ls -l -a格式的命令行参数,那么ls -la格式的参数该怎么解析呢? - 现在再让你用C语言写个代码,用来实现读取并显示某文件的内容,那你会怎么写代码呢?
欢迎大家在评论区留下你的思考和答案。
6. 总结
通过命令行参数,应用程序可以获得更高的灵活性和实用性。通过合理地处理命令行参数,你可以让用户轻松地为程序提供输入数据、配置选项或其他必要的运行参数。这不仅提高了程序的灵活性,还使得程序更加易于使用和扩展。结合适当的错误处理和命令帮助,可以创建出既强大又易于使用的命令行工具。
最后,再补充一点,如果程序本身的命令行参数比较简单,比如就一两个参数或者说只是传递一个配置文件路径,那其实没有使用getopt()函数来解析,代码里使用strcmp()函数简单几行代码做个字符串匹配就可以了。工具诞生的本意是服务于人,把人从繁重的工作中解放出来。谨记以实用为主,切勿杀鸡用牛刀。