CLI Tools

Turn a Java class into a real command-line tool — read arguments and stdin, return exit codes, use environment variables, and graduate to picocli and a runnable jar.

Learn CLI Tools in our free Java course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.

Part of the free Java course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

Before You Start

You should be comfortable with methods and arrays, and have seen exception handling (parsing text can throw). To package the final tool, the Deployment lesson covers building a jar.

A Real-World Analogy

Think of a command-line tool as a kitchen appliance . The arguments you type are the dials you set before turning it on ( --count 3 ). Stdin is the food you feed in through the chute. The result comes out one spout ( stdout ) while error noises come out another ( stderr ). And when it finishes, a light shows green for success or red for a jam — that light is the exit code .

You can wire all of that by hand with raw args[] — like building the appliance from loose parts. picocli is the pre-built appliance: you label the dials with annotations and it handles parsing, validation, and the --help manual for you.

1️⃣ The Entry Point: main(String[] args)

Every Java program starts at one method: public static void main(String[] args) . The args array holds whatever words you typed after the program name. If you run java Greet Alice 3 , then args[0] is "Alice" and args[1] is "3" .

Two things trip everyone up. First, every argument arrives as a String — "3" is text, so convert it with Integer.parseInt before doing maths. Second, args can be empty , so always check args.length before reading args[0] .

2️⃣ Reading Input from stdin

Stdin ("standard input") is the stream of text a user types — or that another program pipes in with | . Two classes read it. Scanner is friendly for interactive prompts ( nextLine , nextInt ). BufferedReader is faster and ideal when lots of lines are piped in.

The key pattern: readLine() returns the next line, or null when input ends (the user presses Ctrl-D , or the pipe finishes). Looping while ((line = in.readLine()) != null) processes every line and stops cleanly.

3️⃣ Exit Codes, Environment Variables & System Properties

When your tool finishes, it returns an exit code to the shell: 0 means success, any non-zero number means failure. Scripts read it (the shell stores it in $? ) to decide whether to keep going. Set it with System.exit(code) , and send error text to System.err , not System.out .

Configuration comes from two places. Environment variables are set by the shell before your program runs — read them with System.getenv("HOME") (returns null if unset). System properties are JVM settings you pass with -D , like java -Denv=prod App , read with System.getProperty("env", "dev") where "dev" is the fallback.

🎯 Your Turn #1 — Word Counter

🎯 Your Turn #2 — Sum Numbers from stdin

4️⃣ Real CLIs with picocli

Hand-rolled args[] parsing works for tiny scripts, but it gets ugly fast: options with values, flags, defaults, type conversion, --help text, and subcommands all become your problem. picocli is a small, popular library that does all of that from annotations.

You annotate fields: @Parameters marks a positional argument ( greet Alice ), @Option marks a named option ( --count 3 or the flag --upper ), and mixinStandardHelpOptions = true adds --help and --version for free. Your class implements Callable<Integer> ; picocli parses the arguments, converts "3" into an int , runs your call() method, and uses its return value as the exit code.

For bigger tools, picocli also supports subcommands — @Command(subcommands = { UserAdd.class, UserList.class } ) gives you a git -style mytool user add / mytool user list hierarchy, each with its own options and help.

5️⃣ Packaging a Runnable Jar

To share your tool, bundle it into a runnable jar — a single file whose manifest records which class has main . Anyone with a JDK can then run it without compiling.

For an even faster, JVM-free binary, compile the jar with GraalVM's native-image tool — it produces a single executable that starts in milliseconds. That is covered in the Deployment lesson.

Common Errors (and the Fix)

Pro Tips

📋 Quick Reference

Task

Code

Notes

Read an argument

args[0]

Check args.length first!

Count arguments

args.length

0 when none were passed

Text → number

Integer.parseInt(s)

Throws on bad input

Read a line of stdin

in.readLine()

null at end of input

Interactive prompt

new Scanner(System.in)

nextLine(), nextInt()

Exit with a code

System.exit(0)

0 ok, non-zero error

Print an error

System.err.println(s)

Not System.out

Environment variable

System.getenv("HOME")

null if unset

System property

System.getProperty("env","dev")

Pass with -Denv=prod

picocli option

@Option(names = {"-c", "--count"})

Named flag/value

picocli positional

@Parameters(index = "0")

Bare argument

Build a runnable jar

jar --create --file x.jar --main-class X X.class

Run: java -jar x.jar

Frequently Asked Questions

Mini-Challenge — Calculator CLI

🎉 Lesson Complete!

You can now build a real Java command-line tool: read args[] from main , pull input from stdin with Scanner or BufferedReader , return meaningful exit codes via System.exit , and read configuration from environment variables and -D system properties. You also know when to graduate from hand-rolled parsing to picocli , and how to package the result as a runnable jar.

Next up: Clean Architecture — designing maintainable, testable applications with SOLID and DDD.

Practice quiz

In public static void main(String[] args), what is args[0] when you run 'java Greet Alice 3'?

  • "Greet"
  • "3"
  • "Alice"
  • The program name

Answer: "Alice". args holds the words AFTER the program name, so args[0] is "Alice" and args[1] is "3".

Every command-line argument arrives in main as:

  • A String
  • An int
  • An Object

Answer: A String. All args are Strings. Convert text like "3" with Integer.parseInt before doing arithmetic.

Why must you check args.length before reading args[0]?

  • args is never empty

If no arguments are passed, args has length 0 and reading args[0] throws ArrayIndexOutOfBoundsException.

BufferedReader.readLine() returns what value at the end of input?

  • An empty string
  • null
  • -1
  • It throws EOFException

Answer: null. readLine() returns null at end-of-input, which is why the loop is while ((line = in.readLine()) != null).

By convention, what exit code means SUCCESS?

  • 0
  • 1
  • -1
  • 2

Answer: 0. 0 means success; any non-zero value means failure. 2 is commonly a usage/bad-arguments error.

Error messages from a CLI tool should be sent to:

  • System.out
  • A log file only
  • System.err
  • System.getenv

Answer: System.err. Errors go to System.err so a user can redirect results (System.out) to a file while still seeing errors.

How do you read an environment variable like HOME?

  • System.getProperty("HOME")
  • System.getenv("HOME")

Answer: System.getenv("HOME"). System.getenv("HOME") reads an environment variable (set by the shell); it returns null if unset.

You run 'java -Denv=prod App'. How do you read that value inside the program?

  • System.getenv("env")

A -D flag sets a JVM system property, read with System.getProperty("env") (with an optional fallback).

When should you graduate from hand-parsing args[] to a library like picocli?

picocli replaces brittle parsing with annotations and gives you validation, --help, and type conversion for free.

What does Integer.parseInt(args[0]) throw when the user types 'abc'?

  • ArrayIndexOutOfBoundsException
  • NullPointerException
  • NumberFormatException
  • Nothing — it returns 0

Answer: NumberFormatException. parseInt throws NumberFormatException on non-numeric text; wrap it in try/catch and print a usage message.

Continue this course

Related lessons