Стандартная библиотека Ruby — это клад полезных классов, и класс Pathname
безусловно, является одним из них. Представленный в Ruby 1.9, Pathname
представляет путь в файловой системе, предоставляя удобный доступ к функциям, которые иначе разбросаны по горстке других классов, таких как File
, FileTest
и Dir
.
Теперь, когда жизнь Ruby 1.8 официально подошла к концу , а 1.9 близится к своему пятому дню рождения , пришло время отказаться от устаревшей поддержки и принять элегантность и простоту использования Pathname
.
Чтобы показать, почему Pathname
было столь необходимым дополнением, давайте рассмотрим более традиционный способ манипулирования информацией о путях. Вот как мы делали вещи за 1,8 дня:
CONFIG_PATH = File.join(File.dirname(__FILE__), '..', 'config.yml')
Обратите внимание на использование методов класса, редкое зрелище в языке, где все является объектом. Но какова альтернатива? Объектно-ориентированной задачей было бы приблизить функциональность к данным, с которыми он работает. В этом случае мы просто манипулируем строками, но на самом деле не имеет смысла добавлять join
и dirname
в String
. Строка — это слишком общий тип данных для такой специфической функциональности.
Введите имя Pathname
:
CONFIG_PATH = Pathname.new(__FILE__).dirname.join('..', 'config.yml')
Pathname
строку в объект Pathname
, мы получаем чистый API для манипулирования путем. Этот код уже больше похож на идиоматический Ruby. Важным отличием от ванильных строк Ruby является то, что экземпляры Pathname
являются неизменяемыми. Нет методов, которые изменяют Pathname
экземпляр Pathname
. Вместо этого все методы, такие как dirname
, join
или basename
возвращают новый экземпляр Pathname
, оставляя существующий экземпляр без изменений.
API Pathname
Методы Pathname
делятся примерно на одну из двух категорий: либо они просто манипулируют строкой пути, не обращаясь к файловой системе, либо они обращаются к операционной системе для выполнения проверок и операций над самим файлом или каталогом. Полный список из восьмидесяти методов вы можете найти в официальной документации Pathname API . Стоит отметить, что документы также доступны из терминала через ri Pathname
или в irb с help 'Pathname'
.
Давайте посмотрим на некоторые методы, которые могут оказаться полезными.
Тип файла и проверки разрешений
pn = Pathname.new('/usr/bin/ruby') pn.file? # => true pn.directory? # => false pn.absolute? # => true pn.relative? # => false pn.executable? # => true pn.readable? # => true pn.writable? # => false pn.root? # => false
Большинство из них соответствуют методам FileTest
Навигация и запросы к файловой системе
pn = Pathname.getwd # current working directory pn.children # => [ #<Pathname...>, ... ] pn.each_child {|ch| ... } pn = pn.parent Pathname.glob('**/*.rb') # like Dir['**/*.rb'] but returns Pathnames
Путь манипуляции
pn = Pathname.new('lib/mylib/awesome.rb') pn.dirname # => #<Pathname:lib/mylib> pn.basename # => #<Pathname:awesome.rb> pn.extname # => ".rb" pn.expand_path('/home/arne') # => #<Pathname:/home/arne/lib/mylib/awesome.rb>
Работа с актуальным файлом
pn = Pathname.new(ENV['HOME']).join('.bashrc') pn.size pn.read pn.open {|io| ... } pn.each_line {|line| ... } pn.rename('/tmp/foo') pn.delete
Pathname()
и to_path
Существует два способа создания экземпляров Pathname
: с помощью явной конструкции, как описано выше, или с помощью Pathname()
преобразования Pathname()
. Хотя заглавные идентификаторы обычно зарезервированы для классов и констант, в Ruby есть ряд встроенных методов для преобразования произвольных значений в определенный тип, названный в честь классов, в которые они преобразуются.
Array(1) # => [1] Array(nil) # => [] Array([1,2]) # => [1,2] String(7) # => "7" URI("http://example.com") # => #<URI::HTTP:0x1c0e URL:http://example.com>
Они идут рука об руку с несколькими конверсионными хуками, которые могут реализовать объекты, например to_ary
или to_str
. Аналогично, объекты могут реализовать to_path
чтобы указать, как они могут быть преобразованы в имя пути. Конструктор pathname реализован в C для эффективности, но он будет примерно переведен в Ruby следующим образом:
class Pathname def initialize(path) path = path.to_path if path.respond_to? :to_path @path = String(path) end end
Допустим, вы пишете несколько сценариев, которые помогут вам управлять своей музыкальной коллекцией. AudioFile
может выглядеть так:
class AudioFile def initialize(path) @path = path end def to_path @path end # ... end
Многие встроенные методы оборачивают свои аргументы имени файла при вызове Pathname()
, поэтому вы можете передать свой экземпляр AudioFile
точно так же.
af = AudioFile.new('...') File.open(af) do |io| #... end
При написании кода библиотеки используйте этот шаблон для максимальной гибкости:
def read_configuration(config_file) parse_configuration(Pathname(config_file).read) end
Теперь вызывающая config_file
этого кода может передаваться как config_file
- строка
- путь
- объект, который отвечает на
to_path
- любой объект, который может быть преобразован с помощью
String()
Создание браузера музыкальной коллекции
Чтобы показать, насколько мощна и интуитивно понятна работа с Pathname
, мы напишем небольшой API для просмотра нашей музыкальной коллекции. Файлы организованы с песнями, сгруппированными в каталогах для альбома, которые снова сгруппированы по исполнителю. Например: ~/Music/Arsenal/Oyebo Soul/09_How Come.flacc
.
Мы стремимся к простому в использовании API, который выглядит следующим образом:
mc = MusicCollection.new('~/Music') # => #<MusicCollection 'Music'> mc.artists.first # => #<Artist 'Arsenal'> mc.artists.first.albums.first # => #<Album 'Oyebo Soul'> mc.songs.count # => 120 mc.songs.first.path # => #<Pathname: ...> mc.songs.first.play
Класс Song
по сути является нашим классом AudioFile
сверху:
class Song attr_reader :path alias to_path path def initialize(path) @path = Pathname(path).expand_path end def name String(path.basename(path.extname)) end def play(options = {}) exec(options.fetch(:player, 'mplayer'), String(path)) # OS X users can use player: '/usr/bin/afplay' end end
Мы немедленно вызываем expand_path
для нормализации нашего ввода. Это развернет ~
в домашний каталог пользователя и разрешит любые относительные имена файлов (например .
И ..
). Таким образом, у нас обязательно будет абсолютное имя файла.
Теперь нам нужно добавить классы для Album
, Artist
и, наконец, самой MusicCollection
. Поскольку все они являются классами- Pathname
вокруг Pathname
, мы можем Pathname
общую систему в базовый класс.
class PathnameWrapper attr_reader :path alias to_path path def initialize(path) @path = Pathname(path).expand_path end def name # ... end def to_s # ... end end class Song < PathnameWrapper def play # ... end end
Теперь мы можем добавить Album
и предоставить методы навигации туда и обратно. Мы также добавим to_proc
files
и directories
в наш базовый класс вместе с to_proc
класса to_proc
. При передаче объекта в виде блока с &
, Ruby будет использовать to_proc
чтобы найти реализацию для блока. Таким образом, мы получаем сокращение для типизации всех элементов коллекции.
class PathnameWrapper # ... def directories path.children.select(&:directory?) end def files path.children.select(&:file?) end def self.to_proc ->(path) { new(path) } end end class Album < PathnameWrapper EXTENSIONS = %w[.flacc .m4a .mp3 .ogg .wav] def songs files.select do |file| EXTENSIONS.any? {|ext| ext == file.extname } end.map(&Song) end end class Song < PathnameWrapper # ... def album Album.new(path.dirname) end end
Чтобы MusicCollection
итоги, мы представляем Artist
и MusicCollection
.
class MusicCollection < PathnameWrapper def artists directories.map(&Artist) end def albums artists.flat_map(&:albums) end def songs albums.flat_map(&:songs) end end class Artist < PathnameWrapper def albums directories.map(&Album) end end class Album < PathnameWrapper # ... def artist Artist.new(path.dirname) end end class Song < PathnameWrapper # ... def artist album.artist end end
Теперь мы можем просматривать между песнями, альбомами и исполнителями.
На этом мы завершаем основы нашего API MusicCollection
, а также познакомимся с классом Pathname
. Хорошее понимание стандартной библиотеки Ruby может помочь сделать ваш код более элегантным, корректным и лаконичным. Здесь мы увеличили один небольшой компонент, выделив его многократное использование. Идите вперед и используйте Pathname
умом.