Skip to content

citty

优雅的 CLI 生成器

特性

  • 基于 mri 的参数解析器,快速且轻量
  • 智能解析类型转换值、布尔参数简写值,处理未知的标识符
  • 支持嵌套子命令
  • 支持懒加载和异步加载命令
  • 支持插件式和组合式 API
  • 自动生成用法或帮助,但要传递 --help-h 选项才输出用法提示

基础用法

ts
import { defineCommand, runMain } from "citty";
const main = defineCommand({
  meta: {
    name: "hello",
    version: "1.0.0",
    description: "My Awesome CLI App",
  },
  args: {
    name: {
      type: "positional",
      description: "Your name",
      required: true,
    },
    friendly: {
      type: "boolean",
      description: "Use friendly greeting",
    },
  },
  run({ args }) {
    console.log(`${args.friendly ? "Hi" : "Greetings"} ${args.name}!`);
  },
});
runMain(main);

核心代码解析

defineCommand

ts
export function defineCommand<T extends ArgsDef = ArgsDef>(
  def: CommandDef<T>
): CommandDef<T> {
  return def;
}

该函数炒鸡简单,即传参是啥,返回值就是啥。
但是,这里要着重看其类型定义,它才是关键点。

ts
//* 参数类型的三种情况,布尔类型、字符串类型、占位类型
export type ArgType = "boolean" | "string" | "positional" | undefined;
//* 通用的单个参数的类型定义,即每种类型的参数的定义都是该结构
export type _ArgDef<T extends ArgType, VT extends boolean | string> = {
  //* 参数类型
  type?: T;
  //* 描述说明,help用法时会输出
  description?: string;
  //* 字符串参数在help时的值提示,没有则使用default字段值提示,否则为空
  valueHint?: string;
  //* 别名,即快捷方式,比如 --force 可以写成 -f
  alias?: string | string[];
  //* 默认值
  default?: VT;
  //* 是否必填
  required?: boolean;
};
//* 布尔类型参数,值类型为布尔类型
export type BooleanArgDef = _ArgDef<"boolean", boolean>;
//* 字符串类型参数,值类型为字符串类型
export type StringArgDef = _ArgDef<"string", string>;
//* 占位类型参数,该类型参数没有别名,值类型为字符串
export type PositionalArgDef = Omit<_ArgDef<"positional", string>, "alias">;
//* 真正对外暴露的参数定义类型,只有这三种情况
export type ArgDef = BooleanArgDef | StringArgDef | PositionalArgDef;
//* 完整参数定义,通过对象方式组合多个参数即可
export type ArgsDef = Record<string, ArgDef>;

//* 可解析的:类型T或者带类型T的Promise或者函数返回类型T或者函数返回带类型T的Promise
export type Resolvable<T> = T | Promise<T> | (() => T) | (() => Promise<T>);
//* 元数据类型定义
export interface CommandMeta {
  //* 命令名称
  name?: string;
  //* 版本
  version?: string;
  //* 描述
  description?: string;
}
//* 子命令定义,是一个Record,值是可解析的命令定义,产生嵌套
export type SubCommandsDef = Record<string, Resolvable<CommandDef<any>>>;
//* 已解析的参数
export type ParsedArgs<T extends ArgsDef = ArgsDef> =
  //* 解析后的占位参数及额外参数
  { _: string[] }
  & Record<
    //* 所有的占位参数
    { [K in keyof T]: T[K] extends { type: "positional" } ? K : never }[keyof T],
    string
  >
  & Record<
    //* 所有的字符串参数
    { [K in keyof T]: T[K] extends { type: "string" } ? K : never; }[keyof T],
    string
  >
  & Record<
    //* 所有的布尔参数
    { [K in keyof T]: T[K] extends { type: "boolean" } ? K : never; }[keyof T],
    boolean
  >
  //* 未定义的参数
  & Record<string, string | boolean>;
