Command Line Tools

Reading input

void main() {
    var name = IO.readln("Your name? ");
    IO.println("Hello, " + name + "!");
}

IO.readln returns null at end of input (Ctrl+D, or redirected files) — check for it.

The classic way:

var scanner = new Scanner(System.in);
var n = scanner.nextInt();
scanner.nextLine();          // consume the rest of the line!
var text = scanner.nextLine();
Warning

nextInt() leaves the newline behind, so the next nextLine() returns empty. Safer: read whole lines and parse them yourself.

int readInt(String prompt, int min, int max) {
    while (true) {
        var input = IO.readln(prompt);
        if (input == null) throw new IllegalStateException("input closed");
        try {
            var n = Integer.parseInt(input.strip());
            if (n < min || n > max) { IO.println("out of range"); continue; }
            return n;
        } catch (NumberFormatException e) {
            IO.println("not a number");
        }
    }
}

Parsing arguments

void main(String[] args) {
    String file = null;
    var verbose = false;
    var limit = 10;

    for (var i = 0; i < args.length; i++) {
        switch (args[i]) {
            case "-v", "--verbose" -> verbose = true;
            case "-n", "--limit"   -> limit = Integer.parseInt(args[++i]);
            case "-h", "--help"    -> { help(); return; }
            default -> {
                if (args[i].startsWith("-")) { IO.println("unknown option"); return; }
                file = args[i];
            }
        }
    }
    if (file == null) { help(); return; }
    run(file, limit, verbose);
}

void help() {
    IO.println("""
        usage: java Tool.java [options] <file>

          -n, --limit N   number of results (default 10)
          -v, --verbose   verbose output
          -h, --help      this help
        """);
}

For anything bigger, use picocli:

@Command(name = "count", mixinStandardHelpOptions = true, version = "1.0")
public class Count implements Runnable {

    @Parameters(index = "0", description = "file to read")
    private Path file;

    @Option(names = {"-n", "--limit"}) private int limit = 10;

    @Override public void run() {  }

    public static void main(String[] args) {
        System.exit(new CommandLine(new Count()).execute(args));
    }
}

Formatting output

IO.println("%-20s %8s %6s".formatted("Item", "Price", "Qty"));
IO.println("-".repeat(36));
items.forEach(i -> IO.println("%-20s %8.2f %6d".formatted(i.name(), i.price(), i.qty())));
void progress(int done, int total) {
    var width = 30;
    var filled = done * width / total;
    System.out.print("\r[%s%s] %3d%%".formatted(
        "#".repeat(filled), " ".repeat(width - filled), done * 100 / total));
}
final String RED   = "\u001B[31m";
final String GREEN = "\u001B[32m";
final String RESET = "\u001B[0m";

IO.println(GREEN + "OK" + RESET);

Streams and exit codes

System.out.println("normal output");   // stdout, can be piped
System.err.println("error message");   // stderr, stays visible

System.exit(0);   // success
System.exit(1);   // failure  scripts and CI depend on this
java Tool.java data.txt | sort | head -5
java Tool.java data.txt 2> errors.log

Environment and properties

System.getenv("HOME")
System.getProperty("user.name")
System.getProperty("os.name")
System.getProperty("mode", "production")   // java -Dmode=test 

A complete tool

import java.nio.file.*;

void main(String[] args) {
    if (args.length == 0) {
        System.err.println("usage: java Search.java <pattern> [file…]");
        System.exit(2);
    }

    var pattern = args[0].toLowerCase();
    var hits = 0;

    for (var i = 1; i < args.length; i++) {
        var path = Path.of(args[i]);
        try (var lines = Files.lines(path)) {
            var n = new int[]{0};
            hits += (int) lines.peek(l -> n[0]++)
                .filter(l -> l.toLowerCase().contains(pattern))
                .peek(l -> IO.println("%s:%d: %s".formatted(path, n[0], l.strip())))
                .count();
        } catch (java.io.IOException e) {
            System.err.println("cannot read " + path);
        }
    }

    IO.println("%d hits".formatted(hits));
    System.exit(hits > 0 ? 0 : 1);
}

Making it executable

On macOS and Linux, with no .java extension:

#!/usr/bin/env java --source 25

void main() {
    IO.println("running as a script!");
}
chmod +x tool
./tool

★ Exercises

  1. An interactive calculator: the user types 3 + 4, you answer, until they type exit.
  2. A simple wc: count lines, words and characters of a file.
  3. Add -l, -w, -c options to it.
  4. A quiz with five questions and a percentage score at the end.
  5. Print a price list as an aligned table with a totals row.
  6. Return exit code 2 for bad arguments and 1 for “nothing found”. Check with echo $?.