which@5.0.0源码阅读

2 阅读8分钟

发布日期 2024 年 10 月 1 日

whichNode.js 版的系统 which 命令,核心作用是跨平台查找可执行命令对应的完整文件路径(比如找到 node 对应的 node.exenpm 对应的 npm.cmd)。

package.json

node-which-5.0.0/package.json

{
  "author": "GitHub Inc.",
  "name": "which",
  "description": "Like which(1) unix command. Find the first instance of an executable in the PATH.",
  "version": "5.0.0",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/npm/node-which.git"
  },
  "main": "lib/index.js",
  "bin": {
    "node-which": "./bin/which.js"
  },
  "license": "ISC",
  "dependencies": {
    "isexe": "^3.1.1"
  },
  "devDependencies": {
    "@npmcli/eslint-config": "^5.0.0",
    "@npmcli/template-oss": "4.23.3",
    "tap": "^16.3.0"
  },
  "scripts": {
    "test": "tap",
    "lint": "npm run eslint",
    "postlint": "template-oss-check",
    "template-oss-apply": "template-oss-apply --force",
    "lintfix": "npm run eslint -- --fix",
    "snap": "tap",
    "posttest": "npm run lint",
    "eslint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\""
  },
  "files": [
    "bin/",
    "lib/"
  ],
  "tap": {
    "check-coverage": true,
    "nyc-arg": [
      "--exclude",
      "tap-snapshots/**"
    ]
  },
  "engines": {
    "node": "^18.17.0 || >=20.5.0"
  },
  "templateOSS": {
    "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
    "version": "4.23.3",
    "publish": "true"
  }
}

bin/which.js 文件

node-which-5.0.0/bin/which.js

#!/usr/bin/env node

const which = require('../lib')
const argv = process.argv.slice(2)

// 用法提示函数(usage)
const usage = (err) => {
  if (err) {
    console.error(`which: ${err}`)
  }
  console.error('usage: which [-as] program ...')
  process.exit(1) // 退出进程,错误码 1(表示参数错误)
}

if (!argv.length) {
  // 没有任何参数时,直接输出用法
  return usage()
}

let dashdash = false // 标记是否遇到 -- 分隔符

// 用 reduce 拆分「要查找的命令(commands)」和「标志位(flags)」
const [commands, flags] = argv.reduce((acc, arg) => {
  // 若遇到 -- 或已标记 dashdash,后续参数都当命令,不解析
  if (dashdash || arg === '--') {
    dashdash = true
    return acc
  }

  // 不以 - 开头的参数 → 是要查找的命令,加入 acc 数组
  if (!/^-/.test(arg)) {
    acc[0].push(arg)
    return acc
  }
  // 以 - 开头的参数 → 是标志位,解析每个字符
  for (const flag of arg.slice(1).split('')) {
    if (flag === 's') {
      // -s:静默模式(找到路径也不输出,仅通过退出码判断)
      acc[1].silent = true
    } else if (flag === 'a') {
      // -a:返回所有匹配的命令路径(如 PATH 中有多个 node 版本)
      acc[1].all = true
    } else {
      // 非法标志位,输出用法并退出
      usage(`illegal option -- ${flag}`)
    }
  }

  return acc
}, [[], {}])

for (const command of commands) {
  try {
    // 同步查找命令路径:all: flags.all → 为 true 时返回所有匹配路径,否则返回第一个
    const res = which.sync(command, { all: flags.all })

    // 非静默模式(!flags.silent):输出结果(数组转换行字符串,兼容多路径)
    if (!flags.silent) {
      console.log([].concat(res).join('\n'))
    }
  } catch (err) {
    // 查找失败(命令不存在):设置退出码 1,但不退出进程(继续处理下一个命令)
    process.exitCode = 1
  }
}

入口文件

node-which-5.0.0/lib/index.js

// 导入可执行文件检查工具
const { isexe, sync: isexeSync } = require('isexe')
// 导入路径处理工具
const { join, delimiter, sep, posix } = require('path')


module.exports = which
which.sync = whichSync

API 之 which

node-which-5.0.0/lib/index.js

