Perintah baca pada Bagian 2 aman dicoba berkali-kali karena tidak mengubah apa pun di disk. Bagian ini berbeda sifatnya: tulis, salin, dan hapus sungguh mengubah isi disk, jadi rancangannya perlu lebih hati-hati. Pendekatan kita ada tiga lapis — pesan konfirmasi yang jelas untuk setiap operasi sukses, konfirmasi eksplisit lewat opsi --ya untuk penghapusan, serta pembedaan kode keluar 1 dan 2 supaya pemanggil tahu apakah kesalahan ada pada cara memanggil atau pada keadaan berkasnya.

Perintah tulis menerima path berkas dan isi berupa satu baris teks. Fungsi pembantunya di src/perintah-tulis.js terlihat ringkas:

js src/perintah-tulis.js
async function pastikanFolder(tujuan) {
  const folder = dirname(tujuan);
  if (folder !== '.') {
    await mkdir(folder, { recursive: true });
  }
}

export async function tulis(berkas, isi, opsi = {}) {
  await pastikanFolder(berkas);
  if (opsi.lanjut) {
    await appendFile(berkas, `${isi}\n`, 'utf8');
  } else {
    await writeFile(berkas, `${isi}\n`, 'utf8');
  }
  return `berkas ditulis: ${berkas}`;
}

Empat keputusan kecil di sini membuat alat terasa ramah dipakai:

  1. pastikanFolder: folder induk dibuat otomatis dengan mkdir({ recursive: true }), jadi tulis data/catatan/baru.txt langsung berhasil walau data belum ada. Pemeriksaan folder !== '.' menjaga berkas di folder kerja saat ini tidak memicu mkdir yang tidak perlu.
  2. appendFile vs writeFile: tanpa opsi, isi lama ditimpa; dengan --lanjut, isi baru disambung di akhir berkas. Satu opsi boolean cukup untuk dua perilaku ini.
  3. ${isi}\n: satu baris baru selalu ditambahkan di ujung, sehingga setiap kali tulis dipanggil, isinya selalu berakhir rapi sebagai baris utuh — konsisten dengan cara baca menomori baris.
  4. Pesan konfirmasi: fungsi mengembalikan berkas ditulis: ... alih-alih diam, supaya pengguna yakin operasinya benar-benar terjadi.

Berikut alur lengkapnya: menulis dua baris ke berkas di folder yang belum ada, lalu membaca hasilnya.

bash
node src/cli.js tulis data/catatan/baru.txt 'Baris pertama'
node src/cli.js tulis data/catatan/baru.txt 'Baris kedua' --lanjut
node src/cli.js baca data/catatan/baru.txt

Output yang Dihasilkan:

text
berkas ditulis: data/catatan/baru.txt
berkas ditulis: data/catatan/baru.txt
  1 | Baris pertama
  2 | Baris kedua

Menyalin Berkas #

Perintah salin memindahkan isi satu berkas ke path tujuan. Intinya satu panggilan copyFile, tetapi fungsi ini paling banyak belajar dari keanehan galat sistem:

js src/perintah-tulis.js
export async function salin(asal, tujuan) {
  const rincianAsal = await stat(asal);
  if (rincianAsal.isDirectory()) {
    throw new GalatBerkas(`asal adalah folder, bukan berkas: ${asal}`);
  }
  const rincianTujuan = await stat(tujuan).catch(() => null);
  if (rincianTujuan !== null && rincianTujuan.isDirectory()) {
    throw new GalatBerkas(`tujuan adalah folder, bukan berkas: ${tujuan}`);
  }

  await pastikanFolder(tujuan);
  await copyFile(asal, tujuan);
  return `berkas disalin: ${asal} -> ${tujuan}`;
}

Pemeriksaan eksplisit di awal bukan hiasan. Ketika copyFile gagal karena salah satu path adalah folder, galat EISDIR yang dibuang menyebut path asal — padahal bisa jadi tujuannyalah yang salah — dan kalau path memang tidak ada, kita ingin pesan tidak ditemukan menunjuk path yang tepat. Pola stat(tujuan).catch(() => null) juga perlu diperhatikan: tujuan yang belum ada itu wajar untuk operasi salin, jadi kegagalannya dianggap bukan galat melainkan tanda bahwa penyalinan boleh lanjut.

bash
node src/cli.js salin data/asal.txt data/salinan.txt
node src/cli.js daftar data

Output yang Dihasilkan:

text
berkas disalin: data/asal.txt -> data/salinan.txt
nama                      ukuran  jenis
asal.txt                    12 B  berkas
salinan.txt                 12 B  berkas

total: 2 butir

Menghapus dengan Konfirmasi #

Menghapus adalah satu-satunya perintah yang tidak bisa dibatalkan lewat tulis ulang, karena itu gerbangnya paling ketat. Opsi --ya wajib ada; tanpa opsi tersebut perintah menolak bekerja dan keluar dengan kode 1 — kegagalannya dianggap salah pemakaian, bukan galat berkas.

js src/perintah-tulis.js
export async function hapus(berkas, opsi = {}) {
  if (!opsi.ya) {
    throw new GalatPemakaian('hapus butuh konfirmasi; jalankan kembali dengan opsi --ya');
  }
  const rincian = await stat(berkas);
  if (rincian.isDirectory()) {
    throw new GalatBerkas(`hapus hanya mendukung berkas, bukan folder: ${berkas}`);
  }
  await unlink(berkas);
  return `berkas dihapus: ${berkas}`;
}

Urutan pemeriksaan pada fungsi itu disusun dari yang paling murah: opsi diperiksa lebih dulu tanpa menyentuh disk, baru kemudian stat memastikan path berupa berkas. Folder ditolak dengan sengaja — menghapus folder beserta seluruh isinya adalah pekerjaan yang jauh lebih berisiko, dan alat ini memilih tidak melakukannya sama sekali.

bash
node src/cli.js hapus data/draft.txt
echo kode keluar: $?
node src/cli.js hapus data/draft.txt --ya
node src/cli.js daftar data

Output yang Dihasilkan:

text
manajer: hapus butuh konfirmasi; jalankan kembali dengan opsi --ya
kode keluar: 1
berkas dihapus: data/draft.txt
nama                      ukuran  jenis

total: 0 butir

Jalur Galat Perintah Tulis #

Kontrak galat terakhir yang belum kita lihat adalah penulisan ke path yang ternyata folder. Berbeda dengan kasus baca pada Bagian 2, galat EISDIR dari writeFile membawa path pada error.path, jadi penerjemahan di ubahGalat bekerja tanpa pemeriksaan eksplisit — inilah alasan tabel PETA_PESAN tetap dipertahankan meskipun sebagian perintah memeriksa sendiri.

bash
node src/cli.js tulis data 'isi'

Output yang Dihasilkan:

text
manajer: path menuju folder, bukan berkas: data
kode keluar: 2

Seluruh perintah manajer berkas kini berfungsi: tiga perintah baca dari Bagian 2 dan tiga perintah tulis dari bagian ini, semuanya tunduk pada kontrak argumen dan kode keluar yang ditetapkan di Bagian 1. Tinggal satu pertanyaan yang belum terjawab: bagaimana kita memastikan enam perintah ini tetap benar setelah kode berubah? Bagian 4 menjawabnya dengan node:test — dari unit test fungsi murni sampai test integrasi yang menjalankan CLI sebagai proses nyata.