普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月28日首页

mri@1.2.0源码阅读

作者 米丘
2026年1月28日 18:23

mri(全称 Minimalist CLI Argument Parser)的核心作用是解析命令行传入的参数,把用户在终端输入的命令行参数(比如 --port 3000-v)转换成易于在代码中使用的 JavaScript 对象。

有什么特点?

  1. 超轻量:体积只有几百 KB,无依赖,适合对包体积敏感的 Node.js/ 前端工具开发;
  2. 易用性:API 极简,几行代码就能完成参数解析;
  3. 常用场景:前端工程化工具(如自定义构建脚本、本地开发服务器、CLI 工具)中解析命令行参数。

index文件

mri-1.2.0/src/index.js

export default function (args, opts) {
  args = args || []; // 命令行参数数组(比如 process.argv.slice(2))
  opts = opts || {}; // 解析配置(别名、默认值、类型等)

  var k, arr, arg, name, val, out={ _:[] }; // out 解析结果对象
  var i=0, j=0, idx=0, len=args.length;

  // 标记是否配置了别名、严格模式、默认值
  const alibi = opts.alias !== void 0;
  const strict = opts.unknown !== void 0;
  const defaults = opts.default !== void 0;

  opts.alias = opts.alias || {};
  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++) {
        (opts.alias[arr[i]] = arr.concat(k)).splice(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] || [];
      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]; // 当前处理的参数(比如 '--port'、'-p=3000')

    // 1. 遇到 "--" 终止解析,后续参数全部放入 out._
    if (arg === '--') {
      out._ = out._.concat(args.slice(++i));
      break;
    }

    // 2. 计算参数前的 "-" 数量(比如 --port 是 2 个,-p 是 1 个)
    for (j=0; j < arg.length; j++) {
      if (arg.charCodeAt(j) !== 45) break; // "-"
    }

    // 3. 无 "-":非选项参数,放入 out._
    if (j === 0) {
      out._.push(arg);

      // 4. 以 "no-" 开头(比如 --no-open):布尔值取反
    } else if (arg.substring(j, j + 3) === 'no-') {
      name = arg.substring(j + 3);
      if (strict && !~keys.indexOf(name)) {
        return opts.unknown(arg);
      }
      out[name] = false;

      // 5. 正常选项参数(比如 --port=3000、-p3000、-p 3000)
    } else {
      // 找到 "=" 的位置(分割参数名和值)
      for (idx=j+1; idx < arg.length; idx++) {
        if (arg.charCodeAt(idx) === 61) break; // "="
      }

      // 取值:有=则取=后的值;无=则取下一个参数(如果下一个不是-开头)
      name = arg.substring(j, idx);
      val = arg.substring(++idx) || (i+1 === len || (''+args[i+1]).charCodeAt(0) === 45 || args[++i]);
      arr = (j === 2 ? [name] : name);

      // 遍历解析(比如 -pv 会拆成 p 和 v 分别处理)
      for (idx=0; idx < arr.length; idx++) {
        name = arr[idx];
        if (strict && !~keys.indexOf(name)) return opts.unknown('-'.repeat(j) + name);
        // 把解析后的值存入 out(toVal 是核心工具函数)
        toVal(out, name, (idx + 1 < arr.length) || val, opts);
      }
    }
  }

  // 1. 补全默认值:如果参数未解析到,用默认值填充
  if (defaults) {
    for (k in opts.default) {
      if (out[k] === void 0) {
        out[k] = opts.default[k];
      }
    }
  }

  // 2. 同步别名:比如解析到 -p=3000,自动给 out.port 也赋值 3000
  if (alibi) {
    for (k in out) {
      arr = opts.alias[k] || [];
      while (arr.length > 0) {
        out[arr.shift()] = out[k];
      }
    }
  }

  return out;
}

toArr

function toArr(any) {
return any == null ? [] : Array.isArray(any) ? any : [any];
}

toVal