//* 命令上下文
export type CommandContext<T extends ArgsDef = ArgsDef> = {
  //* 命令行原始参数
  rawArgs: string[];
  //* 解析后的参数
  args: ParsedArgs<T>;
  //* 命令定义
  cmd: CommandDef;
  //* 子命令定义
  subCommand?: CommandDef<T>;
};
//* 整个命令的类型定义
export type CommandDef<T extends ArgsDef = ArgsDef> = {
  meta?: Resolvable<CommandMeta>;
  args?: Resolvable<T>;
  subCommands?: Resolvable<SubCommandsDef>;
  //* 安装钩子
  setup?: (context: CommandContext<T>) => any | Promise<any>;
  //* 清除钩子
  cleanup?: (context: CommandContext<T>) => any | Promise<any>;
  //* 执行钩子
  run?: (context: CommandContext<T>) => any | Promise<any>;
};

TIP

所以,命令行参数只有三种情况,即布尔类型、字符串类型、占位类型!

runMain

ts
import { bgRed } from "colorette";
export interface RunMainOptions {
  rawArgs?: string[];
}
//* 解析值,是函数则执行,否则直接返回
//* 这里可以进行结果缓存,不必每次都调用函数
export function resolveValue<T>(input: Resolvable<T>): T | Promise<T> {
  return typeof input === "function" ? (input as any)() : input;
}
//* 解析子命令
export async function resolveSubCommand(
  cmd: CommandDef,
  rawArgs: string[],
  parent?: CommandDef
): Promise<[CommandDef, CommandDef?]> {
  const subCommands = await resolveValue(cmd.subCommands);
  //* 如果有子命令的配置,则处理
  if (subCommands && Object.keys(subCommands).length > 0) {
    //* 在原始命令行参数中查找子命令,第一个非选项的参数
    //* 因为普通的选项参数,都是-或者--开头
    //* 由此可知,命令行参数可以按子命令拆分,子命令前面的选项参数都是父命令的
    const subCommandArgIndex = rawArgs.findIndex((arg) => !arg.startsWith("-"));
    const subCommandName = rawArgs[subCommandArgIndex];
    const subCommand = await resolveValue(subCommands[subCommandName]);
    //* 如果找到,则递归的找最后一个子命令并返回
    //* 一般地,只有一层嵌套吧。。。。
    if (subCommand) {
      return resolveSubCommand(
        subCommand,
        //* 后面的参数给剩余的子命令用
        rawArgs.slice(subCommandArgIndex + 1),
        cmd
      );
    }
  }
  return [cmd, parent];
}
export async function runMain(cmd: CommandDef, opts: RunMainOptions = {}) {
  //* 读取原始命令行参数
  const rawArgs = opts.rawArgs || process.argv.slice(2);
  try {
    //* 如果参数中包含使用帮助,则打印使用帮助并退出
    if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
      await showUsage(...(await resolveSubCommand(cmd, rawArgs)));
      process.exit(0);
    } else {
      //* 如果不执行使用帮助,则真正执行命令了
      await runCommand(cmd, { rawArgs });
    }
  } catch (error: any) {
    //* 错误处理
    const isCLIError = error instanceof CLIError;
    if (!isCLIError) {
      console.error(error, "\n");
    }
    //* 打印红色背景的消息
    console.error(
      `\n${bgRed(` ${error.code || error.name} `)} ${error.message}\n`
    );
    //* 如果是内部可捕获的CLI错误,则打印使用帮助,一般是因为用法不对
    if (isCLIError) {
      await showUsage(...(await resolveSubCommand(cmd, rawArgs)));
    }
    process.exit(1);
  }
}

TIP

colorette 是无依赖且高性能的终端文本样式美化工具。

showUsage

TIP

runMain 方法在命令行参数存在 --help-h 参数时,则输出使用帮助。 或者程序出错且是内部可捕获的错误(比如参数传的不对),也输出使用帮助,因为极可能是用法不对,所以告诉用户该如何使用。

