首选的现代C++风格命令行参数解析器!
(本课程包含两段教学视频。)
- 以文件对象监控程序为实例,五分钟实现从命令行读入多个监控目标路径;
- 区分两大时机,学习 CLI11 构建与解析参数两大场景下的异常处理;
- 区分三种选项:匿名vs.具名、选项vs.标志、必选vs.可选,全面掌握CLI11
0. 楔
本课含两段教学视频
在代码中,将关键业务数据写死,通常不是一个好做法。譬如前面课堂中基于 libwatch 所写的文件对象变动信息监控工具,用户拿到手后,就只能监控一个既定的位置:d:/tmp ,在真实工作中,这是妥妥要主动离职的节奏……
有两种常用方法用于解决类似此类的,要在运行前才能获得关键信息的问题:
- 命令行参数:运行程序时,通过在程序后面加参数(称为 command-line arguments)获取信息;
- 配置文件:程序运行后,从指定文件中读取相关配置。
这节课我们讲第1种方法,下一节课讲第2种方法,以及(真正的重点)两种方法的配合。
命令行参数和配置文件本就是为了同一个业务目标(程序运行时取得外部关键可变信息),因此,如果库本身未能支持与另一方的配合,二者的配合逻辑就只能由程序员完成。CLII很好的支持和配置文件的结合,是我们将它作为首选学习(也推荐在工作中使用)的命令行参数解析库的重要原因(甚至没有之一)。
1. 快速感受
CLI11 是一个纯头文件的库,使用起来非常方便。
纯头文件库:只有头文件,使用时只需 include 必要的头文件即可,无需事先编译库本身。缺点是:通常此类库的头文件都相对巨大,会造成目标项目每次编译时,为其耗费较长的时间。在后面的练习中,你将痛苦地感受到一点(不要问视频中为什么编译那么快,因为很多等待过程被一剪没了……)。
在 msys2 (以UCRT64环境为例)中安装 CLI11 :
pacman -S mingw-w64-ucrt-x86_64-cli11
我们以实现文件监控工具监控目标可通过命令行参数设定为案例,快速感受 CLI11 的功能与基本使用方式。
- 视频1
007-CLI11命令行解析-1快速感受-C++108杰
- 关键代码
加入CLI以实现从命令行读取待监控路径(支持多个)功能后,HellFSWatch 项目的主函数为:
int main(int argc, char** argv)
{// 一、定义一个CLI::App 的变量CLI::App app("HelloFSWatch");// 二、定义需要从命令行参数读入的变量std::vector<std::string> paths;// 三、添加参数项app.add_option("paths", paths, "paths to watch"); // 四、开始解析命令行app.parse(argc, argv);if (paths.empty()){std::cerr << "paths is empty!" << std::endl;return -1;}else{for (auto const& path : paths){std::cout << path << std::endl;}}auto *monitor = fsw::monitor_factory::create_monitor(system_default_monitor_type,paths,on_file_changed);// 启动监控monitor->start(); // 进入死循环
}
这段代码能工作,但相当脆弱,用户一不小心给错参数就可能造成程序发生异常,并相当不体面地直接退出……,详细改进方法,请同学们学完下一小节 “2. 用法详解” 后,结合作业完成(完成后可提交给老师)。
2. 用法详解
在“用法详解”的视频中,我们将学习到:
- 异常处理:构建阶段的异常处理和解析阶段的异常处理;
- 匿名参数与具名参数:其中的匿名参数也称为位置参数,因为此类区分此类参数在命令行中没有名字指示,因此,最终识别它们的唯一方法,是通过它们出现在命令行中的位置加以区分;
- 长名字、短名字(注意,超过一个字母,就叫长名字);
- option 和 flag 之分;
- 必选和可选之分;
- 命令行参数与配置文件的基本配合思路。
- 视频2
008-CLI11命令行解析-2用法详解-C++108杰
- 代码 - CMakeLists.txt
cmake_minimum_required(VERSION 3.10.0)
project(HelloCLI11 VERSION 0.1.0 LANGUAGES C CXX)set(CMAKE_EXE_LINKER_FLAGS "-static")add_executable(HelloCLI11 main.cpp)
其中添加 “-staic” 链接选项并非必须,仅为方便在控制台(包括VSCODE集成的终端中)直接运行例程(否则,需要复制一大堆相关的动态库到程序目录下才能运行)。
- 代码 - main.cpp
#include <cstdlib>#include <iostream>
#include <vector>
#include <string>
#include <iomanip>#include <CLI/CLI.hpp>int main(int argc, char** argv)
{std::system("chcp 65001 > nul");CLI::App app ("一个CLI11命令行参数解析的例子");std::vector<std::string> paths;int maxCount = 0;bool createdOnly = false;try{app.add_option("paths", paths, "待监控的文件夹路径(可含多个)")->required();app.add_option("--max-count,-m", maxCount, "最大消息数");app.add_flag("-c,--created-only", createdOnly, "只关注新建信息");}catch(CLI::ConstructionError const& e){return app.exit(e);}CLI11_PARSE(app, argc, argv);std::cout << "待监控的目录:\n";for (auto const& path: paths){std::cout << path << "\n";}std::cout << "max-count : " << maxCount << "\n";std::cout << "created-only: " << std::boolalpha << createdOnly << "\n";std::system("pause");
}
其中的 CLI11_PARSE 是 CLI11 提供的宏,CLI11_PARSE (app, argc, argv) 大体将对应到以下代码:
try{app.parse(argc, argv); }catch(CLI::ParseError const& e){return app.exit(e);}
3. 知识小结
- 构建时期产生的异常,基类为 CLI::ConstructionError ,此类错误通常是我们在使用add_option()或add_flag()时,所配置的参数项逻辑不合理,比如:同名的参数,或者误以为可以添加名字超过一个字母的“短名字”;
- app.parse() 过程产生的异常,称为 “CLI::ParseError”,是一种典型的运行期异常:因为它通常由程序用户的错误造成(给出错误的命令行参数,比如,未给必选项参数),躲都没处躲,因此一定要加 try-catch 处理,为此,CLI11 干脆给了一个宏来简化处理;
- 再次强调:超过一个字母,就算长名字,必须在前面加两个杠
--
;一个字母才是短名字,加一个杠-
;一个杠都不用加的,课程称之为 “匿名参数”,不过 CLI11 自身称之为 positionals 参数,即基于位置识别的参数; - 绑定变量类型主要支持:整数、字符串、布尔值等,以及对应的动态数组(std::vector);
- 当绑定的变量是 std::vector 类型,CLI11 会自动帮我们尝试读入连续的参数,存放到目标 vector 对象中;
- 类型是布尔值时,也可以通过是否提供该参数(名字)来推理参数值:有提供为true,否则为false,使用此方法时 ,需要通过
app.add_flag()
来添加参数; - 同一参数的短名称和长名称,可在指定名称时,通过逗号分隔,二者次序无关,如例程代码中的 “–max-count,-m” 以及 “-c,–created-only”;
- 通过级联调用
->required()
,可将对应参数设置为必选项; - CLI11 已经自动添加了
--help,-h
参数,因此我们不能再添加它们。
如何避免“看着都懂了,一要写代码就又不会”的学习困境?想想,网络学习,总是差了点什么?当然是 “作业” 呀!请到 d2school.com 网站进入本文对应的课堂,做练习并交给老师批改(可能需要花10毛)。