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
Menulis ke File #
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")
Menulis dengan Buffer dan Flush #
# 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 #
putsvsp—putsuntuk output user-friendly,puntuk debugging (tampilkan dengan inspect),$stdout.sync = trueuntuk output real-time — tanpa ini, output mungkin di-buffer dan tidak muncul langsung.File.opendengan blok selalu — auto-close file meskipun terjadi exception, tidak perluclosemanual.File.foreachuntuk file besar — membaca baris per baris tanpa memuat semua ke memori; jauh lebih aman dariFile.readlinesuntuk file besar.File.read,File.write,File.foreachadalah shortcut class method yang paling sering digunakan untuk operasi file sederhana.Pathnamemembuat manipulasi path lebih ekspresif dengan operator/dan method yang berorientasi objek.FileUtilsuntuk operasi tingkat tinggi —cp_r,mkdir_p,rm_rfyang tidak ada diFiledanDirbiasa.- Mode file
"rb"dan"wb"untuk binary — penting untuk file non-teks seperti gambar, PDF, atau data biner.StringIOuntuk testing I/O — bisa menangkap output atau mensuplai input tanpa menggunakan file sistem sungguhan.Open3.capture3untuk menjalankan perintah eksternal secara aman — tangkap stdout, stderr, dan exit status sekaligus.