50

Node.js is very popular these days and I've been writing some scripts on it. Unfortunately, compatibility is a problem. Officially, the Node.js interpreter is supposed to be called node, but Debian and Ubuntu ship an executable called nodejs instead.

I want portable scripts that Node.js can work with in as many situations as possible. Assuming the filename is foo.js, I really want the script to run in two ways:

  1. ./foo.js runs the script if either node or nodejs is in $PATH.
  2. node foo.js also runs the script (assuming the interpreter is called node)

Note: The answers by xavierm02 and myself are two variations of a polyglot script. I'm still interested in a pure shebang solution, if such exists.

  • I think there is no real solution to this, as one is allowed to name the executable arbitarily by the build systems. There is nothing that keeps you from naming the python interpreter alphacentauri, you just follow the conventions and name it python. I'd suggest either using standard name node for your script, or having a kind of make script that modifies the shebang. –  Feb 20 '13 at 12:17
  • @G.Kayaalp Politics and conventions aside, there are a lot of Debian/Ubuntu/Fedora users, and I want to make scripts that work for them. I don't want to setup a build system for this (whoever builds shell scripts before running them?), nor do I want to support alphacentauri and such. If there's an executable called nodejs, you can be 99% sure it's Node.js. Why not support both nodejs and node? – StackExchange saddens dancek Feb 20 '13 at 13:28
  • Install the nodejs-legacy package. The reason this is needed is that the name is too arrogantly generic, someone else got the name first. That other package was, however, willing to share the name. – user3710044 May 27 '18 at 11:16

6 Answers6

66

The best I have come up with is this "two-line shebang" that really is a polyglot (Bourne shell / Node.js) script:

#!/bin/sh
':' //; exec "$(command -v nodejs || command -v node)" "$0" "$@"

console.log('Hello world!');

The first line is, obviously, a Bourne shell shebang. Node.js bypasses any shebang that it finds, so this is a valid javascript file as far as Node.js is concerned.

The second line calls the shell no-op : with the argument // and then executes nodejs or node with the name of this file as parameter. command -v is used instead of which for portability. The command substitution syntax $(...) isn't strictly Bourne, so opt for backticks if you run this in the 1980s.

Node.js just evaluates the string ':', which is like a no-op, and the rest of the line is parsed as a comment.

The rest of the file is just plain old javascript. The subshell quits after the exec on second line is completed, so the rest of the file is never read by the shell.

