一个函数解析命令行选项
自己写了一个解析命令行选项的函数,类似 Linux 的 getopt
,感觉用起来更方便一些,可以随时扩展。
假设要执行的程序为 a.out
,给出的命令行参数如下:
./a.out -a=1 -bc ccc ccc -d ddd ddd -e-f- - -- --long-g=ggg "g g g g" ""
要求该程序只接受 -a
、-b
、-c
、-d
和自定义选项,其中 -a
、-b
、-c
可以指定选项值,-d
不能指定选项值。
执行结果如下:
程序名称:./a.out
赋值选项:a = 缺少值
无效选项:= # 为了保持格式统一,没有支持 -a=xxx 的写法
无效选项:1
赋值选项:b = 缺少值
赋值选项:c = ccc
自定选项:ccc
开关选项:d
自定选项:ddd
自定选项:ddd
无效选项:e
无效选项:f
特殊选项:-
特殊选项:-
特殊选项:--
--长选项:long-g=ggg # 长选项需要自己解析,格式也可以自己定义
自定选项:g g g g
自定选项:
可以看到该程序可以识别各种短选项的写法,也可以识别长选项,而且支持 -
、--
和 空选项
这样的特殊选项。但是不支持类似 gcc 的 -ldl
这样的写法,必须写成 -l dl
,但是简单扩展一下就可以支持 -l dl pthread gl -W all -o hello
这样的写法。
程序代码如下:
#include <stdio.h>
#include <string.h>
#define OPT_CUSTOM '\0' // 用于标记“遇到自定义选项”(不可修改该值,因为它与字符串扫描中的 \0 相呼应)
#define OPT_MINUS2 '\1' // 用于标记“遇到特殊选项”(单独的 --)
#define OPT_LONG '\2' // 用于标记“遇到长选项”(--xxx)
// 处理解析结果的函数类型
typedef int(OptHandler)(const char opt, char* const val);
// 命令行选项解析函数(支持短选项,不完全支持长选项)
// argc 和 argv 是 main 函数的参数。
// opts 是具有选项值的选项列表,比如 "abc" 表示 -a、-b、-c 具有选项值
// f 是处理单个解析结果的函数
void parseCmdline(const int argc, char* const argv[], const char* opts, OptHandler f) {
int i = 1; // 当前 argv 索引
int n = 0; // 当前选项字符的偏移(比如 -abcd 中 -c 的偏移就是 2)
while (i < argc) {
// 当前的 argv 元素,当前选项,当前选项值
char *arg = argv[i], opt = 0, *val = NULL;
switch(*arg) {
// 遇到选项标记
case '-': {
arg += n+1; // 定位到当前选项位置(比如 -abcd 中定位到 -c)
opt = *arg++; // 获取选项字符,然后 arg 指向下一个字符
// 如果选项字符也是 "-" 字符
if (opt == '-') {
if (n == 0) {
// 处理开头的 "--"
if (*arg == '\0') { // -- 后面无内容(特殊选项)
i++; opt = OPT_MINUS2;
} else { // -- 后面有内容(长选项)
opt = OPT_LONG; val = arg; i++;
}
} else {
// 跳过多余的 "-",比如 -a-b-c 中除了第一个之外的 "-" 字符
n++; continue;
}
// 选项字符也为 "-" 的情况处理完毕
break;
}
// 选项字符为 \0 的情况,说明只有一个 "-"(特殊选项)
if (opt == '\0') { opt = '-'; arg--; } // 让 arg 后退,指向 \0
// 判断是否为最后一个字符(比如 -abcd 中的 -d)
// 如果是最后一个字符,则处理下一个 arg,否则处理下一个选项
if (*arg == '\0') { i++; n = 0; } else n++;
// 选项名解析完毕,接下来检查该选项是否接受选项值
const char* idx = strchr(opts, opt);
// 接受选项值
if (idx != NULL) {
// n == 0 表示该选项名后面是空格,可能给出选项值,若不是空格
// 则不可能给出选项值,因为选项值和选项名之间必须以空格分隔。
// 同时 n == 0 也表示 i 指向了下一个 arg(见上面的代码)。
if (n == 0) {
val = argv[i]; // 获取选项值
// 如果该内容也是以 "-" 开头,则不是选项值,交给下一轮去处理。
// 如果不是以 "-" 开头,则是选项值,继续处理下一个 arg。
if (*val == '-') val = NULL; else i++;
}
}
// 其它情况:要么不接受选项值(不在 opts 中),要么不可能有选项值(无空格)
break;
}
// 自定义选项(不以 "-" 开头的选项)
default: val = arg; i++;
}
// 处理单个解析结果(如果返回非 0 则停止解析)
if (f(opt, val)) break;
}
}
// 命令行选项处理函数
// 返回值:0=继续解析,其它=中止解析
int optHandler(char opt, char* val) {
switch (opt) {
// 有选项值
case 'a':
case 'b':
case 'c':
if (val == NULL) {
printf("赋值选项:%c = 缺少值\n", opt);
// return 1; // 遇到无效选项,中止解析
} else
printf("赋值选项:%c = %s\n", opt, val);
break;
// 无选项值
case 'd':
printf("开关选项:%c\n", opt);
break;
// 特殊选项 -
case '-':
printf("特殊选项:%c\n", opt);
break;
// 特殊选项 --
case OPT_MINUS2:
printf("特殊选项:--\n");
break;
// 长选项 --xxx(可以在这里進一步分析长选项的值)
case OPT_LONG:
printf("--长选项:%s\n", val);
break;
// 自定义选项(无选项名,有选项值)
case OPT_CUSTOM:
printf("自定选项:%s\n", val);
break;
// 无效选项(以 "-" 开头的其它选项)
default:
printf("无效选项:%c\n", opt);
// return 1; // 遇到无效选项,中止解析
}
// 继续解析
return 0;
}
int main(int argc, char* argv[]) {
printf("程序名称:%s\n", argv[0]);
// 处理命令行参数(-a、-b、-c 可以有选项值)
parseCmdline(argc, argv, "abc", optHandler);
return 0;
}
下面来实现 -l dl phtread gl
这种选项的解析(只在改动的地方做了注释):
#include <stdio.h>
#include <string.h>
#define OPT_CUSTOM '\0'
#define OPT_MINUS2 '\1'
#define OPT_LONG '\2'
typedef int(OptHandler)(const char opt, char* const val);
// opts 是可接受的选项列表,无冒号表示可以接受一个选项值,有冒号表示可以接受多个选项值。
// 比如 "ab:c" 中的 -a、-c 可以有一个选项值,-b 可以有多个选项值,其它选项不能有选项值。
// 传递命令行参数时,可以使用 -- 切断多选项的延续,使后面的内容属于新的选项。
void parseCmdline(const int argc, char* const argv[], const char* opts, OptHandler f) {
int i = 1;
int n = 0;
char c = '\0'; // 增加一个变量,用来记录最后遇到的选项名
while (i < argc) {
char *arg = argv[i], opt = 0, *val = NULL;
switch(*arg) {
case '-': {
c = '\0'; // 复位最后遇到的选项名
arg += n+1;
opt = *arg++;
if (opt == '-') {
if (n == 0) {
if (*arg == '\0') {
i++; opt = OPT_MINUS2;
} else {
opt = OPT_LONG; val = arg; i++;
}
} else {
n++; continue;
}
break;
}
if (opt == '\0') { opt = '-'; arg--; }
if (*arg == '\0') { i++; n = 0; } else n++;
const char* idx = strchr(opts, opt);
if (idx != NULL) {
if (n == 0) {
// 记录最后遇到的选项名
if (*(idx+1) == ':') c = opt;
val = argv[i];
if (*val == '-') val = NULL; else i++;
}
}
break;
}
default: val = arg; i++;
}
if (c != '\0') opt = c; // 传入最后遇到的选项名
if (f(opt, val)) break;
}
}
int optHandler(char opt, char* val) {
switch (opt) {
case 'l':
case 'W':
case 'o':
if (val == NULL) {
printf("赋值选项:%c = 缺少值\n", opt);
// return 1;
} else
printf("赋值选项:%c = %s\n", opt, val);
break;
case 'g':
printf("开关选项:%c\n", opt);
break;
// 用来切断多选项值列表,这里不需要做任何处理
case OPT_MINUS2:
break;
case OPT_LONG: {
// 自己解析长选项
char* idx = strchr(val, '='); // 等号位置
char name[256] = {0}; // 选项名
if (idx > 0) { // 有等号
strncpy(name, val, idx - val); // 获取选项名
val = idx + 1; // 获取选项值
printf("--长选项:%s = %s\n", name, val);
} else { // 无等号
printf("--长选项:%s\n", val);
}
break;
}
case OPT_CUSTOM:
printf("自定选项:%s\n", val);
break;
default:
printf("无效选项:%c\n", opt);
// return 1;
}
return 0;
}
int main(int argc, char* argv[]) {
printf("程序名称:%s\n", argv[0]);
// 处理命令行参数(没有冒号表示可以接受一个选项值,有冒号表示可以接受多个选项值)
parseCmdline(argc, argv, "Wol:", optHandler);
return 0;
}
使用下面的命令行参数進行测试:
./a.out -g -l dl pthread gl -- a.cpp -W all -o hello b.cpp -l mylib1 mylib2 -- c.cpp --std=c++11 --nostdinc
执行结果如下:
程序名称:./a.out
开关选项:g
赋值选项:l = dl
赋值选项:l = pthread
赋值选项:l = gl
自定选项:a.cpp
赋值选项:W = all
赋值选项:o = hello
自定选项:b.cpp
赋值选项:l = mylib1
赋值选项:l = mylib2
自定选项:c.cpp
--长选项:std = c++11
--长选项:nostdinc
把上面的命令行写整齐一点就是这样:
# -g 启用调试
# -W all 开启所有警告
# -o hello 输出文件名为 hello
# a.cpp ... 要编译的源文件
# -l dl ... 要链接的库文件
# --std=c++11 使用 C++ 2.0 标准
# --nostdinc 不在缺省路径中搜索 include 文件
./a.out -g -W all -o hello a.cpp b.cpp c.cpp -l dl pthread gl mylib1 mylib2 --std=c++11 --nostdinc