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
?
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. :-(
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 mypackage.json
'soptionalDependencies
, and then use atry/catch
(or a convenience likeoptional
) 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With