Materi pada Section CLI dan Automation sudah memperkenalkan cara membangun parser dengan argparse, tetapi contoh pada chapter tersebut masih berupa skrip kecil satu fungsi. Proyek ini memakai pola yang sama pada skala yang lebih serius: satu program dengan banyak subperintah, masing-masing punya argumen sendiri. Pada bagian ini kita membangun kerangka filetool beserta tiga perintah baca: daftar, info, dan cari.

Menyiapkan Pohon Latihan #

Agar setiap transkrip pada proyek ini bisa dibandingkan, kita memakai satu pohon latihan yang sama. Jalankan perintah berikut dari sebuah folder kerja kosong:

bash
mkdir -p files/dokumen files/foto files/.git files/__pycache__
echo "Laporan penjualan bulan Juni." > files/laporan.txt
echo "beli kopi" > files/catatan.txt
echo "print('tes')" > files/skrip-tes.py
echo "arsip" > files/arsip-lama.zip
echo '{"versi": 1}' > files/konfig-lama.json
echo "draf" > files/draf-sementara.tmp
echo "pesanan lama" > files/catatan~
echo "isi kontrak" > files/dokumen/kontrak.txt
echo "isi proposal" > files/dokumen/proposal.txt
echo "foto pantai" > files/foto/pantai.jpg
echo "foto gunung" > files/foto/gunung.png
echo "foto liburan" > files/foto-liburan.jpg
echo "[core]" > files/.git/config
echo "ref: refs/heads/main" > files/.git/HEAD
echo "bytecode" > files/__pycache__/modul.pyc

Struktur pohon yang dihasilkan:

text
files/
├── dokumen/
│   ├── kontrak.txt
│   └── proposal.txt
├── foto/
│   ├── gunung.png
│   └── pantai.jpg
├── arsip-lama.zip
├── catatan.txt
├── catatan~
├── draf-sementara.tmp
├── foto-liburan.jpg
├── konfig-lama.json
├── laporan.txt
└── skrip-tes.py

Berkas catatan~ dan draf-sementara.tmp sengaja dibuat sebagai “sampah” yang akan dimanfaatkan pada bagian konfigurasi nanti.

Struktur Program #

filetool mengikuti tiga aturan struktur yang dipakai konsisten sampai bagian terakhir:

  1. Fungsi buat_parser() membangun seluruh definisi argumen dan mengembalikan object ArgumentParser.
  2. Setiap subperintah dihubungkan ke fungsinya lewat set_defaults(func=...).
  3. Fungsi main(argv=None) mem-parsing argumen lalu mendelegasikan pekerjaan; blok if __name__ == "__main__": hanya meneruskan exit code ke sys.exit().

Berikut kerangka beserta tiga perintah pertama — versi lengkap berkas main.py tersedia pada bagian Berkas Sumber di akhir halaman:

python
"""filetool: CLI pengelola file sederhana."""

import argparse
import sys
from datetime import datetime
from pathlib import Path

VERSI = "1.0.0"


def buat_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="filetool",
        description="Pengelola file dari terminal.",
    )
    parser.add_argument("--versi", action="version", version=f"filetool {VERSI}")
    sub = parser.add_subparsers(dest="perintah", required=True, metavar="PERINTAH")

    p_daftar = sub.add_parser("daftar", help="tampilkan isi sebuah direktori")
    p_daftar.add_argument("path", help="direktori yang ingin ditampilkan")
    p_daftar.add_argument("--panjang", action="store_true",
                          help="tampilkan ukuran, waktu ubah, dan nama")
    p_daftar.set_defaults(func=perintah_daftar)

    p_info = sub.add_parser("info", help="tampilkan metadata sebuah berkas")
    p_info.add_argument("path", help="berkas yang ingin diperiksa")
    p_info.set_defaults(func=perintah_info)

    p_cari = sub.add_parser("cari", help="cari berkas berdasarkan pola glob")
    p_cari.add_argument("path", help="direktori awal pencarian")
    p_cari.add_argument("--pola", default="*", help='pola nama berkas, misalnya "*.txt"')
    p_cari.set_defaults(func=perintah_cari)

    return parser


def main(argv: list[str] | None = None) -> int:
    args = buat_parser().parse_args(argv)
    return args.func(args)


if __name__ == "__main__":
    sys.exit(main())

