1. 概述

本文将介绍如何使用 Kotlin 官方实验性库 kotlinx-cli 快速构建结构清晰、易于维护的命令行(CLI)应用程序。该库的核心功能是解析并校验来自命令行的参数,让你无需手动处理 args 数组,避免“自己造轮子”的踩坑经历。

对于熟悉 Java 或 Kotlin 的开发者来说,直接用 main(args) 手动解析参数不仅繁琐还容易出错。而 kotlinx-cli 提供了声明式 API,让 CLI 参数定义变得直观且类型安全。

2. 添加 Maven 依赖

pom.xml 中引入 kotlinx-cli 的 JVM 版本依赖:

<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-cli-jvm</artifactId>
    <version>0.3.5</version>
</dependency>

你可以在 Maven Central 查看最新版本。⚠️ 注意:当前为实验性库,正式环境使用前建议评估稳定性。

3. 参数处理机制

CLI 参数通常以 --- 开头。要实现自动解析,我们需要创建一个 ArgParser 实例,它负责配置、验证和消费这些参数。

基础解析器示例

fun main(args: Array<String>) {
    val parser = ArgParser("example")
    parser.parse(args)
}

运行 example -h 即可看到自动生成的帮助信息:

Usage: example options_list
Options:
    --help, -h -> Usage info

✅ 可见,-h--help 已被自动支持,开箱即用。

3.1 可选参数(Optional Arguments)

为了让程序真正可用,我们需要定义具体的参数。通过调用 parser.option() 方法即可完成:

fun main(args: Array<String>) {
    val parser = ArgParser("example")

    val name by parser.option(
        ArgType.String,
        shortName = "n",
        description = "User name"
    )

    parser.parse(args)

    println("Hello, $name!")
}

这里使用了 Kotlin 的 委托属性(delegated properties)name 的值由解析器在 parse() 调用后自动注入。

运行 example -n John 输出:

Hello, John!

若未传参,namenull(因为默认是可选的)。帮助信息中会显示:

Usage: example options_list
Options:
    --name, -n -> User name { String }
    --help, -h -> Usage info

其中 { String } 表示参数类型。支持的类型包括:

  • ArgType.String
  • ArgType.Boolean
  • ArgType.Int
  • ArgType.Double
  • ArgType.Choice

3.2 必填参数(Required Arguments)

通过 .required() 扩展函数将参数设为必填:

val name by parser.option(
    ArgType.String,
    shortName = "n",
    description = "User name"
).required()

此时 name 变为非空类型(non-nullable),如果未提供 -n 参数,程序将抛出异常并打印提示:

Value for option --name should always be provided on the command line.
Usage: example options_list
Options:
    --name, -n -> User name (always required) { String }
    --help, -h -> Usage info

✅ 类型系统提前帮你拦截了空值风险,减少运行时错误。

3.3 默认值参数(Default Arguments)

使用 .default(value) 设置默认值:

val name by parser.option(
    ArgType.String,
    shortName = "n",
    description = "User name"
).default("N/A")

帮助信息中会体现默认值 [N/A]

Usage: example options_list
Options:
    --name, -n [N/A] -> User name { String }
    --help, -h -> Usage info

运行时不带 -n 将输出 Hello, N/A!

⚠️ 注意:.required().default() 互斥,不能同时使用,否则编译或运行时报错。

3.4 多值参数(Multiple Arguments)

当需要接收多个相同类型的参数时,使用 .multiple()

fun main(args: Array<String>) {
    val parser = ArgParser("example")

    val names by parser.option(
        ArgType.String,
        shortName = "n",
        description = "User name"
    ).multiple()

    parser.parse(args)
    names.forEach {
        println("Hello, $it!")
    }
}

命令行传参方式:example -n John -n Smith

输出:

Hello, John!
Hello, Smith!

此时 namesList<String> 类型,便于后续处理。

4. Choice 类型支持

ArgType.Choice 用于限制参数取值范围,防止非法输入。

4.1 枚举 Choice(Enum Choice)

推荐使用枚举类型,简洁又安全:

enum class Format {
    HTML,
    CSV,
    PDF
}

fun main(args: Array<String>) {
    val parser = ArgParser("example")
    val format by parser.option(
        ArgType.Choice<Format>(),
        shortName = "f",
        description = "Format for the output file"
    )
    parser.parse(args)
    println("Hello, $format")
}

运行 example -f csv 输出 Hello, CSV

若输入非法值如 example -f cs ❌,则报错:

Option format is expected to be one of [html, csv, pdf]. cs is provided.
Usage: example options_list
Options:
    --format, -f -> Format for the output file { Value should be one of [html, csv, pdf] }
    --help, -h -> Usage info

✅ 自动提示合法选项,用户体验友好。

4.2 自定义 Choice(Custom Choice)

也可以传入字符串列表定义选项:

fun main(args: Array<String>) {
    val parser = ArgParser("example")

    val typeFormat by parser.option(
        ArgType.Choice(listOf("html", "csv", "pdf"), { it }),
        shortName = "sf",
        description = "Format as a string for the output file"
    )

    parser.parse(args)
    println("Hello, $typeFormat")
}

其中 { it } 是转换函数,用于将输入映射到 choice 列表中的值。由于我们直接使用字符串,所以原样返回即可。

还可以组合使用扩展函数,例如设置默认值并允许多选:

val typeFormats by parser.option(
    ArgType.Choice(listOf("html", "csv", "pdf"), { it }),
    shortName = "sf",
    description = "Format as a string for the output file"
).default("csv").multiple()

此时 typeFormats 类型为 List<String>,默认包含 "csv"

5. 子命令(Subcommands)

当 CLI 工具功能较多时,使用子命令划分职责更清晰。比如 Git 中的 git commitgit push 等。

定义子命令

继承 Subcommand 类并重写 execute() 方法:

class Multiply : Subcommand("mul", "Multiply") {

   val numbers by argument(ArgType.Int, description = "Numbers").multiple(3)
     var result = 0

   override fun execute() {
       result = numbers.reduce { acc, it -> acc * it }
   }
}

注意:

  • 使用 argument() 而不是 option() 来定义位置参数(positional arguments)
  • multiple(3) 表示至少需要 3 个整数
  • execute() 在参数校验通过后自动调用

注册并使用子命令

fun main(args: Array<String>) {
    val parser = ArgParser("example")
    val outNumber by parser.option(ArgType.Int, "outer number", "o").default(0)
    val multiple = Multiply()
    parser.subcommands(multiple)
    parser.parse(args)
    println("Total ${outNumber.plus(multiple.result)}")
}

运行命令:example -o 4 mul 1 2 3

输出:Total 10

解释:

  • -o 4 是顶层选项
  • mul 是子命令名
  • 1 2 3 是传递给 Multiply 子命令的位置参数

✅ 结构清晰,逻辑解耦,适合复杂 CLI 应用。

6. 总结

通过本文我们掌握了使用 kotlinx-cli 构建专业级 CLI 应用的关键技巧:

  • ✅ 使用 ArgParser 统一管理参数解析
  • ✅ 支持可选、必填、默认、多值等常见参数模式
  • ✅ 借助 Choice 实现枚举或自定义选项校验
  • ✅ 利用 Subcommand 实现模块化命令设计
  • ✅ 全程类型安全,减少 runtime error

虽然 kotlinx-cli 目前仍处于实验阶段,但其 API 设计简洁现代,非常适合 Kotlin 项目快速搭建 CLI 工具。如果你正在开发脚本类工具或微服务命令行入口,不妨试试这个轻量高效的解决方案。


原始标题:How to Build CLI Applications Using Kotlinx-Cli