Perintah git commit, git push, dan git log sebenarnya satu program saja: git membaca argument posisi pertama, memutuskan subcommand yang dimaksud, lalu menjalankan logika subcommand tersebut. Pola ini membuat satu binary melayani banyak fungsi tanpa nama program yang bertaburan di PATH. Pada chapter ini kita akan membangun pola git <perintah> secara manual memakai standard library: switch untuk dispatch, flag.NewFlagSet untuk kumpulan flag tiap subcommand, serta bantuan dan exit code yang rapi untuk perintah tak dikenal. Versi otomatis dari pola yang sama dengan framework sudah kita lihat pada chapter Cobra dan CLI Framework .

Pola Dispatch Manual #

Program demo todo-demo.go menyediakan tiga subcommand: add menambah tugas, list menampilkan seluruh tugas, dan done menandai satu tugas selesai. Setiap subcommand memanggil FlagSet sendiri sehingga flag --prioritas hanya ada pada add dan flag --selesai hanya ada pada list. Berikut isi lengkap berkas tersebut:

go
package main

import (
	"flag"
	"fmt"
	"os"
	"strconv"
)

type tugas struct {
	nama      string
	prioritas string
	selesai   bool
}

// daftar diisi contoh bawaan agar tiap perintah bisa diuji sendiri-sendiri.
var daftar = []tugas{
	{nama: "belajar Go", prioritas: "tinggi"},
	{nama: "merapikan kode", prioritas: "rendah"},
}

func main() {
	if len(os.Args) < 2 {
		fmt.Fprintln(os.Stderr, "pemakaian: todo <perintah> [argument]")
		fmt.Fprintln(os.Stderr, "perintah tersedia: add, list, done")
		os.Exit(2)
	}

	cmd, sisa := os.Args[1], os.Args[2:]
	switch cmd {
	case "add":
		addCmd(sisa)
	case "list":
		listCmd(sisa)
	case "done":
		doneCmd(sisa)
	default:
		fmt.Fprintf(os.Stderr, "perintah tidak dikenal: %q\n", cmd)
		fmt.Fprintln(os.Stderr, "perintah tersedia: add, list, done")
		os.Exit(2)
	}
}

func addCmd(args []string) {
	// Setiap subcommand memakai FlagSet sendiri.
	fs := flag.NewFlagSet("add", flag.ExitOnError)
	prioritas := fs.String("prioritas", "rendah", "tingkat prioritas tugas")
	fs.Parse(args)

	if fs.NArg() == 0 {
		fmt.Fprintln(os.Stderr, "pemakaian: todo add [flag] <nama tugas>")
		os.Exit(2)
	}
	daftar = append(daftar, tugas{nama: fs.Arg(0), prioritas: *prioritas})
	fmt.Printf("ditambahkan: %s (prioritas: %s)\n", fs.Arg(0), *prioritas)
	cetakDaftar(false)
}

func listCmd(args []string) {
	fs := flag.NewFlagSet("list", flag.ExitOnError)
	hanyaSelesai := fs.Bool("selesai", false, "hanya tampilkan tugas selesai")
	fs.Parse(args)

	cetakDaftar(*hanyaSelesai)
}

func doneCmd(args []string) {
	fs := flag.NewFlagSet("done", flag.ExitOnError)
	fs.Parse(args)

	if fs.NArg() != 1 {
		fmt.Fprintln(os.Stderr, "pemakaian: todo done <nomor tugas>")
		os.Exit(2)
	}
	nomor, err := strconv.Atoi(fs.Arg(0))
	if err != nil || nomor < 1 || nomor > len(daftar) {
		fmt.Fprintf(os.Stderr, "nomor tugas tidak valid: %q\n", fs.Arg(0))
		os.Exit(1)
	}
	daftar[nomor-1].selesai = true
	fmt.Printf("tugas #%d selesai: %s\n", nomor, daftar[nomor-1].nama)
	cetakDaftar(false)
}

