Utilitas HitungKata pada chapter sebelumnya cukup dengan satu argumen posisi, tetapi utilitas sungguhan jarang sesederhana itu. Program nyata mengenal flag tanpa nilai (-v), opsi dengan nilai (-o laporan.csv), bentuk pendek dan panjang (-v dan --verbose), argumen posisi (SUMBER...), penanda -- yang mengakhiri wilayah opsi, dan beragam bentuk kesalahan yang harus ditolak dengan pesan yang jelas. Array args pada main datang apa adanya — setiap elemen adalah string utuh tanpa tanda hubung yang ditafsirkan — sehingga seluruh pekerjaan penguraian ada di tangan kita. Chapter ini membangun parser manual yang tertib, lalu melihat kapan waktunya beralih ke library parsing.

Anatomi Argumen Command Line #

Empat jenis token mengisi array args, dan parser yang baik memperlakukan keduanya dengan aturan berbeda:

JenisContohPerlakuan
Flag-v, --verbosenilainya implisit: aktif
Opsi bernilai-o laporan.csv, --format jsonnilai ada di argumen berikutnya
Posisidata-a, sumber.txtmakna bergantung urutan kemunculan
Penanda akhir--semua argumen setelahnya dianggap posisi

Penanda -- sering dilupakan padahal menyelamatkan nama berkas yang diawali tanda hubung: java EksporData.java -- -v mengartikan -v sebagai nama sumber, bukan flag verbose.

Parser Manual yang Tertib #

Program berikut menguraikan seluruh jenis token itu dengan satu loop dan switch — bentuk yang mudah diperiksa baris demi baris. Setiap pelanggaran (opsi tak dikenal, nilai hilang, nilai tidak sah) berujung pada satu method gagal yang mencetak pemakaian dan keluar dengan kode 2, konsisten dengan konvensi galat pemakaian:

java EksporData.java
import java.util.ArrayList;
import java.util.List;

public class EksporData {
    public static void main(String[] args) {
        boolean verbose = false;
        String berkasKeluaran = null;
        String format = "csv";
        List<String> sumber = new ArrayList<>();

        boolean opsiSelesai = false;   // true setelah bertemu "--"
        int i = 0;
        while (i < args.length) {
            String a = args[i];
            if (opsiSelesai) {         // sisanya positional, apa pun bentuknya
                sumber.add(a);
                i++;
                continue;
            }
            switch (a) {
                case "-v", "--verbose" -> verbose = true;
                case "-o", "--output" -> {
                    if (i + 1 >= args.length) {
                        gagal("opsi " + a + " memerlukan nama berkas");
                    }
                    berkasKeluaran = args[++i];
                }
                case "--format" -> {
                    if (i + 1 >= args.length
                            || !List.of("csv", "json").contains(args[i + 1])) {
                        gagal("opsi --format memerlukan nilai csv atau json");
                    }
                    format = args[++i];
                }
                case "--" -> opsiSelesai = true;
                default -> {
                    if (a.startsWith("-")) {
                        gagal("opsi tidak dikenal: " + a);
                    }
                    sumber.add(a);
                }
            }
            i++;
        }

        System.out.println("verbose : " + verbose);
        System.out.println("keluaran: " + berkasKeluaran);
        System.out.println("format  : " + format);
        System.out.println("sumber  : " + sumber);
    }

    private static void gagal(String pesan) {
        System.err.println("Galat: " + pesan);
        System.err.println("Penggunaan: java EksporData.java "
                + "[-v] [-o BERKAS] [--format csv|json] SUMBER...");
        System.exit(2);
    }
}

Output yang Dihasilkan:

text
verbose : true
keluaran: laporan.csv
format  : csv
sumber  : [data-a, data-b]

Mari kita telaah struktur loop-nya:

  1. Baris if (opsiSelesai): begitu -- ditemukan, seluruh argumen sisanya masuk daftar sumber tanpa pemeriksaan — tanda hubung pun diterima sebagai nama.
  2. Baris case "-v", "--verbose": multi-label case menyatukan bentuk pendek dan panjang dalam satu cabang — satu aturan, dua ejaan.
  3. Baris berkasKeluaran = args[++i]: ++i melompati argumen nilai sekaligus mengonsumsinya, sehingga nilai tidak diproses ulang sebagai token opsi.
  4. Baris !List.of("csv", "json").contains(args[i + 1]): validasi nilai dilakukan saat parsing, bukan belakangan saat dipakai — galat melapor dekat sumbernya.
  5. Method gagal: satu pintu keluar untuk semua pelanggaran pemakaian; pesan galat dan baris pemakaian selalu tampil berpasangan, dan exit code 2 konsisten dengan konvensi GNU.

