31

Given that Java 9 is upon us and we can finally have a java REPL with jshell I was hoping there was a way to add a shebang to a script and have jshell interpret it.

I tried creating test.jsh:

#!/usr/bin/env jshell -s
System.out.println("Hello World")
/exit

However that gives:

⚡ ./test.jsh
|  Error:
|  illegal character: '#'
|  #!/usr/bin/env jshell -s
|  ^
|  Error:
|  illegal start of expression
|  #!/usr/bin/env jshell -s
|    ^
Hello World

It turns out there is an enhancement request for this in OpenJDK https://bugs.openjdk.java.net/browse/JDK-8167440.

Is there any other way to do this?

steinybot
  • 5,491
  • 6
  • 37
  • 55

4 Answers4

40

Use

//usr/bin/env jshell --show-version --execution local "$0" "$@"; exit $?

as the first line of test.jsh. The test.jsh script could look like:

//usr/bin/env jshell --show-version "$0" "$@"; exit $?
System.out.println("Hello World")
/exit

The command line option --show-version is optional, of course, but gives immediate feedback that the tool is running.

The extra command line option --execution local prevents jshell to spawn another VM. This speeds up launch time and if an exception is thrown by your script code, the local VM will exit.

Consult the output of jshell --help and jshell --help-extra for more options.

Update

Also take a look at https://github.com/jbangdev/jbang Having fun with Java scripting, which offers a neat wrapper around running .java files from the command line.

Sormuras
  • 8,491
  • 1
  • 38
  • 64
  • 1
    How does this even work? I wonder how portable this is. Granted that `#!` isn't standard POSIX anyway but surely as far as magic numbers go it would be the most widely adopted. – steinybot Jul 05 '17 at 18:41
  • 3
    At least `bash` supports the inital `//` line. See this thread for details http://mail.openjdk.java.net/pipermail/kulla-dev/2016-October/thread.html#1679 ... with Brian Goetz stating that making a `.jsh` file executable _ is trying to turn jshell into something it wasn't designed for._ – Sormuras Jul 05 '17 at 18:48
  • 1
    > _How does this even work?_ See the description of the "hack" at section _"Mimic the shebang via Bash"_ at http://golangcookbook.com/chapters/running/shebang/ – Sormuras Jul 05 '17 at 18:51
  • Just got to try this with ZSH and it also works, although I had to use `usr/bin/env jshell` instead and quote the variable expansions. Much nicer output than my answer. – steinybot Jul 05 '17 at 19:14
  • I added the **ZSH** example without testing it. Is it: `//usr/bin/env jshell --show-version "$0" "$@"; exit $?` – Sormuras Jul 05 '17 at 19:26
  • 1
    Yeah although that should be the same for BASH too? The changes are more likely because I don't have JAVA_HOME set and the quotes are just to handle spaces correctly. – steinybot Jul 05 '17 at 19:32
  • Seems like a bit of a cop out by Brian Goetz. I think it should have been designed for that. Groovy and Scala both support this. – steinybot Jul 05 '17 at 19:34
  • 1
    Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/148445/discussion-between-sormuras-and-steiny). – Sormuras Jul 05 '17 at 19:42
6

It turns out that with a bit of trickery there is a way, although I haven't fully managed to suppress the interpreted commands but pretty close to what I want.

Change test.jsh to:

#!/usr/bin/env sh
tail -n +4 "$0" | jshell -s "$@"
exit $?
System.out.println("Hello World")
/exit

Which gives us:

⚡ ./test.jsh
-> System.out.println("Hello World")
Hello World
-> /exit
steinybot
  • 5,491
  • 6
  • 37
  • 55
4

The below works too; put it into a someScript.jsh file and run it with ./someScript.jsh. All arguments received by someScript.jsh will go to String[] args.

#!/home/gigi/.sdkman/candidates/java/current/bin/java --source 11

import java.util.Arrays;
import ro.go.adrhc.*; // example of using your classes, e.g. App below

public class X {
    public static void main(String[] args) {
        // do whatever you want here, e.g.:
        // System.out.println("Hello World");
        // or
        // use a class:
        // App.main(args);
        // e.g. from ro.go.adrhc package, by running:
        // CLASSPATH="/path-to-ro.go.adrhc-classes" ./someScript.jsh 
    }
}

The usage of the wrapping class, here X, is a mandatory trick for this to work. Use the Java version you have by changing /home/gigi/.sdkman/candidates/java/current/bin/java.
Inspired by https://blog.codefx.org/java/scripting-java-shebang/.

Adrian
  • 3,321
  • 2
  • 29
  • 46
2

Inspired by steiny answer, I came up with a more generic solution

https://gist.github.com/ffissore/012d7e32a096fde5266f49038c93dcaf

In essence: jshell-wrapper will strip the first line of the script (which is supposed to be the shebang) and will add a /exit at the end of the script

Federico Fissore
  • 712
  • 4
  • 18