Dekstop Programming 06 Aug 2026 69 views 0 komentar

Membuat CLI Tool dengan Python - dari Script Sederhana hingga Aplikasi Command Line Profesional

Membuat CLI Tool dengan Python - dari Script Sederhana hingga Aplikasi Command Line Profesional

Membuat CLI Tool dengan Python: dari Script Sederhana hingga Aplikasi Command Line Profesional

Pernah nggak sih, kamu punya script Python yang selalu kamu jalankan berulang kali setiap hari? Misalnya script untuk rename file bulk, script untuk fetch data dari API, atau script untuk generate laporan dari database. Setiap kali mau jalankan, harus ketik python script.py --arg1 value1 --arg2 value2 lama-lama capek juga. Apalagi kalau argument-nya banyak, pasti sering typo dan harus ulang lagi.

Kabar baiknya, Python punya solusi keren untuk masalah ini: CLI tool. Dengan CLI tool, kamu bisa bikin command yang clean, reusable, dan bahkan share ke tim kamu. Di artikel ini, saya bakal tunjukin cara bikin CLI tool pakai Python mulai dari argparse (yang sudah built-in), pakai library Click yang powerful, sampai Typer yang super modern. Terakhir, kita juga bakal packaging supaya bisa di-install via pip install. Yuk mulai!

Kenapa Harus Bikin CLI Tool?

Sebelum masuk ke kode, saya mau jelasin dulu kenapa CLI tool itu penting. Dalam pekerjaan sehari-hari sebagai developer, kita sering banget nulis script yang cuma sekali jalan. Tapi seiring waktu, script itu jadi penting dan dipakai orang lain juga.

Bayangkan kalau script kamu cuma punya sys.argv untuk handle argument. Nggak ada help message, nggak ada validasi, user harus baca source code dulu buat tau cara pakai. Itu buruk dari sisi UX.

CLI tool yang baik itu punya:

  • Help message otomatis user tinggal ketik --help
  • Validasi input error yang jelas kalau argument salah
  • Auto-completion bisa auto-complete di terminal
  • Nested commands seperti git commit atau docker compose up
  • Package yang bisa di-install tinggal pip install dan langsung pakai

Seru kan? Let's build one!

Cara Pertama: Menggunakan argparse (Built-in)

argparse itu sudah ada di Python standard library. Nggak perlu install apapun. Cocok banget untuk CLI tool sederhana. Saya mulai dari sini karena ini fondasi yang harus dipahami dulu.

Misalnya saya mau bikin tool sederhana untuk konversi ukuran file dari bytes ke berbagai satuan lain:

import argparse

def convert_size(size_bytes, unit):
 """Convert bytes ke unit yang dipilih."""
 units = {
 "kb": 1024,
 "mb": 1024 ** 2,
 "gb": 1024 ** 3,
 "tb": 1024 ** 4,
 }
 if unit not in units:
 raise ValueError(f"Unit '{unit}' tidak didukung. Pilih: {', '.join(units.keys())}")
 return size_bytes / units[unit]

def main():
 parser = argparse.ArgumentParser(
 description="Konversi ukuran file dari bytes ke satuan lain.",
 epilog="Contoh: python filesize.py 1048576 --unit mb"
 )
 parser.add_argument(
 "size",
 type=int,
 help="Ukuran file dalam bytes"
 )
 parser.add_argument(
 "--unit", "-u",
 type=str,
 default="mb",
 choices=["kb", "mb", "gb", "tb"],
 help="Satuan output (default: mb)"
 )
 parser.add_argument(
 "--precision", "-p",
 type=int,
 default=2,
 help="Jumlah angka di belakang koma (default: 2)"
 )

 args = parser.parse_args()
 result = convert_size(args.size, args.unit)
 print(f"{args.size} bytes = {result:.{args.precision}f} {args.unit.upper()}")

if __name__ == "__main__":
 main()

Sekarang kalau saya jalankan:

$ python filesize.py 1048576 --unit mb
1048576 bytes = 1.00 MB

$ python filesize.py 1073741824 --unit gb --precision 4
1073741824 bytes = 1.0000 GB

$ python filesize.py --help
usage: filesize.py [-h] [--unit {kb,mb,gb,tb}] [--precision PRECISION] size

