Skip to Content
Course content

63: Building a Command-Line Application in Kotlin

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've noticed a recurring pattern when developers first dive into CLI tools: they treat the main function like a dumping ground. The common misconception is that a command-line application is just a "script" where you use a Scanner to ask the user questions in a linear sequence. They think the primary interaction is a conversation: "What is your name?" → "What do you want to do?"

Here is why that approach fails in the real world: it's non-automatable. If I'm using your tool in a CI/CD pipeline or piping it into another process, I can't sit there and manually type "Yes" into a prompt. A real CLI app isn't a conversation; it's a set of instructions passed via arguments. If you rely on readLine() for everything, you've built a chatbot, not a professional utility.

The "Main-Function-as-a-Script" Trap

Usually, the "beginner" version of a CLI app looks something like this:

fun main() {
    println("Welcome to the File Tool!")
    println("Enter the path to your file:")
    val path = readLine() 
    // ... more prompts ...
}

This is tedious for the user. If I want to process 100 files, I have to run the program 100 times and type the path every single time. It's a productivity killer. In professional software, we shift the focus from interactive prompts to command-line arguments.

Treating Arguments as a First-Class API

The args: Array<String> parameter in your main function isn't just a formality; it's your application's API. Instead of asking the user for data, you expect them to provide it upfront. Let's imagine we're building a simple TaskReminder tool. Instead of asking "Which task do you want to mark as done?", we want the user to run task-remind complete 42.

I prefer using a when expression to dispatch these commands. It keeps the entry point clean and makes it trivial to add new features without nesting ten if statements. Here is how I would structure the skeleton of a real tool:

fun main(args: Array<String>) {
    if (args.isEmpty()) {
        println("Usage: task-remind <command> [options]")
        println("Commands: add, complete, list")
        return
    }

    val command = args[0]
    val remainingArgs = args.drop(1)

    when (command) {
        "add" -> handleAdd(remainingArgs)
        "complete" -> handleComplete(remainingArgs)
        "list" -> handleList()
        else -> println("Unknown command: $command")
    }
}

fun handleAdd(args: List<String>) {
    if (args.isEmpty()) {
        println("Error: Please provide a task description.")
        return
    }
    val task = args.joinToString(" ")
    println("Added task: $task")
    // Logic to save to a file or DB would go here
}

fun handleComplete(args: List<String>) {
    val id = args.firstOrNull()?.toIntOrNull() 
        ?: throw IllegalArgumentException("Please provide a valid numeric Task ID.")
    println("Marking task #$id as complete.")
}

fun handleList() {
    println("Listing all your tasks...")
}

Moving from If-Else Spaghetti to a Command Dispatcher

Notice how I separated the parsing (determining what the user wants) from the execution (actually doing the work). This is crucial. As your app grows, your main function should remain a thin wrapper. I've seen CLI projects where the main function is 500 lines long—it's a nightmare to test and even worse to maintain.

One quick tip: I used args.drop(1) to separate the command from the data. This allows you to pass the remaining list of strings directly into a handler function. Also, notice the use of toIntOrNull(). When dealing with CLI input, never assume the user is providing the right type. They will give you a string when you expect an integer, and if you don't handle that, your app will crash with a ugly stack trace. Always fail gracefully with a helpful error message.




📋 Practical Task

Build a "Project Resource Calculator" CLI

Create a command-line application that helps a project manager calculate the total cost of resources. The app should not use any interactive prompts (no Scanner or readLine); it must rely entirely on command-line arguments.

Requirements:

  • The app must support two commands: add-resource and total.
  • add-resource <name> <hourly_rate> <hours>: This should calculate the cost for a single resource (rate * hours) and print: "Resource [name] cost: $[amount]".
  • total <amount1> <amount2> ...: This should take a variable number of numeric arguments, sum them up, and print: "Total Project Cost: $[sum]".
  • Error Handling: If the user provides a non-numeric value for rates, hours, or totals, the app should print a clear error message like "Invalid number provided" instead of crashing.
  • Help: If no arguments are provided, print a usage guide explaining the available commands.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.