ts
//* 输出显示用法
export async function showUsage(cmd: CommandDef, parent?: CommandDef) {
  try {
    console.log((await renderUsage(cmd, parent)) + "\n");
  } catch (error) {
    console.error(error);
  }
}
export function toArray(val: any) {
  if (Array.isArray(val)) {
    return val;
  }
  return val !== undefined ? [val] : [];
}
//* 把配置的对象类型参数解析成数组格式,对alias进行了格式化处理
export function resolveArgs(argsDef: ArgsDef): Arg[] {
  const args: Arg[] = [];
  for (const [name, argDef] of Object.entries(argsDef || {})) {
    args.push({
      ...argDef,
      name,
      alias: toArray((argDef as any).alias),
    });
  }
  return args;
}
//* 解析配置参数,打印用法信息
export async function renderUsage(cmd: CommandDef, parent?: CommandDef) {
  const cmdMeta = await resolveValue(cmd.meta || {});
  //* 解析后,是一个数组
  const cmdArgs = resolveArgs(await resolveValue(cmd.args || {}));
  //* 当前可能是子命令,获取父命令
  const parentMeta = await resolveValue(parent?.meta || {});

  const commandName =
    `${parentMeta.name ? `${parentMeta.name} ` : ""}` +
    //* 根命令、主命令
    (cmdMeta.name || process.argv[1]);

  //* 字符串和布尔类型的参数在用法输出行上的配置
  const argLines: string[][] = [];
  //* 占位类型的参数在用法输出行上的配置
  const posLines: string[][] = [];
  //* 子命令在用法输出行上的配置
  const commandsLines: string[][] = [];
  //* 用法总览在用法输出行上的配置
  const usageLine = [];

  for (const arg of cmdArgs) {
    if (arg.type === "positional") {
      //* 占位参数大写
      const name = arg.name.toUpperCase();
      //* 是否必填,required不为false且没有默认值
      //* 对比下面的逻辑,为啥这里不判断===true呢
      //* 因为required字段是可选的,即这里arg.required可能是undefined
      //* 所以:未配置默认值,只要没有明确说明不是必填,则默认其都是必填
      //* 如果存在多个可选的占位参数,就分不清哪个对应哪个了
      //* 接收到三个占位参数 a b c,但定义了5个可选的占位,怎么判断哪个对应哪个嘛?
      const isRequired = arg.required !== false && arg.default === undefined;
      //* (isRequired ? " (required)" : " (optional)"
      //* 用法提示,直接用默认值
      const usageHint = arg.default ? `="${arg.default}"` : "";
      //* 第一项为参数+默认值或值提示,第二项为描述
      posLines.push([name + usageHint, arg.description || ""]);
      //* 必填项用尖括号标识,非必填项使用中括号标识
      usageLine.push(isRequired ? `<${name}>` : `[${name}]`);
    } else {
      //* 非占位参数,必填必须要required为true且default未定义
      //* 所以:未配置默认值,明确说明是必填,否则是非必填
      //* 这里区别于占位参数,是因为其有明确的标识使得能判断出传入的参数对应哪个定义的参数
      const isRequired = arg.required === true && arg.default === undefined;
      const argStr =
        (arg.type === "boolean" && arg.default === true
          //* 布尔类型,默认值为true
          //* 对别名和本身的参数名生成带no前缀的参数
          //- 这里有个问题嚯,就是打印的是 --no-flag
          //- 此时 description 应该表达成反义
          //- 或者可以想个解决方案
          ? [
              ...(arg.alias || []).map((a) => `--no-${a}`),
              `--no-${arg.name}`,
            ].join(", ")
          //* 布尔类型,默认值为false
          : [
              ...(arg.alias || []).map((a) => `-${a}`),
              `--${arg.name}`,
            ].join(", ")
        ) +
        (arg.type === "string" && (arg.valueHint || arg.default)
          //* 字符串类型,必须配置值提示或者默认值
          //* 有提示,则用尖括号包裹,表示必填?
          //* 无提示,则使用默认值,用双引号包裹
          ? `=${
              arg.valueHint ? `<${arg.valueHint}>` : `"${arg.default || ""}"`
            }`
          : "");
      argLines.push([
        argStr + (isRequired ? " (required)" : ""),
        arg.description || "",
      ]);
      //* 必填则在用法总览行展示
      if (isRequired) {
        usageLine.push(argStr);
      }
    }
  }

  //* 处理子命令
  if (cmd.subCommands) {
    //* 记录所有子命令
    const commandNames: string[] = [];
    //* 这里还是读取的原始配置,如果是异步,则应该先resolve
    //* 之前可能resolve过了,又要重新resolve
    //* 这里可以考虑在resolve时进行缓存。。。。。。
    const subCommands = await resolve(cmd.subCommands);
    for (const [name, sub] of Object.entries(subCommands)) {
      const subCmd = await resolveValue(sub);
      const meta = await resolveValue(subCmd?.meta);
      commandsLines.push([name, meta?.description || ""]);
      commandNames.push(name);
    }
    //* 用法总览显示所有可用子命令,通过|分隔
    usageLine.push(commandNames.join("|"));
  }

  const usageLines: (string | undefined)[] = [];

  const version = cmdMeta.version || parentMeta.version;
  usageLines.push(
    commandName + (version ? ` v${version}` : ""),
    cmdMeta.description,
    ""
  );

  const hasOptions = argLines.length > 0 || posLines.length > 0;
  usageLines.push(
    `USAGE: ${commandName}${hasOptions ? " [OPTIONS]" : ""} ${usageLine.join(
      " "
    )}`,
    ""
  );

  if (posLines.length > 0) {
    usageLines.push("ARGUMENTS:", "");
    usageLines.push(formatLineColumns(posLines, "  "));
    usageLines.push("");
  }

  if (argLines.length > 0) {
    usageLines.push("OPTIONS:", "");
    usageLines.push(formatLineColumns(argLines, "  "));
    usageLines.push("");
  }

  if (commandsLines.length > 0) {
    usageLines.push("COMMANDS:", "");
    usageLines.push(formatLineColumns(commandsLines, "  "));
    usageLines.push(
      "",
      `Use \`${commandName} <command> --help\` for more information about a command.`
    );
  }

  return usageLines.filter((l) => typeof l === "string").join("\n");
}
//* 每行格式化,保证左右对齐
export function formatLineColumns(lines: string[][], linePrefix = "") {
  const maxLengh: number[] = [];
  for (const line of lines) {
    for (const [i, element] of line.entries()) {
      maxLengh[i] = Math.max(maxLengh[i] || 0, element.length);
    }
  }
  return lines.map((l) => l.map((c, i) => linePrefix + c[i === 0 ? "padStart" : "padEnd"](maxLengh[i])).join("  ")).join("\n");
}

