execa icon indicating copy to clipboard operation
execa copied to clipboard

Progressive iteration over a piped process

Open MaddyGuthridge opened this issue 1 month ago • 4 comments

While trying to work around #1210, I have encountered another issue. It appears that I am unable to progressively iterate over the stdout of a piped process.

const process = execa`cat temp.txt`
  .pipe(execa`tr \\r \\n`);

for await (const line of process) {
  console.log(line);
  // TypeError: line of execa`cat temp.txt`.pipe is not a function. (In 'line of execa`cat temp.txt`.pipe(execa`tr \\r \\n`)', 'line of execa`cat temp.txt`.pipe' is undefined)
}

Is this possible? If so, how can I do so? If not, can it be added please :)

MaddyGuthridge avatar Oct 26 '25 16:10 MaddyGuthridge

Hi @MaddyGuthridge,

Thanks for reporting this.

The return value of .pipe() is a Promise<Result> with an additional .pipe() method. As opposed to a Subprocess, which means it lacks the additional methods/properties:

  • iteration: Symbol.asyncIterator, .iterable()
  • stream conversion: .all, .readable(), .writable(), .duplex()
  • IPC: .sendMessage(), .getOneMessage(), .getEachMessage()
  • any of the underlying ChildProcess methods/properties: .kill(),.pid, etc.
    • note: in the future, this will be under the .nodeChildProcess property (https://github.com/sindresorhus/execa/issues/413)

I think we should add this feature at some point, since it is somewhat unexpected that the shape of the return value of .pipe() is different. The types and documentation would need to be updated too.

In the meantime, you can work around it like this:

const destination = execa`tr \\r \\n`
const subprocess = execa`cat temp.txt`.pipe(destination)

for await (const line of destination) {
  console.log(line);
}

However, if subprocess is rejected with an error, this would lead to an unhandled rejection, which would crash the current process. So the following should be done instead:

import {execa} from 'execa'

const destination = execa`tr \\r \\n`
await Promise.all([
  execa`cat temp.txt`.pipe(destination),
  (async () => {
    for await (const line of destination) {
      console.log(line);
    }
  })(),
])

ehmicky avatar Oct 26 '25 17:10 ehmicky

Thanks for the example! For the time being, before this is implemented, do you think it's worth documenting the workaround?

MaddyGuthridge avatar Oct 27 '25 04:10 MaddyGuthridge

I would personally prefer to keep this workaround in this issue only, to encourage myself to fix this. :)

ehmicky avatar Oct 27 '25 16:10 ehmicky

Honestly valid hahahaha

MaddyGuthridge avatar Oct 30 '25 10:10 MaddyGuthridge