Variasinya bekerja seperti dugaan — opsi panjang dengan nilai, lalu -- yang mengubah makna -v:

text
$ java EksporData.java --format json sumber.txt
verbose : false
keluaran: null
format  : json
sumber  : [sumber.txt]

$ java EksporData.java -- -v
verbose : false
keluaran: null
format  : csv
sumber  : [-v]

$ java EksporData.java -x
Galat: opsi tidak dikenal: -x
Penggunaan: java EksporData.java [-v] [-o BERKAS] [--format csv|json] SUMBER...

Kapan Beralih ke Library Parsing #

Parser manual menjadi mahal ketika kebutuhan tumbuh: opsi bersarang, nilai default yang terdokumentasi, pembangkitan --help otomatis, dan penyelesaian nama berkas. Dua library lazim mengisi celah itu — picocli dengan anotasi deklaratif, dan Apache Commons CLI dengan builder berantai. Keduanya berada di luar JDK, sehingga contoh berikut tampil sebagai gambaran bentuk, bukan blok yang dieksekusi di buku ini:

java Bentuk picocli — deklaratif dan ringkas
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.util.List;

@Command(name = "ekspor", mixinStandardHelpOptions = true,
        description = "Mengekspor data ke format csv atau json.")
public class Ekspor implements Runnable {

    @Option(names = {"-v", "--verbose"}, description = "tampilkan progres detail")
    boolean verbose;

    @Option(names = {"-o", "--output"}, paramLabel = "BERKAS",
            description = "berkas keluaran")
    String berkas;

    @Option(names = "--format", defaultValue = "csv",
            description = "csv atau json")
    String format;

    @Parameters(paramLabel = "SUMBER", description = "sumber data")
    List<String> sumber;

    @Override
    public void run() { /* ... */ }

    public static void main(String[] args) {
        System.exit(new CommandLine(new Ekspor()).execute(args));
    }
}
xml Dependensi Maven untuk picocli
<dependency>
    <groupId>info.picocli</groupId>
    <artifactId>picocli</artifactId>
    <version>4.7.7</version>
</dependency>
PertimbanganParser manualLibrary (picocli / Commons CLI)
Ketergantungantidak adasatu artefak eksternal
--help otomatisditulis tangandibangkitkan dari deklarasi
Opsi kompleksdikode sendirisiap pakai
Cocok untukutilitas < ±10 opsiaplikasi dengan banyak opsi

Dengan masukan terurai, komponen berikutnya dari otomasi adalah memproses data dalam volume besar secara bertahap — tanpa mengulang dari awal ketika proses terputus di tengah jalan.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

EksporData.java

Unduh berkas
java
import java.util.ArrayList;
import java.util.List;

public class EksporData {
    public static void main(String[] args) {
        boolean verbose = false;
        String berkasKeluaran = null;
        String format = "csv";
        List<String> sumber = new ArrayList<>();

        boolean opsiSelesai = false;   // true setelah bertemu "--"
        int i = 0;
        while (i < args.length) {
            String a = args[i];
            if (opsiSelesai) {         // sisanya positional, apa pun bentuknya
                sumber.add(a);
                i++;
                continue;
            }
            switch (a) {
                case "-v", "--verbose" -> verbose = true;
                case "-o", "--output" -> {
                    if (i + 1 >= args.length) {
                        gagal("opsi " + a + " memerlukan nama berkas");
                    }
                    berkasKeluaran = args[++i];
                }
                case "--format" -> {
                    if (i + 1 >= args.length
                            || !List.of("csv", "json").contains(args[i + 1])) {
                        gagal("opsi --format memerlukan nilai csv atau json");
                    }
                    format = args[++i];
                }
                case "--" -> opsiSelesai = true;
                default -> {
                    if (a.startsWith("-")) {
                        gagal("opsi tidak dikenal: " + a);
                    }
                    sumber.add(a);
                }
            }
            i++;
        }

        System.out.println("verbose : " + verbose);
        System.out.println("keluaran: " + berkasKeluaran);
        System.out.println("format  : " + format);
        System.out.println("sumber  : " + sumber);
    }

    private static void gagal(String pesan) {
        System.err.println("Galat: " + pesan);
        System.err.println("Penggunaan: java EksporData.java "
                + "[-v] [-o BERKAS] [--format csv|json] SUMBER...");
        System.exit(2);
    }
}