runCommand

ts
export async function runCommand(
  cmd: CommandDef,
  opts: RunCommandOptions
): Promise<void> {
  //* 获取配置的可用参数
  const cmdArgs = await resolveValue(cmd.args || {});
  //* 用配置的参数和当前传入的参数进行解析
  const parsedArgs = parseArgs(opts.rawArgs, cmdArgs);

  const context: CommandContext = {
    rawArgs: opts.rawArgs,
    args: parsedArgs,
    cmd,
  };

  //* Setup hook
  if (typeof cmd.setup === "function") {
    await cmd.setup(context);
  }

  //* Handle sub command
  const subCommands = await resolveValue(cmd.subCommands);
  if (subCommands && Object.keys(subCommands).length > 0) {
    const subCommandArgIndex = opts.rawArgs.findIndex(
      (arg) => !arg.startsWith("-")
    );
    const subCommandName = opts.rawArgs[subCommandArgIndex];
    if (!subCommandName && !cmd.run) {
      throw new CLIError(
        `Missing sub command. Use --help to see available sub commands.`,
        "ESUBCOMMAND"
      );
    }
    if (!subCommands[subCommandName]) {
      throw new CLIError(
        `Unknown sub command: ${subCommandName}`,
        "ESUBCOMMAND"
      );
    }
    const subCommand = await resolveValue(subCommands[subCommandName]);
    if (subCommand) {
      await runCommand(subCommand, {
        rawArgs: opts.rawArgs.slice(subCommandArgIndex + 1),
      });
    }
  }

  //* Handle main command
  if (typeof cmd.run === "function") {
    await cmd.run(context);
  }
}

