Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
| `OptionValue` | 单个选项取值(flag 计数 + values) |
| `ParseError` | 解析错误(kind + message) |
| `Argv` | `std::vector<std::string>` 的别名,用于 `parse_from` |
| `Shell` | 目标 shell 枚举:`bash` / `fish` / `zsh` |
| `Command` | 命令树快照,用于生成补全脚本 |

解析返回 `ParseResult`,即 `std::expected<ParsedArgs, ParseError>`。`-h`/`--help`、`--version` 时,`ParseError::kind` 分别为 `help` / `version`,不携带 `message`,通过 `is_error()` 可区分真实错误。

Expand Down Expand Up @@ -40,6 +42,14 @@

---

## Shell

| 函数 | 说明 |
|------|------|
| `shell_from_string(sv)` | 字符串 → `std::optional<Shell>`(如 `"fish"` → `Shell::fish`) |
| `to_string(shell)` | `Shell` → `std::string_view` |
| `shell_supported(shell)` | 该 shell 补全是否已实现(fish / bash / zsh) |

## ParseError

| 成员 | 说明 |
Expand Down Expand Up @@ -168,3 +178,66 @@ app.option(cmdline::Option("yes").long_opt("yes").global().help("Auto confirm"))
app.arg(cmdline::Arg("input").required().help("Input file"));
app.subcommand(cmdline::App("add").description("Add").arg(cmdline::Arg("x").required()).action([](const cmdline::ParsedArgs&) {}));
```

---

## Shell 补全

### 生成流程

1. 从 `App` 构建 `completions::Command` 树(`snapshot(app)` 或自动由 `generate_completions` 调用)
2. `Command` 包含名称、描述、版本、`Arg`/`Option` 列表、子命令列表
3. 选择目标 `Shell` 后调用 `generate_completions`,输出补全脚本文本

### completions::Command

| 成员 | 说明 |
|------|------|
| `name` | 命令名 |
| `description` | 描述文本 |
| `version` | 版本号 |
| `args` / `options` / `subcommands` | `vector<Arg>` / `vector<Option>` / `vector<Command>` |
| `.has_positionals()` | 是否有位置参数 |
| `.has_subcommands()` | 是否有子命令 |

> `snapshot()` 构建的 `Command` 会自动包含 `-h`/`--help` 和 `--version` 选项(与 `App::parse_impl` 行为一致)。

### 生成函数

| 重载 | 说明 |
|------|------|
| `generate_completions(cmd, shell)` | `Command` → `std::string` |
| `generate_completions(cmd, shell, out)` | `Command` → `std::ostream&` |
| `generate_completions(app, shell)` | `App` → `std::string`(内部调用 snapshot) |
| `generate_completions(app, shell, out)` | `App` → `std::ostream&` |

### App::completions

```cpp
// App 上的便捷方法,等价于 generate_completions(*this, shell)
std::string completions(Shell shell) const;
```

### 示例:添加 completions 子命令

```cpp
app.subcommand("completions")
.description("Generate shell completions")
.option(Option("shell").takes_value().value_name("SHELL")
.help("Target shell [possible values: bash, fish, zsh]"))
.action([&app](const ParsedArgs& p) {
auto shell_opt = p.value("shell");
if (!shell_opt) {
std::println(std::cerr, "SHELL not provided.");
return;
}
auto shell = shell_from_string(*shell_opt);
if (!shell || !shell_supported(*shell)) {
std::println(std::cerr, "Unsupported shell: {}", *shell_opt);
return;
}
std::print("{}", app.completions(*shell));
});
```

> 完整可运行示例见 `examples/completions/`。
1 change: 1 addition & 0 deletions examples/completions/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target/
8 changes: 8 additions & 0 deletions examples/completions/mcpp.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "completions"
version = "0.1.0"
description = "A mimicked xlings using mcpplibs.cmdline with shell completions"
license = "Apache-2.0"

[dependencies]
cmdline = { path = "../../" }
143 changes: 143 additions & 0 deletions examples/completions/src/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import std;
import mcpplibs.cmdline;

using namespace mcpplibs;
using cmdline::App;
using cmdline::Option;
using cmdline::ParsedArgs;

[[nodiscard]] App create_subos();

void dummy_action(const ParsedArgs& p) {
std::println("Options:");
for (const auto& [key, optvalue] : p.opts) {
std::println(" {} -> count: {}, value: {}", key, optvalue.count, optvalue.value());
}

std::println("Positionals:");
for (const auto& positional : p.positionals) {
std::println(" {}", positional);
}
}

int main(int argc, char* argv[]) {
// Partially mimicked xlings
auto app = App("completions")
.version("0.1.0")
.author("d2learn community")
.description("A mimicked xlings using mcpplibs.cmdline with shell completions")
// Global options
.option("yes").short_name('y').help("Skip confirmation prompts").global()
.option("verbose").short_name('v').help("Enable verbose output").global()
.option("quiet").short_name('q').help("Suppress non-essential output").global()
.option("agent").help("Plain-text output without TUI formatting (for LLM agents)").global()
// End of Global options
.subcommand("install")
.description("Install packages (e.g. xlings install gcc@15 node)")
.option(Option("global").short_name('g').help("Install to global scope (not project-local subos)"))
.option(Option("use").short_name('u').help("Activate the installed version even if another version is currently active"))
.arg("packages").help("Package names with optional version")
.action(dummy_action)
.subcommand("remove")
.description("Remove a package")
.arg("package").required().help("Package to remove (name or name@ver)")
.arg("version").help("Optional version (alternative to name@ver form)")
.action(dummy_action)
.subcommand("update")
.description("Update package index or a specific package")
.arg("package").help("Package to update (omit for index only)")
.arg("version").help("Optional version (alternative to name@ver form)")
.action(dummy_action)
.subcommand("search")
.description("Search for packages")
.arg("keyword").required().help("Search keyword")
.action(dummy_action)
.subcommand("list")
.description("List installed packages")
.option(Option("all").short_name('a').help("Show packages across all subos (default: current subos only)"))
.arg("filter").help("Filter pattern")
.action(dummy_action)
.subcommand("info")
.description("Show package information")
.arg("package").required().help("Package name (or name@ver)")
.arg("version").help("Optional version (alternative to name@ver form)")
.action(dummy_action)
.subcommand("use")
.description("Switch tool version")
.option(Option("all").short_name('a').help("Show versions across all subos (default: current subos only)"))
.arg("target").required().help("Tool name (or name@ver one-shot)")
.arg("version").help("Version to switch to (omit to list installed versions)")
.action(dummy_action)
.subcommand("config")
.description("Show or modify xlings configuration")
.option(Option("lang").takes_value().value_name("LANG").help("Set language (en/zh)"))
.option(Option("mirror").takes_value().value_name("MIRROR").help("Set mirror (GLOBAL/CN)"))
.option(Option("add-xpkg").takes_value().value_name("FILE").help("Add xpkg file to package index"))
.option(Option("index-repo").takes_value().value_name("NS:URL").help("Add/update index repo (e.g. myns:https://...git)"))
.action(dummy_action);
app.subcommand(create_subos());

// Completions
app.subcommand("completions")
.description("Generate shell completions")
.option(Option("shell").takes_value().value_name("SHELL").help("Target shell [possible values: bash, fish, zsh]"))
.action([&app](const ParsedArgs& p) {
auto shell_opt = p.value("shell");
if (!shell_opt) {
std::println(std::cerr, "SHELL not provided.");
return;
}

auto shell = cmdline::shell_from_string(*shell_opt);
if (!shell || !cmdline::shell_supported(*shell)) {
std::println(std::cerr, "Unsupported shell: {}", *shell_opt);
return;
}

std::print("{}", app.completions(*shell));
});

return app.run(argc, argv);
}

App create_subos() {
auto subos = App("subos")
.description("Manage sub-OS environments")
.subcommand("list")
.description("List all sub-OS environments")
.action(dummy_action)
.subcommand("ls")
.description("alias: list")
.action(dummy_action)
.subcommand("remove")
.description("Remove a sub-OS")
.arg("name").required().help("sub-OS name")
.action(dummy_action)
.subcommand("rm")
.description("alias: rm")
.arg("name").required().help("sub-OS name")
.action(dummy_action)
.subcommand("new")
.description("Create a new sub-OS")
.option(Option("storage").takes_value().value_name("mode").help("storage mode"))
.option(Option("image-size").takes_value().value_name("size").help("image size"))
.option(Option("from").takes_value().value_name("spec").help("from spec"))
.arg("name").required().help("sub-OS name")
.action(dummy_action)
.subcommand("use")
.description("Switch active sub-OS")
.option(Option("global").help("persist the choice into ~/.xlings.json + symlinki (legacy behavior; affects every shell)"))
.option(Option("shell").takes_value().value_name("kind").help("emit shell code on stdout for the user to eval/Invoke-Expression."))
.option(Option("sandbox").help("Linux-only: enter via proot fs-isolation."))
.arg("name").required().help("sub-OS name")
.action(dummy_action)
.subcommand("info")
.description("Show sub-OS details")
.arg("name").help("sub-OS name")
.action(dummy_action)
.subcommand("i")
.description("alias: info")
.arg("name").help("sub-OS name")
.action(dummy_action);
return subos;
}
58 changes: 58 additions & 0 deletions src/cmdline.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ export module mcpplibs.cmdline;

import std;

export import :completions;
export import :options;
export import :parse;

export import :completions.bash;
export import :completions.fish;
export import :completions.zsh;

namespace mcpplibs::cmdline {

class SubcommandBuilder;
Expand All @@ -25,6 +30,7 @@ export class App {
friend class SubcommandBuilder;
friend class OptBuilder;
friend class ArgBuilder;
friend completions::Command snapshot(const App&);
public:
explicit App(std::string_view name) : name_(name) {}
App(App&&) noexcept = default;
Expand Down Expand Up @@ -161,6 +167,8 @@ public:
}
}

[[nodiscard]] std::string completions(Shell shell) const;

private:
using global_keys_t = std::set<std::string, std::less<>>;
using global_opts_t = std::map<std::string, OptionValue, std::less<>>;
Expand Down Expand Up @@ -493,4 +501,54 @@ SubcommandBuilder AppItemBuilder<D>::subcommand(std::string_view name) { this->c
inline OptBuilder SubcommandBuilder::option(std::string_view name) { return OptBuilder(&sub_, Option(name)); }
inline SubcommandArgBuilder SubcommandBuilder::arg(std::string_view name) { return SubcommandArgBuilder(this, Arg(name)); }

/// Rebuild the command tree for shell completions.
completions::Command snapshot(const App& app) {
completions::Command cmd;
cmd.name = app.name_;
cmd.description = app.description_;
cmd.version = app.version_;

cmd.args = app.args_;
cmd.options = app.options_;

// Special cases in `App::parse_impl`:
//
// `-h/--help` is always available.
cmd.options.emplace_back(detail::Option("help").short_name('h').help("Print help").global());
// `--version` is available when app has a non-empty version.
if (!cmd.version.empty()) cmd.options.emplace_back(detail::Option("version").help("Print version").global());

for (const auto& sub : app.subcommands_) cmd.subcommands.emplace_back(snapshot(sub));

return cmd;
}

export void generate_completions(const completions::Command& cmd, Shell shell, std::ostream& out) {
switch (shell) {
case Shell::bash: completions::bash::generate(cmd, out); return;
case Shell::fish: completions::fish::generate(cmd, out); return;
case Shell::zsh: completions::zsh::generate(cmd, out); return;
}
}

export std::string generate_completions(const completions::Command& cmd, Shell shell) {
std::ostringstream oss;
generate_completions(cmd, shell, oss);
return oss.str();
}

export void generate_completions(const App& app, Shell shell, std::ostream& out) {
auto cmd = snapshot(app);
generate_completions(cmd, shell, out);
}

export std::string generate_completions(const App& app, Shell shell) {
auto cmd = snapshot(app);
return generate_completions(cmd, shell);
}

std::string App::completions(Shell shell) const {
return generate_completions(*this, shell);
}

} // namespace mcpplibs::cmdline
66 changes: 66 additions & 0 deletions src/completions.cppm
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
module;

export module mcpplibs.cmdline:completions;

import std;
import :options;

namespace mcpplibs::cmdline {

export enum class Shell {
bash,
fish,
zsh,
};

struct ShellEntry {
Shell value;
std::string_view name;
};

constexpr std::array<ShellEntry, 3> shell_entries {{
{Shell::bash, "bash"},
{Shell::fish, "fish"},
{Shell::zsh, "zsh"},
}};

export [[nodiscard]] std::optional<Shell> shell_from_string(std::string_view sv) {
for (const auto& [value, name] : shell_entries) {
if (name == sv) return value;
}
return std::nullopt;
}

export [[nodiscard]] std::string_view to_string(Shell shell) {
for (const auto& [value, name] : shell_entries) {
if (value == shell) return name;
}
return {};
}

export [[nodiscard]] bool shell_supported(Shell shell) {
switch (shell) {
case Shell::fish: return true;
case Shell::bash: return true;
case Shell::zsh: return true;
}
return false;
}

}; // namespace mcpplibs::cmdline

namespace mcpplibs::cmdline::completions {

export struct Command {
std::string name;
std::string description;
std::string version;
std::vector<detail::Arg> args;
std::vector<detail::Option> options;
std::vector<Command> subcommands;

[[nodiscard]] bool has_positionals() const { return !args.empty(); }
[[nodiscard]] bool has_subcommands() const { return !subcommands.empty(); }
};

}; // namespace mcpplibs::cmdline::completions
Loading