Thanks to xavierm02 for inspiration and all the commenters for additional information!

  • I've found a two-line one :) | I don't think one line is possible because apparently, you're not allowed to use -c for sh in the shebang. – xavierm02 Feb 19 '13 at 12:15
  • Thanks! Very clever! I tested on Windows, ubuntu and OSX and it works. I just added "$@" after "$0" to pass the command line arguments to the subprocess. – Bruno Jouhier Sep 05 '13 at 07:58
  • @BrunoJouhier good point about arguments! I'll add that. – StackExchange saddens dancek Sep 05 '13 at 09:19
  • 1
    Great solution. An alternative to the ':' approach is to use // 2>/dev/null (which is what nvm does): to bash, it's an error (bash: //: Is a directory), which the redirection 2>/dev/null quietly ignores; to JavaScript, the entire line becomes a comment. Also - and I don't expect this to be a problem IRL - command has a quirk where it also reports shell functions and aliases with -v - even though it wouldn't actually invoke them. Thus, if you happen to have exported shell functions or even aliases (assuming shopt -s expand_aliases) named node or nodejs, things could break. – mklement0 Dec 17 '13 at 17:35
  • Nice solution. Please consider using $(…) instead of U+0060 (accent gravis) however. – mirabilos Dec 24 '13 at 00:06
  • @mirabilos thanks for pointing that out. The original Bourne shell only supported backticks (thus my choice) but $(...) turns out to be very well supported these days. If it's good enough for dash, it's good enough for me :) – StackExchange saddens dancek Dec 29 '13 at 15:44
  • 1
    Well, POSIX sh is ubiquitous these days (except Solaris /bin/sh – but Solaris does come with AT&T ksh out of the box, which is POSIX compatible), and Markus Kuhn recommends (with justification) to stay off it. IMHO you never need it. But yes, it's enough for mksh, bash, dash and other shells in modern Unix and GNU systems. – mirabilos Dec 29 '13 at 20:50
  • 1
    Great solution; though even more portable would be to use #!/usr/bin/env sh instead of #!/bin/sh (per https://en.wikipedia.org/wiki/Shebang_%28Unix%29#Portability) – user7089 Feb 07 '14 at 20:35
  • 3
    I would be careful about advertising #!/usr/bin/env sh as "more portable". That same Wikipedia article says "This mostly works because the path /usr/bin/env is commonly used for the env utility..." That's not a wonderful endorsement, and my guess is that you'll run into systems without /usr/bin/env more frequently than you run into systems without /bin/sh, if there's any difference in frequency at all. – sheldonh Apr 10 '15 at 11:52
  • 1
    @dancek Hi from 2018, wouldn't be //bin/sh -c :; exec ... a cleaner version of the second line? – alephreish Jan 27 '18 at 17:43
  • @alephreish Techically, POSIX says that two or more consecutive slashes in a pathname are equivalent to a single one... except exactly two slashes at the beginning, which can invoke implementation-defined behaviour. (I suspect, but don’t know, that this is done to enable implementations to implement “URL-style” access to remote hosts, //host/dir/file; cf. UNC paths in Windows.) So you’d have to say ///bin/sh -c :; ... (or simply ///bin/true; ..., for that matter). Yes, this is perverse. – Alex Shpilkin Apr 02 '21 at 17:13
17

This is only a problem on Debian-based systems, where policy overcame sense.

I don't know when Fedora provided a binary called nodejs, but I never saw it. The package is called nodejs and it installs a binary called node.

Just use a symlink to apply common sense to your Debian-based systems, and then you can use a sane shebang. Other people will be using sane shebangs anyway, so you're going to need that symlink.

#!/usr/bin/env node

console.log("Spread the love.");
sheldonh
  • 345
  • 1
    I'm sorry, but this does not answer the question. I do understand the political point of view, but that is not the point here. – StackExchange saddens dancek Aug 05 '13 at 08:58
  • For the record, some Fedora systems do have a nodejs executable, but that is not Fedora's fault. Sorry for misrepresenting the fact. – StackExchange saddens dancek Aug 05 '13 at 09:01
  • 11
    No need to apologise. You're quite right. My response doesn't answer your quesiton. It speaks to your question, suggesting that the question limits scope to less useful solutions than are available. I'm offering practical advice informed by history. Node is not the first interpreter to find itself here. You should have seen the commotion that led to Larry Wall declaring that the perl shebang MUST be #!/usr/bin/perl. That said, you aren't obliged to like or apply my suggestions. Peace. – sheldonh Aug 06 '13 at 09:14
10
#!/bin/sh
//bin/false || `which node || which nodejs` << `tail -n +2 $0`
console.log('ok');

//bin/false is the same thing as /bin/false except that the second slash transforms it into a comment for node, and that's why it's here. Then, the right side of the first || gets evaluated. 'which node || which nodejs' with backquotes instead of quotes launches node and the << feeds it whatever is on the right. I could've used a delimiter starting with // like dancek did, it would've worked but I find it cleaner to have only two lines at the beginning so I used tail -n +2 $0 to have the file read itself except the first two lines.

And if you run it in node, the first line is recognized as a shebang and ignored and the second is a one-line comment.

(Apparently, sed could be used to replace tail Print file content without the first and last lines)


Answer before edit:

#!/bin/sh
`which node || which nodejs` <<__HERE__
console.log('ok');
__HERE__

You can't do what you want so what you do instead is run a shell script, hence the #!/bin/sh. That shell script will get the path of the file needed to execute node, that is which node || which nodejs. The backquotes are here so that it gets executed, so 'which node || which nodejs' (with the backquotes instead of the quotes) simply calls node. Then, you just feed your script to it with <<. The __HERE__ are the delimiters of yours script. And the console.log('ok'); is an example of script that you should replace with your script.

xavierm02
  • 447
4

If you won't mind creating a little .sh file, I've a little solution for you. You can create a little shell script to determine which node executable to use, and use this script in your shebang:

shebang.sh:

#!/bin/sh
`which node || which nodejs` $@

script.js:

#!./shebang.sh
console.log('hello');

Mark both executable, and run ./script.js.

This way, you avoid polyglot scripting. I do not think using multiple shebang lines are possible, although it seems like a good idea.

Although this solves the problem the way you want, it seems that no-one cares about this. For example, uglifyjs and coffeescript uses #!/usr/bin/env node, npm uses a shell script as an entry point, which again calls the executable explicitly with name node. I'm an Ubuntu user, and I didn't know this as I always compile node. I'm considering to report this as a bug.

  • I'm quite sure you'd at least have to ask the user to chmod +x on the sh script... so you might as well ask them to set a variable giving the location to their node executable... – xavierm02 Feb 20 '13 at 14:57
  • @xavierm02 One can release the script chmod +x'd. And I concur that a NODE variable is better than which node || which nodejs. But the inquirer wants to provide an out-of-the-box experience, although many major node projects just use #!/usr/bin/env node. –  Feb 20 '13 at 15:44
1

Just for completeness here are a couple of other ways of doing the second line:

// 2>/dev/null || echo yes
false //|| echo yes

But neither has any advantage over the selected answer:

':' //; || echo yes

Also, if you knew that either node or nodejs (but not both) would be found, the following works:

exec `command -v node nodejs` "$0" "$@"

But that's a big "if", so I think the selected answer remains the best one.

Kevdog777
  • 3,224
1

I understand that this does not answer the question, but I am convinced that the question is asked with a false premise.

Ubuntu is in the wrong here. Writing a universal shebang for your own script will not change other packages you have no control over which use the de-facto standard of #!/usr/bin/env node. Your system must provide node in PATH if it wishes for any scripts targeting nodejs to run on it.

For example, not even the Ubuntu-provided npm package rewrites shebangs in packages:

$ cd
$ rm -rf test_npm
$ mkdir test_npm
$ cd test_npm
$ npm install mkdirp 2>&1 | grep -ve home
`-- mkdirp@0.5.1
  `-- minimist@0.0.8

npm WARN test_npm No description
npm WARN test_npm No repository field.
npm WARN test_npm No README data
npm WARN test_npm No license field.
$ node_modules/.bin/mkdirp testdir
/usr/bin/env: 'node': No such file or directory
$ head -n1 node_modules/.bin/mkdirp
#!/usr/bin/env node
$ npm --version
3.5.2
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.2 LTS"
binki
  • 781