Hi everybody,
I’ve been experimenting with Purescript using it to create a CLI application. I would like to read text from the stdin stream in the same fashion as other CLI apps, e.g. cat ./somefile | grep sometext
. I’ve searched for how you would implement this using node and found the following:
var stdin = process.openStdin();
var data = "";
stdin.on('data', function(chunk) {
data += chunk;
});
stdin.on('end', function() {
console.log("DATA:\n" + data + "\nEND DATA");
});
Searching Pursuit, there is a package purescript-node-process
which exposes the stdin stream. However the docs note that this stream will never emit an end
event. Additionally, the node variant above uses callbacks to get the stdin data. How would one go about implementing this functionality in Pursecript? Ideally I would want to write something like this:
main :: Effect Unit
main = do
textInput <- readStringFrom stdin UTF8
log textInput
where
readStringFrom :: forall w. Readable w -> Encoding -> Effect (Maybe String)
readStringFrom readable encoding = -- implementation
Considering how the node example uses callbacks however, I don’t think it’s possible to write the implementation for readStringFrom
above. Perhaps this can be solved using Aff
?
Any help is greatly appreciated, thanks!
Those docs are wrong; the stdin
in purescript-node-process
is the exact same object as Node.js’ process.stdin
, so if it can emit end
in the program you posted there, then it can emit end
in PureScript too. We should probably just remove all of the doc-comments and point to the Node.js documentation instead.
You will need Aff if you want to get the result of an asynchronous function just by doing <-
. If you stay in Effect, you’ll need to use callbacks instead, so your code would end up looking more like the original JS. It would be along the lines of
main = do
dataRef <- Ref.new ""
onDataString stdin UTF8 \chunk ->
Ref.modify_ (_ <> chunk) dataRef
onEnd stdin do
stdinData <- Ref.read dataRef
log $ "DATA:\n" <> stdinData <> "\nEND DATA"
although I haven’t tried to compile that, let alone test it.
1 Like
Thank you for your response.
I’ll have a look into using Aff
to use <-
to extract the value from the stdin stream as I would like to avoid having to use callbacks. I would just like to be able to get any text from the stdin stream inside a Maybe
so that I can use that as input for my app.
1 Like
Okay, so for future reference in the hope of helping others, here is what I came up with.
Initial solution
I decided to use Aff
to get the result from stdin so I can just use <-
to get a Maybe String
.
readText :: forall w. Readable w -> Aff (Maybe String)
readText r = makeAff $ \res -> do
dataRef <- Ref.new ""
onDataString r UTF8 \chunk ->
Ref.modify_ (_ <> chunk) dataRef
onEnd r do
allData <- Ref.read dataRef
res $ Right (Just allData)
onError r $ Left >>> res
pure $ effectCanceler (pause r)
main :: Effect Unit
main = launchAff_ do
text <- readText stdin
liftEffect $ logShow text
This can then be used like echo 'test' | .spago/run.js
which will output (Just "test\n")
. However, if you then attempt to just execute the app without anything coming in over stdin like so .spago/run.js
, the app would forever hang waiting for input to arrive.
In order to prevent the app from hanging, I needed access to the stdin.isTTY
boolean property. However, this is not exposed in the purescript-node-process
package. So I created a JS file Main.js
with just the following contents
exports.stdinIsTTY = !!process.stdin.isTTY;
The enables you to foreign import that property like so foreign import stdinIsTTY :: Boolean
Putting it all together. You can use the stdinIsTTY
to check whether or not you should setup the stdin
stream events.
Current solution
foreign import stdinIsTTY :: Boolean
readText :: forall w. Readable w -> Aff (Maybe String)
readText r = makeAff $ \res ->
if stdinIsTTY then do
res $ Right Nothing
pure nonCanceler
else do
dataRef <- Ref.new ""
onDataString r UTF8 \chunk ->
Ref.modify_ (_ <> chunk) dataRef
onEnd r do
allData <- Ref.read dataRef
res $ Right (Just allData)
onError r $ Left >>> res
pure $ effectCanceler (pause r)
main :: Effect Unit
main = launchAff_ do
text <- readText stdin
liftEffect $ logShow text
Final thoughts
Looking at this current solution however, I’m considering implementing the stdin reading logic in JS and providing a convenient interface for Purescript to consume. Even though I want to code as much logic as possible inside of Purescript.
3 Likes
Here is a package for reading from stdin with Node.
https://pursuit.purescript.org/packages/purescript-node-streams-aff/
It doesn’t solve TTY problem, but your solution will also work with this package.
1 Like