Language selector

More on shebang in JavaScript(s)

A few more words about shebang

This is an update to the previous post. I haven’t discussed the portability of the “script with shebang” before, because I assumed that:

  • this is a general scripting issue, not specific to Java™,
  • it was discussed in the Wikipedia link provided in the previous entry.

However, it wasn’t referenced directly, so here it comes, a direct link: portability of Unix shebang from Wikipedia.

Let’s discuss this then. Shebang is an absolute path. That’s obvious, because the script should work everywhere. The everywhere applies not only to every directory, but also every computer. Because of that, putting in shebang something like /home/piotr/.sdkman… works only for some demo or evaluation. Else, for any kind of serious usage, it’s one of those Really Bad Ideas™.

What shall be used then? It really depends, sadly there’s no one-size-fits-all solution, at least I’m not aware of it. Therefore, my first suggestion would be to ask someone who knows the context you’re in, like a sysadmin, DevOps friends, etc. (If they’re okay with such “scripting” in the first place. ;-) )

Nevertheless, there are some paths you can try to follow here. You can start with #!/usr/bin/java. If you’re using the SDK MANager, and you switch the versions/vendors often, you might go for /home/YOUR-USERNAME/.sdkman/candidates/java/current/bin/java. Unfortunately, something like #!/usr/bin/env java --source 11 doesn’t work just like that. When I try to use such a shebang on my machine, an error is shown:

/usr/bin/env: ‘java --source 11’: No such file or directory
It’s caused by character interpolation, because env can’t split java --source 11 and is looking for a programme with exactly this name. There’s not much we can do about that in many systems, like Ubuntu 18.04 LTS with env version 8.28.

Things look different in other OSes and in recent GNU/Linux versions, I’ve tested FreeBSD 11 and Ubuntu 20.04 LTS. If you’re using one of them, or GNU/Linux with a modern env version (like 8.30 or later), you can use -S, which splits the command and its arguments.

Then the shebang can look like this:

#!/usr/bin/env -S java --source 11

What’s more, the -S caused also other parameters to work. Using that we can also set the classpath and other stuff.

In the following snippet we’re going to (over)use Apache Commons Lang, to check if none of the passed arguments is password:

 1#!/usr/bin/env -S java --source 8
 2import org.apache.commons.lang3.ArrayUtils;
 3
 4public class ClasspathExample {
 5    public static void main(String[] args){
 6        if (ArrayUtils.contains(args, "password")) {
 7            System.out.println("Don't pass passwords as arguments.");
 8        } else {
 9            System.out.println("Password not detected in arguments.");
10        }
11    }
12}

Of course, it’s not going to work if we don’t have Commons Lang JAR in your default classpath (which I think shouldn’t be the case, if you’d like to know my opinion, because of the JAR hell). It fails more or less this way:

$ ./classpathScript.example 
./classpathScript.example:2: error: package org.apache.commons.lang3 does not exist
import org.apache.commons.lang3.ArrayUtils;
                               ^
./classpathScript.example:6: error: cannot find symbol
        if (ArrayUtils.contains(args, "password")) {
            ^
  symbol:   variable ArrayUtils
  location: class ClasspathExample
2 errors
error: compilation failed

There are at least three options here (provided we’re not going to remove the import):

  • add the JAR to the default classpath (not recommended IMO),
  • set the CLASSPATH environment variable during the call (doable, but verbose),
  • set the classpath in shebang, which we’re going to explore here.

Let’s update the last example then (assuming we keep the JARs the old-fashion way, like /usr/local/share/...):

 1#!/usr/bin/env -S java --class-path /usr/local/share/JARs/commons-lang3-3.11.jar --source 8 -Xmx100m 
 2import org.apache.commons.lang3.ArrayUtils;
 3
 4public class ClasspathExample {
 5    public static void main(String[] args){
 6        if (ArrayUtils.contains(args, "password")) {
 7            System.out.println("Don't pass passwords as arguments.");
 8        } else {
 9            System.out.println("Password not detected in arguments.");
10        }
11    }
12}

It runs very nice with OpenJDK 11.0.7 and 11.0.8 (at the moment of writing):

Java 11 on FreeBSD 11.3

So, do we have it? Do we have a portable* script with shebang, which will work in FreeBSD in Ubuntu, with all Java versions? Well, not so fast… This works with Java 11. If you’d like to run it with, let’s say, Java 15 (which is the supported version at the moment of writing), then you have to put e.g. --class-path after the --source, as demonstrated in the picture below:

Java 15 on Ubuntu 20.04

Please note this has nothing to do with the operating system AFAICT. It’s about the java's behaviour when reading the parameters, as depicted e.g. in ticket JDK-8242911. However, I can’t tell if it’s a bug or a feature. Nevertheless, please be aware (when talking about the “portability”) that not only the order of the JARs in the classpath matters, but that the position of the --class-path argument matters too! ;-) And maybe setting the CLASSPATH environment variable isn’t that bad after all…

So for Java 15 the shebang from above has to be set as:

1#!/usr/bin/env -S java --source 8 -Xmx100m --class-path /usr/local/share/JARs/commons-lang3-3.11.jar 

Summary

I won’t risk here giving you a silver bullet ‘this will work for sure’ when it comes to shebang in “Java Scripts”, because I’ve seen way too many ways Java is “installed” or “deployed”. As you’ve seen, this also varies between versions. Not this time, sorry ;-) However, if your OS supports this, and it matches the way you have Java installed, you can start with:

#!/usr/bin/env -S java --source 11

Also, please remember: just because you can, doesn’t mean you should. Maybe just sticking to the good ol’ java -jar myProgramme.jar could save you some headaches.

Language selector