最近有一个写命令行小软件的需求,用户输入服务器所在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 简单写写