export function parseArgs(rawArgs: string[], argsDef: ArgsDef): ParsedArgs {
  //* 分别存储不同类型的参数、别名、默认值等
  const parseOptions = {
    boolean: [] as string[],
    string: [] as string[],
    mixed: [] as string[],
    alias: {} as Record<string, string | string[]>,
    default: {} as Record<string, boolean | string>,
  };
  //* 解析配置的参数,解析后,是一个数组
  const args = resolveArgs(argsDef);
  //* 遍历配置的参数
  for (const arg of args) {
    //* 占位参数,不处理
    if (arg.type === "positional") {
      continue;
    }
    //* 字符串类型参数
    if (arg.type === "string") {
      //* 保存name
      parseOptions.string.push(arg.name);
    } else if (arg.type === "boolean") {
      //* 布尔类型参数,保存name
      parseOptions.boolean.push(arg.name);
    }
    //* 默认值处理
    if (arg.default !== undefined) {
      //* 如果配置了默认值,则保存该参数名和对应的默认值
      parseOptions.default[arg.name] = arg.default;
    }
    //* 解析之后一定存在,且是数组格式
    if (arg.alias) {
      //* 保存该参数名和对应的别名,虽然别名可能是空数组
      parseOptions.alias[arg.name] = arg.alias;
    }
  }
  //* 解析实际传入的参数,这里是核心中的核心
  //* 返回的数据结构为:{ _: ['x', 'y'], booleanA: false, booleanB: true, stringA: '001', stringB: '002' }
  const parsed = parseRawArgs(rawArgs, parseOptions);
  //* 所有非布尔类型和字符串类型的参数,都当中是占位参数
  const [...positionalArguments] = parsed._;
  //* 代理,主要处理参数key的各种格式,驼峰、连字符
  const parsedArgsProxy = new Proxy(parsed, {
    get(target: ParsedArgs<any>, prop: string) {
      //* import { kebabCase, camelCase } from "scule";
      //* https://www.npmjs.com/package/scule
      return target[prop] ?? target[camelCase(prop)] ?? target[kebabCase(prop)];
    },
  });

  for (const [, arg] of args.entries()) {
    //* 占位参数处理
    if (arg.type === "positional") {
      //* 取出第一个传入的占位参数
      const nextPositionalArgument = positionalArguments.shift();
      if (nextPositionalArgument !== undefined) {
        //* 第一个占位参数怎么保证?顺序与for in 一致,先数字升序、后按定义时的顺序
        parsedArgsProxy[arg.name] = nextPositionalArgument;
      } else if (arg.default !== undefined) {
        //* positionalArguments是空数组,但是该占位参数有默认值
        parsedArgsProxy[arg.name] = arg.default;
      } else {
        //* 没有传占位参数且占位参数没有默认值
        throw new CLIError(
          `Missing required positional argument: ${arg.name.toUpperCase()}`,
          "EARG"
        );
      }
    } else if (arg.required && parsedArgsProxy[arg.name] === undefined) {
      //* 布尔和字符串类型的参数
      throw new CLIError(`Missing required argument: --${arg.name}`, "EARG");
    }
  }

  return parsedArgsProxy;
}

parseRawArgs

mri 拷贝而来

