最近有一个写命令行小软件的需求,用户输入服务器所在ip及端口,而后输入账户及密码后由命令行软件将请求发出,并将返回的JSON打印到控制台上。

软件的功能比较简单,但由于我没有写过完整的命令行软件,因此将整套命令行软件所需要的一些东西都搞了一遍,本文即为记录该软件编写过程及设计的备忘文。
命令行设计
这里首先讨论下命令行软件的一些常见设计:
- “-X” 短横加单字母则为缩写参数
- “–XXX” 双横加单词为完成参数
- “-h” 及 “–help” 为打印帮助手册后退出
- 当某必填参数未填写时,提示用户某参数未填写,而后退出
- 当程序第一次启动时生成默认配置文件(格式一般为yml、ini、cfg、conf等),当程序第二次运行时自动读取配置文件存储的用户配置信息
其实以上几点就包含了我暂时能想到的命令行软件的一些基本点,后面有时间再写下nodejs的命令行软件博客。
第三方库
- 命令行参数解析库 Argh!
- HTTP请求封装库 elnormous/HTTPRequest
- Yaml解析库 mini-yaml
命令行解析
首先命令行参数解析我之前一直直接用字符串匹配解析的方式,这次我在awsome c++ repo中找了个命令行参数解析库Argh!。

这个库相对来说使用起来也比较简单,这里就不多叙述了。
HTTP请求封装库
HTTP请求使用elnormous/HTTPRequest库,这个库使用起来比较简单,而且是single-header library非常易于集成,因此就偷懒使用这个库作为HTTP请求封装库。

这个库的使用方法不是很复杂就不多赘述了。
Yaml配置文件解析库
这个软件中只是将一个简单的键值对yaml作为配置文件,因此这里没有使用一些重量级的yaml库,而是使用一个轻量级的mini-yaml库作为整个软件的配置文件写入及读出。

代码
这里没有对account、password及其他数据进行合法性校验,但做校验难度也不大,这里就不额外修改代码了。
默认情况使用admin作为登录用户名,默认服务器端口和路径分别为1000、/api/login。
软件必填项为服务器IP及账户密码,当输入tLogin.exe -s=192.168.0.1 -P=123456时其余参数将使用缺省配置,即
POST http://192.168.0.1/api/login {"username":"admin",password:"123465"}
代码如下:
#include <iostream>
#include <string>
#include "./libs/argh.h"
#include "libs/HTTPRequest.hpp"
#include "libs/Yaml.hpp"
class LoginEntity{//简单封装
public:
std::string username;//用户名
std::string userpassword;//密码
std::string server;//服务器IP
std::string port;//服务端口
};
void show_help_header();//帮助手册打印函数
void login(LoginEntity& entity);//登录函数
Yaml::Node config;//yaml配置单例
void load_config();//加载配置文件
int main(int argc, char* argv[])
{
load_config();
LoginEntity entity;
argh::parser cmdl(argc,argv);//解析命令行参数
if(cmdl[{ "-h", "--help" }])//检查help参数是否存在
{
show_help_header();
return 0;
}
if(cmdl({ "-s", "--server" }))//检查 服务器IP 参数是否存在
{
entity.server = cmdl({ "-s", "--server" }).str();
config["server"]=entity.server;
}else{
if(!config["server"].IsNone()){
std::cout<< "[WARNING]:use default server address "<<config["server"].As<std::string>()<<std::endl;
entity.server = config["server"].As<std::string>();
}
else{
std::cout<< "[ERROR]:please input server address use '-s=<server>'\n";
return EXIT_FAILURE;
}
}
if(cmdl({ "-p", "--port" })){//检查 服务器端口 参数是否存在
try{
entity.port = cmdl({ "-p", "--port" }).str();
int port = stoi(entity.port);
if(port < 0 || port > 25565){
std::cout<<"[ERROR]:error server port ("<<cmdl({ "-p", "--port" }).str()<<")"<<std::endl;
return EXIT_FAILURE;
}
config["port"]=entity.port;
}
catch (...){
std::cout<<"[ERROR]:error server port ("<<cmdl({ "-p", "--port" }).str()<<")"<<std::endl;
return EXIT_FAILURE;
}
}
else{
if(!config["server"].IsNone()){
entity.port = config["port"].As<std::string>();
}
else{
config["port"] = "1000";
entity.port = "1000";
}
std::cout<<"[WARNING]:use default port ("<<entity.port<<")"<<std::endl;
}
if(cmdl({ "-P", "--password" }))//检查 密码是否存在
{
entity.userpassword = cmdl({ "-P", "--password" }).str();
}
else{
std::cout<<"[ERROR]:user password not configured."<<std::endl;
return EXIT_FAILURE;
}
if(cmdl({ "-a", "--account" })){//检查是否配置账户名
entity.username = cmdl({ "-a", "--account" }).str();
config["username"] = entity.username;
}
else{
if(!config["username"].IsNone()){
entity.username = config["username"].As<std::string>();
}
else{
std::cout<<"[ERROR]:user account not configured."<<std::endl;
return EXIT_FAILURE;
}
}
login(entity);
return EXIT_SUCCESS;
}
void show_help_header(){
std::cout << "-a=<account>, --account=<account>\n"
"\t\t user account name\n"
"-P=<password>, --password=<password>\n"
"\t\t user password\n"
"-s=<server>, --server=<server>\n"
"\t\t server ip address\n"
"-p=<port>, --port=<port>\n"
"\t\t server port\n";
}
void login(LoginEntity& entity){
//构建登录api URL
std::string url = config["protocol"].As<std::string>() +"://" + entity.server + ":" + entity.port +config["path"].As<std::string>();
//构建HTTP请求对象
http::Request request(url);
//HTTP请求体构建
//JSON类型数据,包含username及password数据,password没有加密,由于只是一个简单的应用因此没有做数据有效性校验
std::string body = "{\"username\":\""+entity.username+"\",\"password\":\""+entity.userpassword+"\"}";
try{
std::cout<<"Login Request Send to:"<<url<<std::endl;
const http::Response postResponse = request.send("POST",body,
{ std::make_pair("Content-Type","application/json")},
//5s 超时
std::chrono::milliseconds{5000}
);
//打印返回数据
std::cout << std::string(postResponse.body.begin(), postResponse.body.end()) << '\n';
//序列化到配置文件中
Yaml::Serialize(config,"tlogin.yml");
}
catch (http::ResponseError& e){
std::cout<<e.what()<<".Please check configurations Or try again."<<std::endl;
}
}
void load_config(){
Yaml::Node& root = config;
try{
Yaml::Parse(root, "tlogin.yml");
}
catch (...){
//默认配置文件
Yaml::Node & ref = root;
ref["port"]="1000";//服务器默认端口
ref["username"]="admin";//默认用户名
ref["path"]="/api/login";//服务器默认登录API路径
ref["protocol"]="http";//服务器协议
Yaml::Serialize(root,"tlogin.yml");//序列化默认配置文件(覆盖原文件)
}
}
生成的默认yaml文件:

v0.3wep 简单写写