I/O

I/O #

Input/Output adalah fondasi dari hampir semua program yang berguna — membaca konfigurasi dari file, menulis log, memproses CSV besar, mengambil input dari pengguna, atau menjalankan perintah shell. Ruby menyediakan hierarki kelas I/O yang kaya: IO sebagai kelas dasar, File untuk operasi file sistem, Dir untuk direktori, Pathname untuk manipulasi path yang lebih ekspresif, dan StringIO untuk I/O ke string di memori. Yang membuat I/O Ruby idiomatik adalah penggunaan blok untuk manajemen resource otomatis — File.open dengan blok memastikan file selalu ditutup meskipun terjadi exception. Artikel ini membahas semua aspek I/O dari output sederhana ke layar hingga pemrosesan file besar yang efisien.

Output ke Layar #

Ruby punya beberapa method untuk menulis ke stdout, masing-masing dengan perilaku berbeda:

# puts — tambahkan newline di akhir, array dicetak per baris
puts "Halo, dunia!"       # => "Halo, dunia!\n"
puts [1, 2, 3]            # => "1\n2\n3\n"
puts nil                  # => hanya newline kosong
puts                      # => newline saja

# print — tidak tambahkan newline
print "Nama: "
print "Budi"
print "\n"    # harus manual

# p — cetak dengan inspect, cocok untuk debugging
p "halo"        # => "halo"  (dengan tanda kutip)
p [1, 2, 3]     # => [1, 2, 3]
p nil           # => nil  (bukan baris kosong seperti puts)
p 42            # => 42
# p mengembalikan nilai argumennya — berguna untuk debug di tengah chain
hasil = [1, 2, 3].map { |n| n * 2 }.tap { |a| p a }.select { |n| n > 3 }

# pp — pretty print, lebih rapi untuk struktur data kompleks
require 'pp'
pp({ nama: "Rina", alamat: { kota: "Bandung", kode_pos: "40111" }, aktif: true })
# Output yang diformat dengan indentasi

# printf — format output seperti C
printf("%-15s %5d %8.2f\n", "Laptop", 5, 15_000_000.0)
printf("%-15s %5d %8.2f\n", "Mouse", 50, 350_000.0)
# => Laptop            5 15000000.00
# => Mouse            50   350000.00

# sprintf / format — buat string terformat tanpa langsung cetak
baris = format("%-15s %5d", "Monitor", 10)
puts baris

# $stdout vs STDOUT
$stdout.puts "Ke stdout"    # sama dengan puts
$stderr.puts "Ke stderr"    # ke error stream — tidak tercampur dengan output biasa
STDERR.puts "Error!"        # konstanta, sama dengan $stderr

Flush Buffer #

Output Ruby di-buffer secara default — output mungkin tidak langsung muncul di layar:

# ANTI-PATTERN: buffer menyebabkan output tidak muncul di tengah proses panjang
10.times do |i|
  print "Memproses #{i}..."
  sleep 0.5
  puts " selesai"
end
# Output muncul setelah semua selesai, bukan real-time!

# BENAR: flush setelah setiap output yang penting
10.times do |i|
  print "Memproses #{i}..."
  $stdout.flush    # atau: STDOUT.flush
  sleep 0.5
  puts " selesai"
end

# Atau: nonaktifkan buffering secara global
$stdout.sync = true   # setiap puts/print langsung di-flush

# Cara idiomatik — sync di awal program
STDOUT.sync = true

Input dari Pengguna #

# gets — baca satu baris input termasuk newline
print "Masukkan nama kamu: "
nama = gets.chomp    # chomp menghapus newline di akhir
puts "Halo, #{nama}!"

# gets tanpa chomp — ada \n di akhir
baris = gets
puts baris.length    # panjangnya termasuk karakter \n

# STDIN.gets — eksplisit dari stdin (berguna jika ada ARGV)
nama = STDIN.gets.chomp

# Baca integer dari input
print "Masukkan angka: "
angka = Integer(gets.chomp) rescue nil
if angka
  puts "Dua kali lipat: #{angka * 2}"
else
  puts "Input bukan angka yang valid"
end

# Loop input sampai kondisi terpenuhi
loop do
  print "Masukkan 'keluar' untuk berhenti: "
  input = gets&.chomp   # &. karena gets bisa nil jika EOF
  break if input.nil? || input.downcase == "keluar"
  puts "Kamu ketik: #{input}"
end

ARGV — Argumen Command Line #

# Akses argumen command line
# Jalankan: ruby program.rb file1.txt file2.txt --verbose

puts ARGV.inspect         # => ["file1.txt", "file2.txt", "--verbose"]
puts ARGV.length          # => 3

