EN

微型应用

使用 func/parser 构建用于原型验证的小型 CLI。

更新于

func 应用本身的体积并不高,通常适合绝大多数场景,但如果用户处于 Node.js 兼容环境中并希望使用极低的体积进行快速原型测试,或是应用逻辑简单暂时不需要学习 func 的思维模型,那么 func/parser 会更合适。

func/parser 是微型应用的极佳选择,本身只携带少量语法,适合一小段命令式代码可表达的 CLI 项目。如包初始化器、仓库维护脚本、已有程序的薄包装器,通常只需要解析几个选项,调用一个业务函数等。

func/parser 实现是复用 func 的核心 argv 编译与解析能力,不会加载完整的装饰器应用模型,也不会接管命令执行;parse() 返回数据后,分支、帮助、输出、退出码和业务调用需要开发者自行实现。

何时适合使用

同时满足大部分条件时,func/parser 通常比完整应用模型更省事:

  • CLI 只有根选项,或少量单层命令;
  • 选项只需要 Boolean、String、非常有限的参数数量;
  • 位置输入可以作为字符串数组交给业务函数,不需要声明式校验;
  • 帮助文本很短,手写后不容易和实现失去同步;
  • 业务依赖较少,通常可以在单文件内直接处理,不需要依赖注入或资源生命周期。

典型场景包括 create-* 初始化器、单用途代码生成器、CI 辅助脚本、Git hook 入口,以及只增加一层默认配置的进程包装器。

如果项目已经需要嵌套命令、共享服务、结构化帮助、统一错误输出或可靠的调用级测试,直接使用 func 可以减少之后的二次建模。

定义并调用 parser

func/parser 导入 createParser,传入根选项和可选的命令表。parser 会从定义推导结果类型。

src/index.ts
import { ParserError, createParser } from 'func/parser'

const parser = createParser({
  options: {
    help: { alias: 'h', type: Boolean },
  },
  commands: {
    build: {
      options: {
        outDir: { alias: 'o', name: 'out-dir', type: String },
        tags: { alias: 't', multiple: true, name: 'tag', type: String },
      },
    },
  },
})

export const main = async (argv = process.argv.slice(2)) => {
  try {
    const invocation = parser.parse(argv)
    if (invocation.options.help) {
      printHelp(invocation.command)
      return
    }

    if (invocation.command === 'build') {
      await build({
        entries: invocation.inputs,
        outDir: invocation.options.outDir,
        tags: invocation.options.tags,
      })
      return
    }

    printHelp()
  } catch (error) {
    if (!(error instanceof ParserError)) throw error
    console.error(error.message)
    process.exitCode = 1
  }
}

上面的定义接受以下形式:

tool --help
tool build entry.ts --out-dir dist --tag next --tag latest
tool build entry.ts -o dist -t next

parse() 返回四个字段:

字段内容
command选中的单层命令名称;没有命令时为 undefined
inputsparser 未消费的位置输入,保留原顺序。
options以定义键名为属性的类型化值。
supplied用户显式提供过的定义键名;可区分省略选项和显式的 --no-<name>

根选项会对每个命令可见,也可以写在命令名称前后。普通值接受 --name value--name=value;已知的短布尔选项可以组合。未提供时,Boolean 为 false,单值为 undefined,重复值为空数组。后出现的单值覆盖先前值,重复值按输入顺序累积。

选项定义

简单形式直接使用构造函数:

const parser = createParser({
  options: {
    color: Boolean,
    port: Number,
    profile: String,
  },
})

对象形式可以调整公开名称与解析行为:

属性作用
type必填;只能是 BooleanStringNumber
name--<name> 使用的公开长名称;默认采用对象中的键。
alias一个非数字的单字符短别名。
aliases多个单字符短别名;会与 alias 合并并去重。
multiple只适用于 String / Number,把重复输入收集为只读数组。
negatable只适用于 Boolean,额外接受 --no-<name>

未知命令、未知选项和无效值会抛出 ParserErrorcode 分别为 unknown-commandunknown-optioninvalid-argument;定义本身无效时,createParser() 会以 invalid-definition 失败。

