// 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()