# Proses argumen sederhana
verbose = ARGV.delete("--verbose")   # hapus flag dan kembalikan nilainya
file_paths = ARGV   # sisanya adalah nama file

puts "Mode verbose: #{!verbose.nil?}"
puts "File yang akan diproses: #{file_paths.join(', ')}"

# Parsing argumen yang lebih lengkap dengan OptionParser
require 'optparse'

opsi = {}
parser = OptionParser.new do |opts|
  opts.banner = "Penggunaan: program.rb [opsi] file..."

  opts.on("-v", "--verbose", "Mode verbose") do
    opsi[:verbose] = true
  end

  opts.on("-o", "--output NAMA_FILE", "File output") do |f|
    opsi[:output] = f
  end

  opts.on("-n", "--jumlah N", Integer, "Jumlah baris") do |n|
    opsi[:jumlah] = n
  end

  opts.on("-h", "--help", "Tampilkan bantuan") do
    puts opts
    exit
  end
end

parser.parse!   # parse dan hapus opsi dari ARGV
puts opsi.inspect
puts "Sisa argumen: #{ARGV.inspect}"

Membaca File #

Cara Membaca — Pilih yang Tepat #

# File.read — baca seluruh isi file ke String
# COCOK untuk file kecil, BERBAHAYA untuk file besar!
konten = File.read("config.json")
puts konten

# File.readlines — baca semua baris ke Array of String
baris = File.readlines("data.txt")
puts "Jumlah baris: #{baris.length}"
baris.each { |b| puts b.chomp }

# File.readlines dengan chomp: true — buang newline otomatis (Ruby 2.4+)
baris = File.readlines("data.txt", chomp: true)
baris.each { |b| puts b }   # tanpa \n di akhir

# File.foreach — baca satu baris per iterasi, TIDAK load semua ke memori
# CARA TERBAIK untuk file besar!
File.foreach("data_besar.csv") do |baris|
  proses(baris.chomp)
end

# File.foreach dengan chomp
File.foreach("data.txt", chomp: true) { |b| puts b }

# Lazy evaluation pada file besar — proses dengan filter
File.foreach("access.log")
    .lazy
    .select { |baris| baris.include?("ERROR") }
    .map    { |baris| baris.chomp }
    .first(10)
    .each   { |b| puts b }
flowchart TD
    A[Perlu membaca file] --> B{Ukuran file?}
    B --> C["Kecil\n< 10MB"]
    B --> D["Besar\n> 10MB atau tidak tahu"]
    C --> E{Butuh semua baris?}
    E --> F["Ya → File.readlines"]
    E --> G["Tidak → File.read\natau File.foreach"]
    D --> H["File.foreach\natau IO.foreach\nBaca baris per baris"]
    H --> I["Tidak load semua\nke memori sekaligus"]

File.open dengan Blok — Paling Idiomatik #

# ANTI-PATTERN: buka file tanpa blok — harus close manual
file = File.open("data.txt", "r")
konten = file.read
file.close   # mudah terlupa, dan tidak jalan jika ada exception sebelumnya!

# BENAR: File.open dengan blok — otomatis close saat blok selesai
File.open("data.txt", "r") do |file|
  file.each_line do |baris|
    puts baris.chomp
  end
end
# file.closed? => true di sini

# Membaca dengan encoding eksplisit
File.open("data_utf8.txt", "r:UTF-8") do |f|
  puts f.read
end

File.open("data_latin.txt", "r:ISO-8859-1:UTF-8") do |f|
  # baca sebagai ISO-8859-1, konversi ke UTF-8
  puts f.read
end

# Baca sejumlah byte tertentu
File.open("binary.dat", "rb") do |f|   # rb = read binary
  header = f.read(4)   # baca 4 byte pertama
  puts header.bytes.map { |b| format("%02X", b) }.join(" ")
end

Method Baca pada Objek File #

File.open("data.txt") do |f|
  # Posisi dan navigasi
  puts f.pos          # posisi pointer saat ini (byte)
  puts f.size         # ukuran file dalam byte
  puts f.eof?         # apakah sudah di akhir file?

  # Baca dengan berbagai granularitas
  karakter = f.getc        # baca satu karakter
  byte     = f.getbyte     # baca satu byte
  baris    = f.gets        # baca satu baris (termasuk \n)
  baris    = f.readline    # seperti gets, tapi raise EOFError di akhir
  chunk    = f.read(1024)  # baca 1024 byte

  # Navigasi pointer
  f.rewind           # kembali ke awal file
  f.seek(100)        # pindah ke byte ke-100
  f.seek(-50, IO::SEEK_END)  # 50 byte sebelum akhir
  f.seek(20, IO::SEEK_CUR)   # 20 byte dari posisi saat ini

  # Baca seluruh baris yang tersisa
  sisa_baris = f.readlines
