Skip to content

Commit 17e9ce0

Browse files
committed
base impl
0 parents  commit 17e9ce0

File tree

13 files changed

+1364
-0
lines changed

13 files changed

+1364
-0
lines changed

.github/workflows/ci.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
pull_request:
7+
branches: [main, master]
8+
9+
jobs:
10+
build-and-test:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout
14+
uses: actions/checkout@v4
15+
16+
- name: Setup xmake
17+
uses: xmake-io/github-action-setup-xmake@v1
18+
with:
19+
xmake-version: latest
20+
package-cache: true
21+
22+
- name: Install dependencies
23+
run: |
24+
sudo apt-get update
25+
sudo apt-get install -y build-essential
26+
27+
- name: Install Xlings
28+
run: curl -fsSL https://d2learn.org/xlings-install.sh | bash
29+
30+
- name: Install GCC 15.1 with Xlings
31+
run: |
32+
export PATH=/home/xlings/.xlings_data/bin:$PATH
33+
xlings install gcc@15.1 -y
34+
35+
- name: Build
36+
run: |
37+
export PATH=/home/xlings/.xlings_data/bin:$PATH
38+
xmake -y
39+
40+
- name: Test
41+
run: |
42+
export PATH=/home/xlings/.xlings_data/bin:$PATH
43+
xmake run cmdline_test
44+
45+
- name: Run examples (smoke)
46+
run: |
47+
export PATH=/home/xlings/.xlings_data/bin:$PATH
48+
xmake run with_dispatch -- add python 3.12
49+
xmake run with_dispatch -- remove foo

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.xmake
2+
build