const which = async (cmd, opt = {}) => {
  // 获取路径相关信息
  // pathEnv:PATH 分割后的目录数组(如 ['/usr/bin', '/bin'])。
  // pathExt:需要尝试的文件扩展名数组(如 ['.exe', '.cmd'] 或 ['', '.sh'])。
  // pathExtExe:用于 isexe 检查的完整扩展名列表。
  const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt)
  // 存储找到的所有可执行文件路径
  const found = []

  // 遍历 PATH 中的每个目录
  for (const envPart of pathEnv) {
    // 拼接目录与命令,生成基础路径
    const p = getPathPart(envPart, cmd)

    // 遍历每个可能的扩展名
    for (const ext of pathExt) {
      // 拼接基础路径与扩展名,生成完整路径
      const withExt = p + ext
       // 检查该路径是否为可执行文件
      const is = await isexe(withExt, { pathExt: pathExtExe, ignoreErrors: true })
      if (is) {
        // 若不需要返回所有结果,直接返回第一个匹配项
        if (!opt.all) {
          return withExt
        }
        // 否则加入结果数组
        found.push(withExt)
      }
    }
  }

  if (opt.all && found.length) {
    return found
  }

  // 若允许不抛出错误,返回 null
  if (opt.nothrow) {
    return null
  }

  // 否则抛出“未找到”错误
  throw getNotFoundError(cmd)
}

const { isexe } = require('isexe')

getPathInfo

const getPathInfo = (
  cmd, // 要查找的命令(如 npm、ls、./script.sh)
  {
    // 指定的搜索路径(默认使用环境变量 PATH)
    path: optPath = process.env.PATH,
    // 可执行文件的扩展名列表(默认使用环境变量 PATHEXT,Windows 特有)
    pathExt: optPathExt = process.env.PATHEXT,
    // 路径分隔符(默认根据系统自动确定,Windows 用 ;,类 Unix 用 :)
    delimiter: optDelimiter = delimiter,
  },
) => {
  
  // 第一步:生成「搜索路径列表(pathEnv)」
  // 判断命令是否包含「路径分隔符」(/ 或 \)
  const pathEnv = cmd.match(rSlash)
    // 命令包含路径分隔符
    ? [''] // [''],表示无需搜索系统路径
    : [
      // Windows:先加「当前工作目录(process.cwd ())」
      // Windows 原生规则:优先搜当前目录
      // Unix(Linux/Mac):不加当前目录
      ...(isWindows ? [process.cwd()] : []),
      // 把 optPath(默认系统 PATH)按分隔符(Windows ;、Unix :)拆分为目录列表
      ...(optPath || '').split(optDelimiter),
    ];

  // 第二步:Windows 系统的扩展名特殊处理
  if (isWindows) {
    // 1、获取可执行文件扩展名列表
    const pathExtExe =
      optPathExt || ['.EXE', '.CMD', '.BAT', '.COM'].join(optDelimiter);

    // 将 pathExtExe 拆分后,为每个扩展名添加小写版本(如 .EXE → .EXE 和 .exe)
    // 因为 Windows 文件名不区分大小写,确保大小写不同的扩展名都能被匹配
    const pathExt = pathExtExe
      .split(optDelimiter)
      .flatMap((item) => [item, item.toLowerCase()]);

    // 若命令包含 .(如 my.script),可能用户已指定部分扩展名
    // 此时在 pathExt 开头添加空字符串(表示不添加扩展名直接匹配)
    if (cmd.includes('.') && pathExt[0] !== '') {
      pathExt.unshift('');
    }

    return { pathEnv, pathExt, pathExtExe };
  }

  // 类 Unix 系统(macOS、Linux)中,无需复杂的扩展名处
  return { pathEnv, pathExt: [''] };
};
const {  delimiter } = require('path')

getPathPart

node-which-5.0.0/lib/index.js

const getPathPart = (
  raw, // 原始的路径片段(可能包含引号,如 "C:\Program Files")
  cmd // 要拼接的命令(如 node.exe、script.sh,可能包含相对路径前缀
) => {
  // 处理带引号的路径片段(pathPart 生成)
  const pathPart = /^".*"$/.test(raw) ? raw.slice(1, -1) : raw

  // 提取相对路径前缀(prefix 生成)
  const prefix = !pathPart && rRel.test(cmd) ? cmd.slice(0, 2) : ''

  // 拼接路径片段与命令
  return prefix + join(pathPart, cmd)
}
const rSlash = new RegExp(
  `[${posix.sep}${sep === posix.sep ? '' : sep}]`.replace(/(\\)/g, '\\$1'),
);

const rRel = new RegExp(`^\\.${rSlash.source}`);

const { join, sep, posix } = require('path')

API 之 which.sync

node-which-5.0.0/lib/index.js

const whichSync = (cmd, opt = {}) => {
  const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt)
  const found = []

  for (const pathEnvPart of pathEnv) {
    const p = getPathPart(pathEnvPart, cmd)

    for (const ext of pathExt) {
      const withExt = p + ext
      const is = 
        isexeSync(withExt, { pathExt: pathExtExe, ignoreErrors: true })
      
      if (is) {
        if (!opt.all) {
          return withExt
        }
        found.push(withExt)
      }
    }
  }

  if (opt.all && found.length) {
    return found
  }

  if (opt.nothrow) {
    return null
  }

  throw getNotFoundError(cmd)
}

const { sync: isexeSync } = require('isexe')

isexe@3.1.1

发布日期 2023 年 8 月 3 日

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

package.json

{
  "name": "isexe",
  "version": "3.1.1",
  "description": "Minimal module to check if a file is executable.",
  "main": "./dist/cjs/index.js",
  "module": "./dist/mjs/index.js",
  "types": "./dist/cjs/index.js",
  "files": [
    "dist"
  ],
  "exports": {
    ".": {
      "import": {
        "types": "./dist/mjs/index.d.ts",
        "default": "./dist/mjs/index.js"
      },
      "require": {
        "types": "./dist/cjs/index.d.ts",
        "default": "./dist/cjs/index.js"
      }
    },
    "./posix": {
      "import": {
        "types": "./dist/mjs/posix.d.ts",
        "default": "./dist/mjs/posix.js"
      },
      "require": {
        "types": "./dist/cjs/posix.d.ts",
        "default": "./dist/cjs/posix.js"
      }
    },
    "./win32": {
      "import": {
        "types": "./dist/mjs/win32.d.ts",
        "default": "./dist/mjs/win32.js"
      },
      "require": {
        "types": "./dist/cjs/win32.d.ts",
        "default": "./dist/cjs/win32.js"
      }
    },
    "./package.json": "./package.json"
  },
  "devDependencies": {
    "@types/node": "^20.4.5",
    "@types/tap": "^15.0.8",
    "c8": "^8.0.1",
    "mkdirp": "^0.5.1",
    "prettier": "^2.8.8",
    "rimraf": "^2.5.0",
    "sync-content": "^1.0.2",
    "tap": "^16.3.8",
    "ts-node": "^10.9.1",
    "typedoc": "^0.24.8",
    "typescript": "^5.1.6"
  },
  "scripts": {
    "preversion": "npm test",
    "postversion": "npm publish",
    "prepublishOnly": "git push origin --follow-tags",
    "prepare": "tsc -p tsconfig/cjs.json && tsc -p tsconfig/esm.json && bash ./scripts/fixup.sh",
    "pretest": "npm run prepare",
    "presnap": "npm run prepare",
    "test": "c8 tap",
    "snap": "c8 tap",
    "format": "prettier --write . --loglevel warn --ignore-path ../../.prettierignore --cache",
    "typedoc": "typedoc --tsconfig tsconfig/esm.json ./src/*.ts"
  },
  "author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)",
  "license": "ISC",
  "tap": {
    "coverage": false,
    "node-arg": [
      "--enable-source-maps",
      "--no-warnings",
      "--loader",
      "ts-node/esm"
    ],
    "ts": false
  },
  "prettier": {
    "semi": false,
    "printWidth": 75,
    "tabWidth": 2,
    "useTabs": false,
    "singleQuote": true,
    "jsxSingleQuote": false,
    "bracketSameLine": true,
    "arrowParens": "avoid",
    "endOfLine": "lf"
  },
  "repository": "https://github.com/isaacs/isexe",
  "engines": {
    "node": ">=16"
  }
}

入口文件

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

isexe-3.1.1/src/posix.ts

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

checkMode

isexe-3.1.1/src/posix.ts

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)
  )
}

posix.sync

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
  }
}

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
  }
}

checkStat

isexe-3.1.1/src/win32.ts

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
  }
}