IO #
Hampir setiap program yang berguna melakukan operasi Input/Output: membaca konfigurasi dari file, menulis log, memproses data CSV, atau berinteraksi dengan terminal. Ruby menyediakan hierarki kelas IO yang komprehensif — IO sebagai kelas dasar, File sebagai turunannya untuk operasi file sistem, dan StringIO untuk simulasi IO berbasis memori. Memahami cara kerja IO di Ruby bukan sekadar hafal method File.read — kamu perlu mengerti mode pembukaan file, buffering, pengelolaan resource dengan block, serta kapan harus membaca sekaligus versus membaca baris per baris. Kesalahan dalam pengelolaan IO adalah salah satu sumber bug dan kebocoran resource yang paling umum di program produksi.
Hierarki dan Konsep Dasar IO #
Sebelum masuk ke penggunaan praktis, penting untuk memahami struktur kelas IO di Ruby dan bagaimana hubungan antar kelas tersebut.
classDiagram
class IO {
+read()
+write()
+close()
+each_line()
+flush()
}
class File {
+path()
+stat()
+truncate()
+chmod()
}
class StringIO {
+string()
+rewind()
+pos()
}
class BasicSocket
IO <|-- File
IO <|-- StringIO
IO <|-- BasicSocketRuby memiliki tiga stream standar yang selalu tersedia sejak program dimulai:
# Stream standar — selalu tersedia, tidak perlu dibuka manual
$stdin # => #<IO:<STDIN>> — membaca input dari keyboard / pipe
$stdout # => #<IO:<STDOUT>> — menulis output normal
$stderr # => #<IO:<STDERR>> — menulis pesan error
# Konstanta alias
STDIN == $stdin # true
STDOUT == $stdout # true
STDERR == $stderr # true
# puts, print, p semuanya menulis ke $stdout secara default
$stdout.puts "Halo" # sama dengan puts "Halo"
$stderr.puts "Error!" # menulis ke stderr, tidak tercampur stdout
# Redirect $stdout ke file (berguna untuk logging sementara)
$stdout = File.open("output.log", "w")
puts "Ini masuk ke file"
$stdout = STDOUT # kembalikan ke semula
Mode Pembukaan File #
Mode pembukaan file menentukan operasi apa yang boleh dilakukan dan apa yang terjadi pada konten yang sudah ada. Memilih mode yang salah adalah kesalahan umum yang bisa merusak data.
| Mode | Keterangan | Posisi Awal | Buat Jika Tidak Ada | Hapus Konten |
|---|---|---|---|---|
"r" | Read only | Awal | Tidak (error) | Tidak |
"w" | Write only | Awal | Ya | Ya |
"a" | Append only | Akhir | Ya | Tidak |
"r+" | Read + Write | Awal | Tidak (error) | Tidak |
"w+" | Read + Write | Awal | Ya | Ya |
"a+" | Read + Append | Akhir | Ya | Tidak |
"b" | Binary (suffix) | — | — | — |
# Mode "r" — hanya baca, file harus sudah ada
File.open("config.txt", "r") do |f|
puts f.read
end
# Mode "w" — tulis baru, MENGHAPUS konten lama!
File.open("output.txt", "w") do |f|
f.write("Konten baru")
end
# Mode "a" — tambahkan di akhir, konten lama aman
File.open("log.txt", "a") do |f|
f.puts "#{Time.now} — entri baru"
end
# Mode binary — untuk file non-teks (gambar, PDF, dll)
File.open("gambar.png", "rb") do |f|
data = f.read
puts "Ukuran: #{data.bytesize} bytes"
end
Mode"w"dan"w+"langsung menghapus seluruh konten file saat file dibuka, bahkan sebelum kamu menulis apapun. Jika kamu bermaksud menambahkan konten, gunakan mode"a". Kesalahan ini sangat umum dan bisa menghilangkan data penting.
Membaca File #
Ruby menyediakan beberapa cara membaca file, masing-masing cocok untuk skenario yang berbeda. Pilihan yang tepat berdampak langsung pada penggunaan memori program.
Membaca Sekaligus #
# File.read — cara paling sederhana, baca semua sekaligus ke memori
konten = File.read("artikel.txt")
puts konten
# File.readlines — baca semua baris ke dalam array
baris = File.readlines("daftar.txt")
baris.each { |b| puts b.chomp }
# File.readlines dengan chomp otomatis (Ruby 2.4+)
baris = File.readlines("daftar.txt", chomp: true)
# Baca dengan encoding tertentu
konten = File.read("arab.txt", encoding: "UTF-8")
Membaca Baris per Baris (Efisien Memori) #
# each_line — iterasi baris tanpa muat semua ke memori
File.open("data_besar.csv", "r") do |file|
file.each_line do |baris|
kolom = baris.chomp.split(",")
proses(kolom)
end
end
# foreach — shortcut tanpa perlu open/close manual
File.foreach("data_besar.csv") do |baris|
puts baris.chomp
end
# Baca N bytes sekaligus — untuk file biner atau streaming
File.open("video.mp4", "rb") do |f|
while (chunk = f.read(4096)) # baca 4KB sekaligus
proses_chunk(chunk)
end
end
Membaca dengan Posisi (Seek) #
File.open("data.bin", "rb") do |f|
# Pindah ke posisi byte ke-100
f.seek(100)
puts f.pos # => 100
# Baca 10 bytes dari posisi saat ini
data = f.read(10)
# Pindah relatif dari posisi saat ini
f.seek(50, IO::SEEK_CUR)
# Pindah dari akhir file
f.seek(-20, IO::SEEK_END)
# Kembali ke awal
f.rewind
puts f.pos # => 0
end
flowchart TD
A[Perlu baca file] --> B{Seberapa besar\nfile-nya?}
B -- "Kecil < 10MB" --> C{Butuh semua\nbaris sekaligus?}
B -- "Besar > 10MB" --> D[each_line atau\nread dengan chunk]
C -- Ya --> E["File.readlines"]
C -- Tidak --> F["File.read"]
D --> G{Format terstruktur?}
G -- "CSV" --> H["CSV.foreach"]
G -- "Baris teks" --> I["File.foreach"]
G -- "Biner / custom" --> J["read(chunk_size)"]Menulis File #
Menulis ke file juga punya beberapa pendekatan, tergantung apakah kamu menulis sekaligus atau secara bertahap.
# File.write — tulis sekaligus, mengembalikan jumlah bytes ditulis
bytes = File.write("output.txt", "Konten artikel\n")
puts bytes # => 16
# File.write dengan mode append
File.write("log.txt", "Baris baru\n", mode: "a")
# Menulis bertahap dengan open block
File.open("laporan.txt", "w") do |f|
f.puts "Laporan Harian" # puts menambahkan newline otomatis
f.puts "=" * 30
f.write "Tanpa newline otomatis"
f.print "Alias dari write"
f.printf "Nama: %-10s Nilai: %d\n", "Ahmad", 95
end
# Shovel operator untuk append
File.open("log.txt", "a") do |f|
f << "#{Time.now}: Pesan log\n"
f << "Baris berikutnya\n"
end
# flush — paksa tulis buffer ke disk sebelum close
File.open("penting.txt", "w") do |f|
f.write("Data kritis")
f.flush # pastikan sudah di disk, bukan di buffer OS
end
Menulis dengan sync #
# Aktifkan sync — setiap write langsung ke disk, tanpa buffer
File.open("realtime.log", "a") do |f|
f.sync = true
loop do
f.puts "#{Time.now}: status OK"
sleep 1
end
end
Pengelolaan Resource dengan Block #
Salah satu sumber bug yang paling sering diabaikan adalah lupa menutup file. Ruby menyelesaikan masalah ini dengan idiom block yang menjamin file selalu ditutup.
# ANTI-PATTERN: membuka file tanpa block
file = File.open("data.txt", "r")
konten = file.read
# ... kode lain yang mungkin raise exception ...
file.close # baris ini mungkin tidak pernah dieksekusi!
# BENAR: gunakan block — file otomatis ditutup bahkan jika ada exception
File.open("data.txt", "r") do |file|
konten = file.read
puts konten
end
# file sudah pasti tertutup di sini
# BENAR: atau gunakan shortcut class method
konten = File.read("data.txt") # buka, baca, tutup otomatis
sequenceDiagram
participant Program
participant OS
participant File
Note over Program: Tanpa block (berbahaya)
Program->>OS: File.open("data.txt")
OS-->>Program: file descriptor
Program->>File: file.read
Program-xFile: Exception! close() tidak dipanggil
Note over OS: File descriptor bocor!
Note over Program: Dengan block (aman)
Program->>OS: File.open("data.txt") do |f|
OS-->>Program: file descriptor
Program->>File: f.read
File-->>Program: konten
Program->>OS: ensure: f.close()
Note over OS: File descriptor dibebaskanSetiap file descriptor yang tidak ditutup adalah resource OS yang bocor. Pada program yang membuka banyak file atau berjalan lama, ini bisa menyebabkan error Too many open files.
Operasi Metadata File #
Selain membaca dan menulis konten, sering kali kamu perlu mengecek informasi tentang file itu sendiri — apakah file ada, berapa ukurannya, kapan terakhir dimodifikasi.
# Pengecekan keberadaan
File.exist?("config.yml") # => true / false
File.file?("config.yml") # => true jika regular file
File.directory?("folder/") # => true jika direktori
File.symlink?("link_ke_file") # => true jika symbolic link
File.readable?("data.txt") # => true jika bisa dibaca
File.writable?("log.txt") # => true jika bisa ditulis
File.executable?("script.rb") # => true jika executable
# Ukuran file
File.size("video.mp4") # => ukuran dalam bytes
File.zero?("kosong.txt") # => true jika file berukuran 0
# Stat — informasi lengkap
stat = File.stat("data.txt")
stat.size # ukuran bytes
stat.mtime # waktu modifikasi terakhir (Time object)
stat.atime # waktu akses terakhir
stat.ctime # waktu perubahan status
stat.mode # permission bits
stat.owned? # apakah dimiliki proses saat ini
# Path manipulation
File.basename("/home/user/dokumen/laporan.pdf") # => "laporan.pdf"
File.basename("/home/user/dokumen/laporan.pdf", ".pdf") # => "laporan"
File.dirname("/home/user/dokumen/laporan.pdf") # => "/home/user/dokumen"
File.extname("laporan.pdf") # => ".pdf"
File.split("/home/user/laporan.pdf") # => ["/home/user", "laporan.pdf"]
# Expand path — jadikan path absolut
File.expand_path("../config", __FILE__)
File.expand_path("~/.bashrc") # expand home directory
Operasi File Sistem #
Selain membaca/menulis konten, Ruby juga menyediakan method untuk memanipulasi file sebagai entitas dalam sistem file.
# Salin file
FileUtils.cp("asli.txt", "salinan.txt")
FileUtils.cp_r("folder_asal/", "folder_tujuan/") # rekursif
# Pindah / rename
FileUtils.mv("lama.txt", "baru.txt")
File.rename("lama.txt", "baru.txt") # alternatif built-in
# Hapus file
File.delete("temp.txt")
FileUtils.rm_f("mungkin_ada.txt") # tidak error jika tidak ada
FileUtils.rm_rf("folder_temp/") # hapus rekursif (HATI-HATI!)
# Buat direktori
Dir.mkdir("folder_baru")
FileUtils.mkdir_p("a/b/c/d") # buat termasuk parent (seperti mkdir -p)
# Cek dan buat jika belum ada
FileUtils.mkdir_p("logs") unless Dir.exist?("logs")
# Ubah permission
File.chmod(0o755, "script.rb") # rwxr-xr-x
FileUtils.chmod_R(0o644, "folder/") # rekursif
# Buat symbolic link
File.symlink("target.txt", "link.txt")
# Buat file temporer — otomatis dihapus setelah block selesai
require "tempfile"
Tempfile.create("prefix") do |f|
f.write("Data sementara")
f.flush
proses_file(f.path)
end
# file otomatis dihapus
Operasi Direktori #
Mengiterasi isi direktori dan mencari file dengan pola tertentu adalah operasi yang sangat umum dalam scripting dan tooling.
# Daftar isi direktori
Dir.entries(".") # => [".", "..", "file1.rb", "folder1", ...]
Dir.children(".") # => ["file1.rb", "folder1", ...] (tanpa . dan ..)
# Glob — pencarian dengan wildcard
Dir.glob("*.rb") # semua file .rb di direktori saat ini
Dir.glob("**/*.rb") # semua .rb rekursif
Dir.glob("config/*.{yml,yaml}") # file yml atau yaml di folder config
Dir.glob("test/*_test.rb") # file dengan suffix _test.rb
# Shortcut [] sama dengan glob
Dir["**/*.log"]
# Iterasi direktori
Dir.each_child(".") do |nama|
puts nama
end
# Pindah direktori kerja
Dir.chdir("/tmp") do
puts Dir.pwd # => "/tmp"
# operasi dalam tmp
end
puts Dir.pwd # kembali ke direktori semula
# Direktori saat ini dan home
Dir.pwd # => "/home/user/proyek"
Dir.home # => "/home/user"
Dir.home("root") # => "/root" (home user tertentu)
# Pola idiomatis: proses semua file Ruby dalam proyek
Dir.glob("**/*.rb").each do |path|
konten = File.read(path)
baris_count = konten.lines.count
puts "#{path}: #{baris_count} baris"
end
# Cari file terbesar di direktori
file_terbesar = Dir.glob("**/*")
.select { |f| File.file?(f) }
.max_by { |f| File.size(f) }
puts "File terbesar: #{file_terbesar} (#{File.size(file_terbesar)} bytes)"
StringIO — IO Berbasis Memori #
StringIO memungkinkan kamu menggunakan API IO yang sama persis, tapi dengan String sebagai backing store — bukan file di disk. Ini sangat berguna untuk testing, buffering output, dan memproses teks seolah-olah dari file.
require "stringio"
# Buat StringIO seperti membuka file
sio = StringIO.new("Baris pertama\nBaris kedua\nBaris ketiga\n")
# Semua method IO tersedia
sio.readline # => "Baris pertama\n"
sio.pos # => 15
sio.rewind # kembali ke awal
sio.read # => seluruh konten
# StringIO untuk tulis
output = StringIO.new
output.puts "Header"
output.puts "=" * 20
output.puts "Konten laporan"
puts output.string # ambil hasilnya sebagai String
StringIO untuk Testing #
StringIO sangat berguna untuk mengisolasi kode yang menulis ke $stdout atau $stderr dalam unit test:
require "stringio"
# Capture output yang biasanya ke stdout
def capture_output
old_stdout = $stdout
$stdout = StringIO.new
yield
$stdout.string
ensure
$stdout = old_stdout
end
# Dalam test
output = capture_output do
puts "Halo dari fungsi yang dites"
print "Baris kedua"
end
puts output # => "Halo dari fungsi yang dites\nBaris kedua"
flowchart LR
A[Kode yang menggunakan IO] --> B{Tujuan IO?}
B -- "File nyata" --> C["File.open(path)"]
B -- "Test / buffer" --> D["StringIO.new"]
B -- "Output terminal" --> E["$stdout / STDOUT"]
C --> F[Disk]
D --> G[RAM / String]
E --> H[Terminal]Penanganan Error IO #
Operasi IO selalu bisa gagal — file tidak ditemukan, permission ditolak, disk penuh, atau koneksi jaringan terputus. Program yang robust harus menangani semua kemungkinan ini dengan tepat.
# Hierarki exception IO
# Exception
# └── StandardError
# └── IOError
# ├── EOFError
# └── SystemCallError (Errno::*)
# ├── Errno::ENOENT (file tidak ditemukan)
# ├── Errno::EACCES (permission ditolak)
# ├── Errno::ENOSPC (disk penuh)
# ├── Errno::EISDIR (target adalah direktori)
# └── Errno::EEXIST (file sudah ada)
# Penanganan error yang spesifik
begin
File.open("rahasia.txt", "r") do |f|
puts f.read
end
rescue Errno::ENOENT => e
puts "File tidak ditemukan: #{e.message}"
rescue Errno::EACCES => e
puts "Akses ditolak: #{e.message}"
rescue IOError => e
puts "Error IO: #{e.message}"
end
# Mengecek tanpa exception (untuk kondisi yang diprediksi)
def baca_config(path)
return nil unless File.exist?(path)
return nil unless File.readable?(path)
File.read(path)
end
# EOFError — saat membaca melampaui akhir file
File.open("data.txt") do |f|
loop do
begin
baris = f.readline
proses(baris)
rescue EOFError
break
end
end
end
# Cara lebih idiomatis untuk iterasi sampai EOF
File.open("data.txt") do |f|
f.each_line { |baris| proses(baris) }
end
# ANTI-PATTERN: rescue Exception terlalu luas
begin
File.write("/root/system.conf", konten)
rescue Exception => e # menangkap SEMUA, termasuk interrupt!
puts "Gagal"
end
# BENAR: rescue exception yang spesifik
begin
File.write("/root/system.conf", konten)
rescue Errno::EACCES
puts "Permission ditolak — butuh akses root"
rescue Errno::ENOSPC
puts "Disk penuh — bersihkan ruang terlebih dahulu"
rescue IOError => e
puts "Error IO tidak terduga: #{e.message}"
end
Buffering dan Performa IO #
Ruby melakukan buffering IO secara default untuk efisiensi. Memahami buffering membantu kamu menghindari masalah data yang hilang atau output yang terlambat muncul.
# sync=false (default) — buffer diisi dulu, baru ditulis ke OS
# sync=true — setiap write langsung ke OS
# Masalah buffering yang umum: output tidak muncul saat crash
$stdout.sync = true # aktifkan jika butuh output real-time
# WAJIB saat deploy ke heroku / container yang line-buffer stdout
# Biasanya dengan: STDOUT.sync = true di awal program, atau
# environment variable: RUBY_STDOUT_SYNC=1
# flush — paksa drain buffer sekarang
$stdout.flush
# Cara menulis data besar dengan efisien
File.open("besar.txt", "w") do |f|
10_000.times do |i|
f.print "Baris #{i}\n" # buffer otomatis dikumpulkan
end
# flush terjadi otomatis saat block berakhir dan file ditutup
end
# Untuk log file yang harus segera tertulis
File.open("audit.log", "a") do |f|
f.sync = true
f.puts "#{Time.now.iso8601}: #{pesan}"
end
| Pengaturan | Kapan Digunakan |
|---|---|
sync = false (default) | Menulis data besar secara batch, performa lebih baik |
sync = true | Log real-time, output yang harus langsung terlihat |
flush manual | Titik tertentu di mana data harus aman di disk |
fsync | Data kritis yang harus benar-benar tersimpan ke hardware |
Pola Idiomatis IO di Ruby #
Setelah memahami semua mekanisme IO, berikut pola-pola yang sering dipakai dalam kode Ruby produksi.
Baca-Transformasi-Tulis #
# Pipeline klasik: baca, proses, tulis
File.open("output.txt", "w") do |out|
File.foreach("input.txt") do |baris|
out.puts baris.chomp.upcase.gsub(/\s+/, "_")
end
end
Atomic Write — Tulis Aman Tanpa Kerusakan Data #
# ANTI-PATTERN: tulis langsung ke file tujuan
# Jika program crash di tengah, file menjadi rusak/parsial
File.open("config.yml", "w") do |f|
f.write(konten_baru) # crash di sini = config.yml rusak!
end
# BENAR: tulis ke file temporer dulu, lalu rename (atomic di OS)
require "tempfile"
def atomic_write(path, konten)
dir = File.dirname(path)
Tempfile.create("atomic", dir) do |tmp|
tmp.write(konten)
tmp.flush
tmp.fsync
File.rename(tmp.path, path)
end
end
atomic_write("config.yml", konten_baru)
Proses File CSV Besar #
require "csv"
# ANTI-PATTERN: baca semua ke memori
semua = CSV.read("data_besar.csv", headers: true)
semua.each { |row| proses(row) } # bisa OOM untuk file GB
# BENAR: streaming per baris
CSV.foreach("data_besar.csv", headers: true) do |row|
proses(row["nama"], row["email"])
end
Log Rotasi Sederhana #
def tulis_log(pesan, path: "app.log", maks_size: 10 * 1024 * 1024)
# Rotasi jika file terlalu besar
if File.exist?(path) && File.size(path) > maks_size
File.rename(path, "#{path}.#{Time.now.strftime('%Y%m%d%H%M%S')}")
end
File.open(path, "a") do |f|
f.sync = true
f.puts "[#{Time.now.iso8601}] #{pesan}"
end
end
Kapan Beralih ke Pendekatan Lain #
Tetap gunakan IO/File bawaan jika:
✓ Membaca dan menulis file teks biasa
✓ Operasi file sistem (copy, move, delete, stat)
✓ Streaming data besar baris per baris
✓ Membuat file temporer
✓ Log sederhana ke file
Pertimbangkan library/pendekatan lain jika:
✗ Parsing CSV kompleks — gunakan library csv dari stdlib
✗ File ZIP/TAR — gunakan Zip gem atau Zlib bawaan
✗ Akses database — gunakan adapter spesifik (PG, MySQL2, SQLite3)
✗ IO jaringan (HTTP) — gunakan Net::HTTP atau Faraday/HTTParty
✗ Watch perubahan file — gunakan Listen gem atau inotify
✗ File format biner kompleks (PDF, XLSX) — gunakan library spesifik
Ringkasan #
- Selalu gunakan block dengan
File.open— menjamin file selalu ditutup bahkan jika ada exception, menghindari kebocoran file descriptor.- Pilih mode pembukaan dengan benar —
"w"menghapus konten lama secara instan; gunakan"a"untuk append dan"r+"untuk edit tanpa menghapus.File.foreachvsFile.read— gunakanforeachataueach_lineuntuk file besar agar tidak membebani memori;readhanya untuk file kecil.StringIOuntuk testing — ganti$stdout/$stderrdenganStringIO.newdi test untuk menangkap output tanpa menulis ke disk.- Atomic write untuk data kritis — tulis ke file temporer lalu
rename, bukan langsung ke file tujuan, untuk mencegah data rusak jika program crash.sync = trueuntuk log real-time — aktifkan saat output harus langsung terlihat, terutama di lingkungan container yang buffering stdout secara default.- Rescue exception IO yang spesifik — tangani
Errno::ENOENT,Errno::EACCES, danErrno::ENOSPCsecara terpisah agar pesan error bermakna bagi pengguna.Dir.globuntuk pencarian file — lebih ekspresif dan aman daripadaDir.entriesdengan filter manual; mendukung wildcard*,**, dan{a,b}.FileUtils.mkdir_puntuk buat direktori — setaramkdir -p, membuat seluruh tree direktori sekaligus tanpa error jika sudah ada.