Setiap alat yang tumbuh akan sulit dirawat bila seluruh logika menumpuk dalam satu berkas. Karena itu kita memulai proyek ini dari kerangkanya: memisahkan pintu masuk, parsing argumen, dan kontrak galat ke dalam modul-modul kecil. Pada bagian ini kerangka tersebut harus sudah bisa berjalan — perintah help tampil rapi, pemakaian yang salah ditolak dengan pesan yang jelas, dan setiap keadaan punya kode keluar yang tetap. Operasi berkas sungguhan baru menyusul di Bagian 2 dan 3, tetapi kontrak yang kita tetapkan di sini akan dipakai terus sampai pengujian di Bagian 4.
Anatomi Modul #
Empat berkas pertama yang kita bangun adalah fondasi seluruh proyek. Tabel berikut merangkum peran masing-masing modul; tiga modul sisanya (format.js, perintah-baca.js, perintah-tulis.js) dibahas pada bagian berikutnya.
| Berkas | Peran |
|---|---|
src/cli.js | Pintu masuk: menerjemahkan argumen menjadi panggilan fungsi perintah dan menentukan kode keluar |
src/argumen.js | Memvalidasi bentuk pemanggilan dengan util.parseArgs |
src/bantuan.js | Menyimpan teks bantuan yang dicetak perintah help |
src/galat.js | Dua jenis galat bawaan proyek serta penerjemah pesan galat sistem |
Pemisahan ini memberi satu bonus penting untuk pengujian: fungsi perintah tidak mencetak apa pun ke layar, melainkan mengembalikan teks. Test di Bagian 4 cukup membandingkan nilai kembalian, tanpa perlu menangkap keluaran terminal.
Parsing Argumen dengan util.parseArgs #
Modul src/argumen.js memakai parseArgs dari node:util dengan tiga pengaturan: allowPositionals: true agar argumen tanpa awalan -- diterima sebagai argumen posisi, options yang mendeklarasikan dua opsi boolean (--ya dan --lanjut), dan mode ketat yang aktif secara default sehingga opsi asing langsung melempar galat. Fungsi parseArgumen membungkus semuanya, lalu memvalidasi perintah dan jumlah argumennya.
export function parseArgumen(argv) {
let hasil;
try {
hasil = parseArgs({
args: argv,
allowPositionals: true,
options: {
ya: { type: 'boolean' },
lanjut: { type: 'boolean' },
},
});
} catch (galat) {
throw new GalatPemakaian('opsi tidak dikenal (jalankan help untuk daftar opsi)');
}
const [perintah, ...posisi] = hasil.positionals;
if (perintah === undefined) {
throw new GalatPemakaian('perintah tidak diberikan (jalankan help untuk bantuan)');
}
if (!(perintah in JUMLAH_ARGUMEN)) {
throw new GalatPemakaian(`perintah tidak dikenal: ${perintah} (jalankan help)`);
}
const jumlah = JUMLAH_ARGUMEN[perintah];
if (posisi.length !== jumlah) {
throw new GalatPemakaian(
`perintah ${perintah} butuh ${jumlah} argumen, diterima ${posisi.length}`,
);
}
return { perintah, posisi, opsi: hasil.values };
}Mari kita bedah fungsi di atas baris per baris:
- Blok
try:parseArgsmenerima salinanargv(bukanprocess.argvlangsung) supaya test dapat mengirim daftar argumen buatan. TanpaallowPositionals: true, semua argumen posisi justru ditolak. - Blok
catch: galat dariparseArgs(misalnya opsi tidak dikenal) ditangkap lalu dibungkus ulang sebagaiGalatPemakaian. Pesan aslinya berbahasa Inggris dan panjang, seperti contoh di bawah — kita ganti dengan satu kalimat Indonesia yang menunjuk kehelp. const [perintah, ...posisi]: destructuring memisahkan elemen pertama sebagai nama perintah dan sisanya sebagai argumen posisi.- Tiga pemeriksaan berurutan: perintah kosong, perintah di luar tabel
JUMLAH_ARGUMEN(help: 0,daftar: 1,baca: 1,info: 1,tulis: 2,salin: 2,hapus: 1), dan jumlah argumen yang tidak sesuai. Semuanya melemparGalatPemakaian— kelas galat padasrc/galat.jsyang membawakodeKeluarbernilai1.
Untuk merasakan mengapa pembungkusan perlu, jalankan parseArgs mentah dengan opsi yang tidak dikenal:
import { parseArgs } from 'node:util';
try {
parseArgs({ args: ['--hmm', 'x'], allowPositionals: true });
} catch (galat) {
console.log(galat.message.split('.')[0]);
}Output yang Dihasilkan:
Unknown option '--hmm'Kalimat pertama pesan aslinya sudah cukup untuk diagnosis: opsi --hmm memang tidak ada dalam deklarasi. Kalimat lanjutannya masih panjang karena mengingatkan pemakaian pemisah -- untuk argumen posisi yang kebetulan berawalan tanda minus — misalnya nama berkas -awal.txt — supaya tidak dianggap opsi. Daripada menampilkan teks Inggris sepanjang itu, alat kita menerjemahkan seluruh kegagalan parsing menjadi satu baris pesan Indonesia yang menunjuk ke help.
Dispatch Perintah dan Kode Keluar #
Pintu masuk src/cli.js menghubungkan hasil parsing dengan fungsi perintah lewat tabel AKSI. Nama perintah menjadi kunci; nilainya fungsi panah yang menerima argumen posisi dan opsi lalu mengembalikan teks keluaran. Fungsi main tinggal melihat ke tabel itu, dengan help sebagai satu-satunya pengecualian karena isinya murni teks bantuan.
const AKSI = {
daftar: (posisi) => daftar(posisi[0]),
baca: (posisi) => baca(posisi[0]),
info: (posisi) => info(posisi[0]),
tulis: (posisi, opsi) => tulis(posisi[0], posisi[1], opsi),
salin: (posisi) => salin(posisi[0], posisi[1]),
hapus: (posisi, opsi) => hapus(posisi[0], opsi),
};
// Menjalankan satu perintah lalu mengembalikan teks keluarannya.
export async function main(argv) {
const { perintah, posisi, opsi } = parseArgumen(argv);
if (perintah === 'help') return BANTUAN;
return AKSI[perintah](posisi, opsi);
}Sisa pekerjaan cli.js adalah menangani keberhasilan dan kegagalan di satu tempat. Fungsi jalankan mencetak hasil main ke stdout, menangkap semua galat, menyeragamkannya lewat ubahGalat dari src/galat.js, mencetak pesannya ke stderr, lalu mengembalikan kode keluar yang dibawa galat tersebut.
// Membungkus main: mencetak hasil, menangkap galat, menentukan kode keluar.
export async function jalankan(argv) {
try {
console.log(await main(argv));
return 0;
} catch (galat) {
const akhir = ubahGalat(galat);
console.error(`manajer: ${akhir.message}`);
return akhir.kodeKeluar;
}
}
// Jalankan hanya bila berkas ini dieksekusi langsung, bukan saat diimpor.
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
process.exitCode = await jalankan(process.argv.slice(2));
}Dua keputusan pada potongan terakhir patut dicermati. Pertama, kode keluar dipasang lewat process.exitCode, bukan process.exit(), agar Node.js sempat menuntaskan penulisan keluaran sebelum proses berakhir. Kedua, blok if terakhir membandingkan import.meta.url dengan path berkas yang dieksekusi — jadi seluruh isi cli.js tetap bisa diimpor oleh test tanpa efek samping, sementara node src/cli.js tetap berjalan normal.
Help sebagai Dokumen Kontrak #
Perintah help mencetak seluruh kontrak alat: daftar perintah, opsi, kode keluar, dan contoh pemakaian. Teksnya disimpan sebagai satu konstanta BANTUAN pada src/bantuan.js, sehingga mudah dirawat di satu tempat.
node src/cli.js helpOutput yang Dihasilkan:
Manajer Berkas — alat CLI untuk mengelola berkas lewat terminal.
Pemakaian:
node src/cli.js <perintah> [argumen] [opsi]
Perintah:
daftar <folder> Tampilkan isi folder (nama, ukuran, jenis)
baca <berkas> Tampilkan isi berkas dengan nomor baris
info <berkas> Tampilkan metadata berkas (ukuran, waktu diubah)
tulis <berkas> <isi> Tulis isi ke berkas (menimpa isi lama)
salin <asal> <tujuan> Salin berkas ke lokasi baru
hapus <berkas> Hapus berkas (wajib dengan opsi --ya)
help Tampilkan pesan bantuan ini
Opsi:
--lanjut Sambung isi di akhir berkas (khusus tulis)
--ya Konfirmasi penghapusan (khusus hapus)
Kode keluar:
0 Perintah berhasil dijalankan
1 Pemakaian salah (perintah atau argumen tidak sesuai)
2 Galat berkas (berkas tidak ditemukan, dsb.)
Contoh:
node src/cli.js daftar data
node src/cli.js tulis data/halo.txt 'Halo dari manajer berkas.'
node src/cli.js salin data/halo.txt data/halo-salinan.txt
node src/cli.js hapus data/halo-salinan.txt --yaKontrak kode keluar kini bisa kita uji langsung. Pemanggilan tanpa perintah, dengan perintah asing, maupun dengan argumen kurang semuanya ditolak dengan kode 1 — perhatikan bahwa pesan galat muncul di stderr, bukan stdout.
node src/cli.jsOutput yang Dihasilkan:
manajer: perintah tidak diberikan (jalankan help untuk bantuan)
kode keluar: 1node src/cli.js buang dataOutput yang Dihasilkan:
manajer: perintah tidak dikenal: buang (jalankan help)
kode keluar: 1node src/cli.js salin data/asal.txtOutput yang Dihasilkan:
manajer: perintah salin butuh 2 argumen, diterima 1
kode keluar: 1Kerangka kita kini hidup: argumen tervalidasi, bantuan tersedia, dan tiga jenis pemakaian salah ditolak dengan pesan yang seragam. Yang masih kosong adalah isi perintahnya — daftar, baca, dan info baru mengembalikan teks, belum benar-benar membaca disk. Bagian 2 mengisi ketiganya dengan node:fs/promises lengkap dengan format tampilan dan penerjemahan galat sistem menjadi pesan Indonesia.