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.

OperasiFile/Dir (lama)Pathname (modern)
Gabung pathFile.join(a, b, c)Pathname(a) / b / c
Nama fileFile.basename(path)path.basename
Direktori indukFile.dirname(path)path.dirname
EkstensiFile.extname(path)path.extname
Cek adaFile.exist?(path)path.exist?
Cek direktoriFile.directory?(path)path.directory?
Baca fileFile.read(path)path.read
Tulis fileFile.write(path, isi)path.write(isi)
Glob rekursifDir.glob("**/*.rb")path.glob("**/*.rb")
Iterasi direktoriDir.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