Konversi ukuran file dari bytes ke satuan lain.

positional arguments:
 size Ukuran file dalam bytes

optional arguments:
 -h, --help show this help message and exit
 --unit {kb,mb,gb,tb}, -u {kb,mb,gb,tb}
 Satuan output (default: mb)
 --precision PRECISION, -p PRECISION
 Jumlah angka di belakang koma (default: 2)

Nah, argparse udah bisa handle semua itu auto generate help message, validasi argument, bahkan choices supaya user nggak bisa masukin unit yang salah. Tapi kalau CLI tool-nya makin kompleks, misalnya butuh subcommand kayak tool init, tool build, tool deploy, maka argparse mulai terasa ribet.

Makanya, saya lanjut ke alternatif yang lebih modern.

Cara Kedua: Click Powerful dan Flexible

Click adalah library yang sangat populer untuk bikin CLI tool di Python. Banyak project besar pakai Click misalnya Flask, Pipenv, dan Poetry. Click pakai decorator-based approach, jadi code-nya lebih clean dan readable.

Install Click dulu:

$ pip install click

Sekarang saya bikin tool yang sama tapi pakai Click. Perhatikan betapa lebih ringkasnya:

import click

@click.command()
@click.argument("size", type=int)
@click.option("--unit", "-u", default="mb", type=click.Choice(["kb", "mb", "gb", "tb"]), help="Satuan output")
@click.option("--precision", "-p", default=2, help="Jumlah angka di belakang koma")
def filesize(size, unit, precision):
 """Konversi ukuran file dari bytes ke satuan lain."""
 units = {"kb": 1024, "mb": 1024 ** 2, "gb": 1024 ** 3, "tb": 1024 ** 4}
 result = size / units[unit]
 click.echo(f"{size} bytes = {result:.{precision}f} {unit.upper()}")

if __name__ == "__main__":
 filesize()

Clean banget kan? Dan kalau saya tambahkan --help, Click otomatis generate help message dari docstring dan parameter. Sekarang saya mau tunjukin kekuatan Click yang sebenarnya: subcommand.

Misalnya saya mau bikin tool bernama mytools yang punya beberapa subcommand:

import click
import os

@click.group()
def cli():
 """MyTools toolkit untuk developer."""
 pass

@cli.command()
@click.argument("filename")
def info(filename):
 """Tampilkan informasi file."""
 if not os.path.exists(filename):
 click.echo(click.style(f"File '{filename}' tidak ditemukan!", fg="red"), err=True)
 raise SystemExit(1)

 size = os.path.getsize(filename)
 units = {"B": 1, "KB": 1024, "MB": 1024 ** 2}
 unit = "B"
 display_size = size

 for u, mult in reversed(units.items()):
 if size >= mult:
 unit = u
 display_size = size / mult
 break

 click.echo(f" File : {filename}")
 click.echo(f" Size : {display_size:.2f} {unit}")
 click.echo(f" Dir : {os.path.abspath(filename)}")

@cli.command()
@click.argument("path", default=".")
@click.option("--extension", "-e", help="Filter berdasarkan extension")
def ls(path, extension):
 """List files di direktori."""
 files = os.listdir(path)
 if extension:
 extension = extension.lstrip(".")
 files = [f for f in files if f.endswith(f".{extension}")]
 for f in sorted(files):
 click.echo(f" {f}")

@cli.command()
@click.argument("name")
@click.option("--create-dir/--no-create-dir", default=True, help="Buat directory juga?")
def init(name, create_dir):
 """Inisialisasi project baru."""
 os.makedirs(name, exist_ok=True)
 if create_dir:
 dirs = ["src", "tests", "docs"]
 for d in dirs:
 os.makedirs(os.path.join(name, d), exist_ok=True)
 click.echo(f" Project '{name}' berhasil dibuat dengan struktur: src/, tests/, docs/")
 else:
 click.echo(f" Project '{name}' berhasil dibuat!")

if __name__ == "__main__":
 cli()

Sekarang saya bisa jalankan tool ini seperti ini:

$ python mytools.py info myfile.txt
 File : myfile.txt
 Size : 1.05 KB
 Dir : /home/user/myfile.txt

