Photo by James Wainscoat on Unsplash
Hello, kids and older kids! Today’s adventure is in writing a Hello World CLI script in Scala. This seems like it should be trivial, like it is in other scriptable languages like Python, Ruby, and Bash – and it should be – but that turns out not to be the case. Rather, bash
(the shell), scala
(the Scala REPL), and scalac
(the Scala compiler) all have different ideas about what makes a valid Scala script. So we’re going to write one that all three of them can agree on.
I’m going to work through all the background, options, and details here. If you just want to see the final version, feel free to skip to the end.
Note: I’ll be making small adjustments to the source Scala code for consistency and style, but the substance remains the same.
Simple version
This is the code Alvin Alexander cites from an older version of Getting Started with Scala, in the official documentation. It’s the first thing I found every time I’ve looked up Scala scripting. As we expect, this does run successfully, but as we’ll see, it is not ideal.
hello.sh
1#!/bin/sh2exec scala "$0" "$@"3!#45object Hello {6 def main(args: Array[String]): Unit = {7 println(s"Hello ${args.mkString(", ")}!")8 }9}1011Hello.main(args)
Before we even run this code, there are a few yellow flags:
- Any file extension is made redundant by having a shebang.
- The file extension is an implementation detail that should be hidden from the user.
- The
.sh
file extension is outdated (it’s for the Bourne Shell, Bash’s predecessor). - Similarly, the
/bin/sh
in the shebang is outdated. - This Scala script looks like a Bash script from the file extension, claims to be a Bash script in the shebang, and any editor will syntax-highlight it like a Bash script by default.
Thankfully, the first three of these issues are fixed by just removing the .sh
file extension, and naming the file simply hello
.
Unfortunately, correcting the shebang to /usr/bin/env bash
results in scala
trying to run the Bash exec
command. Plus, the shebang still makes it look like Bash, and some editors will have syntax highlighting trouble due to the mismatched shebang and the lack of file extension. We’ll return to that later. In the meantime, let’s try running the code in scala
.
Shell
1$ scala hello Mal Inara Kaylee2hello.sh:11: warning: Script has a main object but statement is disallowed3Hello.main(args)4 ^5one warning found6Hello Mal, Inara, Kaylee!
So it runs just fine – but it gives us a warning about calling main
. Perhaps this comes as a surprise, because of course we’re calling main
. But it turns out this is an artifact from older versions of Scala. These days, main
is run automatically, just like in an app. So that’s a simple fix: we just remove the last line, and get this.
hello
(no file extension)
1#!/bin/sh2exec scala "$0" "$@"3!#45object Hello {6 def main(args: Array[String]): Unit = {7 println(s"Hello ${args.mkString(", ")}!")8 }9}
Better! Still, if we were to try running it like we would normally run a script, without calling scala
directly, we can’t. Alvin does mention making the file executable, and though he doesn’t specify how, it’s very simple: we call chmod
(change file modes) with +x
(add executable permission) on our script. Now it will run just like it does when calling scala
directly.
Shell
1$ ./hello Mal Inara Kaylee2bash: ./hello: Permission denied3$ chmod +x hello4$ ./hello Mal Inara Kaylee5Hello Mal, Inara, Kaylee!
Everything seems to be in order so far. But what was that I said about this being from an older version of the documentation? Well, let’s take a look at the latest version:
script.sh
1#!/usr/bin/env scala23object Hello extends App {4 println(s"Hello ${args.mkString(", ")}!")5}67Hello.main(args)
In a few ways – like having a non-descriptive filename – this is actually a regression of where we’ve gotten to, but we can see a couple improvements:
scala
actually has its own shebang now, which solves the issue of the file looking like a shell script.- Scala offers the
App
trait, which essentially turnsHello
itself intomain
, and providesargs
automatically – a nice convenience.
So let’s add these improvements to our code.
Note: The
App
trait will be partly broken by Scala 3, expected to be released in early 2020.
hello
(no file extension)
1#!/usr/bin/env scala23object Hello extends App {4 println(s"Hello ${args.mkString(", ")}!")5}
We can actually make it even simpler: scala
doesn’t actually require scripts to have that top-level object. It will just run everything at the top level, and it will still provide args
automatically.
hello
(no file extension)
1#!/usr/bin/env scala23println(s"Hello ${args.mkString(", ")}!")
Let’s try it out.
Shell
1$ scala hello Mal Inara Kaylee2Hello Mal, Inara, Kaylee!3$ ./hello Mal Inara Kaylee4Hello Mal, Inara, Kaylee!
Shiny! If you’re not using an IDE, and you’re only running this code as a script and not using it from elsewhere, you’re done. That’s it. Simple as can be. But if you’re doing either of those things, read on.
Complicated version
So let’s say you’re like me, and probably most other Scala developers, and you write your Scala code in an IDE, like IntelliJ IDEA. And let’s say you do that even when you’re scripting, because you like being able to see the Scala source and you have your IDE open anyway. You may have noticed a couple problems:
- When we remove the file extension, IntelliJ stops syntax highlighting. We’ll address this later.
- When we put the file extension back, we see a lot of red on that shebang.
This is because scalac
doesn’t recognize shebangs. Because it doesn’t know to ignore them, it tries to compile them as Scala. So IntelliJ, for example, will inform you:
- Cannot resolve symbol
#!/
- Cannot resolve symbol
usr
- Cannot resolve symbol
/
- ⋮
And if you actually try to compile hello
, scalac
will be sure to let you know how unhappy it is:
Shell
1$ scalac hello2hello:1: error: expected class or object definition3#!/usr/bin/env scala4^5hello:3: error: expected class or object definition6println(s"Hello ${args.mkString(", ")}!")7^8two errors found
Oh, good, there’s a second compilation error, just to keep us on our toes. That one’s not much of a surprise: if we’re going to script in the IDE, we need to keep all our top-level declarations sanitary. So we revert to wrapping everything in a runnable object
.
hello
(no file extension)
1#!/usr/bin/env scala23object Hello extends App {4 println(s"Hello ${args.mkString(", ")}!")5}
But that shebang is going to cause problems regardless. We could try removing the shebang, and offering the .scala
file extension as a hint to Bash, but not only does that revert to showing an implementation detail, it also doesn’t work. We can run it directly through scala
, but without a shebang, bash
assumes it’s a Bash script.
Shell
1$ ./hello.scala2./hello.scala: line 1: syntax error near unexpected token `s"Hello ${args.mkString(", ")}!"'3./hello.scala: line 1: `println(s"Hello ${args.mkString(", ")}!")'
So it looks like we’re going to have to be creative. We could try playing with the shebang until we find something that works, but I’ll save you the effort: I’ve already tried, and scalac
can’t be tricked. So our one remaining option appears to be two separate files:
- A Bash script, which runs:
- A Scala script (with sanitary top-level declarations).
As a starting-point for the Bash script, we can actually use our modernized version of the shebang from the old documentation. We do have to make one change, though: the "$0"
would just repeat the ./hello
. We can’t have both files named hello
, so we’ll need to replace that. Let’s hard-code it for now.
hello
(no file extension)
1#!/usr/bin/env bash23exec scala "Hello.scala" "$@"
Hello.scala
1object Hello extends App {2 println(s"Hello ${args.mkString(", ")}!")3}
Good news! Adding the .scala
back to our Scala file gets our syntax highlighting back. We still won’t have it for our Bash file, but hopefully, after we’re done here, that file will never need to be read or modified again. And now we’re actually done! If you want. Or we can keep going.
Quibbles
Scala scripts cannot be in packages
Feel free to put your Scala script inside a package directory, but it can’t have a package
declaration: scala
just won’t have it. Your IDE will complain, and rightly so, but the important thing is that scalac
can compile it even if it doesn’t have a package
declaration, while scala
can’t run it if it does – which is, after all, the ultimate goal.
Scala code outside packages
Redditor mcandre mentioned using scripts as “modulinos, little self-contained modules that double as both command line programs and importable libraries.” I’ve done some cursory searching, and near as I can tell, code not in a package
can’t be imported by code in a package
. It can, however, be used without importing. And if you’ve ever used a language with globals, that will scare you as much as it scares me: all Scala code without a package
in the src
directory or any subdirectories is treated as global. So if you’re going to have your scripts in the src
directory of a project, please be careful.
Hard-coded Scala filename
Our Bash script will work as it is, so if you’re tired of me by now, feel free to use it. But I strongly dislike hard-coding values like that, so let’s give it some simple portability. Let’s use HelloWorld
for the moment for demonstration. The easiest way to accomplish our goal requires giving our Bash script a Scala-like, CamelCase name (or our Scala an alllowercase name), and then simply replacing "$0"
with "$0.scala"
.
HelloWorld
(no file extension)
1#!/usr/bin/env bash23exec scala "$0.scala" "$@"
HelloWorld.scala
1object HelloWorld extends App {2 println(s"Hello ${args.mkString(", ")}!")3}
But let’s say we want to leave the names in their language-appropriate cases. There’s no way to do this with a multi-word name, without giving Bash a dictionary, some time, and maybe a cup of coffee (or Java if you will) to decode it. But for a one-word name, we could strip the path from "$0"
(which expands to "./hello"
) and capitalize the first letter.
hello
(no file extension)
1#!/usr/bin/env bash23name="$(basename "$0")"4exec scala "${name^}.scala" "$@"
I’ll leave any further Bash-scripting to the reader, if you feel like torturing yourself.
Final code
For just bash
and scala
compatibility
hello
(no file extension)
1#!/usr/bin/env scala23println(s"Hello ${args.mkString(", ")}!")
Shell
1$ ./hello Mal Inara Kaylee2Hello Mal, Inara, Kaylee!3$ scala hello Mal Inara Kaylee4Hello Mal, Inara, Kaylee!5$ scalac hello6hello:1: error: expected class or object definition7#!/usr/bin/env scala
For scalac
and IDE compatibility
hello
(no file extension)
1#!/usr/bin/env bash23name="$(basename "$0")"4exec scala "${name^}.scala" "$@"
Hello.scala
1object Hello extends App {2 println(s"Hello ${args.mkString(", ")}!")3}
Shell
1$ ./hello Mal Inara Kaylee2Hello Mal, Inara, Kaylee!3$ scala Hello.scala Mal Inara Kaylee4Hello Mal, Inara, Kaylee!5$ scalac Hello.scala; ls6Hello$.class Hello.class Hello.scala hello