命令行参数解析,竟然这么简单?!

645 阅读4分钟

大家好,我是春哥,一名拥有10多年Linux后端研发经验的BAT互联网老兵。

作为一个后端研发人员,在日常工作中,避免不了的都会写一些命令行的小工具,用于提升工作效率。一个完备的命令行工具需要支持灵活的命令行参数解析。由于在C/C++中getopt系列的库函数晦涩难用,因此劝退很多人使用getopt相关的函数。结果就是在实现上工具的命令行的每个位置上的参数含义都是固定的,不能随意调整,毫无美感可言,在工作中也经常看到这样的实现。

那么怎么解决这个问题呢?还是一贯的思路,没有好用的我们就自己造,「只有自己创造过的,才能真正理解」。在之前分享的文章中,春哥已经自己编写了极简的单元测试框架、配置文件读取与业务配置读取类的自动生成。相关文章列表如下。

ini配置文件还可以这么读,以前的代码都白写了

极简框架,助力单元测试落地实施

今天就和大家一起来聊聊怎么来实现简单易用的命令行参数解析库。

实现

现在让我们来看一下具体的实现。

  • cmdline.h
#pragma once

#include <string>

namespace CmdLine {
typedef void (*Usage)();
void BoolOpt(bool* value, std::string name);  // 非必设的bool类型的选项,默认值都是false,bool选项是命令行的功能开关
void Int64Opt(int64_t* value, std::string name, int64_t default_value);  // 非必设的int64_t类型的选项,可以指定默认值
void StrOpt(std::string* value, std::string name,
            std::string default_value);                     // 非必设的string类型的选项,可以指定默认值
void Int64OptRequired(int64_t* value, std::string name);    // 必设的int64_t类型的选项
void StrOptRequired(std::string* value, std::string name);  // 必设的string类型的选项
void SetUsage(Usage usage);
void Parse(int argc, char* argv[]);
}  // namespace CmdLine

  • cmdline.cpp
#include "cmdline.h"

#include <stdint.h>
#include <stdio.h>
#include <string.h>

#include <map>

namespace CmdLine {
class Opt;  // 前置声明
static Usage usage_ = nullptr;
static std::map<std::string, Opt> opts_;

enum OptType {
  INT64_T = 1,
  BOOL = 2,
  STRING = 3,
};

class Opt {
 public:
  Opt() = default;
  Opt(bool* value, std::string name, bool default_value, bool required) {
    init(BOOL, value, name, required);
    *(bool*)value_ = default_value;
  }
  Opt(int64_t* value, std::string name, int64_t default_value, bool required) {
    init(INT64_T, value, name, required);
    *(int64_t*)value_ = default_value;
  }
  Opt(std::string* value, std::string name, std::string default_value, bool required) {
    init(STRING, value, name, required);
    *(std::string*)value_ = default_value;
  }
  bool IsBoolOpt() { return type_ == BOOL; }
  void SetBoolValue(bool value) {
    value_is_set_ = true;
    *(bool*)value_ = value;
  }
  void SetValue(std::string value) {
    if (type_ == STRING) *(std::string*)value_ = value;
    if (type_ == INT64_T) *(int64_t*)value_ = atoll(value.c_str());
    value_is_set_ = true;
  }
  bool CheckRequired() {
    if (not required_) return true;
    if (required_ && value_is_set_) return true;
    printf("required option %s not set argument\n", name_.c_str());
    return false;
  }

 private:
  void init(OptType type, void* value, std::string name, bool required) {
    type_ = type;
    name_ = name;
    value_ = (void*)value;
    required_ = required;
    if (required_) value_is_set_ = false;
  }

