1const { isexe, sync: isexeSync } = require('isexe')
2const { join, delimiter, sep, posix } = require('path')
3
4const isWindows = process.platform === 'win32'
5
6// used to check for slashed in commands passed in. always checks for the posix
7// seperator on all platforms, and checks for the current separator when not on
8// a posix platform. don't use the isWindows check for this since that is mocked
9// in tests but we still need the code to actually work when called. that is also
10// why it is ignored from coverage.
11/* istanbul ignore next */
12const rSlash = new RegExp(`[${posix.sep}${sep === posix.sep ? '' : sep}]`.replace(/(\\)/g, '\\$1'))
13const rRel = new RegExp(`^\\.${rSlash.source}`)
14
15const getNotFoundError = (cmd) =>
16  Object.assign(new Error(`not found: ${cmd}`), { code: 'ENOENT' })
17
18const getPathInfo = (cmd, {
19  path: optPath = process.env.PATH,
20  pathExt: optPathExt = process.env.PATHEXT,
21  delimiter: optDelimiter = delimiter,
22}) => {
23  // If it has a slash, then we don't bother searching the pathenv.
24  // just check the file itself, and that's it.
25  const pathEnv = cmd.match(rSlash) ? [''] : [
26    // windows always checks the cwd first
27    ...(isWindows ? [process.cwd()] : []),
28    ...(optPath || /* istanbul ignore next: very unusual */ '').split(optDelimiter),
29  ]
30
31  if (isWindows) {
32    const pathExtExe = optPathExt ||
33      ['.EXE', '.CMD', '.BAT', '.COM'].join(optDelimiter)
34    const pathExt = pathExtExe.split(optDelimiter).flatMap((item) => [item, item.toLowerCase()])
35    if (cmd.includes('.') && pathExt[0] !== '') {
36      pathExt.unshift('')
37    }
38    return { pathEnv, pathExt, pathExtExe }
39  }
40
41  return { pathEnv, pathExt: [''] }
42}
43
44const getPathPart = (raw, cmd) => {
45  const pathPart = /^".*"$/.test(raw) ? raw.slice(1, -1) : raw
46  const prefix = !pathPart && rRel.test(cmd) ? cmd.slice(0, 2) : ''
47  return prefix + join(pathPart, cmd)
48}
49
50const which = async (cmd, opt = {}) => {
51  const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt)
52  const found = []
53
54  for (const envPart of pathEnv) {
55    const p = getPathPart(envPart, cmd)
56
57    for (const ext of pathExt) {
58      const withExt = p + ext
59      const is = await isexe(withExt, { pathExt: pathExtExe, ignoreErrors: true })
60      if (is) {
61        if (!opt.all) {
62          return withExt
63        }
64        found.push(withExt)
65      }
66    }
67  }
68
69  if (opt.all && found.length) {
70    return found
71  }
72
73  if (opt.nothrow) {
74    return null
75  }
76
77  throw getNotFoundError(cmd)
78}
79
80const whichSync = (cmd, opt = {}) => {
81  const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt)
82  const found = []
83
84  for (const pathEnvPart of pathEnv) {
85    const p = getPathPart(pathEnvPart, cmd)
86
87    for (const ext of pathExt) {
88      const withExt = p + ext
89      const is = isexeSync(withExt, { pathExt: pathExtExe, ignoreErrors: true })
90      if (is) {
91        if (!opt.all) {
92          return withExt
93        }
94        found.push(withExt)
95      }
96    }
97  }
98
99  if (opt.all && found.length) {
100    return found
101  }
102
103  if (opt.nothrow) {
104    return null
105  }
106
107  throw getNotFoundError(cmd)
108}
109
110module.exports = which
111which.sync = whichSync
112