Pathname #
Sebelum Pathname, Ruby menyediakan File dan Dir untuk operasi file system — keduanya bekerja dengan string sebagai path. Pendekatan ini berhasil, tapi membuat kode menjadi serangkaian method call yang tidak bisa di-chain dan membutuhkan banyak string manipulation manual. Pathname mengubah cara pandang ini: path bukan sekadar string, melainkan objek dengan method, bisa di-chain, dan bisa di-compose secara alami. Kamu tidak lagi menulis File.join(File.dirname(path), "subdir") — kamu cukup menulis Pathname.new(path).dirname / "subdir". Artikel ini membahas seluruh API Pathname, bagaimana ia bekerja bersama FileUtils, dan kapan ia lebih tepat dibanding File dan Dir.
Membuat Pathname #
Pathname tersedia setelah require "pathname". Cara paling umum membuat Pathname adalah dari string, tapi ada beberapa variasi.
require "pathname"
# Dari string path absolut
home = Pathname.new("/home/user")
config = Pathname.new("/etc/nginx/nginx.conf")
# Dari string path relatif
relative = Pathname.new("lib/utils")
current = Pathname.new(".")
# Shorthand dengan Pathname() kernel method
home = Pathname("/home/user") # sama seperti Pathname.new
# Dari __FILE__ — path file Ruby yang sedang dieksekusi
this_file = Pathname(__FILE__)
this_dir = Pathname(__dir__) # direktori file saat ini
# Pathname adalah immutable — setiap operasi menghasilkan Pathname baru
original = Pathname.new("/home/user")
modified = original / "documents" # tidak mengubah original
# => Pathname("/home/user/documents")
# Konversi bolak-balik dengan String
path_string = home.to_s # => "/home/user"
path_string = home.to_path # => "/home/user" (untuk kompatibilitas IO)
Navigasi dan Komposisi Path #
Operasi yang paling sering dilakukan pada path adalah menggabungkan komponen, mengambil bagian tertentu, dan berpindah antar level direktori.
require "pathname"
base = Pathname.new("/var/www/aplikasi")
# Menggabungkan path dengan / operator
config = base / "config" / "database.yml"
# => Pathname("/var/www/aplikasi/config/database.yml")
logs = base / "log" / "production.log"
# => Pathname("/var/www/aplikasi/log/production.log")
# join — alternatif dari /
base.join("public", "assets", "app.css")
# => Pathname("/var/www/aplikasi/public/assets/app.css")
# Mengambil bagian dari path
config = Pathname.new("/var/www/aplikasi/config/database.yml")
config.dirname # => Pathname("/var/www/aplikasi/config")
config.basename # => Pathname("database.yml")
config.extname # => ".yml"
config.basename(".yml") # => Pathname("database") (tanpa ekstensi)
# split — mengembalikan [dirname, basename]
config.split
# => [Pathname("/var/www/aplikasi/config"), Pathname("database.yml")]
# Navigasi ke parent
config.parent # => Pathname("/var/www/aplikasi/config")
config.parent.parent # => Pathname("/var/www/aplikasi")
# Cek apakah path adalah root
Pathname.new("/").root? # => true
Pathname.new("/home").root? # => false
# Path relatif dan ekspansi
relative = Pathname.new("../config/settings.yml")
absolute = relative.expand_path # resolve terhadap working directory saat ini
# expand_path dengan base directory
relative.expand_path("/var/www/aplikasi")
# => Pathname("/var/www/config/settings.yml")
# Konversi absolut <-> relatif
absolute = Pathname.new("/var/www/aplikasi/public")
base = Pathname.new("/var/www/aplikasi")
# relative_path_from — hitung path relatif dari satu path ke yang lain
absolute.relative_path_from(base)
# => Pathname("public")
Pathname.new("/var/www/aplikasi/config").relative_path_from(base)
# => Pathname("config")
Pathname.new("/var/log").relative_path_from(base)
# => Pathname("../../log")
# cleanpath — bersihkan path dari . dan .. tanpa resolusi symlink
Pathname.new("/var/www/../www/aplikasi/./config").cleanpath
# => Pathname("/var/www/aplikasi/config")
# realpath — resolve symlink dan return absolute path (file/dir harus ada)
# Pathname.new("/etc/nginx").realpath
# => Pathname("/usr/local/etc/nginx") (jika /etc/nginx adalah symlink)
flowchart TD
A["/var/www/app/config/db.yml"] --> B[dirname\n/var/www/app/config]
A --> C[basename\ndb.yml]
A --> D[extname\n.yml]
A --> E[parent\n/var/www/app/config]
B --> F[parent\n/var/www/app]
F --> G["/ 'log'\n/var/www/app/log"]
G --> H["join 'app.log'\n/var/www/app/log/app.log"]Pengecekan Atribut File #
Pathname menyediakan method pengecekan yang lengkap untuk mengetahui sifat-sifat sebuah path — apakah file ada, apakah direktori, berapa ukurannya, dan sebagainya.
require "pathname"
path = Pathname.new("/etc/hosts")
# Keberadaan
path.exist? # => true (jika file ada)
path.file? # => true (jika file reguler, bukan dir atau symlink)
path.directory? # => false (bukan direktori)
path.symlink? # => false (bukan symlink)
path.zero? # => false (bukan file kosong)
path.empty? # => false (alias dari zero? untuk file, cek isi untuk dir)
# Informasi file
path.size # => ukuran dalam byte
path.size? # => ukuran jika > 0, nil jika kosong (berguna untuk kondisional)
# Tipe path
path.absolute? # => true (path absolut)
path.relative? # => false
Pathname.new("config/db.yml").absolute? # => false
Pathname.new("config/db.yml").relative? # => true
# Permission
path.readable? # => true/false
path.writable? # => true/false
path.executable? # => true/false
# Stat — info lengkap
stat = path.stat
stat.size # => ukuran
stat.mtime # => waktu modifikasi terakhir
stat.ctime # => waktu perubahan terakhir
stat.mode # => permission mode
# Pola pengecekan sebelum operasi
def baca_config(path_string)
config = Pathname.new(path_string)
unless config.exist?
raise "File config tidak ditemukan: #{path_string}"
end
unless config.file?
raise "#{path_string} bukan file reguler"
end
unless config.readable?
raise "Tidak punya permission membaca: #{path_string}"
end
config.read
end
# Pengecekan direktori
def pastikan_direktori_ada(path)
dir = Pathname.new(path)
dir.mkpath unless dir.directory? # buat direktori + parent jika belum ada
dir
end
Membaca dan Menulis File #
Pathname mendelegasikan operasi baca/tulis ke IO dan File, tapi dengan antarmuka yang lebih bersih karena path sudah terikat ke objek.
require "pathname"
file = Pathname.new("/tmp/contoh.txt")
# Menulis file
file.write("Hello, Pathname!\n")
file.open("w") { |f| f.write("Konten baru") }
# Menambahkan ke file (append)
file.open("a") { |f| f.puts "Baris tambahan" }
# Membaca file
konten = file.read # seluruh konten sebagai string
baris = file.readlines # array of strings, tiap baris satu elemen
file.each_line { |l| puts l } # iterasi per baris
# Membaca binary
binary_data = file.binread
# Menulis binary
file.binwrite(binary_data)
# Operasi atomik — tulis ke tempfile lalu rename (aman dari crash)
def tulis_atomik(path, konten)
target = Pathname.new(path)
tmp = Pathname.new("#{path}.tmp.#{Process.pid}")
tmp.write(konten)
tmp.rename(target)
end
Iterasi Direktori #
Salah satu fitur terkuat Pathname adalah kemampuan iterasi direktori yang terintegrasi dengan Enumerable — kamu bisa langsung menggunakan map, select, sort, dan semua method Enumerable.
require "pathname"
dir = Pathname.new("/var/www/aplikasi")
# Iterasi isi direktori (satu level)
dir.each_child { |path| puts path }
dir.children # => Array of Pathname (tanpa . dan ..)
# glob — pattern matching seperti Dir.glob
dir.glob("**/*.rb") # semua file .rb secara rekursif
dir.glob("*.{yml,yaml}") # file YAML di satu level
dir.glob("config/*.rb") # file .rb dalam subdirektori config
# Contoh: kumpulkan semua file Ruby dalam proyek
Pathname.new(".")
.glob("**/*.rb")
.reject { |f| f.to_s.include?("vendor/") }
.sort
.each { |f| puts f }
# find — rekursif dengan kontrol penuh
require "find"
dir.find do |path|
# path adalah Pathname di setiap level
Find.prune if path.basename.to_s.start_with?(".") # skip hidden files
puts path if path.file? && path.extname == ".log"
end
# Pemisahan file dan direktori
dir.children.select(&:file?) # hanya file
dir.children.select(&:directory?) # hanya direktori
dir.children.reject(&:symlink?) # kecuali symlink
# Contoh nyata: analyzer struktur proyek Ruby
def analisis_proyek(root)
root = Pathname.new(root)
rb_files = root.glob("**/*.rb")
.reject { |f| f.to_s =~ /vendor|node_modules|\.git/ }
{
total_file: rb_files.count,
total_baris: rb_files.sum { |f| f.readlines.count },
file_terbesar: rb_files.max_by { |f| f.size }&.to_s,
direktori: rb_files.map { |f| f.dirname }.uniq.count,
rata_rata_ukuran: rb_files.sum(&:size) / [rb_files.count, 1].max
}
end
# Contoh output:
# {
# total_file: 47,
# total_baris: 2841,
# file_terbesar: "lib/core/processor.rb",
# direktori: 12,
# rata_rata_ukuran: 3247
# }
Operasi File System #
Pathname menyediakan method untuk operasi file system yang umum, semuanya menggunakan path dari objek itu sendiri.
require "pathname"
sumber = Pathname.new("/tmp/sumber.txt")
tujuan = Pathname.new("/tmp/tujuan.txt")
# Membuat file dan direktori
sumber.write("isi file") # buat file dengan konten
Pathname.new("/tmp/dir/sub").mkpath # buat direktori + semua parent
# Copy dan move — membutuhkan FileUtils
require "fileutils"
FileUtils.cp(sumber, tujuan) # copy file
FileUtils.mv(sumber, tujuan) # pindahkan file
# Rename — sama seperti mv tapi dalam satu filesystem
sumber.rename(tujuan) # built-in Pathname
# Delete
sumber.delete # hapus file
sumber.unlink # alias dari delete
# Untuk direktori
dir = Pathname.new("/tmp/direktori_kosong")
dir.rmdir # hapus direktori kosong
# Membuat symlink
target = Pathname.new("/tmp/link")
target.make_symlink(sumber) # target menunjuk ke sumber
# Mengikuti symlink
link = Pathname.new("/etc/nginx")
link.readlink # => path yang ditunjuk symlink (tanpa resolve)
link.realpath # => path absolut setelah semua symlink di-resolve
# Mengubah permission
Pathname.new("/tmp/script.sh").chmod(0755)
Perbandingan Pathname vs File vs Dir #
Memahami kapan menggunakan masing-masing akan membuat kode lebih bersih dan lebih idiomatis.
| Operasi | File/Dir (lama) | Pathname (modern) |
|---|---|---|
| Gabung path | File.join(a, b, c) | Pathname(a) / b / c |
| Nama file | File.basename(path) | path.basename |
| Direktori induk | File.dirname(path) | path.dirname |
| Ekstensi | File.extname(path) | path.extname |
| Cek ada | File.exist?(path) | path.exist? |
| Cek direktori | File.directory?(path) | path.directory? |
| Baca file | File.read(path) | path.read |
| Tulis file | File.write(path, isi) | path.write(isi) |
| Glob rekursif | Dir.glob("**/*.rb") | path.glob("**/*.rb") |
| Iterasi direktori | Dir.each_child(path) | path.each_child |
# Perbandingan langsung: ambil semua file config di bawah direktori proyek
# ANTI-PATTERN: gaya lama dengan File dan Dir — tidak bisa di-chain
config_dir = File.join(File.dirname(__FILE__), "..", "config")
config_dir = File.expand_path(config_dir)
files = Dir.glob(File.join(config_dir, "**", "*.yml"))
files = files.select { |f| File.file?(f) }
files = files.sort
# BENAR: Pathname — expressif, chainable, OOP
config_dir = Pathname(__FILE__).dirname.parent / "config"
files = config_dir
.glob("**/*.yml")
.select(&:file?)
.sort
# Contoh end-to-end: backup config dengan timestamp
def backup_config(config_path)
source = Pathname.new(config_path)
backup_dir = source.dirname / "backups"
backup_dir.mkpath
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
backup_name = "#{source.basename(".#{source.extname.delete(".")}")}_#{timestamp}#{source.extname}"
destination = backup_dir / backup_name
source.open("r") do |input|
destination.open("w") do |output|
output.write(input.read)
end
end
puts "Backup disimpan ke: #{destination}"
destination
end
Pola Penggunaan di Proyek Ruby #
Beberapa pola yang sering ditemukan di proyek Ruby nyata.
Path Relatif terhadap File Sumber #
# Pattern umum di gem dan aplikasi Ruby
# Mendapatkan path relatif terhadap lokasi file .rb saat ini
LIB_DIR = Pathname(__FILE__).dirname
ROOT_DIR = LIB_DIR.parent
CONFIG_DIR = ROOT_DIR / "config"
DATA_DIR = ROOT_DIR / "data"
# Bisa langsung digunakan
config = CONFIG_DIR / "settings.yml"
require LIB_DIR / "utils" / "helper"
Konfigurasi Berbasis Path #
class Aplikasi
attr_reader :root, :config_dir, :log_dir, :tmp_dir
def initialize(root_path)
@root = Pathname.new(root_path).expand_path
@config_dir = @root / "config"
@log_dir = @root / "log"
@tmp_dir = @root / "tmp"
end
def setup_direktori
[@log_dir, @tmp_dir].each(&:mkpath)
end
def config_untuk(nama)
file = @config_dir / "#{nama}.yml"
raise "Config tidak ditemukan: #{file}" unless file.exist?
YAML.load_file(file.to_s)
end
def log_file(nama)
@log_dir / "#{nama}.log"
end
end
app = Aplikasi.new("/var/www/aplikasiku")
app.setup_direktori
db_config = app.config_untuk("database")
Pencarian File Rekursif #
# Mencari file dengan kriteria kompleks
def cari_file(root, **opsi)
dir = Pathname.new(root)
ekstensi = opsi[:ekstensi]
maks_ukuran = opsi[:maks_ukuran]
dimodifikasi_setelah = opsi[:setelah]
dir.glob("**/*")
.select(&:file?)
.then { |files| ekstensi ? files.select { |f| f.extname == ekstensi } : files }
.then { |files| maks_ukuran ? files.select { |f| f.size <= maks_ukuran } : files }
.then { |files| dimodifikasi_setelah ? files.select { |f| f.stat.mtime > dimodifikasi_setelah } : files }
.sort_by(&:mtime)
end
hasil = cari_file("/var/log",
ekstensi: ".log",
maks_ukuran: 10 * 1024 * 1024, # maks 10 MB
setelah: Time.now - 86400 # dimodifikasi dalam 24 jam terakhir
)
Ringkasan #
- Pathname mengubah path menjadi objek — bukan sekadar string yang diproses dengan method statik
FiledanDir, tapi objek dengan method instance yang bisa di-chain dan di-compose.- Operator
/untuk komposisi path —Pathname("/var/www") / "app" / "config"jauh lebih bersih dariFile.joindengan banyak argumen.globterintegrasi dengan Enumerable —path.glob("**/*.rb").select(&:file?).sortadalah pipeline yang natural dan terbaca.expand_path,cleanpath,realpath— tiga method berbeda untuk normalisasi path:cleanpathmurni string manipulation,expand_pathresolve terhadap working directory,realpathresolve symlink dengan akses file system.mkpath— cara terpendek membuat direktori beserta semua parent-nya; lebih baik dariFileUtils.mkdir_p.__FILE__dan__dir__— gunakanPathname(__FILE__).dirnameuntuk mendapatkan direktori file saat ini; pola ini sangat umum di library dan gem Ruby.- Konversi ke string sebelum API lama — beberapa method seperti
YAML.load_fileataurequiremenerima string, bukan Pathname; panggil.to_ssaat diperlukan.require "pathname"dulu — Pathname tidak otomatis tersedia; perlu di-require sebelum digunakan.