README.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# cmdline
2+
3+
C++23 命令行解析库,风格参考 clap。模块化设计,单 import,无裸指针 API。
4+
5+
- **C++23** 模块
6+
- **类型命名**`ParsedArgs`(解析结果)、`OptionValue`(选项取值)
7+
- **解析 API**`parse(argc, argv)``parse_from(span<string>)``parse_from(string_view)`,支持多种输入
8+
- **子命令分发**`on_run(lambda)` + `dispatch(parsed)`,也可分离构建后自行根据 `subcommand_name()` 分发
9+
- **全局选项**`get_flag()` / `get_one()``std::expected` 解析结果
10+
11+
## 快速开始
12+
13+
```cpp
14+
import std;
15+
import cmdline;
16+
17+
int main(int argc, char* argv[]) {
18+
using namespace cmdline;
19+
20+
auto app = App("myapp")
21+
.version("1.0.0")
22+
.about("My CLI")
23+
.arg(Arg("input").help("Input file").required())
24+
.opt(Opt("verbose").short_name('v').long_opt("verbose").help("Verbose"))
25+
.opt(Opt("config").long_opt("config").takes_value().value_name("FILE"));
26+
27+
auto result = app.parse(argc, argv);
28+
if (!result) {
29+
if (result.error() == "help requested" || result.error() == "version requested")
30+
return 0;
31+
std::println("Error: {}", result.error());
32+
return 1;
33+
}
34+
const ParsedArgs& parsed = *result;
35+
if (parsed.get_flag("verbose")) { /* ... */ }
36+
if (auto c = parsed.get_one("config")) std::println("Config: {}", *c);
37+
std::println("Input: {}", parsed.arg(0));
38+
return 0;
39+
}
40+
```
41+
42+
## 多种输入
43+
44+
```cpp
45+
// C 风格
46+
auto result = app.parse(argc, argv);
47+
48+
// 字符串列表
49+
std::vector<std::string> args = {"myapp", "add", "python", "3.12"};
50+
auto result = app.parse_from(args);
51+
52+
// 单条命令行字符串(按空格/引号拆分)
53+
auto result = app.parse_from("myapp add python 3.12 --yes");
54+
```
55+
56+
## 绑定分发(on_run + dispatch)
57+
58+
子命令推荐用字符串链式:`.subcommand("name").about("...").arg(...).on_run(...)`;也可传入完整 `App` 对象。
59+
60+
```cpp
61+
App app("demo");
62+
app.opt(Opt("yes").long_opt("yes").global().help("Auto confirm"))
63+
.subcommand("add")
64+
.about("Add a target")
65+
.arg(Arg("target").required())
66+
.arg(Arg("version").required())
67+
.on_run([](const ParsedArgs& args) {
68+
std::println("add: {}@{}", args.get_one("target").value_or(""), args.get_one("version").value_or(""));
69+
})
70+
.subcommand("remove")
71+
.about("Remove")
72+
.arg(Arg("target").required())
73+
.on_run([](const ParsedArgs& args) { std::println("remove: {}", args.arg(0)); });
74+
75+
auto result = app.parse(argc, argv);
76+
if (result) app.dispatch(*result);
77+
```
78+
79+
## 分离模式(自行分发)
80+
81+
```cpp
82+
auto result = app.parse(argc, argv);
83+
if (!result) return 1;
84+
const ParsedArgs& parsed = *result;
85+
if (parsed.has_subcommand()) {
86+
auto sub = parsed.subcommand();
87+
if (sub && parsed.subcommand_name() == "add") {
88+
const ParsedArgs& sub_args = sub->get();
89+
// ...
90+
}
91+
}
92+
```
93+
94+
## 构建
95+
96+
```bash
97+
cd cmdline && xmake -P .
98+
xmake run basic
99+
xmake run with_dispatch -- add python 3.12
100+
xmake run parse_from_string
101+
```
102+
103+
测试需 Google Test,使用 **`xmake -y`** 可自动安装依赖(如 gtest)并编译:
104+
105+
```bash
106+
xmake -y # 自动确认并安装 gtest,编译全部(含测试)
107+
xmake -y run cmdline_test # 编译并运行测试
108+
```
109+
110+
## CI
111+
112+
GitHub Actions 在 `push` / `pull_request``main``master` 时自动构建并测试(见 [.github/workflows/ci.yml](.github/workflows/ci.yml)):
113+
114+
- 安装 xmake(package 缓存)、build-essential
115+
- 通过 [Xlings](https://d2learn.org/xlings-install.sh) 安装 GCC 15.1(C++23 模块)
116+
- `xmake -y` 构建(含 gtest)
117+
- `xmake run cmdline_test` 跑测试
118+
- 跑示例 `with_dispatch` 做冒烟检查
119+
120+
## API 概要
121+
122+
| 类型/方法 | 说明 |
123+
|-----------|------|
124+
| `App` | 根或子命令;`.version()`, `.author()`, `.about()`, `.arg()`, `.opt()`, `.subcommand(name\|App)`, `.on_run()`, `.parse()`, `.parse_from()`, `.dispatch()`, `.print_help()` |
125+
| `Arg` | 位置参数;`.help()`, `.required()`, `.default_value()` |
126+
| `Opt` | 选项;`.short_name()`, `.long_opt()`, `.help()`, `.takes_value()`, `.value_name()`, `.multiple()`, `.global()` |
127+
| `ParsedArgs` | 解析结果;`.get_flag()`, `.get_one()`, `.opt()`, `.opt_or_empty()`, `.arg()`, `.arg_count()`, `.has_subcommand()`, `.subcommand_name()`, `.subcommand()` |
128+
| `OptionValue` | 单个选项取值;`.value()`, `.value_or()`, `.is_set()` |
129+
130+
解析返回 `std::expected<ParsedArgs, std::string>``-h`/`--help``--version` 时 error 为 `"help requested"` / `"version requested"`,可据此退出 0。

examples/basic.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import std;
2+
import cmdline;
3+
4+
int main(int argc, char* argv[]) {
5+
using namespace cmdline;
6+
7+
auto app = App("myapp")
8+
.version("1.0.0")
9+
.about("A demo CLI")
10+
.arg(Arg("input").help("Input file").required())
11+
.opt(Opt("verbose").short_name('v').long_opt("verbose").help("Verbose output"))
12+
.opt(Opt("config").short_name('c').long_opt("config").help("Config file").takes_value().value_name("FILE"));
13+
14+
auto result = app.parse(argc, argv);
15+
if (!result) {
16+
if (result.error() == "help requested" || result.error() == "version requested")
17+
return 0;
18+
std::println("Error: {}", result.error());
19+
return 1;
20+
}
21+
const ParsedArgs& parsed = *result;
22+
23+
if (parsed.get_flag("verbose"))
24+
std::println("Verbose on");
25+
if (auto config = parsed.get_one("config"))
26+
std::println("Config: {}", *config);
27+
std::println("Input: {}", parsed.arg(0));
28+
return 0;
29+
}

examples/parse_from_string.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import std;
2+
import cmdline;
3+
4+
int main() {
5+
using namespace cmdline;
6+
7+
auto app = App("tool")
8+
.version("1.0")
9+
.arg(Arg("cmd").required())
10+
.opt(Opt("verbose").long_opt("verbose").help("Verbose"));
11+
12+
// Parse from string (e.g. for tests)
13+
auto result = app.parse_from("tool add --verbose");
14+
if (!result) {
15+
std::println("Parse error: {}", result.error());
16+
return 1;
17+
}
18+
std::println("arg(0) = {}", result->arg(0));
19+
std::println("verbose = {}", result->get_flag("verbose"));
20+
return 0;
21+
}

examples/with_dispatch.cpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import std;
2+
import cmdline;
3+
4+
int main(int argc, char* argv[]) {
5+
using namespace cmdline;
6+
7+
App app("demo");
8+
app.version("0.1.0")
9+
.about("Demo: parse + dispatch with on_run")
10+
.opt("yes").global().help("Auto confirm")
11+
.subcommand("add")
12+
.about("Add a target")
13+
.arg("target").required()
14+
.arg("version").required()
15+
.on_run([](const ParsedArgs& args) {
16+
auto target = args.get_one("target").value_or("");
17+
auto version = args.get_one("version").value_or("");
18+
std::println("add: {}@{}", target, version);
19+
})
20+
.subcommand("remove")
21+
.about("Remove a target")
22+
.arg("target").required()
23+
.on_run([](const ParsedArgs& args) {
24+
std::println("remove: {}", args.arg(0));
25+
});
26+
27+
auto result = app.parse(argc, argv);
28+
if (!result) {
29+
if (result.error() == "help requested" || result.error() == "version requested")
30+
return 0;
31+
std::println("Error: {}", result.error());
32+
return 1;
33+
}
34+
app.dispatch(*result);
35+
return 0;
36+
}

examples/xmake.lua

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
add_rules("mode.debug", "mode.release")
2+
set_languages("c++23")
3+
4+
target("basic")
5+
set_kind("binary")
6+
add_files("basic.cpp")
7+
add_deps("cmdline")
8+
set_policy("build.c++.modules", true)
9+
10+
target("with_dispatch")
11+
set_kind("binary")
12+
add_files("with_dispatch.cpp")
13+
add_deps("cmdline")
14+
set_policy("build.c++.modules", true)
15+
16+
target("parse_from_string")
17+
set_kind("binary")
18+
add_files("parse_from_string.cpp")
19+
add_deps("cmdline")
20+
set_policy("build.c++.modules", true)

0 commit comments

Comments
 (0)