Files
2025-09-29 00:52:08 +02:00

585 lines
14 KiB
Ruby
Executable File

#
# Filename:: path.rb
# Description:: OS Path String Manipulation Functions
#
# Author:: David Muir <david.muir@rockstarnorth.com>
# Author:: Greg Smith <greg@rockstarnorth.com>
# Author:: Luke Openshaw <luke.openshaw@rockstarnorth.com>
# Author:: Marissa Warner-Wu <marissa.warner-wu@rockstarnorth.com>
#
#
#-----------------------------------------------------------------------------
# Uses
#-----------------------------------------------------------------------------
require 'pipeline/util/string'
require 'pipeline/win32/kernel32'
require 'platform'
module Pipeline
module OS
#
# == Description
#
# Contains class methods (static) that can manipulate path strings.
#
# Note: all paths returned from these functions are normalised.
#
class Path
#---------------------------------------------------------------------
# Constants
#---------------------------------------------------------------------
# Normalised path directory separator character.
DIRECTORY_SEPARATOR = '/'
# Normalised path filename and extension separator character.
EXTENSION_SEPARATOR = '.'
# Maximum path length on Windows.
MAX_PATH_LEN = 260
private
DOWNCASE_ON_NORMALISE_DEFAULT = true
@@downcase_on_normalise = DOWNCASE_ON_NORMALISE_DEFAULT
public
#---------------------------------------------------------------------
# Methods
#---------------------------------------------------------------------
# Override the 'downcase_on_normalise' feature.
def Path::set_downcase_on_normalise( state )
@@downcase_on_normalise = state
end
#
# This is the safest way to use 'downcase_on_normalise' because it
# resets it to default after the user-block is executed.
#
def Path::no_downcase_on_normalise( &block )
begin
Path::set_downcase_on_normalise( false )
yield if ( block_given? )
ensure
Path::set_downcase_on_normalise( DOWNCASE_ON_NORMALISE_DEFAULT )
end
end
#
# == Description
# Gets the extension from a file path
#
# == Example Usage
# Path.get_extension( 'x:/test_log.txt' )
# => 'txt'
#
def Path::get_extension( path )
segs = path.split(EXTENSION_SEPARATOR)
return segs[segs.size - 1] if ( segs.size > 1 )
return '' unless ( segs.size > 1 )
end
#
# == Description
# Gets the extension from a file path
#
# == Example Usage
# Path.get_extension( 'x:/test_log.txt' )
# => 'txt'
#
def Path::get_multi_extension( path )
segs = path.split(EXTENSION_SEPARATOR)
return segs[1..(segs.length - 1)].join(EXTENSION_SEPARATOR) if ( segs.size > 1 )
return '' unless ( segs.size > 1 )
end
#
# == Description
# Replace the path's extension with another, returning new path.
#
# == Example Usage
# Path.replace_ext( 'x:/test_log.txt', 'html' )
# => 'x:/test_log.html'
#
def Path::replace_ext( path, new_ext )
ext = Path.get_extension( path )
Path.normalise( path.gsub( /\.#{ext}/, ".#{new_ext}" ) )
end
#
# == Description
# Remove the extension from a path.
#
# == Example Usage
# Path.remove_extension( 'x:/test_log.txt' )
# => 'x:/test_log'
#
def Path::remove_extension( path )
ext = Path::get_extension( path )
return ( path[0...(path.size - ext.size - 1)] ) if ( ext.size > 0 )
return ( path[0...(path.size - ext.size)] ) if ( 0 == ext.size )
end
#
# == Description
# Get the filename from a path.
#
# == Example Usage
# Path.get_basename( 'x:/test_log.txt' )
# => 'test_log'
#
def Path::get_basename( path )
remove_extension( get_filename( path ) )
end
#
# == Description
# Appends to a basename
#
# == Example Usage
# Path.append_basename( 'x:/test_log.txt', '_version1' )
# => 'x:/test_log.txt'
#
def Path::append_basename( path, append )
basename = get_basename(path)
extension = get_extension(path)
temp_filename = remove_filename(path)
temp_filename += "/" + basename + append + "." + extension
end
#
# == Description
# Get the full directory name from a path.
#
# == Example Usage
# Path.get_directory( 'x:/test/folder1/test_log.txt' )
# => 'x:/test/folder1/'
#
def Path::get_directory( path )
npath = Path.normalise( path )
ret = File.dirname( npath )
ret = "" if ret == "."
ret
end
#
# == Description
# Get the filename from a path
#
# == Example Usage
# Path.get_filename( 'x:/test_log.txt' )
# => 'test_log.txt'
#
def Path::get_filename( path )
path = normalise(path)
segs = path.split(DIRECTORY_SEPARATOR)
segs[segs.length - 1]
end
#
# == Description
# Get the drive from a path (rage image and pack mount point aware).
#
# == Example Usage
# Path.get_drive( 'x:/test_log.txt' )
# => 'x'
#
def Path::get_drive( path )
npath = Path.normalise( path )
npath.split( ':' )[0]
end
#
# == Description
# Returns an array of all the separate elements in a path.
#
# == Example Usage
# Path.get_parts( 'x:/tools/test_log.txt' )
# => ['x', 'tools', 'test_log.txt']
#
def Path::get_parts( path )
npath = Path.normalise( path )
segs = npath.split(DIRECTORY_SEPARATOR)
segs[0] = segs[0].split( ':' )[0]
segs
end
#
# == Description
# Returns an array of all the directories in a path.
#
# == Example Usage
# Path.get_directories( 'x:/tools/test/test_log.txt' )
# => ['tools', 'test']
#
def Path::get_directories( path )
npath = Path.normalise( path )
npath = Path.remove_filename( npath )
npath = Path.strip_drive( npath )
segs = npath.split(DIRECTORY_SEPARATOR)
segs
end
#
# == Description
# Remove the filename from a path.
#
# == Example Usage
# Path.remove_filename( 'x:/folder1/test_log.txt' )
# => 'x:/folder1'
#
def Path::remove_filename( path )
path = normalise(path)
# Only remove the filename if it exists
if path.include? EXTENSION_SEPARATOR
segs = path.split(DIRECTORY_SEPARATOR)
segs.delete_at(segs.length - 1)
if segs.length > 0 then
path = segs[segs.length - 1]
segs.delete_at(segs.length - 1)
segs.reverse.each { |seg|
path = seg + DIRECTORY_SEPARATOR + path
}
end
end
path
end
#
# == Description
# Normalises a path string, replacing "\" characters with "/"
# characters, returning the resulting path.
#
# == Example Usage
# Path.normalise( 'X:\\folder1/folder2\\test_file.txt' )
# => 'x:/folder1/folder2/test_file.txt'
#
def Path::normalise( path )
return "" if ( nil == path )
# Ensure path is lowercase
npath = path
npath = path.downcase() if @@downcase_on_normalise
# Replace backslash characters
npath = npath.gsub( '\\', DIRECTORY_SEPARATOR )
# Replace double-forward slash characters
npath = npath.gsub( '//', DIRECTORY_SEPARATOR )
# Reinstate UNC prefix (if available)
if ( path.starts_with( '\\\\' ) || path.starts_with( '//' ) )
npath = "#{DIRECTORY_SEPARATOR}#{npath}"
end
npath
end
# == Description
# Normalises a path string, replacing "/" characters with "\"
# characters, returning the resulting path.
#
# == Example Usage
# Path.normalise( 'X:\\folder1/folder2\\test_file.txt' )
# => 'x:\folder1\folder2\test_file.txt'
#
def Path::dos_format( path )
return "" if ( nil == path )
# Ensure path is lowercase
npath = path
npath = path.downcase() if @@downcase_on_normalise
# Replace backslash characters
npath.gsub!( DIRECTORY_SEPARATOR, '\\')
# Replace double-forward slash characters
npath.gsub!( '//', '\\' )
# Reinstate UNC prefix (if available)
if ( path.starts_with( '\\\\' ) || path.starts_with( '//' ) )
npath = "\\#{npath}"
end
npath
end
#
# == Description
# Strips the prefix drive letter and colon from a path string (if it
# exists), returning the resulting path. Also accepts longer drive
# names and names which include underscores (e.g. "extract_pack:/").
#
# == Example Usage
# Path.strip_drive( 'x:/test_log.txt' )
# => 'test_log.txt'
#
def Path::strip_drive( path )
npath = Path.normalise( path )
if ( npath =~ /^([A-Za-z_]+:\/)/ ) then
npath.gsub!( $1, '' )
end
npath
end
#
# == Description
# Combines two paths ensuring there is an appropriate path name
# separator character between the two parts.
#
# The path is normalised before being returned.
#
# == Example Usage
# Path.combine( "X:\\", "tools_release" ) => "x:/tools_release"
#
def Path::combine( *paths )
npaths = Array.new()
paths.each do |path|
if path != "" then
npaths << Path.normalise( path )
end
end
cpath = npaths.join( '/' )
Path.normalise( cpath )
end
#
# == Description
# Determines whether the specified path ends with the specified
# string.
#
# WARNING: paths are normalised before checking the end characters,
# please ensure that the end token is compatible with normalised
# paths.
#
# == Example Usage
# Path.ends_with( "x:/tools_release/file.txt", "file.txt" ) => true
# Path.ends_with( "x:\\tools_release\\", "\\" ) => false
# Path.ends_with( "x:\\tools_release\\", "/" ) => true
#
def Path::ends_with( path, ends )
npath = Path.normalise( path )
npath.ends_with( ends )
end
#
# == Description
# Determines whether the specified path starts with the specified
# string.
#
# == Example Usage
# Path.starts_with( 'test_file.log', 'test' )
# => true
#
def Path::starts_with( path, start )
npath = Path.normalise( path )
npath.starts_with( start )
end
#
# == Description
# Return a platform-native path String.
#
def Path::platform_native( path )
npath = Path::normalise( path )
case ::Platform::OS
when :win32
npath.gsub( '/', '\\' )
else
npath
end
end
#
# == Description
# works out the common path from an array of paths supplied
#
def Path::get_common( paths_in )
return "" if paths_in == nil
return "" if paths_in.size == 0
paths = Array.new()
paths_in.each { |path|
path = File.dirname(path) if File.directory?(path)
paths << Path::normalise(path).split("/")
}
common_denom = -1
max_size = paths[0].size
paths.each { |path|
max_size = path.size if path.size < max_size
}
paths.each { |path|
0.upto(max_size - 1) { |i|
if paths[0][i] != path[i] then
max_size = i
break
end
}
}
new_path = Array.new()
0.upto(max_size - 1) { |i|
new_path << paths[0][i]
}
ret = new_path.join("/")
ret = ret + "/" if ret.size > 0
ret
end
#
# == Description
# Grab the trailing directory in the path
#
# == Example Usage
# Path.get_trailing_directory( 'X:/dir/subdir/subsubdir/file.txt' )
# => 'subsubdir'
#
def Path::get_trailing_directory(path)
path = Path::remove_filename( path )
segs = path.split(DIRECTORY_SEPARATOR)
if segs.length > 0 then
path = segs[segs.length - 1]
end
path
end
#
# == Description
# Make an absolute path relative to another absolute path
#
# == Example Usage
# Path.make_relative( 'X:/dir/subdir/subsubdir/file.txt', 'X:/dir/subdir2/subsubdir/' )
# => '../../subdir/subsubdir'
#
def Path::make_relative( path, rel )
return path if (rel.index(":") == nil)
path = File.expand_path(path)
rel = File.expand_path(rel)
path_tokens = path.downcase.split("/")
relative_tokens = rel.downcase.split("/")
same = 0
0.upto(path_tokens.size - 1) { |i|
break if i >= relative_tokens.size
break if relative_tokens[i] != path_tokens[i]
same = same + 1
}
return path if same == 0
path_tokens = path.split("/")
relative_tokens = rel.split("/")
relative_tokens = relative_tokens[same..-1]
path_tokens = path_tokens[same..-1]
new_path = ("../" * relative_tokens.size) + path_tokens.join("/")
new_path
end
#
# Return the normalised Windows directory.
#
def Path::get_windows_directory( )
windir = ' '*(MAX_PATH_LEN+1)
Win32::Kernel32::GetSystemWindowsDirectory.call( windir, windir.size )
OS::Path.normalise( windir.strip.chomp.strip )
end
#
# Return the normalised temporary files directory.
#
def Path::get_temp_directory( )
tempdir = ' '*(MAX_PATH_LEN+1)
Win32::Kernel32::GetTempPath.call( tempdir.size, tempdir )
OS::Path.normalise( tempdir.strip.chomp.strip )
end
#
# Return the normalised Windows System directory.
#
def Path::get_windows_system_directory( )
sysdir = ' '*(MAX_PATH_LEN+1)
Win32::Kernel32::GetSystemDirectory.call( sysdir, sysdir.size )
OS::Path.normalise( sysdir.strip.chomp.strip )
end
end
def Path::get_corebasename( path )
basename = path
while basename.index('.') != nil
basename = remove_extension( get_filename( basename ) )
end
basename
end
end # OS module
end # Pipeline module
# End of path.rb