# frozen_string_literal: true require 'pathname/extensions' class Pathname module Extensions module Pathmap def self.included(host) host.load_extensions :ext, :explode, :partial super end # Map the path according to the given specification. The specification # controls the details of the mapping. The following special patterns are # recognized: # # * `%p` - The complete path. # * `%f` - The base file name of the path, with its file extension, # but without any directories. # * `%n` - The file name of the path without its file extension. # * `%d` - The directory list of the path. # * `%x` - The file extension of the path. An empty string if there # is no extension. # * `%X` - Everything *but* the file extension. # * `%s` - The alternate file separator if defined, otherwise use # # the standard file separator. # * `%%` - A percent sign. # # @note # The `%d` specifier can also have a numeric prefix (e.g. `%2d`). # # If the number is positive, only return (up to) `n` directories in the # path, starting from the left hand side. # # If `n` is negative, return (up to) `n` directories from the # right hand side of the path. # # @example # 'a/b/c/d/file.txt'.pathmap("%2d") => 'a/b' # 'a/b/c/d/file.txt'.pathmap("%-2d") => 'c/d' # # Also the `%d`, `%p`, `%f`, `%n`, # `%x`, and `%X` operators can take a pattern/replacement # argument to perform simple string substitutions on a particular part of # the path. The pattern and replacement are separated by a comma and are # enclosed by curly braces. The replacement spec comes after the % # character but before the operator letter. (e.g. `%{old,new}d`). # Multiple replacement specs should be separated by semi-colons (e.g. # `%{old,new;src,bin}d`). # # Regular expressions may be used for the pattern, and back refs may be # used in the replacement text. Curly braces, commas and semi-colons are # excluded from both the pattern and replacement text (let's keep parsing # reasonable). # # @example # "src/org/onestepback/proj/A.java".pathmap("%{^src,class}X.class") # #=> "class/org/onestepback/proj/A.class" # # # @example If the replacement text is '*', then a block may be provided to perform some arbitrary calculation for the replacement. # "/path/to/file.TXT".pathmap("%X%{.*,*}x") { |ext| # ext.downcase # } #=> "/path/to/file.txt" def pathmap(spec = nil, &block) return self if spec.nil? result = +'' # noinspection SpellCheckingInspection spec.scan(/%\{[^}]*\}-?\d*[sdpfnxX%]|%-?\d+d|%.|[^%]+/) do |frag| # noinspection SpellCheckingInspection case frag when '%f' result << basename.to_s when '%n' result << basename.ext.to_s when '%d' result << dirname.to_s when '%x' result << extname.to_s when '%X' result << ext.to_s when '%p' result << to_s when '%s' result << (File::ALT_SEPARATOR || File::SEPARATOR) when '%-' result when '%%' result << '%' when /%(-?\d+)d/ result << partial(Regexp.last_match(1).to_i).to_s when /^%{([^}]*)}(\d*[dpfnxX])/ patterns = Regexp.last_match(1) operator = Regexp.last_match(2) result << pathmap('%' + operator).pathmap_replace(patterns, &block).to_s when /^%/ raise ArgumentError, "Unknown pathmap specifier #{frag} in '#{spec}'" else result << frag end end Pathname(result) end # Perform the pathmap replacement operations on the given path. The # patterns take the form 'pat1,rep1;pat2,rep2...'. # # @param patterns [String] # @param block [Proc] # @return [Pathname] def pathmap_replace(patterns, &block) result = self patterns.split(';').each do |pair| pattern, replacement = pair.split(',') pattern = Regexp.new(pattern) result = if replacement == '*' && block_given? result.sub(pattern, &block) elsif replacement result.sub(pattern, replacement) else result.sub(pattern, '') end end result end end end end