 private:
  OptType type_;             // 选项类型
  std::string name_;         // 选项名称
  void* value_;              // 选项值
  bool value_is_set_{true};  // 选项值是否被设置
  bool required_{false};     // 选项是否必设
};

static bool isValidName(std::string name) {
  if (name == "") return false;
  if (name[0] == '-') {
    printf("option %s begins with -\n", name.c_str());
    return false;
  }
  if (name.find("=") != name.npos) {
    printf("option %s contains =\n", name.c_str());
    return false;
  }
  return true;
}

static int ParseOpt(int argc, char* argv[], int& parse_index) {
  char* opt = argv[parse_index];
  int opt_len = strlen(opt);
  if (opt_len <= 1) {  // 选项的长度必须>=2
    printf("option's len must greater than or equal to 2\n");
    return -1;
  }
  if (opt[0] != '-') {  // 选项必须以'-'开头
    printf("option must begins with '-', %s is invalid option\n", opt);
    return -1;
  }
  opt++;  // 过滤第一个'-'
  opt_len--;
  if (*opt == '-') {
    opt++;  // 过滤第二个'-'
    opt_len--;
  }
  // 过滤完有效的'-'之后还要再check一下后面的内容和长度
  if (opt_len == 0 || *opt == '-' || *opt == '=') {
    printf("bad opt syntax:%s\n", argv[opt_len]);
    return -1;
  }
  // 执行到这里说明是一个选项,接下来判断这个选项是否已经指定了参数
  bool has_argument = false;
  std::string argument = "";
  for (int i = 1; i < opt_len; i++) {
    if (opt[i] == '=') {
      has_argument = true;
      argument = std::string(opt + i + 1);  // 取等号之后的内容赋值为argument
      opt[i] = 0;                           // 这样opt指向的字符串就是'='之前的内容。
      break;
    }
  }
  std::string opt_name = std::string(opt);
  if (opt_name == "help" || opt_name == "h") {  // 有help选项,则直接调用_usage函数,并退出
    if (usage_) usage_();
    exit(0);
  }
  auto iter = opts_.find(opt_name);
  if (iter == opts_.end()) {  // 选项不存在
    printf("option provided but not defined: -%s\n", opt_name.c_str());
    return -1;
  }
  if (iter->second.IsBoolOpt()) {  // 不需要参数的bool类型选项
    iter->second.SetBoolValue(true);
    parse_index++;  // parseIndex跳到下一个选项
  } else {          // 需要参数的选项,参数可能在下一个命令行参数中
    if (has_argument) {
      parse_index++;
    } else {
      if (parse_index + 1 < argc) {  // 选项的值在下一个命令行参数
        has_argument = true;
        argument = std::string(argv[parse_index + 1]);
        parse_index += 2;  // parseIndex跳到下一个选项
      }
    }
    if (not has_argument) {
      printf("option needs an argument: -%s\n", opt_name.c_str());
      return -1;
    }
    iter->second.SetValue(argument);
  }
  return 0;
}

static bool checkRequired() {
  auto iter = opts_.begin();
  while (iter != opts_.end()) {
    if (!iter->second.CheckRequired()) return false;
    iter++;
  }
  return true;
}

static void setOptCheck(const std::string& name) {
  if (opts_.find(name) != opts_.end()) {
    printf("%s opt already set\n", name.c_str());
    exit(-1);
  }
  if (not isValidName(name)) {
    printf("%s is invalid name\n", name.c_str());
    exit(-2);
  }
}

void BoolOpt(bool* value, std::string name) {
  setOptCheck(name);
  opts_[name] = Opt(value, name, false, false);
}

void Int64Opt(int64_t* value, std::string name, int64_t default_value) {
  setOptCheck(name);
  opts_[name] = Opt(value, name, default_value, false);
}

void StrOpt(std::string* value, std::string name, std::string default_value) {
  setOptCheck(name);
  opts_[name] = Opt(value, name, default_value, false);
}

void Int64OptRequired(int64_t* value, std::string name) {
  setOptCheck(name);
  opts_[name] = Opt(value, name, 0, true);
}

void StrOptRequired(std::string* value, std::string name) {
  setOptCheck(name);
  opts_[name] = Opt(value, name, "", true);
}

void SetUsage(Usage usage) { usage_ = usage; }

void Parse(int argc, char* argv[]) {
  if (nullptr == usage_) {
    printf("usage function not set\n");
    exit(-1);
  }
  int parse_index = 1;  // 这里跳过命令名不解析,所以parseIndex从1开始
  while (parse_index < argc) {
    if (ParseOpt(argc, argv, parse_index)) {
      exit(-2);
    }
  }
  if (not checkRequired()) {  // 校验必设选项,非必设的则设置默认值
    usage_();
    exit(-1);
  }
}
}  // namespace CmdLine

Opt类实现了对命令行参数的封装,它支持三种数据类型:bool、int64_t和std::string,并且可以指定选项是否必须设置,非必设的选项还可以设置默认值。

Parse函数用于解析命令行参数并将其转换为对应的选项值。Parse函数接受三个参数:argc表示命令行参数的数量,argv表示命令行参数的数组,parse_index表示当前解析到的命令行参数的索引。

Parse函数首先判断当前命令行参数是否是一个选项,如果不是则返回错误。如果是一个选项,则解析选项的名称和参数(如果有的话),并根据选项的类型设置选项的值。如果选项不存在,则返回错误。如果选项是help选项,则直接调用_usage函数并退出。

BoolOpt、Int64Opt和StrOpt函数用于设置可选选项,并支持设置默认值。Int64OptRequired和StrOptRequired函数用于设置必设选项。

SetUsage函数用于设置输出命令使用说明的函数。

示例

了解了相关的API,我们来看一下如何使用。示例代码文件demo.cpp的内容如下。

#include <stdio.h>

#include <iostream>

#include "cmdline.h"

using namespace std;

std::string type;
std::string name;
int64_t len;
int64_t age;
bool flag;

void usage() {
  cout << "demo -type product -name wanmuc -age 18 -flag" << endl;
  cout << "options:" << endl;
  cout << "    -h,--help      print usage" << endl;
  cout << "    -type          just type" << endl;
  cout << "    -name          just name" << endl;
  cout << "    -len           just len" << endl;
  cout << "    -age           just age" << endl;
  cout << "    -flag          just flag" << endl;
  cout << endl;
}

int main(int argc, char* argv[]) {
  CmdLine::BoolOpt(&flag, "flag");
  CmdLine::Int64Opt(&len, "len", 10);
  CmdLine::StrOpt(&type, "type", "test");
  CmdLine::StrOptRequired(&name, "name");
  CmdLine::Int64OptRequired(&age, "age");
  CmdLine::SetUsage(usage);
  CmdLine::Parse(argc, argv);
  std::cout << "type = " << type << std::endl;
  std::cout << "len = " << len << std::endl;
  std::cout << "flag = " << flag << std::endl;
  std::cout << "name = " << name << std::endl;
  std::cout << "age = " << age << std::endl;
  return 0;
}

完整项目

春哥已将这部分代码开源并托管在GitHub上,项目页面包含了详细的使用说明和编译脚本。传送门:github.com/wanmuc/CmdL…。欢迎大家积极参与,进行fork、star以及提出issue。如果因为网络问题导致无法下载,可以评论加关注,我会直接将项目完整源码发送给你。

写在最后

今天分享的内容就到这里,『如果本文对你有所帮助,记得关注我,我将持续在知乎上分享技术干货,关注我,下期分享不迷路』。

硬核爆肝码字,跪求一赞!!!