scalaintermediate

Command-Line Application

Build CLI applications with argument parsing, interactive prompts, and formatted output.

scala
import scala.io.StdIn
import scala.util.{Try, Success, Failure}

// Simple argument parser
case class Args(
  command: String = "help",
  flags: Map[String, String] = Map.empty,
  positional: List[String] = Nil
)

object ArgParser:
  def parse(args: Array[String]): Args =
    if args.isEmpty then return Args()

    val cmd = args.head
    val rest = args.tail.toList
    val (flags, positional) = parseFlags(rest)
    Args(cmd, flags, positional)

  private def parseFlags(args: List[String]): (Map[String, String], List[String]) =
    val flags = scala.collection.mutable.Map.empty[String, String]
    val pos = scala.collection.mutable.ListBuffer.empty[String]

    var i = 0
    val arr = args.toArray
    while i < arr.length do
      arr(i) match
        case s if s.startsWith("--") && s.contains("=") =>
          val Array(key, value) = s.drop(2).split("=", 2)
          flags(key) = value
        case s if s.startsWith("--") && i + 1 < arr.length =>
          flags(s.drop(2)) = arr(i + 1)
          i += 1
        case s if s.startsWith("-") && s.length == 2 && i + 1 < arr.length =>
          flags(s.drop(1)) = arr(i + 1)
          i += 1
        case s =>
          pos += s
      i += 1

    (flags.toMap, pos.toList)

// REPL-style interactive app
object TodoApp:
  private var todos = Vector.empty[String]

  def run(): Unit =
    println("Todo App — type 'help' for commands")
    var running = true
    while running do
      print("> ")
      val input = Option(StdIn.readLine()).getOrElse("quit")
      input.trim.split("\\s+", 2).toList match
        case "add" :: text :: Nil =>
          todos = todos :+ text
          println(s"  Added: $text")
        case "list" :: Nil =>
          if todos.isEmpty then println("  No todos")
          else todos.zipWithIndex.foreach { (todo, i) =>
            println(f"  ${i + 1}%3d. $todo")
          }
        case "done" :: num :: Nil =>
          Try(num.toInt - 1) match
            case Success(idx) if idx >= 0 && idx < todos.size =>
              val removed = todos(idx)
              todos = todos.patch(idx, Nil, 1)
              println(s"  Done: $removed")
            case _ => println(s"  Invalid number: $num")
        case "clear" :: Nil =>
          todos = Vector.empty
          println("  Cleared all todos")
        case "help" :: Nil =>
          println("  Commands:")
          println("    add <text>  — Add a todo")
          println("    list        — List all todos")
          println("    done <n>    — Mark todo n as done")
          println("    clear       — Remove all todos")
          println("    quit        — Exit")
        case "quit" :: Nil | "exit" :: Nil =>
          running = false
          println("  Bye!")
        case cmd :: _ =>
          println(s"  Unknown: $cmd (try 'help')")
        case Nil => ()

// Progress bar
def progressBar(current: Int, total: Int, width: Int = 40): String =
  val pct = current.toDouble / total
  val filled = (pct * width).toInt
  val bar = "█" * filled + "░" * (width - filled)
  f"[$bar] ${pct * 100}%5.1f%% ($current/$total)"

// Table formatter
def printTable(headers: List[String], rows: List[List[String]]): Unit =
  val widths = headers.indices.map { i =>
    (headers(i).length :: rows.map(r => r.lift(i).map(_.length).getOrElse(0))).max
  }.toList

  def formatRow(row: List[String]): String =
    row.zip(widths).map((s, w) => s.padTo(w, ' ')).mkString(" | ")

  val sep = widths.map("-" * _).mkString("-+-")
  println(formatRow(headers))
  println(sep)
  rows.foreach(r => println(formatRow(r)))

@main def demo(args: String*): Unit =
  // Arg parsing demo
  val parsed = ArgParser.parse(args.toArray)
  println(s"Command: ${parsed.command}")
  println(s"Flags: ${parsed.flags}")
  println(s"Args: ${parsed.positional}")

  // Progress bar
  println()
  for i <- 0 to 10 do
    print(s"\r${progressBar(i * 10, 100)}")
  println()

  // Table
  println()
  printTable(
    List("Name", "Age", "City"),
    List(
      List("Alice", "30", "New York"),
      List("Bob", "25", "San Francisco"),
      List("Carol", "35", "London")
    )
  )

Use Cases

  • Command-line tool development
  • Interactive REPL applications
  • Formatted terminal output

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.