$ python mytools.py ls --extension py
 main.py
 mytools.py
 utils.py

$ python mytools.py init myproject --no-create-dir
 Project 'myproject' berhasil dibuat!

Keren kan? Click handle semua help message, validasi, type conversion, colored output. Tapi kalau kamu suka type hints dan lebih suka approach yang mirip FastAPI, ada opsi yang lebih baru lagi.

Cara Ketiga: Typer Modern, Type-Safe, dan Keren

Typer dibuat oleh creator FastAPI, Sebasti n Ram rez. Typer pakai type hints Python sebagai dasar deklarasi CLI argument. Hasilnya? Code-nya super clean, ada auto-completion support, dan error handling yang lebih baik.

Install Typer:

$ pip install typer

Mari kita rebuild tool konversi file size tadi dengan Typer:

import typer
from typing import Optional
from enum import Enum

app = typer.Typer(help="FileSize konversi ukuran file dari bytes.")

class Unit(str, Enum):
 KB = "kb"
 MB = "mb"
 GB = "gb"
 TB = "tb"

@app.command()
def convert(
 size: int = typer.Argument(..., help="Ukuran file dalam bytes"),
 unit: Unit = typer.Option(Unit.MB, "--unit", "-u", help="Satuan output"),
 precision: int = typer.Option(2, "--precision", "-p", help="Angka desimal"),
):
 """Konversi ukuran file dari bytes ke satuan lain."""
 units = {
 Unit.KB: 1024,
 Unit.MB: 1024 ** 2,
 Unit.GB: 1024 ** 3,
 Unit.TB: 1024 ** 4,
 }
 result = size / units[unit]
 typer.echo(f"{size} bytes = {result:.{precision}f} {unit.value.upper()}")

if __name__ == "__main__":
 app()

Notice yang menarik: saya pakai enum Enum untuk validasi unit. Typer otomatis generate validasi dan pilihan yang benar. Kalau user masukin unit yang salah, Typer langsung kasih error message yang jelas.

Sekarang saya mau tunjukin sesuatu yang lebih real-world. Misalnya saya mau bikin CLI tool untuk generate boilerplate Python project yang benar-benar bisa saya pakai sehari-hari.

Building a Real CLI Tool: Project Scaffolder

Mari kita bikin tool yang berguna: pyscaffold CLI tool untuk generate boilerplate Python project yang sudah siap develop.

"""pyscaffold CLI tool untuk generate Python project boilerplate."""
import typer
import os
from pathlib import Path
from typing import Optional

app = typer.Typer(help=" PyScaffold generate Python project boilerplate dalam hitungan detik.")

@app.command()
def create(
 name: str = typer.Argument(..., help="Nama project"),
 author: Optional[str] = typer.Option(None, "--author", "-a", help="Nama author"),
 license: str = typer.Option("MIT", "--license", "-l", help="Jenis license"),
 with_docker: bool = typer.Option(False, "--docker", "-d", help="Sertakan Dockerfile"),
 with_ci: bool = typer.Option(False, "--github-actions", "-g", help="Sertakan GitHub Actions workflow"),
):
 """Buat Python project baru dengan struktur yang sudah di-setup."""
 
 project_path = Path(name)
 
 if project_path.exists():
 typer.echo(f" Folder '{name}' sudah ada!", err=True)
 raise typer.Exit(1)

 typer.echo(f" Membuat project '{name}'...")

 # Buat direktori utama
 dirs = [f"{name}", f"{name}/src/{name}", f"{name}/tests"]
 if with_docker:
 dirs.append(f"{name}/docker")
 if with_ci:
 dirs.append(f"{name}/.github/workflows")
 
 for d in dirs:
 Path(d).mkdir(parents=True, exist_ok=True)

 # Generate pyproject.toml
 pyproject = f"""[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "{name}"
version = "0.1.0"
description = ""
readme = "README.md"
authors = [
 {{"name" = "{author or 'Your Name'}"}},
]
requires-python = ">=3.9"
license = "{license}"

[tool.pytest.ini_options]
testpaths = ["tests"]
"""
 (project_path / "pyproject.toml").write_text(pyproject)

 # Generate __init__.py
 init_content = f'"""Top-level package for {name}."""

