Scala
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Scala
-
Section 4: Functional Scala
-
Section 5: Collections in Depth
-
Section 6: Type System
-
Section 7: Concurrency and Ecosystem
-
Section 8: Practical Projects
-
Section 9: Interview Practice
-
Section 10: Data Structures and Algorithms in Scala
-
Section 11: More Practice Exercises
-
Section 12: Advanced Functional Patterns
-
Section 13: More Ecosystem
-
Section 14: Scala Collections Library Deep Dive
-
Section 15: Scala Standard Library Deep Dive
-
Section 16: Akka Ecosystem Deep Dive
-
Section 17: Cats and Cats Effect Deep Dive
-
Section 18: Play Framework Deep Dive
-
Section 19: Apache Spark with Scala Deep Dive
-
Section 20: Scala Build Tools Deep Dive
-
Section 21: Scala 3 Specific Features
-
90: Union and Intersection Types
-
Section 22: Scala Testing Deep Dive
-
Section 23: Functional Domain Modeling
-
Section 24: More Data Structures and Algorithms in Scala
-
Section 25: Scala for Data Engineering
-
Section 26: More Practical Projects
-
Section 27: More Interview and Review
-
Section 28: ZIO Ecosystem Deep Dive
-
Section 29: Scala for Machine Learning
-
Section 30: Scala Microservices Architecture
-
Section 31: Scala Type System Deep Dive
-
Section 32: More Practice and Drills
-
Section 33: Scala Performance Deep Dive
-
Section 34: Scala Ecosystem Tooling
-
Section 35: Scala for Reactive Systems
-
Section 36: More Real-World Case Studies
-
Section 37: Scala for Financial Systems
-
Section 38: Scala GraphQL and gRPC
-
Section 39: More Final Projects
-
Section 40: More Interview and Final Review
-
Section 41: Scala for Streaming Data
-
Section 42: Scala Security Practices
-
Section 43: More Language Deep Dive
-
Section 44: Scala Command-Line Tools
-
Section 45: Scala Documentation and Style
-
Section 46: Scala Dependency Management
-
Section 47: More Practical Backend Patterns
-
Section 48: Scala for Event-Driven Architecture
-
Section 49: More Practice Drills Round 2
-
Section 50: Scala Compiler Deep Dive
-
Section 51: Scala for Web Frontends
-
Section 52: More Data Engineering Practice
-
Section 53: Scala Observability
-
Section 54: More Advanced Practice Projects
-
Section 55: Scala for Legacy Java Integration
-
Section 56: More Testing Practice
-
Section 57: Final Mastery Review
-
Section 58: Scala History and Ecosystem Context
-
Section 59: More Concurrency Patterns
-
Section 60: Scala for Configuration Management
-
Section 61: More Domain Modeling Practice
178: Building CLIs with Scala and decline
Imagine you're designing a high-end vending machine. You don't want the customer to just shove a random piece of paper into a slot and hope the machine knows what they want. Instead, you provide a clear interface: buttons for specific snacks, a coin slot that only accepts certain denominations, and a small screen that tells the user exactly why their transaction failed if they didn't provide enough change. You've essentially built a contract between the user and the machine.
Building a Command Line Interface (CLI) is exactly the same. Most of us start by manually hacking away at the args: Array[String] passed into the main method, using a bunch of if/else blocks to check for --verbose or -f. It's messy, it's error-prone, and you have to manually write the help text. That's where decline comes in. It lets you define that "vending machine contract" as a Scala case class, and it handles the parsing, validation, and help-menu generation for you.
Defining the Contract with Case Classes
In decline, your CLI options aren't just strings; they are typed values. Let's say we're building a tool to scan a directory for oversized log files and archive them. We need a path to the folder, a maximum file size, and a flag to determine if we should actually move the files or just list them (dry run).
import com.monovore.decline._
import cats.imprints._
case class LogArchiveConfig(
path: String,
maxSizeMb: Int,
dryRun: Boolean
)
By putting these in a case class, we've created a single source of truth for what our program needs to run. Now we just need to tell decline how to map command-line strings into this class.
Mapping Flags to Values
Here is where the magic happens. We create an Opts object. I like to think of this as the "wiring" phase. We map specific flags (like -s or --size) directly to the fields in our case class.
val archiveOpts = Opts.apply {
(
path: String,
maxSize: Int,
dryRun: Boolean
) => LogArchiveConfig(path, maxSize, dryRun)
}.withArgs(
Opts.argument[String]("path", help = "The directory containing logs to scan"),
Opts.option[Int]("size", short = "s", help = "Max file size in MB before archiving").withDefault(100),
Opts.flag("dry-run", help = "List files without moving them")
)
Notice a few things here. The path is a positional argument—it's required and doesn't need a flag. The size is an option with a default value of 100, and dry-run is a simple boolean flag. If a user tries to pass "abc" instead of a number for the size, decline will automatically stop the program and print a helpful error message without you writing a single try-catch block.
Running the Parser
Once the wiring is done, you just need to execute the parser. I usually wrap this in a small main object. The parse method takes the raw args and returns an Either. If it's a Left, decline has already formatted a beautiful error message or help screen for the user; you just need to print it and exit.
object LogArchiver extends Command {
def main(args: Array[String]): Unit = {
archiveOpts.parse(args) match {
case Left(error) =>
System.err.println(error)
sys.exit(1)
case Right(config) =>
println(s"Scanning ${config.path} for files larger than ${config.maxSizeMb}MB...")
if (config.dryRun) println("Dry run enabled: no files will be moved.")
// Your business logic goes here
}
}
}
I've spent way too many hours of my career writing custom while loops to parse Array[String]. Using a library like decline isn't just about saving keystrokes; it's about ensuring that your tool behaves predictably. When you provide a professional-looking --help output and strict type validation, your tools feel like first-class software rather than a collection of scripts.
📋 Practical Task
Build a "Project Dependency Auditor" CLI
Your task is to build a CLI tool using decline that mimics a dependency auditor. The tool should accept the following inputs:
- A positional argument:
projectPath(String) - The path to the project root. - An option:
--severityor-s(String) - A filter for vulnerability severity (e.g., "High", "Critical"). This should have a default value of "Low". - A flag:
--json- A boolean flag that, when present, tells the program to output results in JSON format instead of plain text.
Requirements:
- Create a case class
AuditorConfigto hold these three values. - Define the
Optsparser to map the command-line arguments to that case class. - Implement the
mainmethod to parse the arguments and print a confirmation message like: "Auditing project at [path] for [severity] vulnerabilities. Format: [JSON/Text]". - Ensure that if the user provides an invalid argument, the program prints the
declineerror and exits with a non-zero status code.
There are no comments for now.