Codementor Events

Shello World: Writing a Scala script

Published Sep 28, 2019Last updated Mar 25, 2020
Shello World: Writing a Scala script

Hello, kids and older kids! Today's adventure is in writing a Hello World CLI (Command Line Interface) 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 (Read Eval Print Loop)), 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
#!/bin/sh
exec scala "$0" "$@"
!#

object Hello {
  def main(args: Array[String]): Unit = {
    println(s"Hello ${args.mkString(", ")}!")
  }
}

Hello.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
$ scala hello Mal Inara Kaylee
hello.sh:11: warning: Script has a main object but statement is disallowed
Hello.main(args)
          ^
one warning found
Hello 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)
#!/bin/sh
exec scala "$0" "$@"
!#

object Hello {
  def main(args: Array[String]): Unit = {
    println(s"Hello ${args.mkString(", ")}!")
  }
}

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
$ ./hello Mal Inara Kaylee
bash: ./hello: Permission denied
$ chmod +x hello
$ ./hello Mal Inara Kaylee
Hello 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
#!/usr/bin/env scala

object Hello extends App {
  println(s"Hello ${args.mkString(", ")}!")
}

Hello.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)
#!/usr/bin/env scala

object Hello extends App {
  println(s"Hello ${args.mkString(", ")}!")
}

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)
#!/usr/bin/env scala

println(s"Hello ${args.mkString(", ")}!")

Let's try it out.

Shell
$ scala hello Mal Inara Kaylee
Hello Mal, Inara, Kaylee!
$ ./hello Mal Inara Kaylee
Hello 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
$ scalac hello
hello:1: error: expected class or object definition
#!/usr/bin/env scala
^
hello:3: error: expected class or object definition
println(s"Hello ${args.mkString(", ")}!")
^
two 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)
#!/usr/bin/env scala

object Hello extends App {
  println(s"Hello ${args.mkString(", ")}!")
}

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
$ ./hello.scala
./hello.scala: line 1: syntax error near unexpected token `s"Hello ${args.mkString(", ")}!"'
./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)
#!/usr/bin/env bash

exec scala "Hello.scala" "$@"
Hello.scala
object Hello extends App {
  println(s"Hello ${args.mkString(", ")}!")
}

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)
#!/usr/bin/env bash

exec scala "$0.scala" "$@"
HelloWorld.scala
object HelloWorld extends App {
  println(s"Hello ${args.mkString(", ")}!")
}

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)
#!/usr/bin/env bash

name="$(basename "$0")"
exec 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)
#!/usr/bin/env scala

println(s"Hello ${args.mkString(", ")}!")
Shell
$ ./hello Mal Inara Kaylee
Hello Mal, Inara, Kaylee!
$ scala hello Mal Inara Kaylee
Hello Mal, Inara, Kaylee!
$ scalac hello
hello:1: error: expected class or object definition
#!/usr/bin/env scala

For scalac and IDE compatibility

hello (no file extension)
#!/usr/bin/env bash

name="$(basename "$0")"
exec scala "${name^}.scala" "$@"
Hello.scala
object Hello extends App {
  println(s"Hello ${args.mkString(", ")}!")
}
Shell
$ ./hello Mal Inara Kaylee
Hello Mal, Inara, Kaylee!
$ scala Hello.scala Mal Inara Kaylee
Hello Mal, Inara, Kaylee!
$ scalac Hello.scala; ls
Hello$.class Hello.class  Hello.scala  hello
Discover and read more posts from Martin Rosenberg
get started
post commentsBe the first to share your opinion
Naftoli Gugenheim
5 years ago

A much better way is to use Ammonite. See https://ammonite.io/#FromBash and the rest of the documentation

Martin Rosenberg
5 years ago

Hi Naftoli! The point with this was to do it with no dependencies other than Scala itself to keep it as portable as possible (for Scala), and as an exercise. However, Ammonite is an excellent tool, and I definitely recommend Li Haoyi’s work in general.

Show more replies