ts
export function parseRawArgs<T = Default>(
  args: string[] = [],
  opts: Options = {}
): Argv<T> {
  let k;
  let arr;
  let arg;
  let name;
  let val;
  /** 输出 */
  const out = { _: [] };
  let i = 0;
  let j = 0;
  let idx = 0;
  /** 原始参数的长度 */
  const len = args.length;
  //* 别名是一个对象,这里肯定是true
  const alibi = opts.alias !== void 0;
  //* 本来没有unknown字段,即是false
  const strict = opts.unknown !== void 0;
  //* 是一个对象,所以是true
  const defaults = opts.default !== void 0;

  opts.alias = opts.alias || {};
  /** toArr 定义重复,本身就是数组 */
  opts.string = toArr(opts.string);
  /** 本身也是数组 */
  opts.boolean = toArr(opts.boolean);
  //* 别名处理
  if (alibi) {
    for (k in opts.alias) {
      //* 别名转数组
      arr = opts.alias[k] = toArr(opts.alias[k]);
      for (i = 0; i < arr.length; i++) {
        //* 每个别名作为key,值为所有别名和本身的参数名,但是去除了当前别名
        (opts.alias[arr[i]] = arr.concat(k)).splice(i, 1);
      }
    }
  }
  //* 这里的下标在for这一行是多1的
  //* 但是到下一行就正确了,因为for这行执行结束,i就减1了
  for (i = opts.boolean.length; i-- > 0; ) {
    //* 获取布尔类型的全部别名
    arr = opts.alias[opts.boolean[i]] || [];
    for (j = arr.length; j-- > 0; ) {
      //* 此时全部布尔类型参数全部都包含了参数名和其对应的所有别名了
      opts.boolean.push(arr[j]);
    }
  }
  //* 同理,字符串类型参数也这样处理
  for (i = opts.string.length; i-- > 0; ) {
    arr = opts.alias[opts.string[i]] || [];
    for (j = arr.length; j-- > 0; ) {
      opts.string.push(arr[j]);
    }
  }
  //* 是否有默认值配置
  if (defaults) {
    for (k in opts.default) {
      //* 获取默认值的类型,字符串或布尔
      name = typeof opts.default[k];
      //* 获取别名
      arr = opts.alias[k] = opts.alias[k] || [];
      //* string 或者 boolean,一定存在且是数组
      if (opts[name] !== void 0) {
        //* 保存参数名
        opts[name].push(k);
        //* 保存别名
        for (i = 0; i < arr.length; i++) {
          opts[name].push(arr[i]);
        }
        //* 这里的操作和上面的操作产生的结果是,有可能会重复
      }
    }
  }
  //* 空数组
  const keys = strict ? Object.keys(opts.alias) : [];
  //* 遍历原始参数
  for (i = 0; i < len; i++) {
    arg = args[i];
    //* 如果只有--,则后面的所有参数都作为占位参数了
    //* 类似 npm run build -- --mode=uat --version=2023
    if (arg === "--") {
      out._ = out._.concat(args.slice(++i));
      break;
    }
    //* 遍历当前参数字符串
    //* 找到不是-的下标
    for (j = 0; j < arg.length; j++) {
      if (arg.charCodeAt(j) !== 45) {
        break;
      } //* "-"
    }
    //* j为0,表示参数不是-开头,则应该是占位参数
    if (j === 0) {
      out._.push(arg);
    } else if (arg.substring(j, j + 3) === "no-") {
      //* 不是0,则表示是布尔或字符串参数了
      //* 如果除去前面的-之后,是以 no- 开头,默认值为true的布尔参数,会自动生成带 -no 的额外参数
      //* 则截取真正的参数名
      name = arg.slice(Math.max(0, j + 3));
      //* strict是false,这里不执行
      if (strict && !~keys.indexOf(name)) {
        return opts.unknown(arg);
      }
      //* 因为是 no- 开头,则该参数值为 false
      out[name] = false;
    } else {
      //* 不是 no- 开头,则是普通的布尔参数和字符串参数了
      //* 参数名至少是一个字符,然后后面接=号进行赋值,即 -a=a
      for (idx = j + 1; idx < arg.length; idx++) {
        if (arg.charCodeAt(idx) === 61) {
          break;
        } //* "="
      }
      //* 此时 idx 指向 = 号
      //* 截取参数名
      name = arg.substring(j, idx);
      //* 解析参数值
      val =
        //* 截取等号后面的全部
        arg.slice(Math.max(0, ++idx)) ||
        //* 最后一个参数,-a 则 解析成 a=true
        i + 1 === len ||
        //* 不是最后一个参数,看下一个参数是不是-开头,是-开头则说明当前参数值为true
        ("" + args[i + 1]).charCodeAt(0) === 45 ||
        //* 下一个参数不是-开头,那么就拿下一个参数作为值,
        //* 跳过下一个,因为当作当前参数的值进行解析了
        args[++i];
      //* 是否前面有2个-
      //* 有2个--表示是字符串参数,否则为1个,表示布尔参数,且布尔参数是每一个字符表示一个参数!
      arr = j === 2 ? [name] : name;

      for (idx = 0; idx < arr.length; idx++) {
        name = arr[idx];
        //* strict是false,这里不执行
        if (strict && !~keys.indexOf(name)) {
          return opts.unknown("-".repeat(j) + name);
        }
        //* 如果是字符串,第三个参数一定是true
        //* 如果是布尔,那么是多个参数
        toVal(out, name, idx + 1 < arr.length || val, opts);
      }
    }
  }
  //* 默认值处理
  if (defaults) {
    for (k in opts.default) {
      //* 解析后的值是undefined,用默认值
      if (out[k] === void 0) {
        out[k] = opts.default[k];
      }
    }
  }
  //* 别名处理
  if (alibi) {
    for (k in out) {
      arr = opts.alias[k] || [];
      while (arr.length > 0) {
        //* 对每个别名的值赋上对应参数名的值
        out[arr.shift()] = out[k];
      }
    }
  }
  return out;
}

