这段时间在公司充当救火员的角色,拯救了一个快要腐烂的项目。其中做的一个工作就是将其变成可配置的,这样可以增加程序的灵活性,如果想改变程序的行为,只需修改参数即可,而不是重新编译。
首先我们需要把程序中可变的部分抽离出来,程序本身只处理业务逻辑,实现配置参数与功能代码的解耦合。在 Linux 环境编程中,通常有两种做法:
- 通过配置文件与程序进行交互
- 通过命令行选项参数进行交互
配置文件的格式可以是常见的 ini、xml、json,也可以是自定义的文件格式,对于配置项较多的程序,这种方式会更方便、更直观。而命令行选项参数在 Linux 更加常见,几乎所有 Linux 命令行工具都支持。本文就给大家讲解,如何让一个命令行程序支持选项参数。
命令行参数的管理
Linux 应用程序是从 main 函数开始执行的,如果需要带选项参数,通常会这么定义 main 函数:
int main(int argc, char *argv[])
{
/* do something */
}
- 第1个参数 argc 表示参数的数目,包含命令本身,也就是说如果命令后面不带参数的话,argc 就等于 1。
- 第2个参数 argv 是字符指针数组,其成员依次指向各个参数,
argv[0]
指向命令本身,argv[1]
指向后面带的第1个参数,指针数组最后一个成员为 NULL,表示参数结束。
比如 ls -w 80
命令,其进程启动之后拿到的 argc 和 argv 参数内容如下:
这里要区分一下命令、选项、参数的概念。它们以空格隔开,第一个就是命令(如果使用管道,一个命令行中可以包含多个命令);选项和参数通常都是可选的,选项分为短选项和长选项,比如这里的 -w
是短选项,它对应的长选项是 --width
;参数 80 是对前面的选项 -w
的描述,并非所有选项都有参数,具体由程序本身决定。参数可以紧跟选项,也可以用空格隔开,对于长选项参数还可以使用等号,所以下面几种写法是等效的:
ls -w 80
ls -w80
ls --width 80
ls --width=80
命令行参数的识别
理解了上面这点,显然我们要解析命令行的选项参数,只需要根据 argc 的值对 argv 进行拆解即可。但这样会增加程序员的工作量,并且命令行的选项参数通常是随意的,不会刻意让某个参数处于第1或者第2的位置,因此,使用 Linux 为我们提供的 getopt()
和 getopt_long()
函数进行命令行参数的识别是更好的选择。
短选项参数
getopt()
函数用于解析命令行参数,其函数原型如下:
int getopt(int argc, char * const argv[], const char *optstring);
参数 | 描述 |
---|---|
argc | 命令参数的个数 |
argv | 指向这些参数的数组 |
optstring | 所有可能的参数字符串 |
返回值 | |
选项字符 | 返回识别成功的选项字符 |
‘?’ | 遇到无效的选项字符或缺少参数时 |
-1 | 当没有其他参数供解析或者出错时 |
第3个参数 optstring 可以是下列元素:
- 单个字符:表示该选项不带参数;
- 单个字符后接一个冒号:表示该选项后必须跟一个参数,参数可以紧跟在选项后面,也可以以空格隔开;
- 单个字符后接两个冒号:表示该选项后可以跟一个参数,参数必须紧跟在选项后面,不能以空格隔开。
PS:两个冒号的用法比较奇特,而且跟长选项参数配合得不好,建议谨慎使用!
为了完成参数识别,Linux 预设了几个全局变量, getopt()
执行后的内容会暂存于此。
extern char *optarg;
extern int optind, opterr, optopt;
- optarg:指向当前选项参数(如果有的话)的指针
- optind:再次调用
getopt()
时的下一个 argv 指针的索引 - opterr:存储错误代码
- optopt:存储未知或出错(比如缺少参数)的选项
默认情况下,getopt 函数会重新排列命令行参数的顺序,所有不可知或错误的命令行参数都排列到最后,当没有其他参数供解析或者出错时,getopt 将返回 -1,同时 optind 中将存放第一个未知或出错选项的下标。
长选项参数
短选项参数言简意赅,对于选项较少的程序是极好的。然而,毕竟只有26个英文字母,选项过多就显得不够用了,而且单个字符很容易重复,造成表意不明。因此,一套善解人意的选项参数,往往需要同时提供短选项和长选项,兼顾实用性和明义性。
Linux 为我们提供了 getopt_long()
和 getopt_long_only()
两个函数,其函数原型如下:
int getopt_long(int argc, char * const argv[],
const char *optstring,
const struct option *longopts, int *longindex);
int getopt_long_only(int argc, char * const argv[],
const char *optstring,
const struct option *longopts, int *longindex);
参数 | 描述 |
---|---|
argc | 命令参数的个数 |
argv | 指向这些参数的数组 |
optstring | 短选项参数字符串 |
longopts | 长选项参数标识 |
longindex | 记录长选项的索引 |
返回值 | |
选项字符 | 返回识别成功的选项字符 |
‘?’ | 遇到无效的选项字符或缺少参数时 |
-1 | 当没有其他参数供解析或者出错时 |
可以看到,前3个参数与 getopt()
是一样的。第4个参数 longopts 是我们要构建的长选项结构体数组,它包含选项的标识及其对应关系。第5个参数可用于记录长选项的索引,如果没有特别的选项参数结构,设置为 NULL 即可。
option 结构体的定义如下:
struct option
{
const char *name; /* 长选项名 */
int has_arg; /* 0(no_argument)表示该参数后面不跟参数值 */
/* 1(required_argument)表示该参数后面一定要跟个参数值 */
/* 2(optional_argument)表示该参数后面可以跟,也可以不跟参数值 */
int *flag; /* 用于决定getopt_long()的返回值 */
/* 如果flag是NULL(通常情况),则返回对应的val值(下一个成员) */
/* 如果flag不是NULL,则将val值赋给flag所指向的内存,并返回0 */
int val; /* 和flag一起决定返回值 */
};
getopt_long_only()
的用法和 getopt_long()
相同,唯一的区别在输入长选项的时候可以不用输入--
而使用 -
。
使用示例
纸上得来终觉浅,绝知此事要躬行。下面通过一个简单的示例,来看看命令行选项参数应该怎么实现。
我们设计一个场景,该示例程序用于输出某人说的某句话,默认输出“Hello, World!”。一共有四个选项,如下:
短选项 | 长选项 | 是否带参数 | 说明 |
---|---|---|---|
-h |
--help |
否 | 查看帮助 |
-v |
--version |
否 | 查看版本 |
-w |
--who |
是 | 设置名字 |
-s |
--say |
是 | 设置内容 |
首先引入必要的头文件:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <getopt.h>
#include <string.h>
许多命令行程序都支持查看帮助和版本信息,所以我们先实现好这两个函数:
#define VER_MAJOR 0
#define VER_MINOR 1
#define VER_PATCH 1
static void show_usage(const char *cmd)
{
printf("Usage: %s [options] ... \n", cmd);
printf("This is a demo for how to use options\n\n");
printf(" -h, --help display this help and exit\n");
printf(" -v, --version output version information and exit\n");
printf(" -w, --who=NAME tell me what is your NAME\n");
printf(" -s, --say=CONTENT what CONTENT do you want to say\n\n");
exit(0);
}
static void show_version(void)
{
printf("version %d.%d.%d\n", VER_MAJOR, VER_MINOR, VER_PATCH);
exit(0);
}
接下来就是要构建 option 结构体,并调用 getopt_long()
进行选项参数的识别:
int main(int argc, char *argv[])
{
int option;
char *name = NULL;
char *content = "Hello, World!";
const char * const short_options = "hvw:s:";
const struct option long_options[] = {
{ "help", 0, NULL, 'h' },
{ "version", 0, NULL, 'v' },
{ "who", 1, NULL, 'w' },
{ "say", 1, NULL, 's' },
{ NULL, 0, NULL, 0 }
};
while ((option = getopt_long(argc, argv, short_options, long_options, NULL)) != -1)
{
switch (option)
{
case 'h':
show_usage(argv[0]);
break;
case 'v':
show_version();
break;
case 'w':
name = strdup(optarg);
break;
case 's':
content = strdup(optarg);
break;
case '?':
default :
printf("Error: option invalid\n");
exit(EXIT_FAILURE);
break;
}
}
if (name)
printf("%s: ", name);
printf("%s\n", content);
return 0;
}
好啦,程序非常简单,编译运行看看效果吧!