一个灵活的程序应该是可配置的

这段时间在公司充当救火员的角色,拯救了一个快要腐烂的项目。其中做的一个工作就是将其变成可配置的,这样可以增加程序的灵活性,如果想改变程序的行为,只需修改参数即可,而不是重新编译。

在这里插入图片描述

首先我们需要把程序中可变的部分抽离出来,程序本身只处理业务逻辑,实现配置参数与功能代码的解耦合。在 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;
}

好啦,程序非常简单,编译运行看看效果吧!

在这里插入图片描述

发布了307 篇原创文章 · 获赞 1317 · 访问量 174万+

猜你喜欢

转载自blog.csdn.net/luckydarcy/article/details/104858551