// 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 /// /// Provides path processing functionality beyond what System.IO.Path offers. /// /// /// Here are a couple of things System.IO.Path doesn't offer: /// /// /// 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" /// 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 // Returns c == '\\' || c == '/'. // public static def IsPathSeparator(c as char) as bool: return ((c == char('\\')) or (c == char('/'))) // // 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. // // 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). // 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. // // // 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. //

Examples (where includeDomain and wantTrailingSlash are true // unless said otherwise):

// // \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) // //
public static def GetDriveOrDomain(path as string, includeDomain as bool, wantTrailingSlash as bool) as string: return path.Substring(0, GetDriveOrDomainLen(path, includeDomain, wantTrailingSlash)) // Behaves like , except that // this function returns the length of the drive or volume string // rather than the string itself. // // 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. // The number of characters at the beginning of the path // which represent the drive or volume name 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 // 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:\) // // // Equivalent to path.Substring(0, FindBeginningOfLastPath(path)). // Examples (assuming stopAtColon is true): // // 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\ // 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)) // // Equivalent to FindBeginningOfPriorPath(path, path.Length). // 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 // 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 from_ should // yield to. // // Path from_ which to start. // Destination path // 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. // // 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. // // The path that gets you from_ 'from_' to 'to'. // 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. // // // 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) // 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 // 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_. // public static def GetRelativePath(from_ as string, to as string) as string: GetRelativePath(from_, to, GRP_RequireSameRootFolder, GRP_AllowRelativePaths, GRP_CaseSensitive) // Adds path2 on the end of path1 in a similar manner to // Path.Combine(), with the exception that path components of // "." and ".." are removed. // public static def Combine(path1 as string, path2 as string) as string: return CollapsePath(System.IO.Path.Combine(path1, path2)) // // 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. // // // 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:// // 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()