Pathname

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)

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 File dan Dir, tapi objek dengan method instance yang bisa di-chain dan di-compose.
  • Operator / untuk komposisi pathPathname("/var/www") / "app" / "config" jauh lebih bersih dari File.join dengan banyak argumen.
  • glob terintegrasi dengan Enumerablepath.glob("**/*.rb").select(&:file?).sort adalah pipeline yang natural dan terbaca.
  • expand_path, cleanpath, realpath — tiga method berbeda untuk normalisasi path: cleanpath murni string manipulation, expand_path resolve terhadap working directory, realpath resolve symlink dengan akses file system.
  • mkpath — cara terpendek membuat direktori beserta semua parent-nya; lebih baik dari FileUtils.mkdir_p.
  • __FILE__ dan __dir__ — gunakan Pathname(__FILE__).dirname untuk 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_file atau require menerima string, bukan Pathname; panggil .to_s saat diperlukan.
  • require "pathname" dulu — Pathname tidak otomatis tersedia; perlu di-require sebelum digunakan.

← Sebelumnya: Comparable   Berikutnya: FileUtils →

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