Github
官方文档
Gflags简明教程 http://dreamrunner.org/blog/2014/03/09/gflags-jian-ming-shi-yong/
GFlags使用文档 http://www.yeolar.com/note/2014/12/14/gflags/
简介
GFlags是Google开源的一套命令行参数处理的开源库,包括C++的版本和python 版本。和 getopt() 之类的库不同,flag的定义可以散布在各个源码中,而不用放在一起。一个源码文件可以定义一些它自己的flag,链接了该文件的应用都能使用这些flag。这样就能非常方便地复用代码。如果不同的文件定义了相同的flag,链接时会报错。
定义Flags
定义一个flag是简单的:只需要使用你想用的类型的相应的宏就可以。
使用flags需要包含头文件 :
1 | #include <gflags/gflags.h> |
example:
1 2 3 4 5 6 | // foo.cc #include <gflags/glags.h> DEFINE_bool(big_menu, true, "Include 'advanced' options in the menu listing"); DEFINE_string(languages, "english,french,german", "comma-separated list of languages to offer in the 'lang' menu"); |
示例代码分别定义了一个bool和一个string类型的参数,该宏的三个参数含义分别为命令行参数名,参数默认值,以及参数的帮助信息。gflag不支持列表,用户通过灵活借助string参数实现,比如上述的languages参数,可以类型为string,但可看作是以逗号分割的参数列表。
支持的类型:
- DEFINE_bool: boolean
- DEFINE_int32: 32-bit integer
- DEFINE_int64: 64-bit integer
- DEFINE_uint64: unsigned 64-bit integer
- DEFINE_double: double
- DEFINE_string: C++ string
DEFINE宏包含三个参数:flag名、默认值、描述方法的帮助。帮助会在执行 --help flag时显示。
可以在任何源文件中定义flag,但是每个只能定义一次。如果需要在多处使用,那么在一个文件中 DEFINE ,在其他文件中 DECLARE 。比较好的方法是在 .cc 文件中 DEFINE ,在 .h 文件中 DECLARE ,这样包含头文件即可使用flag了。
使用flag访问参数
当参数被定义后,通过FLAGS_name就可访问到对应的参数,定义的flag可以像正常的变量一样使用,只需在前面加上 FLAGS_前缀。如前面例子中的定义了 FLAGS_big_menu 和 FLAGS_languages两个变量。可以像一般变量一样读写:
1 2 3 4 | if (FLAGS_consider_made_up_languages) FLAGS_languages += ",klingon"; // implied by --consider_made_up_languages if (FLAGS_languages.find("finnish") != string::npos) HandleFinnish(); |
以上的访问方式,仅在参数定义和访问在同一个文件(或是通过头文件包含)时,FLAGS_name才能访问到参数,如果要访问其他文件里定义的参数,则需要使用DECLARE_type。比如在foo.cc中DEFINE_string(color, “red”, “the color you want to use”); 这时如果你需要在foo_test.cc中使用color这个参数,你需要加入DECLARE_string(color, “red”, “the color you want to use”);
参数检查
定义参数后,可以给参数注册一个检查函数(validator),当从命令行指定参数或通过SetCommandLineOption()指定参数时,检查函数就会被调用,两个参数分别为命令行参数名,以及设置的参数值
1 2 3 4 5 6 7 8 | static bool ValidatePort(const char* flagname, int32 value) { if (value > 0 && value < 32768) // value is ok return true; printf("Invalid value for --%s: %d\n", flagname, (int)value); return false; } DEFINE_int32(port, 0, "What port to listen on"); static const bool port_dummy = RegisterFlagValidator(&FLAGS_port, &ValidatePort) |
建议在定义参数后,立即注册检查函数。RegisterFlagValidator()在检查函数注册成功时返回true;如果参数已经注册了检查函数,或者检查函数类型不匹配,返回false。
初始化所有参数
在引用程序的main()里通过 google::ParseCommandLineFlags(&argc, &argv, true); 即完成对gflags参数的初始,其中第三个参数为remove_flag,如果为true,gflags会移除parse过的参数,否则gflags就会保留这些参数,但可能会对参数顺序进行调整。 比如 “/bin/foo” “arg1” “-q” “arg2” 会被调整为 “/bin/foo”, “-q”, “arg1”, “arg2”,这样更好理解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #include <iostream> #include <gflags/gflags.h> DEFINE_bool(isvip, false, "If Is VIP"); DEFINE_string(ip, "127.0.0.1", "connect ip"); DECLARE_int32(port); DEFINE_int32(port, 80, "listen port"); int main(int argc, char** argv) { google::ParseCommandLineFlags(&argc, &argv, true); //初始化所有参数 std::cout<<"ip:"<<FLAGS_ip<<std::endl; std::cout<<"port:"<<FLAGS_port<<std::endl; if (FLAGS_isvip) { std::cout<<"isvip:"<<FLAGS_isvip<<std::endl; } google::ShutDownCommandLineFlags(); return 0; } |
链接时使用 -gflags ,运行使用 ./gflags -ip=“211.152.52.106” -port=8080 -isvip=true ,但是很遗憾,使用 valgrind 检测有内存泄漏。
输出结果如下:
1 2 3 | ip:211.152.52.106 port:8080 isvip:1 |
在命令行指定参数
比如要在命令行指定languages参数的值,可通过如下4种方式,int32, int64等类型与string类似。
app_containing_foo --languages=“chinese,japanese,korean”
app_containing_foo -languages=“chinese,japanese,korean”
app_containing_foo --languages “chinese,japanese,korean”
app_containing_foo -languages “chinese,japanese,korean”
对于bool类型,则可通过如下几种方式指定参数
app_containing_foo --big_menu
app_containing_foo --nobig_menu
app_containing_foo --big_menu=true
app_containing_foo --big_menu=false
特殊参数
–help 打印定义过的所有参数的帮助信息
–version 打印版本信息 通过google::SetVersionString()指定
–nodefok 但命令行中出现没有定义的参数时,并不退出(error-exit)
–fromenv 从环境变量读取参数值 --fromenv=foo,bar表明要从环境变量读取foo,bar两个参数的值。通过export FLAGS_foo=xxx; export FLAGS_bar=yyy 程序就可读到foo,bar的值分别为xxx,yyy。
–tryfromenv 与–fromenv类似,当参数的没有在环境变量定义时,不退出(fatal-exit)
–flagfile 从文件读取参数值,–flagfile=my.conf表明要从my.conf文件读取参数的值。在配置文件中指定参数值与在命令行方式类似,另外在flagfile里可进一步通过–flagfile来包含其他的文件。
Caffe实例解析
以 tools/caffe.cpp 为例分析gflags使用方法
在tools/caffe.cpp中首先包含了gflags的头文件
1 | #include <gflags/gflags.h> |
命令行参数传递
接下来定义了一些需要从命令行传递参数的变量
1 2 3 4 5 6 7 8 9 10 11 12 13 | DEFINE_string(gpu, "", "Optional; run in GPU mode on given device IDs separated by ','." "Use '-gpu all' to run on all available GPUs. The effective training " "batch size is multiplied by the number of devices."); DEFINE_string(solver, "", "The solver definition protocol buffer text file."); DEFINE_string(model, "", "The model definition protocol buffer text file."); DEFINE_string(phase, "", "Optional; network phase (TRAIN or TEST). Only used for 'time'."); DEFINE_int32(level, 0, "Optional; network level."); ...... |
在main函数中调用的GlobalInit()函数中调用了ParseCommandLineFlags()
1 2 | // Google flags. ::gflags::ParseCommandLineFlags(pargc, pargv, true); |
这个函数的作用就是解析命令行参数。
pargc,pargv为命令行传递的参数个数和参数表,第三个参数作用为:
值 | 作用 |
---|---|
true | 函数处理完成后,argv中只保留argv[0],argc会被设置为1 |
false | argv和argc会被保留,但是注意函数会调整argv中的顺序。 |
执行完这参数后,就可以用FLAGS_xxx访问变量了。
例如caffe.cpp中, FLAGS_solve表示命令行参数传递的模型文件,FLAGS_weights表示令行参数传递的权值文件
1 2 3 4 5 6 | // Train / Finetune a model. int train() { CHECK_GT(FLAGS_solver.size(), 0) << "Need a solver definition to train."; CHECK(!FLAGS_snapshot.size() || !FLAGS_weights.size()) << "Give a snapshot to resume training or weights to finetune " "but not both."; |
帮助信息
gflags::SetUsageMessage函数用于设置命令行帮助信息。
1 2 3 4 5 6 7 8 | // Usage message. gflags::SetUsageMessage("command line brew\n" "usage: caffe <command> <args>\n\n" "commands:\n" " train train or finetune a model\n" " test score a model\n" " device_query show GPU diagnostic information\n" " time benchmark model execution time"); |
设置帮助信息后,当参数错误或加 -help选项是可以打印帮助信息
1 2 3 4 5 6 7 8 9 10 | ./build/tools/caffe caffe: command line brew usage: caffe <command> <args> commands: train train or finetune a model test score a model device_query show GPU diagnostic information time benchmark model execution time ... |
版本信息
gflags::SetVersionString用于设置版本信息
1 2 | // Set version gflags::SetVersionString(AS_STRING(CAFFE_VERSION)); |
这里的CAFFE_VERSION是在Makefile中定义的
当命令行参数加 -version 时会打印版本信息
1 | ./build/tools/caffe -version |
caffe version 1.0.0-rc3
demo
1 2 3 4 | //demo_gflags.cc #include "gflags/gflags.h" DEFINE_string(server_ip, "127.0.0.1", "server ip"); DEFINE_int32(server_port, 8080, "server port"); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // demo.cc #include <iostream> #include "gflags/gflags.h" DEFINE_bool(test_bool, false, "test bool value"); DEFINE_int32(test_int32, 666, "test int value"); DECLARE_string(server_ip); DECLARE_int32(server_port); int main(int argc, char** argv) { google::ParseCommandLineFlags(&argc, &argv, true); // for (int i = 0; i < argc; i++) { // std::cout << argv[i] << std::endl; // } if (FLAGS_test_bool) { std::cout << "test_bool is true." << std::endl; std::cout << "test_int32: " << FLAGS_test_int32 << std::endl; } std::cout << "server ip: " << FLAGS_server_ip << " , server port: " << FLAGS_server_port << std::endl; return 0; } |
1 2 3 4 5 6 7 8 | // CMakeLists.txt cmake_minimum_required(VERSION 3.5.1) project(gflag) find_package(gflags REQUIRED) add_executable(${PROJECT_NAME}_demo demo.cc demo_gflags.cc) target_link_libraries(${PROJECT_NAME}_demo ${GFLAGS_LIBRARIES}) |
1 2 3 | //demo.gflags -server_ip="192.168.0.123" -server_port=8899 |
编译
1 2 3 4 | $ cd gflag $ mkdir build $ cmake .. $ make |
执行
1 2 3 4 5 6 7 8 9 | # 不加命令行 $ ./gflag_demo # 加命令行变量控制 `-test_bool=true` test_bool为程序开头定义的标志 $ ./gflag_demo -test_bool=true -test_int32=666 # 加命令行文件输入 `-flagfile=filename` 注意路径, - 和 -- 一样 $ ./gflag_demo -flagfile=../demo.gflags ##文件绝对路径 # 其他 # -fromenv=value 从环境中读取value # --tryfromenv=value 环境中是否未定义不是致命错误 |
gflags使用规范
任何好用的工具如果使用不当都会带来不好的后果,gflags也是一样。我遇到过一些gflags的“坑”,还从领导和同事那里获得一些好的想法,整理成7条gflags使用规范。有意识的遵循这些规范,对项目的开发维护和自身的技术成长都将有很大的益处。
- 规范1:bool类型的gflags默认值设置成false,防止误启用新功能。
新的功能上线一定要经过代码审查、测试和验证流程,默认为true的gflags风险太大。 - 规范2:应定时清理旧的gflags。
随着时间的流逝,代码里的gflags会越来越多,当你的工程代码里包含成百上千行的gflags时,阅读和维护代码的体验简直是太过酸爽。非常有必要定时删除代码中旧的gflags,根据其开关打开(true)和关闭(false)情况来删除gflags及其相关代码。 - 规范3:清理旧的gflags时应同时删除相应的gflags配置,以保证线上配置的整洁。
配置文件和代码保持同步是一种非常好的开发和维护体验。 - 规范4:if语句中应尽量避免gflags参与逻辑运算。
当if语句中出现与gflags相关的与、或、非逻辑运算时,事情就会变得复杂起来,gflags的开关状态不再是唯一的决定因素,代码阅读和删除gflags也会变得十分困难。我曾经删错过一个旧的gflags,幸运的是在CR阶段(CodeReview,代码审查)被细心的同事指出,避免了一次踩坑。这是那段令我难忘的代码的样子:
在这个例子中同时出现了与、或、非这3个逻辑运算,它是工程中真实存在的代码,绝非由我杜撰。此时gflags a、b、c、d的值都是false,现在的任务是删除这些旧的gflags和它们包住的代码,应该保留哪部分代码呢?如果你和我一样忘记了或运算||和与运算&&谁的优先级更高,那么掉到坑里的概率非常大。言归正传,我们有很多方法避免这样的代码出现,gflags绝不应该参与复杂的逻辑运算。 - 规范5:公共模块的gflags应尽快删除。
公共模块的gflags在上线运行一段时间后,应尽快删除,理想的情况是公共模块都没有gflags包含。这样做的理由是使用公共模块往往不知道这些gflags的存在,非常容易留坑。 - 规范6:不要在单元测试代码中使用gflags。
如果UT(UnitTest,单元测试)代码里用到了gflags,情况会变得复杂,在删除旧的gflags时需要同步修改单元测试代码,否则会导致测试失败,jenkins上的任务会变红(即测试失败)。因此,最好不要在单元测试代码里使用gflags。 - 规范7:提交代码时,应记录新增或删除的gflags配置。
这样的好处是方便测试的同事进行测试,这样利人利己的规范是非常值得遵守的。
最后,我把这7条规范总结并整理成一张图片,欢迎大家留言补充更多、更好的gflags使用规范。
- 金句分享
人的天性之一,就是不会接受别人的批评,总是认为自己永远是对的,喜欢找各种各样的借口为自己辩解。
——出自《人性的弱点》,戴尔·卡耐基(Dale Carnegie),美国著名人际关系学大师。
解读:永远不要批评别人,因为指责只不过是在浪费自己和他人的时间,应该换种方式去沟通和解决问题。