RosenblogNavigate back to the homepage

Shello World

Martin Rosenberg
October 1st, 2018 · 5 min read

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/sh
2exec scala "$0" "$@"
3!#
4
5object Hello {
6 def main(args: Array[String]): Unit = {
7 println(s"Hello ${args.mkString(", ")}!")
8 }
9}
10
11Hello.main(args)

Before we even run this code, there are a few yellow flags:

  1. Any file extension is made redundant by having a shebang.
  2. The file extension is an implementation detail that should be hidden from the user.
  3. The .sh file extension is outdated (it’s for the Bourne Shell, Bash’s predecessor).
  4. Similarly, the /bin/sh in the shebang is outdated.
  5. 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 Kaylee
2hello.sh:11: warning: Script has a main object but statement is disallowed
3Hello.main(args)
4 ^
5one warning found
6Hello 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/sh
2exec scala "$0" "$@"
3!#
4
5object 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 Kaylee
2bash: ./hello: Permission denied
3$ chmod +x hello
4$ ./hello Mal Inara Kaylee
5Hello 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 scala
2
3object Hello extends App {
4 println(s"Hello ${args.mkString(", ")}!")
5}
6
7Hello.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:

  1. scala actually has its own shebang now, which solves the issue of the file looking like a shell script.
  2. Scala offers the App trait, which essentially turns Hello itself into main, and provides args 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 scala
2
3object 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 scala
2
3println(s"Hello ${args.mkString(", ")}!")

Let’s try it out.

Shell
1$ scala hello Mal Inara Kaylee
2Hello Mal, Inara, Kaylee!
3$ ./hello Mal Inara Kaylee
4Hello 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:

  1. When we remove the file extension, IntelliJ stops syntax highlighting. We’ll address this later.
  2. 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 hello
2hello:1: error: expected class or object definition
3#!/usr/bin/env scala
4^
5hello:3: error: expected class or object definition
6println(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 scala
2
3object 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.scala
2./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:

  1. A Bash script, which runs:
  2. 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 bash
2
3exec 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 bash
2
3exec 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 bash
2
3name="$(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 scala
2
3println(s"Hello ${args.mkString(", ")}!")
Shell
1$ ./hello Mal Inara Kaylee
2Hello Mal, Inara, Kaylee!
3$ scala hello Mal Inara Kaylee
4Hello Mal, Inara, Kaylee!
5$ scalac hello
6hello:1: error: expected class or object definition
7#!/usr/bin/env scala

For scalac and IDE compatibility

hello (no file extension)
1#!/usr/bin/env bash
2
3name="$(basename "$0")"
4exec scala "${name^}.scala" "$@"
Hello.scala
1object Hello extends App {
2 println(s"Hello ${args.mkString(", ")}!")
3}
Shell
1$ ./hello Mal Inara Kaylee
2Hello Mal, Inara, Kaylee!
3$ scala Hello.scala Mal Inara Kaylee
4Hello Mal, Inara, Kaylee!
5$ scalac Hello.scala; ls
6Hello$.class Hello.class Hello.scala hello

More articles from Martin Rosenberg

Clean Water

Make sure you drink safe water when you travel.

September 4th, 2017 · 1 min read

ES6 Modules

Basic recipes for imports and exports in ES6 modules

August 14th, 2019 · 1 min read
© 2020 Martin Rosenberg
Link to $https://github.com/MartinRosenbergLink to $https://www.linkedin.com/in/MartinBRosenberg/Link to $https://paypal.me/MartinBRosenbergLink to $https://stackoverflow.com/users/2303326/martinrosenbergLink to $https://twitter.com/Marty_Rosenberg