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)
- ❌ ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0 — you read args[0] but the user passed no arguments. Fix: guard first with if (args.length == 0) { ...System.exit(2); } before touching args[0] .
- ❌ No argument validation — Integer.parseInt(args[0]) throws NumberFormatException when the user types abc . Fix: wrap parsing in try/catch and print a clear usage message to System.err , or let picocli validate the type for you.
- ❌ Tool always "succeeds" in scripts — you printed an error but never set an exit code, so the shell sees 0 . Fix: call System.exit(1) (or 2 for usage errors) on every failure path.
- ❌ Errors disappear when output is redirected — you printed errors with System.out.println , so tool > out.txt swallows them. Fix: send errors to System.err.println , results to System.out .
- ❌ Reinventing argument parsing — 100 lines of brittle if (a.equals("--name")) logic that mishandles edge cases. Fix: use picocli's @Option / @Parameters annotations; you get parsing, defaults, --help , and validation for free.
Pro Tips
- 💡 Support - as a filename — the convention "read from stdin" is the single dash - . Many Unix tools follow it.
- 💡 Always add --help — users expect it. picocli's mixinStandardHelpOptions = true wires it up automatically.
- 💡 Pick stable exit codes — 0 success, 1 general error, 2 usage error. Document them so scripts can branch reliably.
- 💡 Generate tab completion — picocli can emit a bash/zsh completion script from your annotated command for a big UX win.
📋 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
- Previous: Previous Lesson
- Next: Next Lesson