__version__ = "0.1.0"
'
 (project_path / f"src/{name}/__init__.py").write_text(init_content)

 # Generate README.md
 readme = f"""# {name}

{''}

## Installation

```bash
pip install -e .
```

## Usage

```bash
python -m {name}
```
"""
 (project_path / "README.md").write_text(readme)

 # Generate test file
 test_content = f"""def test_version():
 import {name}
 assert {name}.__version__ == "0.1.0"
"""
 (project_path / f"tests/test_{name}.py").write_text(test_content)
 (project_path / "tests/__init__.py").write_text("")

 # Generate Dockerfile kalau diminta
 if with_docker:
 dockerfile = """FROM python:3.11-slim
WORKDIR /app
COPY . .
RUN pip install -e .
CMD ["python", "-m", package_name]
"""
 (project_path / "docker/Dockerfile").write_text(dockerfile)

 # Generate GitHub Actions kalau diminta
 if with_ci:
 workflow = f"""name: CI
on: [push, pull_request]
jobs:
 test:
 runs-on: ubuntu-latest
 strategy:
 matrix:
 python-version: ["3.9", "3.10", "3.11", "3.12"]
 steps:
 - uses: actions/checkout@v4
 - uses: actions/setup-python@v5
 with:
 python-version: ${{{{ matrix.python-version }}}}
 - run: pip install -e .
 - run: python -m pytest
"""
 (project_path / ".github/workflows/ci.yml").write_text(workflow)

 # Summary
 typer.echo("")
 typer.echo(typer.style(" Project berhasil dibuat!", fg=typer.colors.GREEN, bold=True))
 typer.echo("")
 typer.echo(" Struktur:")
 
 for root, dirs_list, files in os.walk(project_path):
 level = root.replace(str(project_path), "").count(os.sep)
 indent = " " * level
 typer.echo(f"{indent} {os.path.basename(root)}/")
 for f in files:
 typer.echo(f"{indent} {f}")
 
 typer.echo("")
 typer.echo(typer.style("Selanjutnya:", fg=typer.colors.CYAN))
 typer.echo(f" cd {name}")
 typer.echo(" pip install -e .")
 typer.echo(" python -m pytest")

@app.command()
def list_licenses():
 """Tampilkan daftar license yang tersedia."""
 licenses = ["MIT", "Apache-2.0", "GPL-3.0", "BSD-3-Clause", "ISC"]
 typer.echo(" Available licenses:")
 for lic in licenses:
 typer.echo(f" {lic}")

if __name__ == "__main__":
 app()

Test run tool-nya:

$ python pyscaffold.py create myawesome --author "Budi" --docker --github-actions
 Membuat project 'myawesome'...

 Project berhasil dibuat!

 Struktur:
 myawesome/
 pyproject.toml
 README.md
 src/
 myawesome/
 __init__.py
 tests/
 test_myawesome.py
 docker/
 Dockerfile
 .github/
 workflows/
 ci.yml

Selanjutnya:
 cd myawesome
 pip install -e .
 python -m pytest

Sekarang tool-nya udah berfungsi. Tapi bagaimana cara bikin orang lain bisa install tool ini tanpa harus clone repo dulu? Jawabannya: packaging.

Packaging CLI Tool dengan pyproject.toml

Supaya CLI tool kita bisa di-install via pip install dan langsung dipakai dari terminal, kita perlu packaging yang benar. Gunakan pyproject.toml dan fitur [project.scripts].

Struktur folder yang dibutuhkan:

pyscaffold/
 pyproject.toml
 README.md
 src/
 pyscaffold/
 __init__.py
 cli.py

Isi pyproject.toml:

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "pyscaffold-cli"
version = "1.0.0"
description = "CLI tool untuk generate Python project boilerplate"
readme = "README.md"
authors = [{ name = "Developer", email = "[email protected]" }]
requires-python = ">=3.9"
license = "MIT"
dependencies = [
 "typer[all]>=0.9.0",
]

[project.scripts]
pyscaffold = "pyscaffold.cli:app"

[project.optional-dependencies]
dev = ["pytest>=7.0", "ruff>=0.1.0"]

Bagian kuncinya ada di [project.scripts]. Ini yang bikin setelah pip install, command pyscaffold langsung bisa dipanggil dari terminal tanpa perlu python di depannya.