end

Mode File #

# Tabel mode file Ruby
# "r"  — read only, file harus ada
# "w"  — write only, buat baru atau hapus isi jika sudah ada
# "a"  — append, tulis di akhir file, buat jika belum ada
# "r+" — read+write, file harus ada, tidak hapus isi
# "w+" — read+write, buat baru atau hapus isi
# "a+" — read+append, pointer baca dari awal, tulis di akhir
# "rb", "wb", "ab" — mode biner (penting untuk file non-teks)

# Tulis ke file baru (hapus jika sudah ada)
File.open("output.txt", "w") do |f|
  f.puts "Baris pertama"
  f.puts "Baris kedua"
  f.write "Tanpa newline"
  f.write "\n"
  f.print "Juga tanpa newline"
  f.puts   # hanya newline
end

# Tambahkan ke file yang sudah ada
File.open("log.txt", "a") do |f|
  f.puts "[#{Time.now}] Event terjadi"
end

# File.write — shortcut untuk menulis string ke file
File.write("config.json", JSON.pretty_generate(config))

# File.write dengan mode append
File.write("log.txt", "#{Time.now}: Event\n", mode: "a")
# Tulis banyak baris secara efisien — buffer di Ruby, flush di akhir
File.open("hasil.csv", "w") do |f|
  f.puts "nama,umur,kota"   # header

  1000.times do |i|
    f.puts "User#{i},#{rand(20..60)},Kota#{rand(10)}"
    # Tidak perlu flush setiap baris — buffer otomatis flush saat close
  end
end   # flush dan close otomatis di sini

# Flush manual saat butuh real-time write (misalnya log yang dibaca live)
File.open("live_log.txt", "a") do |f|
  loop do
    event = ambil_event()
    f.puts "[#{Time.now}] #{event}"
    f.flush   # pastikan tertulis ke disk, bukan hanya buffer
    sleep 1
  end
end

File Utility Methods #

File kelas punya banyak class method untuk operasi file tanpa harus membuka file:

# Informasi file
puts File.exist?("config.txt")       # => true/false
puts File.file?("config.txt")        # => true (adalah file biasa)
puts File.directory?("data/")        # => true (adalah direktori)
puts File.readable?("config.txt")    # => true (bisa dibaca)
puts File.writable?("config.txt")    # => true (bisa ditulis)
puts File.executable?("script.sh")  # => true (bisa dieksekusi)
puts File.empty?("file.txt")         # => true (file kosong / 0 byte)
puts File.size("data.csv")           # => ukuran dalam byte

# Metadata waktu
puts File.mtime("config.txt")        # waktu terakhir dimodifikasi
puts File.ctime("config.txt")        # waktu inode berubah (metadata)
puts File.atime("config.txt")        # waktu terakhir diakses

# Manipulasi path — tanpa mengakses sistem file
puts File.basename("/path/ke/file.txt")         # => "file.txt"
puts File.basename("/path/ke/file.txt", ".txt") # => "file"
puts File.dirname("/path/ke/file.txt")          # => "/path/ke"
puts File.extname("laporan.xlsx")               # => ".xlsx"
puts File.extname("arsip.tar.gz")               # => ".gz"
puts File.split("/path/ke/file.txt").inspect    # => ["/path/ke", "file.txt"]

# Membangun path — lebih portabel dari string concatenation
puts File.join("data", "2024", "laporan.csv")
# => "data/2024/laporan.csv" (otomatis separator yang tepat per OS)

# Expand path — resolve ~ dan path relatif
puts File.expand_path("~/.config/ruby")         # => "/home/user/.config/ruby"
puts File.expand_path("../data", __FILE__)      # relatif dari file saat ini
puts File.expand_path(".")                      # direktori kerja saat ini

# Operasi file
File.rename("lama.txt", "baru.txt")   # rename/pindah file
File.delete("temp.txt")               # hapus file
File.chmod(0644, "script.rb")         # ubah permission
File.truncate("file.txt", 0)          # kosongkan file tanpa hapus

Pathname — OOP untuk Path #

Pathname menyediakan antarmuka berorientasi objek yang lebih ekspresif untuk operasi file dan path:

require 'pathname'

# Buat Pathname
root    = Pathname.new("/var/app")
config  = Pathname.new("config/database.yml")
home    = Pathname.new("~").expand_path

