发布日期 2024 年 10 月 1 日
which 是 Node.js 版的系统 which 命令,核心作用是跨平台查找可执行命令对应的完整文件路径(比如找到 node 对应的 node.exe、npm 对应的 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
}
}