Skip to Content
Course content

37: Building a Command-Line Scala Application

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Where do the command-line arguments actually go?

When you're building a CLI tool, the entry point is everything. In modern Scala (Scala 3), the easiest way to do this is using the @main annotation. The arguments passed from the shell are injected as a List[String] (or Array[String] in older versions) into your method parameters.

Let's say we're building a tool called LogSifter—a utility that scans a log file and prints only the lines containing a specific error code. You don't want to hardcode the file path or the error code; you want to pass them in at runtime.

@main def runLogSifter(filePath: String, errorCode: String): Unit = {
  println(s"Scanning $filePath for errors matching: $errorCode...")
  
  val source = scala.io.Source.fromFile(filePath)
  try {
    source.getLines()
      .filter(_.contains(errorCode))
      .foreach(println)
  } finally {
    source.close()
  }
}

If you run this via sbt "run /main runLogSifter server.log ERR_404", Scala maps server.log to filePath and ERR_404 to errorCode. It's clean, and it avoids the boilerplate of a main method inside an object.

How do I handle optional flags without the app crashing?

Using positional arguments like the example above is fine for simple tools, but it's brittle. If the user forgets an argument, the app crashes with an IndexOutOfBoundsException or a missing argument error. I've spent way too many hours debugging poorly written CLI tools that just crash without telling the user why.

For anything beyond a script, I recommend mapping your arguments to a case class. This separates the "parsing" logic from the "business" logic. While there are great libraries like scopt for this, you can do a lot with basic pattern matching for smaller tools.

case class Config(filePath: String, keyword: String, verbose: Boolean)

def parseArgs(args: List[String]): Config = args match {
  case filePath :: keyword :: "--verbose" :: Nil => Config(filePath, keyword, true)
  case filePath :: keyword :: Nil                => Config(filePath, keyword, false)
  case _                                         => 
    println("Usage: logsifter <file> <keyword> [--verbose]")
    sys.exit(1)
}

@main def runLogSifter(args: List[String]): Unit = {
  val config = parseArgs(args)
  if (config.verbose) println(s"Opening ${config.filePath}...")
  // ... rest of the logic using config.keyword
}

By doing this, you've created a "safety gate." Your main logic now receives a typed Config object instead of a raw list of strings, which makes your code much easier to test.

How do I package this so someone can actually use it?

You can't expect your users to have sbt installed just to run your tool. You need to package your application into a "Fat JAR"—a single archive that contains your code and all its dependencies.

The industry standard for this in the Scala ecosystem is the sbt-assembly plugin. You add the plugin to your project/plugins.sbt file, and then you can run sbt assembly. This bundles everything into target/scala-3.x/your-app-assembly-x.x.jar.

Once you have that JAR, you can run your application from any terminal with a JVM installed using the -jar flag:

java -jar logsifter-assembly-0.1.jar server.log ERR_500 --verbose

If you want to go a step further and make it feel like a real native command (where you just type logsifter), you can create a simple bash alias or a small shell script wrapper that calls the java -jar command behind the scenes. I usually prefer the wrapper script because it allows you to hide the messy JVM flags from the end user.




📋 Practical Task

Exercise: Create the "Project-Word-Counter" CLI

Your task is to build a command-line tool that counts the occurrences of a specific word across a given text file. This will test your ability to handle arguments, perform file I/O, and implement a basic "help" menu.

  • Requirement 1: The app must take two positional arguments: a filePath (String) and a targetWord (String).
  • Requirement 2: The app must support an optional flag --case-insensitive. If present, searching for "Scala" should also find "scala" or "SCALA".
  • Requirement 3: If the user provides the wrong number of arguments or an unknown flag, the program should print a usage message: Usage: wordcount <file> <word> [--case-insensitive] and exit with a non-zero status code.
  • Requirement 4: The output should be a simple sentence: "The word '[word]' appeared [count] times in [file]."

Test your tool with these scenarios:

  1. runWordCount mytext.txt Scala (Case sensitive)
  2. runWordCount mytext.txt Scala --case-insensitive (Case insensitive)
  3. runWordCount mytext.txt (Should trigger usage message)
Rating
0 0

There are no comments for now.

to be the first to leave a comment.