CLI app read text from stdin

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