function toVal(out, key, val, opts) {
  let x;
  const old = out[key];
  //* ~number 只有当number为-1时,结果是0,即false
  const nxt = ~opts.string.indexOf(key)
    //* 是字符串,undefined和true当空串处理
    ? val == undefined || val === true
      ? ""
      //* 转成字符串
      : String(val)
    //* 不是字符串,则是布尔参数
    : typeof val === "boolean"
    //* 值是布尔类型,则就是该值
    ? val
    //* 值不是布尔类型
    //* 先看该参数名在不在布尔参数集合中
    : ~opts.boolean.indexOf(key)
    //* 在布尔参数集合中,对字符串的true和false进行处理
    ? val === "false"
      ? false
      : val === "true" ||
        //* 不是字符串的true和false,是其他字符串(转成数字是否是NaN)
        //* 如果val转成数字是NaN,则保存该字符串,否则保存该数字到out._中
        //* 同时使用该值
        (out._.push(((x = +val), x * 0 === 0) ? x : val), !!val)
    //* 不在布尔集合中
    //* 同理,判断是否能转成数字
    //* 可以则使用数字,不可以则使用原始串
    : ((x = +val), x * 0 === 0)
    ? x
    : val;
  //* 更新
  out[key] =
    //* undefined -> value -> [value1, value2] -> [value1, value2]+[value3, value4]
    old == undefined ? nxt : Array.isArray(old) ? old.concat(nxt) : [old, nxt];
}

技巧总结

  • tsOmit, keyof, in, extends 的使用
  • 有三种命令行参数,占位、字符串、布尔

    占位类型参数没有别名

  • colorette是无依赖且高性能的终端文本样式美化工具
  • Object.entries 的顺序与 for in 的顺序一致,先按数字升序,后按定义时的顺序
  • 循环数组不关注从前往后还是从后往前,代码可以这么写
    ts
    const array = Array.form({ length: 10 }, (_, index) => index);
    for(let i = array.length; i-- > 0; ) { //* 大于的判断执行时,i就减1了
      console.log(i); //* 因为大于的判断后,i已经减1了,所以刚好满足数组下标
    }
  • 判断数组是否某项,用 includes 或用 indexOf 的结果与 0 或 -1 比较,这里有个新方法
    ts
    const array = Array.form({ length: 10 }, (_, index) => index);
    const existA = array.indexOf(5) > -1;
    const existB = !!(~array.indexOf(5));
    //* 因为下标为-1表示不存在,其余大于等于0都是存在
    //* 可以用按位非(~)运算符将indexOf的结果转化
    //* 只有结果为-1时,转化的结果才是0,逻辑上就是非了!!!