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:
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.pycStruktur pohon yang dihasilkan:
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.pyBerkas 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:
- Fungsi
buat_parser()membangun seluruh definisi argumen dan mengembalikan objectArgumentParser. - Setiap subperintah dihubungkan ke fungsinya lewat
set_defaults(func=...). - Fungsi
main(argv=None)mem-parsing argumen lalu mendelegasikan pekerjaan; blokif __name__ == "__main__":hanya meneruskan exit code kesys.exit().
Berikut kerangka beserta tiga perintah pertama — versi lengkap berkas main.py tersedia pada bagian Berkas Sumber di akhir halaman:
"""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:
- Baris 12 (
parser = argparse.ArgumentParser(): parameterprog="filetool"menetapkan nama program pada pesanusage. Tanpa parameter ini,argparsememakai nama berkas (main.py) sebagai nama program. - Baris 16 (
parser.add_argument("--versi", ...): opsiaction="version"membuatargparsemencetak teksversionlalu keluar dengan exit code0begitu opsi ini dipakai. - Baris 17 (
sub = parser.add_subparsers(...)): satu parser induk memperoleh kumpulan subparser. Parameterrequired=Truemembuat subperintah wajib diisi, danmetavar="PERINTAH"mengatur nama placeholder pada pesanusage. - Baris 19 (
p_daftar = sub.add_parser("daftar", ...)): setiap pemanggilanadd_parser()mengembalikan parser baru yang berdiri sendiri, lengkap dengan--helpmiliknya sendiri. - Baris 23 (
p_daftar.set_defaults(func=perintah_daftar)): attributefuncditanam padaNamespacehasil parsing. Setelahparse_args()berhasil,args.funclangsung menunjuk fungsi Python yang harus dijalankan. - Baris 37 (
def main(argv=None)): parameterargv=Nonemembuatparse_args()membacasys.argvketika dipanggil dari terminal, tetapi memungkinkan kita menyuntikkan list argumen sendiri saat menguji dari kode — pola ini dipakai penuh pada Bagian 4. - Baris 38-39 (
return args.func(args)): pemetaan subperintah ke fungsi terjadi tanpaif/matchsama sekali;argparseyang menyimpan pasangannya lewatset_defaults(). - Baris 43 (
sys.exit(main())): nilai balikmain()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:
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")- Baris 3-4 (
if ukuran < 1024:): ukuran di bawah 1 KB ditampilkan apa adanya dalam satuan byte tanpa desimal. - 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. - Baris 14 (
datetime.fromtimestamp(epoch)): methodstat().st_mtimemengembalikan jumlah detik sejak epoch, sehingga kita perlu mengubahnya kembali menjadi objectdatetimesebelum diformat.
Perintah daftar membaca isi satu tingkat direktori dan menghormati opsi --panjang:
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- Baris 2 (
direktori = Path(args.path)): string dariNamespacedikonversi menjadi objectPathsekali di awal; seluruh baris berikutnya bekerja dengan object tersebut. - Baris 3-5 (
if not direktori.is_dir():): methodis_dir()mengembalikanFalsebaik untuk berkas biasa maupun jalur yang tidak ada; pesan galat ditulis kestderrdan exit code1dikembalikan. - Baris 6 (
sorted(direktori.iterdir())): methoditerdir()menghasilkan generatorPathuntuk setiap isi direktori;sorted()menjaga urutan keluaran tetap sama di mesin mana pun. - Baris 11 (
ukuran:>8): ukuran rata kanan selebar delapan karakter agar kolom tetap sejajar ketika ukuran berbeda-beda.
Jalankan dua variasi pemakaian:
python3 main.py daftar files
python3 main.py daftar files --panjangOutput yang Dihasilkan:
.git/
__pycache__/
arsip-lama.zip
catatan.txt
catatan~
dokumen/
draf-sementara.tmp
foto/
foto-liburan.jpg
konfig-lama.json
laporan.txt
skrip-tes.pyd - 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.pyPerintah info menampilkan metadata satu berkas dalam bentuk pasangan label-nilai:
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- Baris 6 (
stat = berkas.stat()): satu panggilanstat()mengembalikan objectos.stat_resultberisi ukuran dan stempel waktu; atributst_sizedanst_mtimedibaca dari object tersebut. - Baris 10 (
berkas.suffix or "(tidak ada)"): attributesuffixmengembalikan string kosong untuk berkas tanpa ekstensi, sehingga operatorormenggantinya dengan keterangan yang lebih jelas. - Baris 14 (
{label:<9}): label rata kiri selebar sembilan karakter menjaga titik dua tetap sejajar antar baris.
python3 main.py info files/laporan.txtOutput yang Dihasilkan:
jalur : files/laporan.txt
jenis : berkas
ekstensi : .txt
ukuran : 1200 byte (1,2 KB)
diubah : 2025-06-15 10:30Perintah cari memakai method rglob() untuk menelusuri pohon secara rekursif:
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- Baris 6 (
akar.rglob(args.pola)): methodrglob()menerima pola glob lalu menelusuri semua tingkat di bawahakar; ekspresi generator menyaring hasil agar hanya berkas (is_file()) yang ditampilkan. - Baris 11 (
jalur.relative_to(akar)): methodrelative_to()mengupas bagianakardari jalur lengkap sehingga keluaran berupa jalur relatif yang pendek dan portabel.
python3 main.py cari files --pola "*.txt"Output yang Dihasilkan:
catatan.txt
dokumen/kontrak.txt
dokumen/proposal.txt
laporan.txtBantuan dan Galat Penggunaan #
Seluruh teks bantuan dibangkitkan argparse dari definisi argumen; kita tidak menulis satu baris pun secara manual. Jalankan python3 main.py --help:
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 exitSetiap subperintah juga punya bantuan sendiri, misalnya python3 main.py daftar --help:
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 namaKetika 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:
filetool: bukan berkas: tidak-ada.txtPesan 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:
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"""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())