5

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?

xzhu
  • 5,447
  • 4
  • 26
  • 49
  • I think you should edit your title to more accurately reflect your question. (From your title it sounds like you don't know what you are doing and need to be told `node script.js | less`.) Also FWIW, I think many users might hate this behavior. – Two-Bit Alchemist Apr 18 '14 at 01:54
  • @Two-BitAlchemist: Actually I'm trying to write a helper script for my server. Because we have a lot of big tables (2G+ CSV/TSV files), we need a script to serve as a "viewer" and "formator", essentially being a tool that reads the file while piping it to `less`. – xzhu Apr 18 '14 at 01:59

3 Answers3

2

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. :-(

Peter Lyons
  • 131,697
  • 28
  • 263
  • 265
  • Thank you. And that's exactly what I'm doing at the moment. The problem is that now I want to add a `if` statement and if some condition is not satisfied I want to stop the piping and output to bash instead. I don't know how should I do that. – xzhu Apr 18 '14 at 02:02
  • Let me just make an aside that in the decades of unix programs, there are basically no widely-adopted programs that do this or anything like it. Unix has a philosophy and this kind of this is expressly condemned. Unix users know how to pipe to less if they want to. My gut says your task here is a fools errand, but don't take that as discouragement - I just want to make sure you have the broader perspective available. – Peter Lyons Apr 18 '14 at 02:04
  • I just want to reiterate that what @PeterLyons is saying here is exactly what I was trying to express in my comment. There are **certainly** use cases for going straight into a pager -- man pages do this. However, there's a reason that `|` exists in Unix to begin with. Since any command you enter at a Linux terminal can effectively become a one-line bash script, you could have your users execute a bash script that only says (besides the shebang line) `node script.js | less`. – Two-Bit Alchemist Apr 18 '14 at 02:14
  • And if you need `if`/`else` conditions, bash is basically a fully functional programming language. Shell scripting is certainly capable of meeting your use case if `node.js` is not. – Two-Bit Alchemist Apr 18 '14 at 02:15
  • @peterlyons: I take it that you are not fond of the 'man' command-line utility. – rici Apr 18 '14 at 02:35
  • man is written in C AFAIK. I don't believe the ability to replace the current process the way man does is available in node, but again I could be wrong. FWIW no the man program and man pages are and always have been crap. WWW FTW. – Peter Lyons Apr 18 '14 at 02:37
  • @PeterLyons: I'll give you another example: git log. FWIW, the mechanism is not important. What he wants is to have less-compatible navigation of large text files. If it can be done by piping to less (I can easily do that in tcl so the explanation about pty above is invalid, if a language wants to implement this feature it can) or it can be done via some sort of readline type library. – slebetman Dec 15 '15 at 02:30
  • @slebetman how about posting an answer? piping node's stdout to less's stdin does not behave as if less know's the pty dimensions nor as if less can see any of the keystrokes. – Peter Lyons Dec 15 '15 at 06:15
  • @PeterLyons: I don't know how to do it in node. But in tcl for example, the `exec` function (similar to node's `child_process`) can redirect the pty's stdin and stdout to the child process directly (not the interpreter's stdin and stdout). This is the same mechanism that bash/ksh/zsh/csh etc. use to execute programs in the foreground and allow that program to take over the terminal. Therefore one cannot say that it can't be done. You may need to write a module in C to do it but it can be done. – slebetman Dec 15 '15 at 06:56
1

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.

Community
  • 1
  • 1
ELLIOTTCABLE
  • 14,649
  • 11
  • 49
  • 70
  • A better implementation is to detect the terminal instead of using a command line flag. Like how git log is implemented (or for that matter less itself) – slebetman Dec 15 '15 at 02:31
  • Oh, eek, I just assumed checking `isTTY` is a given; I didn't even think that he might try to invoke a pager when the `stdout` isn't a TTY. Thanks for the note, I feel foolish for not noting that. I still absolutely do not feel that is enough, though; `--no-pager` isn't for scripts which are piping; it's for people who *don't want to use a pager*, for any reason. – ELLIOTTCABLE Dec 15 '15 at 11:36
1

Among the four APIs from node's built-in child_process, you would want to use spawn that gives you the choice to run less in the current shell which invokes the script. All other choices like exec, fork will simply start a new invisible shell.

const spawn = require("child_process").spawn;

spawn(`cat <<< "${yourOutputString}" | less -r`, {   // <<< read the *HERE* string to cat
                                                     // less -r specifies colored output
            stdio: 'inherit', // use the current shell for stdio
            shell: true
    });

Now you can invoke your script simply with node script.js.

This lengthy tutorial gives you a thorough view of the child_process API.

Hank Chan
  • 1,176
  • 12
  • 17