How do you turn a Java source file into an executable Unix script using single-file execution, and why is the --source flag needed?
answer
- Shebang `#!/.../java --source <ver>`, extension-less file
- chmod +x, then ./script
- --source forces source mode (no .java extension) + pins language level
- Launcher strips the leading #! line before compiling
- env -S for portable shebang
basics
~20 sAdd a shebang line #!/path/to/java --source 21 as the first line of an extension-less file, make it executable with chmod +x, and run ./myscript. The --source flag is required because, without a .java extension, the launcher needs to be told to use source mode and which language level.
solid answer
~50 sJEP 330 supports a shebang form so a Java file can act like a shell script. You create a file **without** a `.java` extension, put `#!/path/to/bin/java --source <version>` as line one, write your program below it, then `chmod +x` and run `./file`. The OS reads the shebang and invokes the `java` launcher on the file. The `--source <version>` flag is mandatory here for two reasons: (1) the launcher normally decides source mode from the `.java` extension, which a script file lacks, so the flag explicitly forces source mode; and (2) it pins the language level used to compile, so preview/newer syntax is interpreted correctly. The launcher also specially handles the shebang line so the `#!` doesn't break compilation. This is genuinely useful for ops scripts you'd rather write in Java than Bash — though for anything multi-file or dependency-heavy you'd still use a build tool.
go deeper
Aware that a Java file can be made executable with a shebang and run like a script, even if hazy on the flags.
Can write the shebang, chmod, and run it, and knows --source is needed; may not fully explain why.
Explains why --source is mandatory (no extension → must force source mode + pin language level) and that the launcher strips the #! line; knows the env -S portability trick.
Weighs Java-as-scripting for ops glue against Bash/Python, considers portability/version-pinning/maintenance trade-offs, and where script-mode fits (or doesn't) in a tooling strategy.
## Background: what a shebang is On Unix-like systems, when you execute a text file, the kernel looks at the **first line**. If it begins with `#!` (called a **shebang** or hashbang), the rest of that line names the **interpreter** to run the file with. For example `#!/bin/bash` runs the file with Bash. JEP 330 made the `java` launcher usable as such an interpreter. ## Building the script, step by step 1. **Create a file with no `.java` extension** — e.g. `count` (scripts conventionally have no extension): ``` #!/usr/lib/jvm/jdk-21/bin/java --source 21 public class count { public static void main(String[] args) { System.out.println("args: " + args.length); } } ``` 2. **Make it executable:** `chmod +x count` (`chmod` changes file permissions; `+x` adds the execute bit). 3. **Run it:** `./count one two` → prints `args: 2`. The kernel reads `#!.../java --source 21`, launches that `java` binary, and effectively runs `java --source 21 count one two`. ## Why `--source <version>` is required here Normally the launcher enters **source mode** because you named a file ending in `.java`. A shebang script has **no extension** (and *shouldn't* — you want to invoke it as `./count`, not `./count.java`). So: - **`--source` forces source mode explicitly.** Without it, `java count` would try to treat `count` as a *class name*, not a source file, and fail. - **It pins the language level.** `--source 21` tells the in-memory compiler to compile against Java 21 semantics. This matters for newer or **preview** syntax (combine with `--enable-preview`). It also documents and stabilizes the script's expected version. ## The launcher's special handling of the shebang line There's a subtlety: the `#!...` line is **not valid Java**, yet it's the first line of the source. The launcher knows that **when in source mode via a shebang**, it must **strip/ignore the first line if it starts with `#!`** before compiling. (Conversely, if you run a normal `.java` file, a leading `#!` is *not* stripped — shebang stripping only applies to the script-launch path.) This is why the same file can't simultaneously be named `count.java` and carry a shebang and be run both ways without care. ## Practical considerations - **Absolute path or `env`?** The shebang must point at a real `java`. Some use `#!/usr/bin/env -S java --source 21` (the `-S` lets `env` pass multiple arguments) for portability, since the JDK path varies per machine. - **Args:** anything after `./count ...` becomes the program's `args`, same as normal source mode. - **Performance:** every run recompiles in memory; fine for scripts, not for hot paths. - **Portability/limits:** still one source file (pre-JEP 458), still no artifact, classpath via `--class-path`. For real tooling, graduate to a build system. ## Why anyone would do this Teams that prefer Java's type safety and libraries over Bash for **operational glue** (deployment helpers, data munging, CI steps) can now write those as first-class executable scripts without a build step — closing part of the gap with Python/Ruby for scripting.
- Why can't you just rely on the file extension to trigger source mode in a shebang script?Shebang scripts are run as `./name` with no `.java` extension (you don't want users typing `.java`), so the launcher can't infer source mode from the name. `--source <version>` explicitly forces source mode and pins the language level.
- How does the JDK stop the `#!` line from causing a compile error?When launched via the script/source path, the launcher detects and strips a leading line starting with `#!` before handing the source to the in-memory compiler, so the shebang isn't treated as Java code.
saying these in an interview costs you the question
- Forgetting --source and assuming the extension-less file auto-enters source mode
- Naming the script file with .java but still expecting shebang behavior
- Thinking the #! line breaks compilation (the launcher strips it in script mode)
- Claiming preview syntax works without --source/--enable-preview