Skip to main content

    Lesson 48 • Expert

    Building Command-Line Tools in Java

    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.

    What You'll Learn in This Lesson

    • Read arguments from public static void main(String[] args)
    • Read input from stdin with Scanner and BufferedReader
    • Return exit codes with System.exit so scripts can react
    • Read environment variables and -D system properties
    • Build a real CLI with picocli: options and subcommands
    • Package your tool as a runnable jar you can ship

    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].

    Worked Example — Reading args[]
    public class Greet {
        // args[] holds the words you typed after the program name.
        // Run as:  java Greet Alice 3
        //   -> args[0] = "Alice", args[1] = "3"
        public static void main(String[] args) {
            // Defaults so the tool still works with no arguments.
            String name = "World";
            int times = 1;
    
            // args.length tells you how many were passed — ALWAYS check it.
            if (args.length >= 1) {
                name = args[0];               // first word after the program name
            }
            if (args.length >= 2) {
                times = Integer.parseInt(args[1]);  // text "3" -> int 3
            }
    
            for (int i = 0; i < times; i++) {
                System.out.println("Hello, " + name + "!");
            }
        }
    }
    Output
    $ java Greet Alice 3
    Hello, Alice!
    Hello, Alice!
    Hello, Alice!
    
    $ java Greet
    Hello, World!
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    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.

    Worked Example — Scanner and BufferedReader
    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    import java.util.Scanner;
    
    public class Echo {
        public static void main(String[] args) throws Exception {
            // --- Scanner: easy, great for prompting interactively ---
            Scanner sc = new Scanner(System.in);
            System.out.print("Your name: ");   // print (no newline) so the prompt sits inline
            String name = sc.nextLine();        // reads one line the user types
            System.out.println("Hi, " + name + "!");
    
            // --- BufferedReader: faster, ideal for piping lots of lines in ---
            BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
            String line;
            int count = 0;
            // readLine() returns null at end-of-input (Ctrl-D / end of a pipe).
            while ((line = in.readLine()) != null) {
                count++;
                System.out.println(count + ": " + line);  // number each line, like 'cat -n'
            }
            System.out.println("Total lines: " + count);
        }
    }
    Output
    $ printf "apple\nbanana\ncherry\n" | java Echo
    Your name: apple
    Hi, apple!
    1: banana
    2: cherry
    Total lines: 2
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    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.

    Worked Example — Exit Codes, getenv, getProperty
    public class Validate {
        public static void main(String[] args) {
            // --- Environment variables: set OUTSIDE the program (the shell) ---
            // System.getenv returns null if the variable is not set.
            String home = System.getenv("HOME");
            System.out.println("HOME = " + home);
    
            // --- System properties: passed with -D or built into the JVM ---
            // java -Denv=prod Validate   ->  System.getProperty("env") = "prod"
            String env = System.getProperty("env", "dev");  // "dev" is the fallback
            System.out.println("env = " + env);
    
            // --- Exit codes: 0 = success, non-zero = failure ---
            if (args.length == 0) {
                System.err.println("Error: expected a filename");  // errors go to stderr
                System.exit(2);   // stops now; the shell sees code 2 (a usage error)
            }
    
            System.out.println("Processing " + args[0] + "...");
            System.exit(0);       // explicit success (0 is also the default)
        }
    }
    Output
    $ java Validate
    HOME = /home/alice
    env = dev
    Error: expected a filename
    $ echo $?
    2
    
    $ java -Denv=prod Validate report.csv
    HOME = /home/alice
    env = prod
    Processing report.csv...
    $ echo $?
    0
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    🎯 Your Turn #1 — Word Counter

    Finish the tool below. It takes one quoted sentence as an argument and prints how many words it contains. Fill in the two ___ blanks, then run it at onecompiler.com/java (set the command-line arguments in the input box).
    🎯 YOUR TURN — Fill in the blanks
    public class WordCount {
        public static void main(String[] args) {
            // 🎯 YOUR TURN — fill in the blanks marked with ___
    
            // 1) If no argument was given, print an error to stderr and exit with code 2.
            if (args.length == 0) {
                System.err.println("Usage: WordCount <sentence>");
                System.exit(___);          // 👉 use the conventional usage-error code
    
            }
    
            // 2) split() turns "a b c" into an array of words.
            String sentence = args[0];
            String[] words = sentence.split(" ");
    
            // 3) Print how many words there are.
            System.out.println("Words: " + ___);   // 👉 the length of the words array
    
            // ✅ Expected output:
            //   $ java WordCount "the quick brown fox"
            //   Words: 4
        }
    }
    Output
    $ java WordCount "the quick brown fox"
    Words: 4
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    🎯 Your Turn #2 — Sum Numbers from stdin

    This tool reads one number per line from stdin and prints the total — the kind of thing you would pipe data into. Fill in the two ___ blanks (the end-of-input sentinel, and the line you just read).
    🎯 YOUR TURN — Fill in the blanks
    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    
    public class SumLines {
        public static void main(String[] args) throws Exception {
            // 🎯 YOUR TURN — read numbers from stdin and add them up
    
            BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
            int total = 0;
            String line;
    
            // 1) Loop until readLine() returns null (end of input).
            while ((line = in.readLine()) != ___) {   // 👉 what does readLine() return at the end?
    
                // 2) Turn the line of text into an int and add it to total.
                total += Integer.parseInt(___);        // 👉 the current line you just read
            }
    
            System.out.println("Sum: " + total);
    
            // ✅ Expected output:
            //   $ printf "10\n20\n30\n" | java SumLines
            //   Sum: 60
        }
    }
    Output
    $ printf "10\n20\n30\n" | java SumLines
    Sum: 60
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    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.

    Worked Example — A greet command with picocli
    import picocli.CommandLine;
    import picocli.CommandLine.Command;
    import picocli.CommandLine.Option;
    import picocli.CommandLine.Parameters;
    import java.util.concurrent.Callable;
    
    @Command(name = "greet", version = "greet 1.0",
             description = "Greet someone N times.",
             mixinStandardHelpOptions = true)  // adds --help and --version for free
    public class Greet implements Callable<Integer> {
    
        @Parameters(index = "0", description = "Who to greet")
        String name;                       // a positional argument: greet Alice
    
        @Option(names = {"-c", "--count"}, defaultValue = "1",
                description = "How many times (default: 1)")
        int count;                         // picocli converts "3" -> int for you
    
        @Option(names = {"-u", "--upper"}, description = "Shout it")
        boolean upper;                     // a flag: present = true, absent = false
    
        @Override
        public Integer call() {            // runs after parsing succeeds
            String msg = "Hello, " + name + "!";
            if (upper) msg = msg.toUpperCase();
            for (int i = 0; i < count; i++) System.out.println(msg);
            return 0;                      // becomes the process exit code
        }
    
        public static void main(String[] args) {
            // execute() parses args, runs call(), and returns the exit code.
            int code = new CommandLine(new Greet()).execute(args);
            System.exit(code);
        }
    }
    picocli is a third-party library, so it needs picocli on the classpath (add it with Maven/Gradle). Then run java -cp picocli.jar:. Greet Alice --count 3 --upper in your terminal. Full guide at picocli.info.

    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.

    # Compile your sources to .class files
    javac Greet.java
    
    # Build a jar that knows its entry point (the class with main)
    jar --create --file greet.jar --main-class Greet Greet.class
    
    # Run it anywhere — no need to remember the class name
    java -jar greet.jar Alice --count 3

    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 validationInteger.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 codes0 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

    TaskCodeNotes
    Read an argumentargs[0]Check args.length first!
    Count argumentsargs.length0 when none were passed
    Text → numberInteger.parseInt(s)Throws on bad input
    Read a line of stdinin.readLine()null at end of input
    Interactive promptnew Scanner(System.in)nextLine(), nextInt()
    Exit with a codeSystem.exit(0)0 ok, non-zero error
    Print an errorSystem.err.println(s)Not System.out
    Environment variableSystem.getenv("HOME")null if unset
    System propertySystem.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 jarjar --create --file x.jar --main-class X X.classRun: java -jar x.jar

    Frequently Asked Questions

    Why does my CLI throw ArrayIndexOutOfBoundsException when I run it with no arguments?

    Because args[0] does not exist when args is empty. main always receives a String[] args, but it can have length 0. Always guard with if (args.length >= 1) (or check args.length == 0 up front) before touching args[0]. picocli removes this problem entirely: a missing @Parameters value produces a clean 'Missing required parameter' message instead of a stack trace.

    What is the difference between an environment variable and a system property?

    An environment variable is set by the shell or operating system before your program starts, and you read it with System.getenv("NAME"). A system property is a JVM-level setting you pass with -D on the command line, like java -Denv=prod App, and you read it with System.getProperty("env"). Use env vars for machine-wide config (PATH, HOME) and -D properties for per-run JVM tweaks.

    What exit code should my command-line tool return?

    By convention, 0 means success and any non-zero value means failure, so scripts can test it with $? or chain with &&. Common choices: 1 for a general error, 2 for a usage or bad-arguments error. Return the code from main by calling System.exit(code), or with picocli by returning an int from call(); execute() turns it into the exit code for you.

    When should I switch from parsing args[] by hand to using picocli?

    Parse args[] by hand only for trivial throwaway scripts. As soon as you need options with values, flags, default values, type conversion, --help text, or subcommands, reach for picocli. It replaces ~100 lines of brittle parsing with a few annotations, and gives you validation, colored usage messages, and tab completion for free.

    How do I package my CLI so people can run it as a single file?

    Compile, then build a runnable JAR whose manifest names the main class: jar --create --file greet.jar --main-class Greet *.class. Users run it with java -jar greet.jar Alice. For a self-contained binary with no JVM startup cost, compile the JAR with GraalVM's native-image, which produces a single executable that starts in milliseconds.

    Mini-Challenge — Calculator CLI

    Now write one from scratch. Build a tiny calculator that takes three arguments — a number, an operator, and a number — and prints the result, using exit codes for bad input. The starter below is an outline only: follow the numbered steps and check your output against the expected results.
    🎯 MINI-CHALLENGE — Write it yourself
    public class Calc {
        public static void main(String[] args) {
            // 🎯 MINI-CHALLENGE: a tiny calculator CLI
            // Run it like:  java Calc 6 + 4   ->  10
            //
            // 1. If args.length is not exactly 3, print a usage message to
            //    System.err and System.exit(2).
            // 2. Read args[0] and args[2] as ints (Integer.parseInt).
            // 3. Look at args[1] ("+", "-", "*", or "/") and compute the result.
            //    Use a switch; for an unknown operator, error out with exit code 2.
            // 4. Print the result, then System.exit(0).
            //
            // ✅ Expected:
            //   $ java Calc 6 + 4   ->  10
            //   $ java Calc 9 / 3   ->  3
            //   $ java Calc 6 ^ 4   ->  (error to stderr, exit code 2)
    
            // your code here
        }
    }
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    🎉 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.

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service