Cara install dalam mode development:

$ pip install -e ".[dev]"
$ pyscaffold create myproject --author "Budi" --docker

Atau kalau mau publish ke PyPI:

$ pip install build twine
$ python -m build
$ twine upload dist/*

Setelah publish, siapapun bisa install tool kamu pakai:

$ pip install pyscaffold-cli
$ pyscaffold create newproject

Gila, bayangin CLI tool buatan kamu bisa dipakai developer di seluruh dunia. That's the power of proper packaging.

Tips Tambahan yang Saya Pelajari dari Lapangan

Beberapa hal yang saya pelajari setelah bikin beberapa CLI tool untuk tim:

  1. Selalu pakai --verbose / --quiet flag. Biar user bisa kontrol output. Click dan Typer keduanya support ini dengan mudah.
  2. Log to stderr, output ke stdout. Biar user bisa pipe output ke command lain tanpa kena log message. Di Click tinggal pakai click.echo(..., err=True).
  3. Handle Ctrl+C gracefully. Jangan biarkan error traceback muncul kalau user cancel. Bungkus main function dengan try-except.
  4. Tambahin version flag. Biar user bisa cek versi tool-nya. Di Typer tinggal typer.__version__.
  5. Test CLI tool kamu. Pakai click.testing.CliRunner atau typer.testing.CliRunner untuk testing command kamu secara unit-test.
# Contoh testing CLI tool pakai Typer
from typer.testing import CliRunner
from myproject.cli import app

runner = CliRunner()

def test_create_project():
 result = runner.invoke(app, ["create", "test-project"])
 assert result.exit_code == 0
 assert "Project berhasil dibuat" in result.output

def test_version_flag():
 result = runner.invoke(app, ["--version"])
 assert result.exit_code == 0
 assert "1.0.0" in result.output

Testing CLI tool itu sering dilupain, padahal penting banget. Saya pernah punya tool yang broken di Python 3.12 karena library click-nya update. Kalau ada test, pasti langsung ketahuan sebelum user complain.

Perbandingan: argparse vs Click vs Typer

Kalau masih bingung mau pakai yang mana, ini rangkuman singkat saya:

  • argparse Cocok untuk script sederhana yang cuma butuh beberapa argument. Gratis, built-in, tapi verbose untuk project yang kompleks.
  • Click Pilihan solid untuk project yang butuh subcommand, plugin system, atau custom workflows. Banyak battle-tested project pakai ini.
  • Typer Paling modern. Type hints native, auto-completion support, dan berbasis Click di baliknya. Kalau kamu suka FastAPI, pasti langsung nyaman.

Saya pribadi sekarang pakai Typer untuk semua CLI tool baru. Type hints-nya bikin code jauh lebih readable, dan auto-complete support-nya bikin user experience jauh lebih baik.

Tapi kalau project yang sudah pakai Click dan kamu mau konsisten, Stick with Click juga nggak masalah keduanya pada dasarnya sama kuat.

Penutup

Nah, itu dia perjalanan dari script Python biasa sampai CLI tool yang profesional. Kita udah lihat gimana argparse bisa jadi fondasi, Click kasih power untuk subcommand dan structured CLI, dan Typer bawa pendekatan modern berbasis type hints.

Yang paling penting: jangan cuma nulis script yang cuma kamu sendiri yang bisa pakai. Bikin CLI tool-nya, package-nya, share ke tim kamu. Productivity naik, error berkurang, dan yang pasti kamu keliatan keren di terminal.

Satu hal lagi packaging dengan pyproject.toml itu sekarang udah gampang banget. Nggak perlu setup.py yang ribet lagi. Hatchling atau setuptools modern udah handle semuanya dengan clean.

Sekarang giliran kamu: Cara apa yang kamu pakai untuk bikin CLI tool di Python? Click, Typer, atau masih setia sama argparse? Atau mungkin ada library lain yang saya belum sebut? Tulis di kolom komentar ya saya penasaran mau denger pengalaman kalian!


Bagikan artikel ini:

Komentar (0)

Belum ada komentar. Jadilah yang pertama memberikan tanggapan!

Tinggalkan Komentar