IO

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 <|-- BasicSocket

Ruby 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.

ModeKeteranganPosisi AwalBuat Jika Tidak AdaHapus Konten
"r"Read onlyAwalTidak (error)Tidak
"w"Write onlyAwalYaYa
"a"Append onlyAkhirYaTidak
"r+"Read + WriteAwalTidak (error)Tidak
"w+"Read + WriteAwalYaYa
"a+"Read + AppendAkhirYaTidak
"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 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
# 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 dibebaskan

Setiap 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
PengaturanKapan Digunakan
sync = false (default)Menulis data besar secara batch, performa lebih baik
sync = trueLog real-time, output yang harus langsung terlihat
flush manualTitik tertentu di mana data harus aman di disk
fsyncData 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.foreach vs File.read — gunakan foreach atau each_line untuk file besar agar tidak membebani memori; read hanya untuk file kecil.
  • StringIO untuk testing — ganti $stdout/$stderr dengan StringIO.new di 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 = true untuk 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, dan Errno::ENOSPC secara terpisah agar pesan error bermakna bagi pengguna.
  • Dir.glob untuk pencarian file — lebih ekspresif dan aman daripada Dir.entries dengan filter manual; mendukung wildcard *, **, dan {a,b}.
  • FileUtils.mkdir_p untuk buat direktori — setara mkdir -p, membuat seluruh tree direktori sekaligus tanpa error jika sudah ada.

← Sebelumnya: Strings   Berikutnya: Math →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact