/
Which.purs
196 lines (176 loc) · 6.72 KB
/
Which.purs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
-- A majority of the below code was ported from this JavaScript library
-- https://github.com/npm/node-which
-- Copyright `node-which` contributors
-- ISC License: https://opensource.org/license/isc/
module Node.Library.Execa.Which where
import Prelude
import Control.Alt ((<|>))
import Control.Monad.Rec.Class (Step(..), tailRecM)
import Data.Array as Array
import Data.Array.NonEmpty (NonEmptyArray)
import Data.Array.NonEmpty as NEA
import Data.Either (Either(..), note)
import Data.Maybe (Maybe(..), fromJust, fromMaybe, isNothing)
import Data.String (Pattern(..))
import Data.String as String
import Data.String.CodeUnits as SCU
import Data.String.Regex (Regex, test)
import Data.String.Regex.Flags (noFlags)
import Data.String.Regex.Unsafe (unsafeRegex)
import Data.Tuple (Tuple(..))
import Effect (Effect)
import Effect.Aff (Aff)
import Effect.Class (liftEffect)
import Node.Library.Execa.IsExe (defaultIsExeOptions, isExe, isExeSync)
import Node.Library.Execa.Utils (CustomError, buildCustomError, envKey)
import Node.Path as Path
import Node.Platform (Platform(..))
import Node.Process (platform)
import Node.Process as Process
import Partial.Unsafe (unsafePartial)
isWindows :: Effect Boolean
isWindows = do
ty <- envKey "OSTYPE"
pure $ platform == Just Win32 || ty == Just "cygwin" || ty == Just "msys"
jsColon :: Effect String
jsColon = do
w <- isWindows
pure if w then ";" else ":"
type WhichOptions =
{ path :: Maybe String
, pathExt :: Maybe String
, colon :: Maybe String
, all :: Boolean
}
defaultWhichOptions :: WhichOptions
defaultWhichOptions =
{ path: Nothing
, pathExt: Nothing
, colon: Nothing
, all: false
}
type WhichPathInfo =
{ pathEnv :: Array String
, pathExtExe :: String
, pathExt :: Array String
}
getNotFoundError :: String -> CustomError (code :: String)
getNotFoundError cmd = buildCustomError ("not found: " <> cmd) { code: "ENOENT" }
getPathInfo :: String -> WhichOptions -> Effect WhichPathInfo
getPathInfo cmd options = do
-- PureScript implementation note: get all the effectful stuff first
-- before we use it in the rest of this function for readability.
cwd <- Process.cwd
mbPath <- envKey "PATH"
mbPathExt <- envKey "PATHEXT"
isWin <- isWindows
colon <- case options.colon of
Nothing -> jsColon
Just c -> pure c
-- If it has a slash, then we don't bother searching the pathenv.
-- just check the file itself, and that's it.
let
pathEnv
| test hasPosixSlashRegex cmd || (isWin && test hasWindowsSlashRegex cmd) = [ "" ]
| otherwise = do
let
paths = String.split (Pattern colon) $ fromMaybe "" $ options.path <|> mbPath
-- windows always checks the cwd first
if isWin then Array.cons cwd paths else paths
pathExtExe
-- PureScript note: small attempt at fixing bug
-- https://github.com/npm/node-which/issues/57
| isWin = fromMaybe ".EXE;.CMD;.BAT;.exe;.cmd;.bat" $ options.pathExt <|> mbPathExt
| otherwise = ""
pathExt
| isWin = String.split (Pattern colon) pathExtExe
| otherwise = [ "" ]
pure { pathEnv, pathExt, pathExtExe }
where
hasPosixSlashRegex = unsafeRegex """\/""" noFlags
hasWindowsSlashRegex = unsafeRegex """\\""" noFlags
which :: String -> WhichOptions -> Aff (Either (CustomError (code :: String)) (NonEmptyArray String))
which cmd options = do
{ pathEnv, pathExt, pathExtExe } <- liftEffect $ getPathInfo cmd options
go pathEnv pathExt pathExtExe
where
go pathEnv pathExt pathExtExe = step [] 0
where
pathEnvLen = Array.length pathEnv
pathExtLen = Array.length pathExt
step :: Array String -> Int -> Aff (Either (CustomError (code :: String)) (NonEmptyArray String))
step found i
| i == pathEnvLen =
pure $ note (getNotFoundError cmd) $ NEA.fromArray found
| otherwise = do
let
ppRaw = unsafePartial fromJust $ pathEnv Array.!! i
pathPart
| test quotedRegex ppRaw = SCU.slice 1 (-1) ppRaw
| otherwise = ppRaw
pCmd = Path.concat [ pathPart, cmd ]
p
| not $ String.null pathPart
, test dotSlashRegex cmd = SCU.slice 0 2 cmd <> pCmd
| otherwise = pCmd
subStep found p i 0
subStep found p i ii
| ii == pathExtLen = step found $ i + 1
| otherwise = do
let
ext = unsafePartial $ fromJust $ pathExt Array.!! ii
pExt = p <> ext
Tuple mbErr is <- isExe pExt (defaultIsExeOptions { pathExt = Just pathExtExe })
if (isNothing mbErr && is) then do
if options.all then do
subStep (Array.snoc found pExt) p i $ ii + 1
else do
pure $ Right $ NEA.singleton pExt
else do
subStep found p i $ ii + 1
whichSync :: String -> WhichOptions -> Effect (Either (CustomError (code :: String)) (NonEmptyArray String))
whichSync cmd options = do
{ pathEnv, pathExt, pathExtExe } <- getPathInfo cmd options
go pathEnv pathExt pathExtExe
where
go pathEnv pathExt pathExtExe = tailRecM loop { found: [], outerLoopIdx: 0, innerLoop: Nothing }
where
loop acc@{ found, outerLoopIdx, innerLoop } =
case innerLoop of
-- outer for loop
Nothing -> case pathEnv Array.!! outerLoopIdx of
Nothing ->
pure $ Done $ Left $ getNotFoundError cmd
Just ppRaw -> do
let
pathPart
| test quotedRegex ppRaw = SCU.slice 1 (-1) ppRaw
| otherwise = ppRaw
pCmd = Path.concat [ pathPart, cmd ]
p
| not $ String.null pathPart
, test dotSlashRegex cmd = SCU.slice 0 2 cmd <> pCmd
| otherwise = pCmd
pure $ Loop $ acc { innerLoop = Just { p, j: 0 } }
-- inner for loop
Just { p, j } -> case pathExt Array.!! j of
Nothing ->
pure $ Loop $ acc { outerLoopIdx = outerLoopIdx + 1, innerLoop = Nothing }
Just pExt -> do
let cur = p <> pExt
eOrB <- isExeSync cur (defaultIsExeOptions { pathExt = Just pathExtExe })
case eOrB of
Right is
| is && not options.all ->
pure $ Done $ Right $ NEA.singleton cur
| is ->
pure $ Loop $ acc { found = Array.snoc found cur, innerLoop = Just { p, j: j + 1 } }
_ ->
pure $ Loop $ acc { innerLoop = Just { p, j: j + 1 } }
-- /^".*"$/
quotedRegex :: Regex
quotedRegex = unsafeRegex ("^" <> "\"" <> ".*" <> "\"" <> "$") noFlags
-- Technically, /^\.[\\\/]/
-- but the extra `\` seems to be unnecessary escaping due to square bracket usage
dotSlashRegex :: Regex
dotSlashRegex = unsafeRegex """^\.[\/]""" noFlags