function toVal(out, key, val, opts) {
  var x,
    old = out[key],
    nxt = !!~opts.string.indexOf(key)
      ? val == null || val === true
        ? ''
        : String(val)
      : typeof val === 'boolean'
        ? val
        : !!~opts.boolean.indexOf(key)
          ? val === 'false'
            ? false
            : val === 'true' ||
              (out._.push(((x = +val), x * 0 === 0) ? x : val), !!val)
          : ((x = +val), x * 0 === 0)
            ? x
            : val;
  out[key] =
    old == null ? nxt : Array.isArray(old) ? old.concat(nxt) : [old, nxt];
}

示例


const mri = require('mri');
const args = mri(process.argv.slice(2));
console.log(args);

image.png

昨天以前首页

isexe@3.1.1源码阅读

作者 米丘
2026年1月26日 18:27

发布日期 2023 年 8 月 3 日

isexe跨平台检查文件是否为「可执行文件」的专用工具包,核心解决「Windows 和 Unix 系统判断可执行文件的规则完全不同」的问题。

unix系统根据文件权限判断;window系统根据文件扩展名判断。

入口文件 index.js

isexe-3.1.1/src/index.ts

import * as posix from './posix.js' // 导入 POSIX 系统(Linux/macOS 等)的实现
import * as win32 from './win32.js' // 导入 Windows 系统的实现
export * from './options.js' // 导出配置选项类型(如 IsexeOptions)
export { win32, posix }  // 允许直接访问特定平台的实现

const platform = process.env._ISEXE_TEST_PLATFORM_ || process.platform
const impl = platform === 'win32' ? win32 : posix

/**
 * Determine whether a path is executable on the current platform.
 */
export const isexe = impl.isexe
/**
 * Synchronously determine whether a path is executable on the
 * current platform.
 */
export const sync = impl.sync

posix.isexe(异步)

isexe-3.1.1/src/posix.ts

const isexe = async (
  path: string,  // 要检查的文件路径(比如 "/usr/bin/node" 或 "C:\\node.exe")
  options: IsexeOptions = {}  // 配置项,默认空对象
): Promise<boolean> => {
  
  const { ignoreErrors = false } = options

  try {
    // await stat(path):获取文件状态
    // checkStat(statResult, options):判断是否可执行
    return checkStat(await stat(path), options)
  } catch (e) {
    // 把错误转为 Node.js 标准错误类型(带错误码)
    const er = e as NodeJS.ErrnoException
    if (ignoreErrors || er.code === 'EACCES') return false
    throw er // 非预期错误,向上抛出
  }
}
import { Stats, statSync } from 'fs'
import { stat } from 'fs/promises'

checkStat

const checkStat = (stat: Stats, options: IsexeOptions) =>
  stat.isFile() && checkMode(stat, options)

checkMode

const checkMode = (
  // 文件的 Stats 对象(通常由 fs.stat 或 fs.lstat 获取)
  // 包含文件的权限位(mode)、所有者 ID(uid)、所属组 ID(gid)等元数据。
  stat: Stats, 
  // 配置对象,允许自定义用户 ID(uid)、组 ID(gid)、用户所属组列表(groups),默认使用当前进程的用户信息。
  options: IsexeOptions
) => {
  // 1、获取用户与组信息
  // 当前用户的 ID(优先使用 options.uid,否则调用 process.getuid() 获取当前进程的用户 ID)。
  const myUid = options.uid ?? process.getuid?.()
  // 当前用户所属的组 ID 列表(优先使用 options.groups,否则调用 process.getgroups() 获取)。
  const myGroups = options.groups ?? process.getgroups?.() ?? []
  // 当前用户的主组 ID(优先使用 options.gid,否则调用 process.getgid(),或从 myGroups 取第一个组 ID)。
  const myGid = options.gid ?? process.getgid?.() ?? myGroups[0]
  // 若无法获取 myUid 或 myGid,抛出错误(权限判断依赖这些信息)
  if (myUid === undefined || myGid === undefined) {
    throw new Error('cannot get uid or gid')
  }

  // 2、构建用户所属组集合
  const groups = new Set([myGid, ...myGroups])

  // 3、解析文件权限位与归属信息
  const mod = stat.mode // 文件的权限位(整数,如 0o755 表示 rwxr-xr-x)
  const uid = stat.uid // 文件所有者的用户 ID
  const gid = stat.gid // 文件所属组的组 ID

  // 4、定义权限位掩码
  // 八进制 100 → 十进制 64 → 对应所有者的执行权限位(x)
  const u = parseInt('100', 8)
  // 八进制 010 → 十进制 8 → 对应所属组的执行权限位(x)
  const g = parseInt('010', 8)
  // 八进制 001 → 十进制 1 → 对应其他用户的执行权限位(x)
  const o = parseInt('001', 8)
  // 所有者和所属组的执行权限位掩码(64 | 8 = 72)
  const ug = u | g

  // 5、权限判断逻辑
  return !!(
    mod & o || // 1. 其他用户有执行权限
    (mod & g && groups.has(gid)) || // 2. 所属组有执行权限,且当前用户属于该组
    (mod & u && uid === myUid) || // 3. 所有者有执行权限,且当前用户是所有者
    (mod & ug && myUid === 0)  // 4. 所有者或组有执行权限,且当前用户是 root(UID=0)
  )
}