Mari kita bedah baris-baris pentingnya:

  1. Baris 12 (parser = argparse.ArgumentParser(): parameter prog="filetool" menetapkan nama program pada pesan usage. Tanpa parameter ini, argparse memakai nama berkas (main.py) sebagai nama program.
  2. Baris 16 (parser.add_argument("--versi", ...): opsi action="version" membuat argparse mencetak teks version lalu keluar dengan exit code 0 begitu opsi ini dipakai.
  3. Baris 17 (sub = parser.add_subparsers(...)): satu parser induk memperoleh kumpulan subparser. Parameter required=True membuat subperintah wajib diisi, dan metavar="PERINTAH" mengatur nama placeholder pada pesan usage.
  4. Baris 19 (p_daftar = sub.add_parser("daftar", ...)): setiap pemanggilan add_parser() mengembalikan parser baru yang berdiri sendiri, lengkap dengan --help miliknya sendiri.
  5. Baris 23 (p_daftar.set_defaults(func=perintah_daftar)): attribute func ditanam pada Namespace hasil parsing. Setelah parse_args() berhasil, args.func langsung menunjuk fungsi Python yang harus dijalankan.
  6. Baris 37 (def main(argv=None)): parameter argv=None membuat parse_args() membaca sys.argv ketika dipanggil dari terminal, tetapi memungkinkan kita menyuntikkan list argumen sendiri saat menguji dari kode — pola ini dipakai penuh pada Bagian 4.
  7. Baris 38-39 (return args.func(args)): pemetaan subperintah ke fungsi terjadi tanpa if/match sama sekali; argparse yang menyimpan pasangannya lewat set_defaults().
  8. Baris 43 (sys.exit(main())): nilai balik main() berupa exit code diteruskan ke sistem operasi, sehingga skrip lain bisa memeriksa $?.

Tiga Perintah Baca #

Tiga perintah pertama sama-sama memakai class Path dari module pathlib: argumen path dari Namespace diubah menjadi object Path lebih dulu, lalu semua operasi berikutnya memanfaatkan method dan attribute milik Path. Dua fungsi pembantu kecil menjaga keluaran tetap rapi:

python
def format_size(ukuran: int) -> str:
    """Ubah ukuran dalam byte menjadi teks yang mudah dibaca manusia."""
    if ukuran < 1024:
        return f"{ukuran} B"
    for satuan in ("KB", "MB", "GB", "TB"):
        ukuran /= 1024
        if ukuran < 1024:
            return f"{ukuran:.1f} {satuan}".replace(".", ",")
    return f"{ukuran:.1f} PB".replace(".", ",")


def format_waktu(epoch: float) -> str:
    """Ubah stempel waktu epoch menjadi teks tanggal dan jam."""
    return datetime.fromtimestamp(epoch).strftime("%Y-%m-%d %H:%M")
  1. Baris 3-4 (if ukuran < 1024:): ukuran di bawah 1 KB ditampilkan apa adanya dalam satuan byte tanpa desimal.
  2. Baris 5-8 (for satuan in ("KB", ...)): ukuran dibagi 1024 berulang kali sampai masuk rentang satuan yang pas; .replace(".", ",") mengubah titik desimal menjadi koma sesuai konvensi penulisan Indonesia.
  3. Baris 14 (datetime.fromtimestamp(epoch)): method stat().st_mtime mengembalikan jumlah detik sejak epoch, sehingga kita perlu mengubahnya kembali menjadi object datetime sebelum diformat.

Perintah daftar membaca isi satu tingkat direktori dan menghormati opsi --panjang:

python
def perintah_daftar(args: argparse.Namespace) -> int:
    direktori = Path(args.path)
    if not direktori.is_dir():
        print(f"filetool: bukan direktori: {direktori}", file=sys.stderr)
        return 1
    for entri in sorted(direktori.iterdir()):
        penanda = "d" if entri.is_dir() else "-"
        if args.panjang:
            stat = entri.stat()
            ukuran = "-" if entri.is_dir() else format_size(stat.st_size)
            print(f"{penanda} {ukuran:>8}  {format_waktu(stat.st_mtime)}  {entri.name}")
        else:
            akhiran = "/" if entri.is_dir() else ""
            print(f"{entri.name}{akhiran}")
    return 0
  1. Baris 2 (direktori = Path(args.path)): string dari Namespace dikonversi menjadi object Path sekali di awal; seluruh baris berikutnya bekerja dengan object tersebut.
  2. Baris 3-5 (if not direktori.is_dir():): method is_dir() mengembalikan False baik untuk berkas biasa maupun jalur yang tidak ada; pesan galat ditulis ke stderr dan exit code 1 dikembalikan.
  3. Baris 6 (sorted(direktori.iterdir())): method iterdir() menghasilkan generator Path untuk setiap isi direktori; sorted() menjaga urutan keluaran tetap sama di mesin mana pun.
  4. Baris 11 (ukuran:>8): ukuran rata kanan selebar delapan karakter agar kolom tetap sejajar ketika ukuran berbeda-beda.

Jalankan dua variasi pemakaian:

bash
python3 main.py daftar files
python3 main.py daftar files --panjang

Output yang Dihasilkan:

text
.git/
__pycache__/
arsip-lama.zip
catatan.txt
catatan~
dokumen/
draf-sementara.tmp
foto/
foto-liburan.jpg
konfig-lama.json
laporan.txt
skrip-tes.py
text
d        -  2025-03-01 08:00  .git
d        -  2025-07-13 19:44  __pycache__
-  50,0 KB  2025-05-02 14:00  arsip-lama.zip
-     15 B  2025-06-15 11:05  catatan.txt
-     10 B  2025-07-12 13:26  catatan~
d        -  2025-07-20 09:15  dokumen
-     45 B  2025-07-12 13:25  draf-sementara.tmp
d        -  2025-07-20 09:15  foto
- 200,0 KB  2025-06-20 16:45  foto-liburan.jpg
-     96 B  2025-07-10 08:40  konfig-lama.json
-   1,2 KB  2025-06-15 10:30  laporan.txt
-     87 B  2025-07-01 09:20  skrip-tes.py

Perintah info menampilkan metadata satu berkas dalam bentuk pasangan label-nilai:

python
def perintah_info(args: argparse.Namespace) -> int:
    berkas = Path(args.path)
    if not berkas.is_file():
        print(f"filetool: bukan berkas: {berkas}", file=sys.stderr)
        return 1
    stat = berkas.stat()
    for label, nilai in [
        ("jalur", str(berkas)),
        ("jenis", "berkas"),
        ("ekstensi", berkas.suffix or "(tidak ada)"),
        ("ukuran", f"{stat.st_size} byte ({format_size(stat.st_size)})"),
        ("diubah", format_waktu(stat.st_mtime)),
    ]:
        print(f"{label:<9}: {nilai}")
    return 0
  1. Baris 6 (stat = berkas.stat()): satu panggilan stat() mengembalikan object os.stat_result berisi ukuran dan stempel waktu; atribut st_size dan st_mtime dibaca dari object tersebut.
  2. Baris 10 (berkas.suffix or "(tidak ada)"): attribute suffix mengembalikan string kosong untuk berkas tanpa ekstensi, sehingga operator or menggantinya dengan keterangan yang lebih jelas.
  3. Baris 14 ({label:<9}): label rata kiri selebar sembilan karakter menjaga titik dua tetap sejajar antar baris.
bash
python3 main.py info files/laporan.txt

Output yang Dihasilkan:

text
jalur    : files/laporan.txt
jenis    : berkas
ekstensi : .txt
ukuran   : 1200 byte (1,2 KB)
diubah   : 2025-06-15 10:30

Perintah cari memakai method rglob() untuk menelusuri pohon secara rekursif:

python
def perintah_cari(args: argparse.Namespace) -> int:
    akar = Path(args.path)
    if not akar.is_dir():
        print(f"filetool: bukan direktori: {akar}", file=sys.stderr)
        return 1
    hasil = sorted(p for p in akar.rglob(args.pola) if p.is_file())
    if not hasil:
        print("tidak ada yang cocok.")
        return 0
    for jalur in hasil:
        print(jalur.relative_to(akar))
    return 0
  1. Baris 6 (akar.rglob(args.pola)): method rglob() menerima pola glob lalu menelusuri semua tingkat di bawah akar; ekspresi generator menyaring hasil agar hanya berkas (is_file()) yang ditampilkan.
  2. Baris 11 (jalur.relative_to(akar)): method relative_to() mengupas bagian akar dari jalur lengkap sehingga keluaran berupa jalur relatif yang pendek dan portabel.
bash
python3 main.py cari files --pola "*.txt"

Output yang Dihasilkan:

text
catatan.txt
dokumen/kontrak.txt
dokumen/proposal.txt
laporan.txt

Bantuan dan Galat Penggunaan #

Seluruh teks bantuan dibangkitkan argparse dari definisi argumen; kita tidak menulis satu baris pun secara manual. Jalankan python3 main.py --help:

text
usage: filetool [-h] [--versi] PERINTAH ...

Pengelola file dari terminal.

positional arguments:
  PERINTAH
    daftar    tampilkan isi sebuah direktori
    info      tampilkan metadata sebuah berkas
    cari      cari berkas berdasarkan pola glob

options:
  -h, --help  show this help message and exit
  --versi     show program's version number and exit

Setiap subperintah juga punya bantuan sendiri, misalnya python3 main.py daftar --help:

text
usage: filetool daftar [-h] [--panjang] path

positional arguments:
  path        direktori yang ingin ditampilkan

options:
  -h, --help  show this help message and exit
  --panjang   tampilkan ukuran, waktu ubah, dan nama

Ketika pengguna salah memakai program, argparse menolak sebelum kode kita sempat berjalan. Pemanggilan python3 main.py info tidak-ada.txt pada berkas yang memang tidak ada ditangani pemeriksaan kita sendiri:

text
filetool: bukan berkas: tidak-ada.txt

Pesan tersebut ditulis ke stderr dan program berakhir dengan exit code 1 — sesuai kontrak pada halaman overview. Galat penggunaan seperti subperintah yang salah tulis ditangani argparse dengan exit code 2, contohnya:

text
usage: filetool [-h] [--versi] PERINTAH ...
filetool: error: argument PERINTAH: invalid choice: 'dftar' (choose from daftar, info, cari)

Kerangka filetool berdiri: parser dengan tiga subperintah, exit code yang disiplin, dan bantuan otomatis. Pada Bagian 2 kita membuat program ini benar-benar mengubah isi disk melalui operasi salin, pindah, dan hapus — dengan pengaman yang menyeluruh.

Berkas Sumber

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

main.py

Unduh berkas
python
"""filetool: CLI pengelola file sederhana."""

import argparse
import sys
from datetime import datetime
from pathlib import Path

VERSI = "1.0.0"


def format_size(ukuran: int) -> str:
    """Ubah ukuran dalam byte menjadi teks yang mudah dibaca manusia."""
    if ukuran < 1024:
        return f"{ukuran} B"
    for satuan in ("KB", "MB", "GB", "TB"):
        ukuran /= 1024
        if ukuran < 1024:
            return f"{ukuran:.1f} {satuan}".replace(".", ",")
    return f"{ukuran:.1f} PB".replace(".", ",")


def format_waktu(epoch: float) -> str:
    """Ubah stempel waktu epoch menjadi teks tanggal dan jam."""
    return datetime.fromtimestamp(epoch).strftime("%Y-%m-%d %H:%M")


def perintah_daftar(args: argparse.Namespace) -> int:
    direktori = Path(args.path)
    if not direktori.is_dir():
        print(f"filetool: bukan direktori: {direktori}", file=sys.stderr)
        return 1
    for entri in sorted(direktori.iterdir()):
        penanda = "d" if entri.is_dir() else "-"
        if args.panjang:
            stat = entri.stat()
            ukuran = "-" if entri.is_dir() else format_size(stat.st_size)
            print(f"{penanda} {ukuran:>8}  {format_waktu(stat.st_mtime)}  {entri.name}")
        else:
            akhiran = "/" if entri.is_dir() else ""
            print(f"{entri.name}{akhiran}")
    return 0


def perintah_info(args: argparse.Namespace) -> int:
    berkas = Path(args.path)
    if not berkas.is_file():
        print(f"filetool: bukan berkas: {berkas}", file=sys.stderr)
        return 1
    stat = berkas.stat()
    for label, nilai in [
        ("jalur", str(berkas)),
        ("jenis", "berkas"),
        ("ekstensi", berkas.suffix or "(tidak ada)"),
        ("ukuran", f"{stat.st_size} byte ({format_size(stat.st_size)})"),
        ("diubah", format_waktu(stat.st_mtime)),
    ]:
        print(f"{label:<9}: {nilai}")
    return 0


def perintah_cari(args: argparse.Namespace) -> int:
    akar = Path(args.path)
    if not akar.is_dir():
        print(f"filetool: bukan direktori: {akar}", file=sys.stderr)
        return 1
    hasil = sorted(p for p in akar.rglob(args.pola) if p.is_file())
    if not hasil:
        print("tidak ada yang cocok.")
        return 0
    for jalur in hasil:
        print(jalur.relative_to(akar))
    return 0


def buat_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="filetool",
        description="Pengelola file dari terminal.",
    )
    parser.add_argument("--versi", action="version", version=f"filetool {VERSI}")
    sub = parser.add_subparsers(dest="perintah", required=True, metavar="PERINTAH")

    p_daftar = sub.add_parser("daftar", help="tampilkan isi sebuah direktori")
    p_daftar.add_argument("path", help="direktori yang ingin ditampilkan")
    p_daftar.add_argument("--panjang", action="store_true",
                          help="tampilkan ukuran, waktu ubah, dan nama")
    p_daftar.set_defaults(func=perintah_daftar)

    p_info = sub.add_parser("info", help="tampilkan metadata sebuah berkas")
    p_info.add_argument("path", help="berkas yang ingin diperiksa")
    p_info.set_defaults(func=perintah_info)

    p_cari = sub.add_parser("cari", help="cari berkas berdasarkan pola glob")
    p_cari.add_argument("path", help="direktori awal pencarian")
    p_cari.add_argument("--pola", default="*", help='pola nama berkas, misalnya "*.txt"')
    p_cari.set_defaults(func=perintah_cari)

    return parser


def main(argv: list[str] | None = None) -> int:
    args = buat_parser().parse_args(argv)
    return args.func(args)


if __name__ == "__main__":
    sys.exit(main())