包装其他程序

命令设置 passthrough: true 后,从命令后的第一个位置输入或 -- 开始,剩余 token 不再被 parser 解释,适合转交给子进程:

src/runner.ts
import { createParser } from 'func/parser'

const parser = createParser({
  commands: {
    run: {
      options: {
        file: { alias: 'f', type: String },
      },
      passthrough: true,
    },
  },
})

const invocation = parser.parse(['run', '-f', 'worker.ts', '--', '--inspect'])
// invocation.options.file === 'worker.ts'
// invocation.inputs === ['--inspect']

建议在文档示例中保留 --。它清楚地区分包装器选项和被包装程序的参数,也避免下游新增选项后改变原有调用的含义。

能力边界

func/parser 刻意只解决 argv 到类型化数据的转换。以下需求需要应用自行实现,或改用完整的 func

需求func/parser 功能
命令结构仅单层命令表;没有嵌套 path、命令 alias、Handler 或缺失命令。
位置输入只返回 string[];没有名称、数量、类型或必填规则。
默认值与校验只有固定空值;没有自定义默认值、required、enum、依赖、互斥或校验器。
帮助与发现不保存 description,也不生成帮助、版本、弃用提示或 Shell 补全。
应用执行不分派业务函数,不管理 stdin/stdout、退出码、取消信号或进程错误。
代码组织不提供 Module、依赖注入、Provider 可见性和初始化或清理生命周期。
测试可以直接测试 parse(argv),但没有调用级依赖覆盖、IO 捕获或执行结果。

parser 会严格拒绝未知选项,不提供 allowUnknown 模式。需要透传时,应为对应命令显式启用 passthrough,并确定从哪里开始把输入交给下游。

何时迁移到 func

出现以下任一信号时,完整应用模型通常开始比手写控制流更简单:

  • 命令或动作超过一层,或者分支代码不断重复解析后的类型判断;
  • 手写帮助、实际选项和测试需要同时修改,已经发生过不一致;
  • 多个命令需要共享配置、客户端、缓存或其他有清理需求的资源;
  • 需要 required、enum、跨字段约束、默认值或更明确的位置语义;
  • 需要统一 IO、错误格式、退出码、取消信号、日志、配置或 Shell 补全;
  • 测试需要执行完整命令,同时替换 Provider 并捕获 stdout / stderr。

迁移不要求重写业务逻辑。可以尝试按下述对应方案把命令层搬到 func

func/parserfunc
commands.build@Command('build')
Boolean@Flag()
String / Number带有 string / number 属性的 @Value()
{ multiple: true, type: String / Number }@ArrayString() / @ArrayNumber()
inputs@Args()args.inputs
if (command === ...)@Handler() 或有 path 的 Handler
根选项@Option();帮助等独立动作使用 @OptionCommand() 或能力包
手写 try/catch 与清理Command / Module 的 onErroronInitonDispose

下面是前面 build 命令的等价起点。业务 build() 保持不变,但命令选择、字段绑定和帮助入口使用 func 实现:

src/index.ts
import { Args, ArrayString, Command, Handler, Module, Value, createApp } from 'func'
import type { Args as CommandArgs } from 'func'
import { HelpOption } from 'func/help'

@Command({
  path: 'build',
  description: 'Build one or more entries',
})
class BuildCommand {
  @Value({ alias: 'o', name: 'out-dir', description: 'Output directory' })
  outDir?: string

  @ArrayString({ alias: 't', name: 'tag', description: 'Release tag' })
  tags: string[] = []

  @Handler()
  run(@Args() args: CommandArgs) {
    return build({
      entries: args.inputs,
      outDir: this.outDir,
      tags: this.tags,
    })
  }
}

@Module({
  commands: [BuildCommand],
  options: [HelpOption],
})
class AppModule {}

const app = createApp(AppModule, { appName: 'tool' })
void app.bootstrap()

完整项目结构见快速开始,命令与字段的建模规则分别见命令字段选项。如果仍处于技术选型阶段,可以继续阅读生态选型