# Navigasi path — menggunakan operator / seperti filesystem
log_dir = root / "log"                   # => Pathname: /var/app/log
access_log = root / "log" / "access.log" # => Pathname: /var/app/log/access.log

puts access_log.dirname    # => /var/app/log
puts access_log.basename   # => access.log
puts access_log.extname    # => .log
puts access_log.exist?     # => true/false
puts access_log.size       # => byte

# Baca dan tulis langsung
konten = (root / "config.json").read
(root / "output.txt").write("hasil")
(root / "log.txt").open("a") { |f| f.puts "entry baru" }

# Iterasi direktori
(root / "log").children.each do |path|
  puts "#{path.basename}: #{path.size} bytes" if path.file?
end

# Glob
(root / "data").glob("**/*.csv").each do |csv|
  puts csv.relative_path_from(root)
end

# Konversi
puts access_log.to_s        # => "/var/app/log/access.log" (String)
puts access_log.to_path     # => "/var/app/log/access.log"

Operasi Direktori #

Dir — Listing dan Navigasi #

# Direktori kerja saat ini
puts Dir.pwd   # => "/home/user/projects/app"

# Pindah direktori (hanya dalam proses ini)
Dir.chdir("/tmp")
puts Dir.pwd   # => "/tmp"
Dir.chdir("/home/user/projects/app")  # kembali

# Pindah sementara dalam blok
Dir.chdir("/tmp") do
  puts Dir.pwd   # => "/tmp"
  # lakukan sesuatu di /tmp
end
puts Dir.pwd   # kembali ke direktori semula

# Isi direktori
puts Dir.entries(".").inspect           # termasuk "." dan ".."
puts Dir.children(".").inspect          # tanpa "." dan ".."
puts Dir["*.rb"].inspect               # hanya file .rb (alias glob)
puts Dir.glob("**/*.rb").inspect       # rekursif semua .rb

# Glob patterns
Dir.glob("data/*.csv")                 # semua CSV di folder data/
Dir.glob("**/*.{rb,rake}")             # semua .rb dan .rake di semua subfolder
Dir.glob("[0-9][0-9][0-9][0-9]/")      # folder yang namanya 4 digit angka
Dir.glob("log/*", File::FNM_DOTMATCH)  # termasuk file dot (hidden)

# Buat direktori
Dir.mkdir("backup")                    # buat satu level
FileUtils.mkdir_p("a/b/c/d")          # buat semua level sekaligus (rekursif)

FileUtils — Operasi File Tingkat Tinggi #

FileUtils menyediakan operasi file yang lebih powerful dari File dan Dir biasa:

require 'fileutils'

# Copy
FileUtils.cp("source.txt", "dest.txt")                 # copy file
FileUtils.cp_r("src_dir/", "dest_dir/")               # copy direktori rekursif

# Move / rename
FileUtils.mv("lama.txt", "baru.txt")                  # pindah/rename file
FileUtils.mv("data/", "backup/data/")                 # pindah direktori

# Hapus
FileUtils.rm("file.txt")                               # hapus file
FileUtils.rm_f("mungkin_tidak_ada.txt")               # hapus, abaikan jika tidak ada
FileUtils.rm_r("direktori/")                           # hapus direktori rekursif
FileUtils.rm_rf("direktori/")                          # hapus, abaikan error (rm -rf)

# Buat direktori
FileUtils.mkdir("satu_level/")
FileUtils.mkdir_p("banyak/level/sekaligus/")          # mkdir -p

# Ubah permission dan owner
FileUtils.chmod(0755, "script.sh")
FileUtils.chmod_R(0644, "data/")                      # rekursif
FileUtils.chown("user", "group", "file.txt")

# Touch — update timestamp atau buat file kosong
FileUtils.touch("file.txt")                            # buat atau update mtime
FileUtils.touch(["a.txt", "b.txt", "c.txt"])          # banyak file sekaligus

# Opsi verbose dan noop (dry run)
FileUtils.rm_rf("dir/", verbose: true)   # cetak perintah yang dijalankan
FileUtils.cp_r("src/", "dst/", noop: true, verbose: true)  # simulasi saja

StringIO — I/O ke String di Memori #

StringIO memungkinkan kamu menggunakan API IO pada string di memori — sangat berguna untuk testing:

require 'stringio'

# Tulis ke string (bukan file)
buffer = StringIO.new
buffer.puts "Baris pertama"
buffer.puts "Baris kedua"
buffer.write "Data: #{42}\n"

puts buffer.string   # ambil isi sebagai String
# => "Baris pertama\nBaris kedua\nData: 42\n"

# Baca dari string
sumber = StringIO.new("apel\nmangga\njeruk\n")
sumber.each_line { |baris| puts baris.chomp.upcase }

