# File path unified in Windows/MacOS/Linux

We are all tremendously happy about back slashes in Windows file systems and do not think that their sole purpose is to make life complicated for smart people who are capable of using different OS. Upvote if disagree [ :) ].

Converting paths from/to Windows backward slashes is a pretty monotonous task. Here is a library class to help with it. GitHub

OSPath path = @"/foo\bar.txt";

// Windows output
WriteLine(path);            // \foo\bar.txt
WriteLine(path.Windows);    // \foo\bar.txt
WriteLine(path.Unix);       // /foo/bar.txt

// MacOS output
WriteLine(path);            // /foo/bar.txt
WriteLine(path.Windows);    // \foo\bar.txt
WriteLine(path.Unix);       // /foo/bar.txt


It also helps converting between relative and absolute paths:

OSPath ap = "/foo/bar";
WriteLine(ap.Relative.Unix); // foo/bar
WriteLine(ap.Absolute.Unix); // /foo/bar

OSPath rp = "foo/bar";
WriteLine(rp.Relative.Unix); // foo/bar
WriteLine(rp.Absolute.Unix); // /foo/bar


And to perform path arithmetic (Windows output):

OSPath root = @"/foo\bar";
WriteLine(root + "file.txt");        // \foo\bar\file.txt
WriteLine(root + "file.txt" - root); // file.txt


Library class is:

public class OSPath
{
public static readonly OSPath Empty = "";

public static bool IsWindows => DirectorySeparatorChar == '\\';

public OSPath(string path)
{
Path = path.Trim();
}

public static implicit operator OSPath(string path) => new OSPath(path);
public static implicit operator string(OSPath path) => path.Normalized;
public override string ToString() => Normalized;

protected string Path { get; }

public string Normalized => IsWindows ? Windows : Unix;
public string Windows => Path.Replace('/', '\\');
public string Unix => Volumeless.Path.Replace('\\', '/');

public OSPath Relative => Volumeless.Path.TrimStart('/', '\\');
public OSPath Absolute => IsAbsolute ? this : "/" + Relative;

public bool IsAbsolute => IsPathRooted(Path);
public bool HasVolume => IsAbsolute && Path[1] == ':';
public OSPath Volumeless => HasVolume
? (this - GetPathRoot(Path)).Absolute
: this;

public OSPath Parent => GetDirectoryName(Path);

public bool Contains(OSPath path) =>
Normalized.StartsWith(path);

public static OSPath operator +(OSPath left, OSPath right) =>
new OSPath(Combine(left, right.Relative));

public static OSPath operator -(OSPath left, OSPath right) =>
left.Contains(right)
? new OSPath(left.Normalized.Substring(right.Normalized.Length)).Relative
: left;
}

• To me it looks like a god-class. I find you should have separate classes for each OS that support conversion to one-another. I also think that Volumeless should be calles OSLess since this is an OS-free path, right? – t3chb0t Jun 13 '18 at 14:37
• @t3chb0t 1) I think it's not bigger than Uri or something like that. Pretty common for stable utility classes with no dependencies and not many reasons to change. 2) Yep, Volumeless sounds ugly, agree :) – Dmitry Nogin Jun 13 '18 at 14:43
• Uri isn't a good example becaue unlike your class, it's a generic representation or the URI concept. It does not hardcode e.g. schemas like your OSPath does with Windows & Unix etc. – t3chb0t Jun 13 '18 at 14:50
• I also think that it's not very usefull because I cannot just exchange how the parth is represented by injecting a different dependency because you need to use Windows or Unix properties which are virtually hardcoded paths representations. – t3chb0t Jun 13 '18 at 14:53
• @t3chb0t It is pretty close (supports / in AbsolutePath and \ in LocalPath. I have general representation of OS Path. Actually, without anything asking for polymorphic behavior - I need Unix representation all the time for persistence, etc. – Dmitry Nogin Jun 13 '18 at 14:55

There are a couple of things that bother me:

1) I don't think you have properly implemented relative and absolute paths. At least in Windows: absolute path = working directory + relative path. Your class only does this conversion correctly, if working directory is root directory, which is almost never the case in .Net. I don't know how things are in Linux, but from Windows perspective this is a surprising behavior. It should be either well documented or fixed.

2) You assume that 1:1 two-way conversion always exists. But does it? Windows is not case sensitive, Linux is. Windows has one letter volumes, Linux does not. Those are just a few differences off the top of my head. You can replace slashes all you want but that won't give you volume information.

3) How would you add macOS to current implementation? AFAIR, it is very much like Linux, but not exactly the same, is it? To me it looks like you would have to pretty much rewrite the entire thing. This is a good indication that overall design can be improved.

• OK, you got me guys :) Inheriting UnixPath and WindowsPath from OSPath :) And it should probably be Rooted instead of Absolute... Also, from a pragmatic point of view, 1:1 is not always necessary, so it does make sense to have something like that... To persist a relative path, or to append it to URL, etc. – Dmitry Nogin Jun 13 '18 at 16:42
• Nope, I was right originally - splitting OSPath to multiple classes is just a useless over-complication with a lot of consequences and no practical gain. P.S. MacOS is the same as *nix, so this class will never ever be interested in modification. It does not make sense to convert damn simple and very useful small utility to anything bigger :) – Dmitry Nogin Jun 13 '18 at 17:40
• @DmitryNogin anything having less than three classes is worthless :-P (just kidding) - but really, having everything in a single class is like having a book where on the left side there is one topic and on the right side another one :-] – t3chb0t Jun 13 '18 at 17:51
• @t3chb0t SRP is all about managing stability (giving one reason to change). This type will not change - it is common for small utilities. They just need to be useful by including everything used together. Look at String or that one I mentioned before -Uri class LocalPath and AbsolutePath properties - they do map from Unix to Windows for the same purposes and implemented in the same class. Unfortunately, Uri can not swallow /foo/bar :) – Dmitry Nogin Jun 13 '18 at 18:34
• @DmitryNogin, just to clarify: I don't think that you should have multiple classes to represent path. The general idea of having an entity to represent OS-agnostic path makes sense to me. I just think that the current implementation can be improved. In particular I think that you should pick one OS's path notation and use it internally as common denominator. Your implementation is too complex, because sometimes Path property holds windows path, and sometimes it holds linux path, and you have to account for both possibilities every step of the way. – Nikita B Jun 13 '18 at 18:42