func cetakDaftar(selesaiSaja bool) {
	for i, t := range daftar {
		// Lewati tugas yang belum selesai bila filter aktif.
		if selesaiSaja && !t.selesai {
			continue
		}
		status := "[ ]"
		if t.selesai {
			status = "[x]"
		}
		fmt.Printf("%d. %s %s (prioritas: %s)\n", i+1, status, t.nama, t.prioritas)
	}
}

Menjalankan Setiap Subcommand #

Subcommand add menerima flag --prioritas diikuti nama tugas sebagai argument posisi:

bash
go run todo-demo.go add --prioritas sedang "menulis dokumentasi"

Output yang Dihasilkan:

text
ditambahkan: menulis dokumentasi (prioritas: sedang)
1. [ ] belajar Go (prioritas: tinggi)
2. [ ] merapikan kode (prioritas: rendah)
3. [ ] menulis dokumentasi (prioritas: sedang)

Subcommand list menampilkan seluruh tugas, sedangkan done menandai tugas nomor tertentu lalu mencetak keadaan terbaru:

bash
go run todo-demo.go list
go run todo-demo.go done 1

Output yang Dihasilkan:

text
1. [ ] belajar Go (prioritas: tinggi)
2. [ ] merapikan kode (prioritas: rendah)
tugas #1 selesai: belajar Go
1. [x] belajar Go (prioritas: tinggi)
2. [ ] merapikan kode (prioritas: rendah)

Satu hal yang perlu dicatat dari demo ini: daftar tugas hidup di memori dan hilang begitu program selesai, sehingga setiap pemanggilan mulai dari daftar contoh yang sama. Perilaku ini sengaja dipilih agar setiap subcommand dapat diuji sendiri dengan output yang pasti; CLI sungguhan akan menyimpan daftar ke berkas atau database setelah setiap perintah, dan bab File dan I/O membahas tekniknya.

Mari kita telaah bagian-bagian penting program di atas:

  1. Baris 23-27 (len(os.Args) < 2): bila tidak ada argument sama sekali, program mencetak pemakaian ke stderr lalu keluar dengan exit code 2, konvensi untuk kesalahan penggunaan.
  2. Baris 29-41 (switch cmd): argument pertama diambil sebagai nama subcommand dan sisanya (os.Args[2:]) diteruskan ke fungsi handler; nama yang tidak cocok jatuh ke default dengan pesan galat.
  3. Baris 44-48 (flag.NewFlagSet("add", ...)): subcommand add memakai FlagSet bernama add, sehingga pesan bantuannya berjudul Usage of add: dan tidak tercampur flag milik subcommand lain.
  4. Baris 50-53 (fs.NArg()): setelah fs.Parse, argument posisi subcommand diambil lewat fs.Arg(0) dan fs.NArg(); validasi jumlah ini menggantikan pemeriksaan manual os.Args.
  5. Baris 71-79 (doneCmd): nomor tugas diubah ke integer dengan strconv.Atoi; nilai di luar jangkauan memicu exit code 1, membedakan galat data dari galat pemakaian.

Bantuan per Subcommand dan Perintah Tak Dikenal #

Karena setiap subcommand punya FlagSet sendiri, flag -h otomatis menjelaskan flag subcommand tersebut saja:

bash
go run todo-demo.go add -h

Output yang Dihasilkan:

text
Usage of add:
  -prioritas string
    	tingkat prioritas tugas (default "rendah")

Pemisahan FlagSet juga menjaga makna flag tetap lokal. Flag bernama sama pada dua subcommand boleh berbeda tipe dan arti — misalnya -p berarti --prioritas pada add dan berarti --proyek pada perintah lain — tanpa saling menimpa, karena parser add dan parser list memang objek yang berbeda. Konsekuensinya, pesan galat parsing selalu menyebut nama FlagSet yang tepat, sehingga pengguna langsung tahu perintah mana yang salah tulis flag.

Sedangkan pemanggilan tanpa subcommand atau dengan nama yang salah langsung ditolak:

bash
go run todo-demo.go hapus

