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.