All pastes #225332 Raw Edit

PathEx

public text v1 · immutable
#225332 ·published 2006-10-27 21:41 UTC
rendered paste body
// PathEx.boo: path processing functionality beyond what System.IO.Path offers.
// Converted to boo from C# (with a small amount of new stuff)
// original: http://pastebin.ca/225328
namespace PathEx

import System
import System.Text
import System.Diagnostics

/// <summary>
/// Provides path processing functionality beyond what System.IO.Path offers.
/// </summary>
/// <remarks>
/// Here are a couple of things System.IO.Path doesn't offer:
///
/// <code>
/// PathEx.RemoveLastPath("/Windows/calc.exe") // = "/Windows"
/// PathEx.RemoveLastPath("/Windows/calc.exe/") // = "/Windows"
///
/// PathEx.GetRelativePath("/Windows/System32", "/Windows/calc.exe") // = "../calc.exe"
/// 
/// PathEx.IsPathSeparator(char('\\')) // = true
/// PathEx.IsPathSeparator(char('/')) // = true
/// PathEx.IsPathSeparator(char('Q')) // = false
///
/// PathEx.CollapsePath("//A/../B/./C") // = "//B/C"
/// </code></remarks>
public class PathEx:
	#region Wrappers for functions in System.IO.Path
	// Note: There is no wrapper for Combine() because Combine() has its
	// own implementation in PathEx.Path.
	
	AltDirectorySeparatorChar: 
		get:
			return System.IO.Path.AltDirectorySeparatorChar
	DirectorySeparatorChar: 
		get:
			return System.IO.Path.DirectorySeparatorChar 
	InvalidPathChars: 
		get:
			return System.IO.Path.InvalidPathChars
	PathSeparator: 
		get:
			return System.IO.Path.PathSeparator
	VolumeSeparatorChar: 
		get:
			return System.IO.Path.VolumeSeparatorChar
	def ChangeExtension(path as string, extension as string): 
		return System.IO.Path.ChangeExtension(path, extension)
	def GetDirectoryName(path as string):
		return System.IO.Path.GetDirectoryName(path)
	def GetExtension(path as string):
		return System.IO.Path.GetExtension(path)
	def GetFileName(path as string):
		return System.IO.Path.GetFileName(path)
	def GetFileNameWithoutExtension(path as string):
		return System.IO.Path.GetFileNameWithoutExtension(path)
	def GetFullPath(path as string):
		return System.IO.Path.GetFullPath(path)
	def GetInvalidFileNameChars():
		return System.IO.Path.GetInvalidFileNameChars()
	def GetInvalidPathChars():
		return System.IO.Path.GetInvalidPathChars()
	def GetPathRoot(path as string):
		return System.IO.Path.GetPathRoot(path)
	def GetTempFileName():
		return System.IO.Path.GetTempFileName()
	def GetTempPath():
		return System.IO.Path.GetTempPath()
	def GetRandomFileName():
		return System.IO.Path.GetRandomFileName()
	def HasExtension(path as string):
		return System.IO.Path.HasExtension(path)
	def IsPathRooted(path as string):
		return System.IO.Path.IsPathRooted(path)
	#endregion

	// <summary>Returns c == '\\' || c == '/'.
	// </summary>
	public static def IsPathSeparator(c as char) as bool:
		return ((c == char('\\')) or (c == char('/')))

	// <summary>
	// Determines how much of the beginning of the path string 
	// represents a drive, volume, protocol, or domain name, and returns
	// it, including trailing slashes.  If the path does not appear to be
	// an absolute path, then an empty string (not null) is returned.
	// </summary>
	// <param name="includeDomain">If this is true, then the return value 
	// includes the computer or domain name; if false, then the return 
	// value includes only the drive or protocol specification and the
	// initial slash(es).
	// <param name="wantTrailingSlash">If true, then the trailing slash on 
	// a computer or domain name is included; otherwise, it is omitted.
	// This parameter does not apply if if includeDomain is false.</param>
	// </param>
	// <remarks>
	// This function assumes that the path is well-formed.  In other
	// words, you can expect garbage-in-garbage-out.  The includeDomain
	// parameter has an effect only if the function believes that the
	// path includes a computer or domain name, which is indicated by
	// the presence of more than one slash.  For example, in
	// "file://A/B", there are two slashes, so A is assumed to be
	// the "volume name".  On the other hand, in "C:/A/B", there is only
	// one slash, so A is considered to be a folder name.  See the 
	// examples below for the consequences of this interpretation.
	// <p>Examples (where includeDomain and wantTrailingSlash are true 
	// unless said otherwise):</p>
	// <code>
	// \Test                             => \
	// \\ComputerName                    => \\ComputerName
	// \\ComputerName                    => \\ (if !includeDomain)
	// \\ComputerName\Test               => \\ComputerName\
	// C:\Windows                        => C:\
	// http://www.google.com/index.html  => http://www.google.com/
	// http://www.google.com/index.html  => http://www.google.com (if !wantTrailingSlash)
	// file:///C:/Windows                => file:///C:/
	// file:///C:/Windows                => file:///C:/ (if !wantTrailingSlash)
	// file:///C:/Windows                => file://    (if !includeDomain)
	// C:D:E                             => C:
	// C:D/E                             => C:
	// file///C:/Windows                 => (returns String.Empty)
	// Relative/Path                     => (returns String.Empty)
	// Hello                             => (returns String.Empty)
	// </code>
	// </remarks>
	public static def GetDriveOrDomain(path as string, includeDomain as bool, wantTrailingSlash as bool) as string:
		return path.Substring(0, GetDriveOrDomainLen(path, includeDomain, wantTrailingSlash))

	// <summary>Behaves like <see cref="GetDriveOrDomain"/>, except that
	// this function returns the length of the drive or volume string
	// rather than the string itself.
	// </summary>
	// <param name="wantTrailingSlash">If true, then the trailing slash on 
	// a computer or domain name is included; otherwise, it is omitted.
	// This parameter does not apply if if includeDomain is false.</param>
	// <returns>The number of characters at the beginning of the path
	// which represent the drive or volume name</returns>
	public static def GetDriveOrDomainLen(path as string, includeDomain as bool, wantTrailingSlash as bool) as int:
		slashes as int = (-1)
		i as int
		i = 0
		goto converterGeneratedName1
		
		while true:
			i += 1
			:converterGeneratedName1
			if i == path.Length:
				// There are no colons or slashes found, so there is NO 
				// drive/protocol/domain in it.
				return 0
			if path[i] == char(':'):
				break 
			if IsPathSeparator(path[i]):
				if i == 0:
					slashes += 1
					break 
				else:
					return 0
				// Relative path detected
		while true:
			if (++i) == path.Length:
				return i
			slashes += 1
			break  unless IsPathSeparator(path[i])
		
		if (slashes > 1) and includeDomain:
			// Multiple slashes indicates a domain (or volume) name.
			
			// Find out where the name of the volume/domain ends.
			while true:
				if (++i) == path.Length:
					return i
				break  unless (not IsPathSeparator(path[i]))
			
			if wantTrailingSlash:
				i += 1
		return i

	
	// <summary>Removes the last path component.  Returns the same
	// string if the path string ends in multiple slashes.  Also, if 
	// stopAtColon is true, returns the same path if the final path
	// component ends in a colon (e.g. X/Y/Z: or C:\)
	// </summary>
	// <remarks>
	// Equivalent to path.Substring(0, FindBeginningOfLastPath(path)).
	// Examples (assuming stopAtColon is true):
	// <code>
	// C:\                                 => C:\
	// C:\                                 => (empty string if !stopAtColon)
	// http://                             => http://
	// test//                              => test//
	// test/                               => (empty string)
	// http://www.google.ca                => http://
	// http://www.google.ca/               => http://
	// http://www.google.ca/index.html     => http://www.google.ca/
	// C:\Test                             => C:\
	// C:\A\B                              => C:\A\
	// C:\A\B\                             => C:\A\
	// </code></remarks>
	public static def RemoveLastPath(path as string, stopAtColon as bool) as string:
		return path.Substring(0, FindBeginningOfLastPath(path, stopAtColon))

	public static def RemoveLastPath(path as string) as string:
		return path.Substring(0, FindBeginningOfLastPath(path, false))

	
	// <summary>
	// Equivalent to FindBeginningOfPriorPath(path, path.Length).
	// </summary>
	public static def FindBeginningOfLastPath(path as string, stopAtColon as bool) as int:
		return FindBeginningOfPriorPath(path, path.Length, stopAtColon)
	
	public static def FindBeginningOfPriorPath(path as string, startAt as int, stopAtColon as bool) as int:
		i as int = (startAt - 1)
		if (i < 0) or ((path[i] == char(':')) and stopAtColon):
			return startAt
		if IsPathSeparator(path[i]):
			i -= 1
			if ((i < 0) or IsPathSeparator(path[i])) or ((path[i] == char(':')) and stopAtColon):
				return startAt
		i -= 1
		goto converterGeneratedName2
		while true:
			i -= 1
			:converterGeneratedName2
			break  unless ((i >= 0) and (not IsPathSeparator(path[i])))
		return (i + 1)

	
	//		private static bool BeginningsMatch(string s1, string s2, int len)
	//			{ return string.Compare(s1, 0, s2, 0, len, true) == 0; }
	
	protected static def GetPathSeparator(path as string) as char:
		for i in range(0, path.Length):
			if IsPathSeparator(path[i]):
				return path[i]
		return System.IO.Path.PathSeparator

	protected static def AreEqual(c1 as char, c2 as char, caseSensitive as bool) as bool:
		if c1 == c2:
			return true
		if caseSensitive:
			return false
		
		if (c1 >= char('a')) and (c1 <= char('z')):
			// boo would not accept the original code:
			// return ((c1 - (char('a') - char('A'))) == c2)
			return ((cast(ushort,c1) - (cast(ushort,char('a')) - cast(ushort,char('A')))) == cast(ushort,c2))
		else:
			return false

	
	// <summary>Converts an absolute path to a relative path, given
	// a starting path.  In a sense, "from_" is "subtracted" from_ "to".
	// Combining, a.k.a. "adding" the return value to <i>from_</i> should 
	// yield <i>to</i>.
	// </summary>
	// <param name="from_">Path from_ which to start.</param>
	// <param name="to">Destination path</param>
	// <param name="requireSameRootFolder">If this is true, then 'from_' 
	// and 'to' must have the same root folder in order to successfully
	// generate a relative path between them.  If a relative path cannot
	// be generated, then 'to' itself is returned.
	// If this is false, then a relative path is generated even if 'from_'
	// and 'to' have different root folders.
	// </param>
	// <param name="allowRelativePaths">If this is true, and 'from_' and
	// 'to' are relative paths, then GetRelativePath attempts to find
	// a relative path between them.  If allowRelativePaths is false,
	// and 'from_' and 'to' are relative paths, then GetRelativePath simply
	// returns 'to' unchanged.
	// </param>
	// <returns>The path that gets you from_ 'from_' to 'to'.</returns>
	// <remarks>In order for this function to work reliably, 'from_' and
	// 'to' must use the same path separator character.  In other words,
	// they both must use a backslash to separate paths or else they both 
	// must use a forward slash.
	// </remarks>
	// <example><code>
	// Examples might not be perfect.
	// 
	// value of 'from_'  value of 'to'  return value
	// ---------------  -------------  ------------
	// A:\B\C\D         A:\B\C\D       (empty string)
	// A:\B\C\D         A:\B\C\D\E     E
	// A:\B\C\D         A:\B\C         ..
	// A:\B\C\D         A:\            ..\..\..   (if requireSameRootFolder)
	// A:\B\C\D         A:\            A:\        (if !requireSameRootFolder)
	// A/B/C            A/B/D          A/B/D      (if !allowRelativePaths)
	// A/B/C            A/B/D          ../D       (if allowRelativePaths)
	// A:\B\C\D         \B\C\E         \B\C\E
	// \B\C\D           \B\C\E         ..\E
	// \B\C\D           \B\C\D\E\      E\
	// \B\C\D           \B\C\E\        ..\E\
	// \B\C             \D\E\          C:\D\E\    (if requireSameRootFolder)
	// \B\C             \D\E\          ..\..\D\E\ (if !requireSameRootFolder)
	// http://W/X       http://Y/Z     http://Y/Z
	// http://W/X       http://W/Y     http://W/Y (if requireSameRootFolder)
	// http://W/X       http://W/Y     ../Y       (if !requireSameRootFolder)
	// </code></example>
	public static def GetRelativePath(from_ as string, to as string, requireSameRootFolder as bool, allowRelativePaths as bool, caseSensitive as bool) as string:
		driveEndsAt as int = GetDriveOrDomainLen(from_, true, true)
		driveEndsAt2 as int = GetDriveOrDomainLen(to, true, true)
		if (driveEndsAt != driveEndsAt2) or ((driveEndsAt == 0) and (not allowRelativePaths)):
			return to
		// Two different domains (no relative path exists),
		// or one of the paths is relative (relative path is unknown)
		separator as char = GetPathSeparator(from_)
		
		difStart = 0
		// The beginning of the difference(s)
		// Find out where they first diverge.
		i = 0
		goto converterGeneratedName3
		while true:
			i += 1
			:converterGeneratedName3
			//break  unless 
			if i == from_.Length:
				if i == to.Length:
					// The strings are equal!
					return string.Empty
				else:
					if IsPathSeparator(to[i]):
						difStart = (i + 1)
					break 
			if i == to.Length:
				if IsPathSeparator(from_[i]):
					difStart = (i + 1)
				break 
			if not AreEqual(from_[i], to[i], caseSensitive):
				break 
			if IsPathSeparator(from_[i]):
				difStart = (i + 1)
		if difStart < driveEndsAt:
			return to
		// Two different domains (no relative path exists)
		if requireSameRootFolder and (difStart == driveEndsAt):
			return to
		// Two different root folders
		relPath = StringBuilder()
		
		// At this point we know that we're going to make a relative path.
		// This is a two-step process: 
		// (1) add ..'s until all the excess parts of 'from_' are gone
		foundOne = true
		i = difStart
		while (i < from_.Length):
			if foundOne:
				foundOne = false
				relPath.Append('..')
				relPath.Append(separator)
			if IsPathSeparator(from_[i]):
				foundOne = true
			++i
		
		// (2) add the end of 'to'
		if difStart < to.Length:
			relPath.Append(to, difStart, (to.Length - difStart))
		
		return relPath.ToString()

	public static GRP_RequireSameRootFolder = false
	public static GRP_AllowRelativePaths = true
	public static GRP_CaseSensitive = false
	// <summary>Calls GetRelativePath() with the following defaults:
	// requireSameRootFolder = false, allowRelativePaths = true, 
	// caseSensitive = false. These defaults can be changed by altering
	// the static variables in this class whose names start with GRP_.
	// </summary>
	public static def GetRelativePath(from_ as string, to as string) as string:
		GetRelativePath(from_, to, GRP_RequireSameRootFolder, GRP_AllowRelativePaths, GRP_CaseSensitive)

	// <summary>Adds path2 on the end of path1 in a similar manner to 
	// Path.Combine(), with the exception that path components of 
	// "." and ".." are removed.
	// </summary>
	public static def Combine(path1 as string, path2 as string) as string:
		return CollapsePath(System.IO.Path.Combine(path1, path2))

	
	// <summary>
	// Removes path components named "." and "..".  Since ".." represents
	// "going back to the parent folder", when a ".." is discovered, both
	// it and the immediately prior component in the string are deleted.
	// </summary>
	// <example><code>
	// Input                Output
	// -----                ------
	// C:\Test              C:\Test
	// \Hello\..            \
	// \Hello\..\           \
	// Hello\..             (empty string)
	// Hello\.              Hello\
	// A\.\B\.\C\           A\B\C\
	// A\..\B\C             B\C
	// \\A\..\B\C           \\B\C
	// http://test/..       http://
	// </code></example>
	public static def CollapsePath(path as string) as string:
		pathStart as int = GetDriveOrDomainLen(path, false, true)
		
		// Begin at the end.
		i as int = path.Length
		while i > pathStart:
			newI as int = FindBeginningOfPriorPath(path, i, false)
			reachedLimit = false
			Debug.Assert((newI >= pathStart))
			if newI == i:
				reachedLimit = true
				// Unable to proceed further backward
			i = newI
			if path[i] == char('.'):
				if (i + 1) < path.Length:
					if IsPathSeparator(path[(i + 1)]):
						path = path.Remove(i, 2)
					else:
						if path[(i + 1)] == char('.'):
							// Found "..".
							if reachedLimit:
								break 
							// Unable to proceed further.
							// Confirm that it is a complete path component.
							endI as int = (i + 2)
							if endI < path.Length:
								if IsPathSeparator(path[endI]):
									endI += 1
								else:
									continue 
							
							priorI as int = FindBeginningOfPriorPath(path, i, false)
							if priorI == i:
								return path
								// Unable to proceed further.
							
							path = path.Remove(priorI, (endI - priorI))
							i = priorI
				else:
					path = path.Remove(i, 1)
			if reachedLimit:
				break 
		return path

	private def constructor():
		pass
	

