Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I pipe my node.js script output in to `less` without typing `| less` when run?

Tags:

bash

node.js

For example, my script will generate a lot of output using process.stdout.write(). I know I can pipe them into less by running it as node mycode.js | less -N.

But is there a way so that I can do the piping inside of my code, so that other people can run my code normally node mycode.js and still get my output piped into less?

like image 891
xzhu Avatar asked Oct 01 '22 00:10

xzhu


2 Answers

Yes, you can pipe the output of your node program into the input less via the normal child_process core module's API. However, the issue will be the controlling pty. If you are running your node program, it will control the pty and less won't have access to the pty, so interacting with less by typing commands won't work. AFAIK (others may very well know better than I) there's no clean way to do this from within your node program, so I would just write a wrapper shell script to do it and call it done.

The closest possibility I found in npm is default-pager but from my quick test harness, it does not seem to work. :-(

like image 195
Peter Lyons Avatar answered Oct 22 '22 23:10

Peter Lyons


This is possible from within Node, if you're willing to use a compiled extension: node-kexec.

I preform almost precisely the tasks you want to in my project's executable as follows (forgive the CoffeeScript):

page = (cb)->

   # If we reach this point in the code and $_PAGINATED is already set, then we've
   # successfully paginated the script and should now actually run the code meant
   # to be run *inside* a pager.
   if process.env['_PAGINATED']?
      return cb()

   # I use tricks like this to control the pager itself; they can be super-dirty,
   # though, and mutating shell-command lines without a *lot* of careful
   # invocation logic is generally a bad idea unless you have a good reason:
   pager = process.env.PAGER || 'less --chop-long-lines'
   pager = pager.replace /less(\s|$)/, 'less --RAW-CONTROL-CHARS$1'

   # I use this elsewhere in my code-base to override `term.columns` if it is
   # unset; because the pager often doesn't properly report terminal-width
   process.env['PAGINATED_COLUMNS'] = term.columns
   process.env['_PAGINATED'] = 'yes'

   # This is a horrible hack. Thanks, Stack Overflow.
   #    <https://stackoverflow.com/a/22827128>
   escapeShellArg = (cmd)-> "'" + cmd.replace(/\'/g, "'\\''") + "'"

   # This is how we *re-invoke* precisely the exact instructions that our script /
   # executable was originally given; in addition to ensuring that `process.argv`
   # is the same by doing this, `kexec` will already ensure that our replacement
   # inherits our `process.stdout` and etc.
   #
   # (These arguments are invoked *in a shell*, as in `"sh" "-c" ...`, by
   # `kexec()`!)
   params = process.argv.slice()
   params = params.map (arg)-> escapeShellArg arg
   params.push '|'
   params.push pager

   log.debug "!! Forking and exec'ing to pager: `#{pager}`"
   log.wtf "-- Invocation via `sh -c`:", params.join ' '

   kexec params.join ' '

This is invoked as simply as you'd expect; something like page(print_help_text) (which is how I'm using it).

There's also a couple obvious gotchas: it's not going to magically fork your program where it is invoked, it's going to re-execute the entire program up to the point where it got invoked; so you'll want to make sure that anything happening before invoking page() is deterministic in nature; i.e. precisely the same things will occur if the program is re-invoked with the same set of command-line arguments. (It's a convenience, not magic.) You probably also want to make sure the code leading up to page() is idempotent, i.e. doesn't have any undesired side-effects when run twice.

(If you want to do this without compiling a native extension, try and get Node core to add an exec function like Ruby's. :P)


Nota bene: If you do decide to do this, please make it configurable with the standard --[no-]pager flag. Pagers can be a nice convenience, but not everybody wants to use one.

On the same note, please realize that compiled dependencies can cause a lot of people trouble; personally, I keep kexec in my package.json's optionalDependencies, and then use a try/catch (or a convenience like optional) to source it. That way, if it fails to install on the user's system, your code still runs as expected, just without the nicety of the pager.

like image 36
ELLIOTTCABLE Avatar answered Oct 22 '22 23:10

ELLIOTTCABLE