Skip to content

Commit f82f394

Browse files
Fix Windows case-sensitive env values; remove unused code; fix shebangCommand (#9)
* Windows env is case-sensitive * Drop unused args from CrossSpawn * Fix windows case-sensitivity in env lookup * Add macos and windows to CI * Drop sleep unit; breaks mac * Remove SpecT * Add tests for Windows * Add shebang command tests; fix code
1 parent 5242b5d commit f82f394

File tree

17 files changed

+270
-149
lines changed

17 files changed

+270
-149
lines changed

.github/workflows/ci.yml

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: CI
22

3-
# Run CI when a PR is opened against the branch `main`
4-
# and when one pushes a commit to `main`.
3+
# Run CI when a PR is opened against the branch `master`
4+
# and when one pushes a commit to `master`.
55
on:
66
push:
77
branches: [master]
@@ -11,43 +11,56 @@ on:
1111
# Run CI on all 3 latest OSes
1212
jobs:
1313
build:
14-
runs-on: ubuntu-latest
14+
strategy:
15+
matrix:
16+
os: [ubuntu-latest, macos-latest, windows-latest]
17+
runs-on: ${{ matrix.os }}
1518

1619
steps:
17-
- uses: actions/checkout@v2
20+
- uses: actions/checkout@v3
1821

19-
- uses: purescript-contrib/setup-purescript@main
22+
- name: Set up Node toolchain
23+
uses: actions/setup-node@v3
2024
with:
21-
purescript: "0.15.7"
22-
purs-tidy: "0.9.2"
23-
psa: "0.8.2"
24-
spago: "0.20.9"
25+
node-version: "16"
26+
27+
- name: Cache NPM dependencies
28+
uses: actions/cache@v3
29+
env:
30+
cache-name: cache-node-modules
31+
with:
32+
path: ~/.npm
33+
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
34+
restore-keys: |
35+
${{ runner.os }}-build-${{ env.cache-name }}-
36+
${{ runner.os }}-build-
37+
${{ runner.os }}-
38+
39+
- name: Setup PureScript tooling
40+
run:
41+
npm i -g purescript@latest purs-tidy@latest purescript-psa@latest spago@latest
2542

2643
- name: Cache PureScript dependencies
27-
uses: actions/cache@v2
44+
uses: actions/cache@v3
2845
with:
2946
key: ${{ runner.os }}-spago-${{ hashFiles('**/*.dhall') }}
3047
path: |
3148
.spago
3249
output
3350
34-
- name: Set up Node toolchain
35-
uses: actions/setup-node@v2
36-
with:
37-
node-version: "16"
38-
3951
# Compile the library/project
4052
# censor-lib: ignore warnings emitted by dependencies
4153
# strict: convert warnings into errors
4254
# Note: `purs-args` actually forwards these args to `psa`
4355
- name: Build the project
4456
run: |
45-
spago build --purs-args "--censor-lib --strict"
57+
npx spago build --purs-args "--censor-lib --strict"
4658
4759
- name: Run tests
4860
run: |
49-
spago -x test.dhall test
61+
npx spago -x test.dhall test
5062
5163
- name: Check Formatting
64+
if: runner.os == 'Linux'
5265
run: |
53-
purs-tidy check src test
66+
npx purs-tidy check src test

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ PureScript port of the [`execa`](https://github.com/sindresorhus/execa) ([NPM li
2121
- [`signal-exit`](https://github.com/tapjs/signal-exit) - ISC
2222
- [`strip-final-newline`](https://github.com/sindresorhus/strip-final-newline) - MIT
2323
- [`which`](https://github.com/npm/node-which) - ISC
24+
- [`path-key`](https://github.com/sindresorhus/path-key) - MIT
25+
- Note: personal experience indicates that Windows still uses case-sensitive keys for its environment. So ignore [path-key#8](https://github.com/sindresorhus/path-key/issues/8). This library's functionality was implemented via `envKey` so that other case-sensitivity issues do not arise on Windows.
2426

2527
The below dependencies of `execa` did not need to be ported since such functionality was implemented primarily via `Aff`.
2628
- `mimic-fn` - functionality unneeded as `Aff` `Fiber`s are a more flexible implementation than `Promise`s.
2729
- `onetime` - functionality provided via `Aff`'s `joinFiber`
2830
- `is-stream` - functionality provided via PureScript's types
29-
- `path-key` - functionality is no longer needed in current versions of Node

src/Node/Library/Execa.purs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,7 @@ execa file args buildOptions = do
446446
, stderr: result.stderr.text
447447
, command
448448
, escapedCommand
449-
, parsed
449+
, execaOptions: parsed.options
450450
, timedOut: is _TimedOut someError
451451
, isCanceled
452452
, killed: killed'
@@ -638,7 +638,7 @@ execaSync file args buildOptions = do
638638
, error: resultError
639639
, signal: resultSignal
640640
, exitCode: toMaybe result.status
641-
, parsed
641+
, execaOptions: parsed.options
642642
, timedOut: Just "ETIMEDOUT" == (map _.code resultError)
643643
, isCanceled: false
644644
, killed: isJust resultSignal
@@ -807,7 +807,7 @@ mkError
807807
, exitCode :: Maybe Int
808808
, command :: String
809809
, escapedCommand :: String
810-
, parsed :: { file :: String, args :: Array String, options :: ExecaRunOptions, parsed :: CrossSpawnConfig }
810+
, execaOptions :: ExecaRunOptions
811811
, timedOut :: Boolean
812812
, isCanceled :: Boolean
813813
, killed :: Boolean
@@ -835,7 +835,7 @@ mkError r =
835835
errorCode = map _.code r.error
836836
prefix
837837
| r.timedOut
838-
, Just timeout <- r.parsed.options.timeout =
838+
, Just timeout <- r.execaOptions.timeout =
839839
"timed out after " <> show timeout <> "milliseconds"
840840
| r.isCanceled =
841841
"was canceled"

src/Node/Library/Execa/CrossSpawn.purs

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,14 @@ import Node.Encoding (Encoding(..))
3333
import Node.FS (FileFlags(..))
3434
import Node.FS.Sync as FS
3535
import Node.Library.Execa.ShebangCommand (shebangCommand)
36+
import Node.Library.Execa.Utils (bracketEffect, envKey)
3637
import Node.Library.Execa.Which (defaultWhichOptions)
3738
import Node.Library.Execa.Which as Which
38-
import Node.Path (FilePath, normalize)
39+
import Node.Path (normalize)
3940
import Node.Path as Path
4041
import Node.Platform (Platform(..))
41-
import Node.Process (lookupEnv, platform)
42+
import Node.Process (platform)
4243
import Node.Process as Process
43-
import Node.Library.Execa.Utils (bracketEffect)
4444

4545
isWindows :: Boolean
4646
isWindows = platform == Just Win32
@@ -65,8 +65,6 @@ type CrossSpawnConfig =
6565
{ command :: String
6666
, args :: Array String
6767
, options :: CrossSpawnOptions
68-
, file :: Maybe FilePath
69-
, original :: { command :: String, args :: Array String }
7068
}
7169

7270
parse :: String -> Array String -> CrossSpawnOptions -> Effect CrossSpawnConfig
@@ -80,11 +78,6 @@ parse command args options = do
8078
{ command
8179
, args
8280
, options
83-
, file: Nothing
84-
, original:
85-
{ command
86-
, args
87-
}
8881
}
8982
parseWindows
9083
| isJust options.shell = pure initParseRec
@@ -104,7 +97,7 @@ parse command args options = do
10497
-- Because the escape of metachars with ^ gets interpreted when the cmd.exe is first called,
10598
-- we need to double escape them
10699
let needsDoubleEscapeChars = test isCommandShimRegex commandFile
107-
comSpec <- fromMaybe "cmd.exe" <$> lookupEnv "comspec"
100+
comSpec <- fromMaybe "cmd.exe" <$> envKey "COMSPEC"
108101
pure $ rec1
109102
{ args =
110103
-- PureScript note: This fix is done in `execa` since
@@ -135,16 +128,15 @@ parse command args options = do
135128
detectShebang parseRec = do
136129
mbFile <- resolveCommand parseRec
137130
case mbFile of
138-
Nothing -> pure $ Tuple (parseRec { file = mbFile }) mbFile
131+
Nothing -> pure $ Tuple parseRec mbFile
139132
Just file -> do
140133
mbShebang <- readShebang file
141134
case mbShebang of
142-
Nothing -> pure $ Tuple (parseRec { file = mbFile }) mbFile
135+
Nothing -> pure $ Tuple parseRec mbFile
143136
Just shebang -> do
144137
let
145138
rec1 = parseRec
146-
{ file = mbFile
147-
, args = Array.cons file parseRec.args
139+
{ args = Array.cons file parseRec.args
148140
, command = shebang
149141
}
150142
newCommand <- resolveCommand rec1
@@ -156,7 +148,7 @@ parse command args options = do
156148
Nothing -> Process.getEnv
157149
Just a -> pure a
158150
resolved <- withOptionsCwdIfNeeded parseRec.options.cwd \_ -> do
159-
map join $ for (Object.lookup "PATH" env) \envPath -> do
151+
map join $ for (Object.lookup "Path" env) \envPath -> do
160152
let getFirst = either (const Nothing) (Just <<< NEA.head)
161153
attempt1 <- map getFirst $ Which.whichSync command $ defaultWhichOptions { path = Just envPath, pathExt = Just Path.delimiter }
162154
if isJust attempt1 then do

src/Node/Library/Execa/IsExe.purs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ import Effect.Uncurried (EffectFn2, runEffectFn2)
2828
import Node.FS.Async as FsAsync
2929
import Node.FS.Stats (Stats(..), isFile, isSymbolicLink)
3030
import Node.FS.Sync as FsSync
31+
import Node.Library.Execa.Utils (envKey)
3132
import Node.Platform (Platform(..))
32-
import Node.Process (lookupEnv, platform)
33+
import Node.Process (platform)
3334
import Unsafe.Coerce (unsafeCoerce)
3435

3536
type IsExeOptions =
@@ -98,7 +99,7 @@ coreWindows =
9899

99100
checkPathExt :: String -> IsExeOptions -> Effect Boolean
100101
checkPathExt path options = do
101-
mbPathExt <- lookupEnv "PATHEXT"
102+
mbPathExt <- envKey "PATHEXT"
102103
case options.pathExt <|> mbPathExt of
103104
Nothing -> pure true
104105
Just p -> do

src/Node/Library/Execa/NpmRunPath.purs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Data.Maybe (Maybe(..), fromMaybe)
1313
import Effect (Effect)
1414
import Foreign.Object (Object)
1515
import Foreign.Object as Object
16+
import Node.Library.Execa.Utils (envKey)
1617
import Node.Path as Path
1718
import Node.Process as Process
1819

@@ -33,7 +34,7 @@ defaultNpmRunPathOptions = mempty
3334
npmRunPath :: NpmRunPathOptions -> Effect String
3435
npmRunPath initialOptions = do
3536
processCwd <- Process.cwd
36-
processPath <- Process.lookupEnv "PATH"
37+
processPath <- envKey "PATH"
3738
processExecPath <- Process.execPath
3839
let
3940
options =

src/Node/Library/Execa/ShebangCommand.purs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,24 @@ import Data.String.Regex.Unsafe (unsafeRegex)
1717

1818
shebangCommand :: String -> Maybe String
1919
shebangCommand firstLineOfFile = do
20-
parts <- match shebangRegex firstLineOfFile
21-
let
22-
extractBinary = Array.last <<< String.split (String.Pattern "/")
20+
regexMatch <- match shebangRegex firstLineOfFile
21+
everythingAfterShebang <- join $ Array.index (NEA.toArray regexMatch) 1
22+
let parts = String.split (String.Pattern " ") everythingAfterShebang
2323

24-
everythingAfterShebang <- NEA.head parts
25-
case String.split (String.Pattern " ") everythingAfterShebang of
26-
[ pathOnly ] -> do
24+
case Array.uncons parts of
25+
Just { head: pathOnly, tail: [] } -> do
2726
binary <- extractBinary pathOnly
2827
binary <$ guard (binary /= "env")
29-
[ path, argument ] -> do
28+
Just { head: path, tail: args } -> do
3029
binary <- extractBinary path
3130
pure
3231
if binary == "env" then
33-
argument
32+
Array.intercalate " " args
3433
else
35-
binary <> " " <> argument
34+
Array.intercalate " " $ Array.cons binary args
3635
_ -> Nothing
3736
where
37+
extractBinary = Array.last <<< String.split (String.Pattern "/")
38+
3839
shebangRegex :: Regex
3940
shebangRegex = unsafeRegex """^#! ?(.*)""" noFlags

src/Node/Library/Execa/Utils.purs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
1+
-- A majority of the below code was ported from this JavaScript library
2+
-- Note: the implementation of `envKey` was
3+
-- based on https://github.com/sindresorhus/path-key
4+
-- Copyright `sindresorhus`
5+
-- MIT License: https://opensource.org/license/mit/
6+
17
module Node.Library.Execa.Utils where
28

39
import Prelude
410

11+
import Control.Alternative (guard)
12+
import Data.FoldableWithIndex (findMapWithIndex)
513
import Data.Function.Uncurried (Fn2, runFn2)
14+
import Data.Maybe (Maybe(..))
15+
import Data.String as String
616
import Data.Symbol (class IsSymbol)
717
import Effect (Effect)
818
import Effect.Exception (Error)
19+
import Foreign.Object (Object)
20+
import Foreign.Object as Object
921
import Node.Buffer.Immutable (ImmutableBuffer)
1022
import Node.Buffer.Immutable as ImmutableBuffer
1123
import Node.Encoding (Encoding(..))
24+
import Node.Platform (Platform(..))
25+
import Node.Process as Process
1226
import Node.Stream (Duplex)
1327
import Prim.Row as Row
1428
import Record as Record
@@ -58,4 +72,13 @@ bracketEffect open close use = do
5872
b <- use resource
5973
b <$ close resource
6074

75+
envKey :: String -> Effect (Maybe String)
76+
envKey key = flip envKey' key <$> Process.getEnv
77+
78+
envKey' :: Object String -> String -> Maybe String
79+
envKey' env key
80+
| Process.platform == Just Win32 =
81+
findMapWithIndex (\k v -> v <$ guard (String.toUpper k == String.toUpper key)) env
82+
| otherwise = Object.lookup key env
83+
6184
foreign import newPassThroughStream :: Effect Duplex

src/Node/Library/Execa/Which.purs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,18 @@ import Data.Tuple (Tuple(..))
2323
import Effect (Effect)
2424
import Effect.Aff (Aff)
2525
import Effect.Class (liftEffect)
26-
import Foreign.Object as Object
2726
import Node.Library.Execa.IsExe (defaultIsExeOptions, isExe, isExeSync)
27+
import Node.Library.Execa.Utils (CustomError, buildCustomError, envKey)
2828
import Node.Path as Path
2929
import Node.Platform (Platform(..))
3030
import Node.Process (platform)
3131
import Node.Process as Process
32-
import Node.Library.Execa.Utils (CustomError, buildCustomError)
3332
import Partial.Unsafe (unsafePartial)
3433

3534
isWindows :: Effect Boolean
3635
isWindows = do
37-
env <- Process.getEnv
38-
let osTypeIs x = Just x == Object.lookup "OSTYPE" env
39-
pure $ platform == Just Win32 || osTypeIs "cygwin" || osTypeIs "msys"
36+
ty <- envKey "OSTYPE"
37+
pure $ platform == Just Win32 || ty == Just "cygwin" || ty == Just "msys"
4038

4139
jsColon :: Effect String
4240
jsColon = do
@@ -72,8 +70,8 @@ getPathInfo cmd options = do
7270
-- PureScript implementation note: get all the effectful stuff first
7371
-- before we use it in the rest of this function for readability.
7472
cwd <- Process.cwd
75-
mbPath <- Process.lookupEnv "PATH"
76-
mbPathExt <- Process.lookupEnv "PATHEXT"
73+
mbPath <- envKey "PATH"
74+
mbPathExt <- envKey "PATHEXT"
7775
isWin <- isWindows
7876

7977
colon <- case options.colon of

test/Test/Main.purs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@ import Prelude
55
import Effect (Effect)
66
import Effect.Aff (launchAff_)
77
import Test.Node.Library.Execa as Execa
8+
import Test.Node.Library.ParseCommand as ParseCommand
9+
import Test.Node.Library.ShebangCommand as ShebangCommand
810
import Test.Spec.Reporter (consoleReporter)
9-
import Test.Spec.Runner (defaultConfig, runSpecT)
11+
import Test.Spec.Runner (runSpec)
1012

1113
main :: Effect Unit
12-
main = launchAff_ $ void $ join $ runSpecT defaultConfig [ consoleReporter ] do
13-
Execa.spec
14+
main = launchAff_ $ do
15+
runSpec [ consoleReporter ] do
16+
Execa.spec
17+
ParseCommand.spec
18+
ShebangCommand.spec

0 commit comments

Comments
 (0)