mod (权限位) :Unix 系统中用 9 位二进制表示文件权限(分为所有者、所属组、其他用户三类,每类 3 位,分别控制读 r、写 w、执行 x 权限)。例如 0o755 对应二进制 111 101 101,表示:

  • 所有者(u):可读、可写、可执行(rwx)。
  • 所属组(g):可读、可执行(r-x)。
  • 其他用户(o):可读、可执行(r-x)。

posix.sync (同步)

isexe-3.1.1/src/posix.ts

const sync = (
  path: string,
  options: IsexeOptions = {}
): boolean => {
  
  const { ignoreErrors = false } = options
  try {
    return checkStat(statSync(path), options)
    
  } catch (e) {
    const er = e as NodeJS.ErrnoException
    if (ignoreErrors || er.code === 'EACCES') return false
    throw er
  }
}
import { Stats, statSync } from 'fs'
import { stat } from 'fs/promises'

win32.isexe (异步)

isexe-3.1.1/src/win32.ts

const isexe = async (
  path: string,
  options: IsexeOptions = {}
): Promise<boolean> => {
  
  const { ignoreErrors = false } = options
  try {
    return checkStat(await stat(path), path, options)
  } catch (e) {
    const er = e as NodeJS.ErrnoException
    if (ignoreErrors || er.code === 'EACCES') return false
    throw er
  }
}
import { Stats, statSync } from 'fs'
import { stat } from 'fs/promises'

checkStat

const checkStat = (stat: Stats, path: string, options: IsexeOptions) =>
  stat.isFile() && checkPathExt(path, options)

checkPathExt

isexe-3.1.1/src/win32.ts

const checkPathExt = (path: string, options: IsexeOptions) => {

  // 获取可执行扩展名列表
  const { pathExt = process.env.PATHEXT || '' } = options

  const peSplit = pathExt.split(';')
  // 特殊情况处理:空扩展名
  // 空扩展名通常表示 “任何文件都视为可执行”,这是一种特殊配置
  if (peSplit.indexOf('') !== -1) {
    return true
  }

  // 检查文件扩展名是否匹配
  for (let i = 0; i < peSplit.length; i++) {
    // 转小写:避免大小写问题(比如.EXE和.exe视为同一个)
    const p = peSplit[i].toLowerCase()
    // 截取文件路径的最后N个字符(N是当前扩展名p的长度),也转小写
    const ext = path.substring(path.length - p.length).toLowerCase()

    // 匹配条件:扩展名非空 + 文件扩展名和列表中的扩展名完全一致
    if (p && ext === p) {
      return true
    }
  }
  return false
}

win32.sync(同步)

isexe-3.1.1/src/win32.ts

const sync = (
  path: string,
  options: IsexeOptions = {}
): boolean => {
  
  const { ignoreErrors = false } = options
  try {
    return checkStat(statSync(path), path, options)
    
  } catch (e) {
    const er = e as NodeJS.ErrnoException
    if (ignoreErrors || er.code === 'EACCES') return false
    throw er
  }
}
❌
❌