# Sangat berguna untuk testing — stub $stdout
def tangkap_output
  buffer = StringIO.new
  $stdout = buffer
  yield
  buffer.string
ensure
  $stdout = STDOUT
end

output = tangkap_output do
  puts "Ini akan ditangkap"
  p [1, 2, 3]
end
puts output.inspect

Pipe dan Proses Eksternal #

Ruby menyediakan beberapa cara untuk berinteraksi dengan proses sistem:

# Backtick / %x{} — jalankan perintah, tangkap output sebagai String
ls_output = `ls -la`
puts ls_output

hostname = %x{hostname}.chomp
puts "Server: #{hostname}"

# Cek exit status
`git status`
puts $?.exitstatus   # => 0 (sukses) atau non-zero (gagal)

# system — jalankan perintah, output langsung ke terminal
system("clear")
system("git", "status")   # lebih aman dari interpolasi string
puts $?.success?   # => true jika sukses

# IO.popen — buka pipe ke proses eksternal
IO.popen("wc -l", "r+") do |pipe|
  pipe.write("baris pertama\nbaris kedua\nbaris ketiga\n")
  pipe.close_write
  puts pipe.read.strip   # => "3"
end

# Open3 — kontrol penuh atas stdin, stdout, stderr
require 'open3'

stdout, stderr, status = Open3.capture3("git log --oneline -5")
if status.success?
  puts "5 commit terakhir:"
  puts stdout
else
  puts "Error: #{stderr}"
end

# Streaming output dari proses
Open3.popen3("ping -c 4 google.com") do |stdin, stdout, stderr, thread|
  stdout.each_line { |line| print line }
  thread.join
end

Pola I/O yang Idiomatik #

# 1. Selalu gunakan blok untuk File.open
# ANTI-PATTERN: close manual
f = File.open("data.txt")
data = f.read
f.close   # bisa terlupa atau tidak jalan jika ada exception

# BENAR: blok yang auto-close
data = File.open("data.txt") { |f| f.read }
# Atau lebih ringkas:
data = File.read("data.txt")

# 2. File.foreach untuk file besar — tidak load semua ke memori
# ANTI-PATTERN: untuk file besar
semua_baris = File.readlines("log_besar.txt")   # muat semua ke RAM!
semua_baris.each { |b| proses(b) }

# BENAR: satu baris per iterasi
File.foreach("log_besar.txt", chomp: true) { |b| proses(b) }

# 3. Gunakan Pathname untuk kode yang banyak manipulasi path
# ANTI-PATTERN: string concatenation untuk path
path = base_dir + "/" + sub_dir + "/" + filename

# BENAR: Pathname dengan operator /
path = Pathname.new(base_dir) / sub_dir / filename

# 4. require 'fileutils' untuk operasi yang tidak ada di File
# File tidak punya cp_r, mkdir_p, rm_rf — pakai FileUtils

# 5. Error handling yang tepat untuk I/O
def baca_config(path)
  File.read(path)
rescue Errno::ENOENT
  raise "File konfigurasi tidak ditemukan: #{path}"
rescue Errno::EACCES
  raise "Tidak ada izin membaca: #{path}"
rescue Errno::EISDIR
  raise "Path menunjuk ke direktori, bukan file: #{path}"
end

Ringkasan #

  • puts vs print vs pputs untuk output user-friendly, p untuk debugging (tampilkan dengan inspect), print untuk output tanpa newline.
  • $stdout.sync = true untuk output real-time — tanpa ini, output mungkin di-buffer dan tidak muncul langsung.
  • File.open dengan blok selalu — auto-close file meskipun terjadi exception, tidak perlu close manual.
  • File.foreach untuk file besar — membaca baris per baris tanpa memuat semua ke memori; jauh lebih aman dari File.readlines untuk file besar.
  • File.read, File.write, File.foreach adalah shortcut class method yang paling sering digunakan untuk operasi file sederhana.
  • Pathname membuat manipulasi path lebih ekspresif dengan operator / dan method yang berorientasi objek.
  • FileUtils untuk operasi tingkat tinggi — cp_r, mkdir_p, rm_rf yang tidak ada di File dan Dir biasa.
  • Mode file "rb" dan "wb" untuk binary — penting untuk file non-teks seperti gambar, PDF, atau data biner.
  • StringIO untuk testing I/O — bisa menangkap output atau mensuplai input tanpa menggunakan file sistem sungguhan.
  • Open3.capture3 untuk menjalankan perintah eksternal secara aman — tangkap stdout, stderr, dan exit status sekaligus.

← Sebelumnya: Multi Threading   Berikutnya: Socket →

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