public class PathExTest:

	protected class OneTest:

		public def constructor(In as string, _Out as string):
			Path1 = In
			Out = _Out

		public def constructor(In as string, _Out as string, flag1 as bool):
			Path1 = In
			Out = _Out
			Flag1 = flag1

		public def constructor(In as string, _Out as string, flag1 as bool, flag2 as bool):
			Path1 = In
			Out = _Out
			Flag1 = flag1
			Flag2 = flag2

		public def constructor(path1 as string, path2 as string, _Out as string):
			Path1 = path1
			Path2 = path2
			Out = _Out

		public def constructor(path1 as string, path2 as string, _Out as string, flag1 as bool):
			Path1 = path1
			Path2 = path2
			Out = _Out
			Flag1 = flag1

		public def constructor(path1 as string, path2 as string, _Out as string, flag1 as bool, flag2 as bool):
			Path1 = path1
			Path2 = path2
			Out = _Out
			Flag1 = flag1
			Flag2 = flag2

		public def constructor(path1 as string, path2 as string, _Out as string, _Out2 as string):
			Path1 = path1
			Path2 = path2
			Out = _Out
			Out2 = _Out2

		public def constructor(path1 as string, path2 as string, _Out as string, _Out2 as string, flag1 as bool):
			Path1 = path1
			Path2 = path2
			Out = _Out
			Out2 = _Out2
			Flag1 = flag1

		public Path as string:
			get:
				return Path1

		public Path1 as string

		public Path2 as string

		public Out as string

		public Out2 as string

		public Flag1 as bool

		public Flag2 as bool

	
	protected static testsGetRelPath as (OneTest) = (of OneTest: OneTest('A:\\B\\C\\D', 'A:\\B\\C\\D', '', '', false), OneTest('A:\\B\\C\\D', 'A:\\B\\C\\D\\E', 'E', 'E', false), OneTest('A:\\B\\C\\D', 'A:\\B\\C', '..\\', '..\\', false), OneTest('A:\\B\\C\\D', 'A:\\', '..\\..\\..\\', 'A:\\', false), OneTest('A:\\B\\C\\D\\', 'A:\\', '..\\..\\..\\', 'A:\\', true), OneTest('A:\\B\\C\\D\\', 'A:', 'A:', 'A:', true), OneTest('A/B/C', 'A/B/D', 'A/B/D', 'A/B/D', false), OneTest('A/B/C/', 'A/B/D/', '../D/', '../D/', true), OneTest('A:\\B\\C\\D', '\\B\\C\\E', '\\B\\C\\E', '\\B\\C\\E', false), OneTest('\\B\\C\\D', '\\B\\C\\E', '..\\E', '..\\E', false), OneTest('\\B\\C\\D', '\\B\\C\\D\\E\\', 'E\\', 'E\\', false), OneTest('\\B\\C\\D', '\\B\\C\\E\\', '..\\E\\', '..\\E\\', false), OneTest('\\B\\C', '\\D\\E\\', '..\\..\\D\\E\\', '\\D\\E\\', false), OneTest('http://W/X', 'http://Y/Z', 'http://Y/Z', 'http://Y/Z', false), OneTest('http://W/X', 'http://W/Y', '../Y', 'http://W/Y', false))

	//  path1, path2, if !requireSameRootFolder, if requireSameRootFolder, allowRelativePaths
	
	protected static testsGetDriveOrDomain as (OneTest) = (of OneTest: OneTest('Hello', '', true, true), OneTest('\\Test', '\\', true, true), OneTest('\\\\ComputerName', '\\\\ComputerName', true, true), OneTest('\\\\ComputerName', '\\\\', false, true), OneTest('\\\\ComputerName', '\\\\', false, false), OneTest('\\\\ComputerName\\Test', '\\\\ComputerName\\', true, true), OneTest('C:\\Windows', 'C:\\', true, true), OneTest('http://www.google.com/index.html', 'http://www.google.com/', true, true), OneTest('http://www.google.com/index.html', 'http://www.google.com', true, false), OneTest('file:///C:/Windows', 'file:///C:/', true, true), OneTest('file:///C:/Windows', 'file:///C:', true, false), OneTest('file:///C:/Windows', 'file:///', false, true), OneTest('file:///C:/Windows', 'file:///', false, false), OneTest('C:D:E', 'C:', true, true), OneTest('C:D/E', 'C:', true, true), OneTest('file///C:/Windows', '', true, true), OneTest('Relative/Path', '', true, true))

	// input, output, includeDomain, wantTrailingSlash
	
	protected static testsRemoveLastPath as (OneTest) = (of OneTest: OneTest('C:\\', 'C:\\', true), OneTest('C:\\', '', false), OneTest('Alpha/Beta:/', 'Alpha/Beta:/', true), OneTest('Alpha/Beta:/', 'Alpha/', false), OneTest('http://', 'http://', true), OneTest('test//', 'test//', true), OneTest('test/', '', true), OneTest('http://www.google.ca', 'http://', true), OneTest('http://www.google.ca/', 'http://', true), OneTest('http://www.google.ca/index.html', 'http://www.google.ca/', true), OneTest('C:\\Test', 'C:\\', true), OneTest('C:\\A\\B', 'C:\\A\\', true), OneTest('C:\\A\\B\\', 'C:\\A\\', true))

	
	protected static testsCollapsePath as (OneTest) = (of OneTest: OneTest('C:\\Test', 'C:\\Test'), OneTest('\\Hello\\..', '\\'), OneTest('\\Hello\\..\\', '\\'), OneTest('Hello\\..', ''), OneTest('Hello\\.', 'Hello\\'), OneTest('A\\.\\B\\.\\C\\', 'A\\B\\C\\'), OneTest('A\\..\\B\\C', 'B\\C'), OneTest('\\\\A\\..\\B\\C', '\\\\B\\C'), OneTest('http://test/..', 'http://'))

	protected static def Log(msg as string):
		print msg
	
	public static def DoTests():
		i as int
		result as string
		t as OneTest
		i = 0
		goto converterGeneratedName4
		while true:
			i += 1
			:converterGeneratedName4
			break  unless (i <= (testsGetDriveOrDomain.Length - 1))
			
			t = testsGetDriveOrDomain[i]
			result = PathEx.GetDriveOrDomain(t.Path1, t.Flag1, t.Flag2)
			Log("GetDriveOrDomain('${t.Path1}', ${t.Flag1}, ${t.Flag2}): '${result}'")
			assert result == t.Out
		i = 0
		goto converterGeneratedName5
		while true:
			i += 1
			:converterGeneratedName5
			break  unless (i <= (testsGetRelPath.Length - 1))
			
			t = testsGetRelPath[i]
			
			result = PathEx.GetRelativePath(t.Path1, t.Path2, false, t.Flag1, false)
			Log("GetRelativePath('${t.Path1}', '${t.Path2}', false, ${t.Flag1}, false): '${result}'")
			Debug.Assert((result == t.Out))
			
			result = PathEx.GetRelativePath(t.Path1, t.Path2, true, t.Flag1, false)
			Log("GetRelativePath('${t.Path1}', '${t.Path2}', true, ${t.Flag1}, false): '${result}'")
			Debug.Assert((result == t.Out2))
		i = 0
		goto converterGeneratedName6
		while true:
			i += 1
			:converterGeneratedName6
			break  unless (i <= (testsRemoveLastPath.Length - 1))
			
			t = testsRemoveLastPath[i]
			result = PathEx.RemoveLastPath(t.Path1, t.Flag1)
			Log ("PathEx.RemoveLastPath('${t.Path1}', ${t.Flag1}): '${result}'")
			Debug.Assert((result == t.Out))
		i = 0
		goto converterGeneratedName7
		while true:
			i += 1
			:converterGeneratedName7
			break  unless (i <= (testsCollapsePath.Length - 1))
			
			t = testsCollapsePath[i]
			result = PathEx.CollapsePath(t.Path1)
			Log ("PathEx.CollapsePath('${t.Path1}'): '${result}'")
			Debug.Assert((result == t.Out))

//PathExTest.DoTests()