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
argsarray, pattern matching for type-safe parsing, and third-party libraries like scopt for complex scenarios - The
argsparameter in the main method is anArray[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.