Scala - Command Line Arguments

Scala's main method receives command line arguments as an `Array[String]` through the `args` parameter. This is the most basic approach for simple scripts.

Key Insights

  • Scala provides multiple approaches to handle command line arguments: raw args array, pattern matching for type-safe parsing, and third-party libraries like scopt for complex scenarios
  • The args parameter in the main method is an Array[String] that requires explicit conversion and validation for non-string types
  • Production applications benefit from structured argument parsing with named parameters, validation, and automatic help generation rather than positional arguments

Accessing Raw Arguments

Scala’s main method receives command line arguments as an Array[String] through the args parameter. This is the most basic approach for simple scripts.

object BasicArgs extends App {
  println(s"Number of arguments: ${args.length}")
  args.zipWithIndex.foreach { case (arg, index) =>
    println(s"Argument $index: $arg")
  }
}

Running scala BasicArgs.scala hello world 123 outputs:

Number of arguments: 3
Argument 0: hello
Argument 1: world
Argument 2: 123

The App trait provides convenient access to args without explicitly defining a main method. For standard Java-style entry points:

object StandardMain {
  def main(args: Array[String]): Unit = {
    if (args.isEmpty) {
      println("No arguments provided")
      sys.exit(1)
    }
    println(s"First argument: ${args(0)}")
  }
}

Type Conversion and Validation

Command line arguments arrive as strings. Converting to other types requires explicit parsing with error handling.

object TypedArgs extends App {
  def parsePort(arg: String): Either[String, Int] = {
    try {
      val port = arg.toInt
      if (port > 0 && port < 65536) Right(port)
      else Left(s"Port must be between 1 and 65535, got: $port")
    } catch {
      case _: NumberFormatException => Left(s"Invalid port number: $arg")
    }
  }

  args.headOption match {
    case Some(portStr) =>
      parsePort(portStr) match {
        case Right(port) => println(s"Starting server on port $port")
        case Left(error) => 
          println(s"Error: $error")
          sys.exit(1)
      }
    case None =>
      println("Usage: program <port>")
      sys.exit(1)
  }
}

For multiple typed arguments:

object MultiTypedArgs extends App {
  case class Config(host: String, port: Int, timeout: Int)

  def parseConfig(args: Array[String]): Either[String, Config] = {
    if (args.length < 3) {
      return Left("Expected: <host> <port> <timeout>")
    }

    for {
      port <- args(1).toIntOption.toRight("Invalid port")
      timeout <- args(2).toIntOption.toRight("Invalid timeout")
    } yield Config(args(0), port, timeout)
  }

  parseConfig(args) match {
    case Right(config) =>
      println(s"Connecting to ${config.host}:${config.port} " +
              s"with timeout ${config.timeout}ms")
    case Left(error) =>
      println(s"Error: $error")
      sys.exit(1)
  }
}

Pattern Matching for Named Arguments

Pattern matching provides a clean way to handle named arguments with flags.

object NamedArgs extends App {
  case class Options(
    verbose: Boolean = false,
    output: Option[String] = None,
    inputs: List[String] = List.empty
  )

  def parseArgs(args: List[String], options: Options = Options()): Options = {
    args match {
      case Nil => options
      case "--verbose" :: tail =>
        parseArgs(tail, options.copy(verbose = true))
      case "--output" :: file :: tail =>
        parseArgs(tail, options.copy(output = Some(file)))
      case "-o" :: file :: tail =>
        parseArgs(tail, options.copy(output = Some(file)))
      case arg :: tail if !arg.startsWith("-") =>
        parseArgs(tail, options.copy(inputs = options.inputs :+ arg))
      case unknown :: _ =>
        println(s"Unknown option: $unknown")
        sys.exit(1)
    }
  }

  val options = parseArgs(args.toList)
  
  if (options.verbose) {
    println(s"Options: $options")
  }
  
  options.output match {
    case Some(file) => println(s"Output file: $file")
    case None => println("No output file specified")
  }
  
  println(s"Input files: ${options.inputs.mkString(", ")}")
}