Output yang Dihasilkan:

text
perintah tidak dikenal: "hapus"
perintah tersedia: add, list, done
exit status 2

Baris exit status 2 adalah laporan go run karena program keluar dengan kode 2. Bila program dibangun menjadi binary, kode 2 tersebut langsung menjadi exit code binary.

Setelah pola perintah tersusun, CLI yang berguna biasanya membaca pengaturan tetap dari berkas konfigurasi, bukan memaksa pengguna mengetik puluhan flag setiap kali. Chapter berikutnya membahas urutan prioritas flag, variabel lingkungan, berkas, dan nilai default.

Referensi #

Berkas Sumber

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

todo-demo.go

Unduh berkas
go
package main

import (
	"flag"
	"fmt"
	"os"
	"strconv"
)

type tugas struct {
	nama      string
	prioritas string
	selesai   bool
}

// daftar diisi contoh bawaan agar tiap perintah bisa diuji sendiri-sendiri.
var daftar = []tugas{
	{nama: "belajar Go", prioritas: "tinggi"},
	{nama: "merapikan kode", prioritas: "rendah"},
}

func main() {
	if len(os.Args) < 2 {
		fmt.Fprintln(os.Stderr, "pemakaian: todo <perintah> [argument]")
		fmt.Fprintln(os.Stderr, "perintah tersedia: add, list, done")
		os.Exit(2)
	}

	cmd, sisa := os.Args[1], os.Args[2:]
	switch cmd {
	case "add":
		addCmd(sisa)
	case "list":
		listCmd(sisa)
	case "done":
		doneCmd(sisa)
	default:
		fmt.Fprintf(os.Stderr, "perintah tidak dikenal: %q\n", cmd)
		fmt.Fprintln(os.Stderr, "perintah tersedia: add, list, done")
		os.Exit(2)
	}
}

func addCmd(args []string) {
	// Setiap subcommand memakai FlagSet sendiri.
	fs := flag.NewFlagSet("add", flag.ExitOnError)
	prioritas := fs.String("prioritas", "rendah", "tingkat prioritas tugas")
	fs.Parse(args)

	if fs.NArg() == 0 {
		fmt.Fprintln(os.Stderr, "pemakaian: todo add [flag] <nama tugas>")
		os.Exit(2)
	}
	daftar = append(daftar, tugas{nama: fs.Arg(0), prioritas: *prioritas})
	fmt.Printf("ditambahkan: %s (prioritas: %s)\n", fs.Arg(0), *prioritas)
	cetakDaftar(false)
}

func listCmd(args []string) {
	fs := flag.NewFlagSet("list", flag.ExitOnError)
	hanyaSelesai := fs.Bool("selesai", false, "hanya tampilkan tugas selesai")
	fs.Parse(args)

	cetakDaftar(*hanyaSelesai)
}

func doneCmd(args []string) {
	fs := flag.NewFlagSet("done", flag.ExitOnError)
	fs.Parse(args)

	if fs.NArg() != 1 {
		fmt.Fprintln(os.Stderr, "pemakaian: todo done <nomor tugas>")
		os.Exit(2)
	}
	nomor, err := strconv.Atoi(fs.Arg(0))
	if err != nil || nomor < 1 || nomor > len(daftar) {
		fmt.Fprintf(os.Stderr, "nomor tugas tidak valid: %q\n", fs.Arg(0))
		os.Exit(1)
	}
	daftar[nomor-1].selesai = true
	fmt.Printf("tugas #%d selesai: %s\n", nomor, daftar[nomor-1].nama)
	cetakDaftar(false)
}

func cetakDaftar(selesaiSaja bool) {
	for i, t := range daftar {
		// Lewati tugas yang belum selesai bila filter aktif.
		if selesaiSaja && !t.selesai {
			continue
		}
		status := "[ ]"
		if t.selesai {
			status = "[x]"
		}
		fmt.Printf("%d. %s %s (prioritas: %s)\n", i+1, status, t.nama, t.prioritas)
	}
}