Scala Project: Command-Line Tool
A command-line tool is a program that takes fixed arguments and prints a report. This lesson builds one from arguments held in code.
Parsing builds a typed options value
The parser turns words into an Options value. A good limit gives a Right result.
case class Options(topic: String, limit: Int, verbose: Boolean)
def parse(args: List[String]): Either[String, Options] =
args match
case topic :: limitText :: _ =>
limitText.toIntOption match
case Some(n) if n > 0 => Right(Options(topic, n, false))
case _ => Left("limit must be a positive number")
case _ => Left("need a topic and a limit")
@main def main(): Unit =
println(parse(List("sales", "2")))
A bad argument gives a Left error
The parser returns a Left for a bad limit. Fold turns the Left into an error message.
case class Options(topic: String, limit: Int, verbose: Boolean)
def parse(args: List[String]): Either[String, Options] =
args match
case topic :: limitText :: _ =>
limitText.toIntOption match
case Some(n) if n > 0 => Right(Options(topic, n, false))
case _ => Left("limit must be a positive number")
case _ => Left("need a topic and a limit")
@main def main(): Unit =
val result = parse(List("sales", "zero"))
println(result.fold(err => "Error: " + err, opts => "Topic: " + opts.topic))
A given formats each type its own way
Display uses the given instance for the value type. The Int instance adds a word, and the String instance uppercases the text.
trait Show[T]:
def show(v: T): String
given Show[Int] with
def show(v: Int): String = v.toString + " items"
given Show[String] with
def show(v: String): String = v.toUpperCase
def display[T](v: T)(using s: Show[T]): String = s.show(v)
@main def main(): Unit =
println(display(42))
println(display("sales"))
Transforms filter, rename, and total the data
Filter keeps only large sales. Map takes the names, and sum totals every amount.
case class Sale(item: String, amount: Int)
@main def main(): Unit =
val sales = List(Sale("apples", 30), Sale("pears", 20), Sale("plums", 50))
println(sales.filter(s => s.amount >= 25).map(s => s.item).sorted.mkString(", "))
println(sales.map(s => s.amount).sum)
Parsing plus formatting makes a report line
Parse first, then format the result. A good parse prints one clean report line.
case class Options(topic: String, limit: Int, verbose: Boolean)
def parse(args: List[String]): Either[String, Options] =
args match
case topic :: limitText :: _ =>
limitText.toIntOption match
case Some(n) if n > 0 => Right(Options(topic, n, false))
case _ => Left("limit must be a positive number")
case _ => Left("need a topic and a limit")
trait Show[T]:
def show(v: T): String
given Show[Options] with
def show(o: Options): String = "Topic: " + o.topic + ", limit: " + o.limit.toString
def display[T](v: T)(using s: Show[T]): String = s.show(v)
@main def main(): Unit =
parse(List("sales", "2")) match
case Left(err) => println("Error: " + err)
case Right(opts) => println(display(opts))
Example
case class Options(topic: String, limit: Int)
case class Sale(item: String, amount: Int)
def parse(args: List[String]): Either[String, Options] =
args match
case topic :: limitText :: _ =>
limitText.toIntOption match
case Some(n) if n > 0 => Right(Options(topic, n))
case _ => Left("limit must be a positive number")
case _ => Left("need a topic and a limit")
trait Show[T]:
def show(v: T): String
given Show[Sale] with
def show(s: Sale): String = s.item + ": " + s.amount.toString
def display[T](v: T)(using s: Show[T]): String = s.show(v)
@main def main(): Unit =
val sales = List(Sale("apples", 30), Sale("pears", 20), Sale("plums", 50), Sale("peaches", 40))
parse(List("sales", "2")) match
case Left(err) => println("Error: " + err)
case Right(opts) =>
val top = sales.sortWith((a, b) => a.amount > b.amount).take(opts.limit)
top.foreach(s => println(display(s)))
println("Total: " + top.map(s => s.amount).sum.toString)
Key points
- Options: the parsed arguments held in one case class.
- Parsing: turning raw words into typed options, with an error for bad input.
- Either: a type that holds a result on the right side or a reason on the left side.
- Type class: a trait that adds one shared ability to many types.
- Formatting: a given instance decides how one type prints in the report.
In this example, the fixed arguments parse into Options. The sales sort from largest to smallest, and the top two print through the Show instance. Their amounts sum to 90.
Note
Scala prints a case class with no space after each comma.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Scastie
Exercise
The fixed arguments are List("sales", "zero"). What does parse return?