Using scopt for Production Applications

The scopt library provides robust argument parsing with automatic help generation and type safety.

Add to build.sbt:

libraryDependencies += "com.github.scopt" %% "scopt" % "4.1.0"

Implementation:

import scopt.OParser

object ScoptExample extends App {
  case class Config(
    input: File = new File("."),
    output: File = new File("."),
    verbose: Boolean = false,
    debug: Boolean = false,
    mode: String = "default",
    maxThreads: Int = 4,
    tags: Seq[String] = Seq.empty
  )

  val builder = OParser.builder[Config]
  val parser = {
    import builder._
    OParser.sequence(
      programName("myapp"),
      head("myapp", "1.0"),
      
      opt[File]('i', "input")
        .required()
        .valueName("<file>")
        .action((x, c) => c.copy(input = x))
        .text("input file is required"),
      
      opt[File]('o', "output")
        .valueName("<file>")
        .action((x, c) => c.copy(output = x))
        .text("output file (optional)"),
      
      opt[Int]('t', "threads")
        .action((x, c) => c.copy(maxThreads = x))
        .validate(x =>
          if (x > 0 && x <= 32) success
          else failure("threads must be between 1 and 32"))
        .text("number of threads (default: 4)"),
      
      opt[String]('m', "mode")
        .action((x, c) => c.copy(mode = x))
        .validate(x =>
          if (Seq("fast", "normal", "thorough").contains(x)) success
          else failure("mode must be: fast, normal, or thorough"))
        .text("processing mode"),
      
      opt[Seq[String]]("tags")
        .valueName("<tag1>,<tag2>...")
        .action((x, c) => c.copy(tags = x))
        .text("comma-separated tags"),
      
      opt[Unit]('v', "verbose")
        .action((_, c) => c.copy(verbose = true))
        .text("verbose output"),
      
      opt[Unit]("debug")
        .hidden()
        .action((_, c) => c.copy(debug = true))
        .text("debug mode"),
      
      help("help").text("prints this usage text"),
      
      checkConfig(c =>
        if (c.input.exists()) success
        else failure("input file must exist"))
    )
  }

  OParser.parse(parser, args, Config()) match {
    case Some(config) =>
      println(s"Processing ${config.input} with ${config.maxThreads} threads")
      if (config.verbose) println(s"Full config: $config")
      // Application logic here
    case _ =>
      // Error messages already printed by scopt
      sys.exit(1)
  }
}

Running with --help generates:

myapp 1.0
Usage: myapp [options]

  -i, --input <file>       input file is required
  -o, --output <file>      output file (optional)
  -t, --threads <value>    number of threads (default: 4)
  -m, --mode <value>       processing mode
  --tags <tag1>,<tag2>...  comma-separated tags
  -v, --verbose            verbose output
  --help                   prints this usage text

Environment Variables as Fallbacks

Combine command line arguments with environment variables for flexible configuration:

object EnvFallback extends App {
  def getConfig(args: Array[String]): Map[String, String] = {
    val argsMap = args.sliding(2, 2).collect {
      case Array(key, value) if key.startsWith("--") =>
        key.stripPrefix("--") -> value
    }.toMap

    Map(
      "host" -> argsMap.getOrElse("host", 
        sys.env.getOrElse("APP_HOST", "localhost")),
      "port" -> argsMap.getOrElse("port", 
        sys.env.getOrElse("APP_PORT", "8080")),
      "apiKey" -> argsMap.getOrElse("api-key",
        sys.env.getOrElse("API_KEY", ""))
    )
  }

  val config = getConfig(args)
  
  if (config("apiKey").isEmpty) {
    println("Error: API key required via --api-key or API_KEY env var")
    sys.exit(1)
  }

  println(s"Connecting to ${config("host")}:${config("port")}")
}

This approach prioritizes command line arguments over environment variables, with sensible defaults as a final fallback. For production systems, consider libraries like PureConfig or Typesafe Config that integrate command line arguments, environment variables, and configuration files into a unified configuration management system.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.