Osy#betaRun it on Osyrin: osyrin.com

Reference / Stdlib

Path — the text of a file path

Path.Combine(a, b) · Path.GetExtension(p) · Path.GetFileName(p) · Path.GetDirectoryName(p) · Path.GetFullPath(p, basePath)

C#'s `System.IO.Path`: build and take apart file paths as text — `Path.Combine`, `GetFileName`, `GetFileNameWithoutExtension`, `GetExtension`, `ChangeExtension`, `GetDirectoryName`, `GetRelativePath`. Pure (nothing reads a disk), with .NET's Linux rules, in a server function and in the browser alike.

stable2 examples compiled by CIpathfilesbcl

Summary#

Path is C#'s System.IO.Path, spelled as in C# and answering what .NET answers on Linux, where Osyrin runs:

Path.Combine("invoices", "2026", "a.pdf");     // "invoices/2026/a.pdf"
Path.GetExtension("report.tar.gz");             // ".gz"
Path.GetFileNameWithoutExtension("/x/y.z.w");   // "y.z"
Path.GetDirectoryName("invoices/2026/a.pdf");   // "invoices/2026"
Path.ChangeExtension("a.txt", "md");            // "a.md"
Path.GetFullPath("../b.txt", "/app/docs");      // "/app/b.txt"

Signature#

string  Path.Combine(string a, string b[, string c[, string d]])   // also Path.Combine(params string[])
string  Path.Join(string? a, string? b[, …])                     // like Combine, but a rooted part does not restart
string? Path.GetFileName(string? path)
string? Path.GetFileNameWithoutExtension(string? path)
string? Path.GetExtension(string? path)                          // ".gz" — with the dot; "" when there is none
bool    Path.HasExtension(string? path)
string? Path.ChangeExtension(string? path, string? extension)
string? Path.GetDirectoryName(string? path)                      // null for "/" (a root) and for null
string? Path.GetPathRoot(string? path)                           // "/" or ""
bool    Path.IsPathRooted(string? path)
bool    Path.IsPathFullyQualified(string path)
bool    Path.EndsInDirectorySeparator(string? path)
string  Path.TrimEndingDirectorySeparator(string path)
string  Path.GetFullPath(string path, string basePath)            // normalizes `.`, `..` and `//` against a base
string  Path.GetFullPath(string path)                             // a ROOTED path only — see below
string  Path.GetRelativePath(string relativeTo, string path)      // both rooted
char    Path.DirectorySeparatorChar                               // '/'
char    Path.AltDirectorySeparatorChar                            // '/'   — Linux has no second separator
char    Path.VolumeSeparatorChar                                  // '/'   — nor a drive letter's ':'
char    Path.PathSeparator                                        // ':'
char[]  Path.GetInvalidFileNameChars()                            // '\0' and '/'
char[]  Path.GetInvalidPathChars()                                // '\0'

Description#

A path here is text: nothing in Path touches storage, so it needs no capability and no use. It is how an app builds the names it hands to File.WriteAllBytes(..., about: row) (File.WriteAllText(Path.Combine("exports", name), …)), takes apart an upload's name, or picks the extension off a URL's last segment.

The rules are .NET's on Linux — / separates, a path is rooted when it starts with one, nothing is a drive, and : separates a list of paths. So all three separator constants are '/': Path.AltDirectorySeparatorChar is the same character because Linux has no second separator, and Path.VolumeSeparatorChar too because there is no drive letter to end with ':'. Code written for Windows that splits on either keeps working. Path.Combine restarts at a rooted part (Combine("docs", "/abs", "a.txt") is "/abs/a.txt") and Path.Join does not; GetExtension answers the dot too, and "" for "a."; GetDirectoryName collapses doubled separators and answers null for a root. The same calls give the same answers in a server function and in the browser.

An app has no current directory. It runs on a shared host, and the host's working directory is not the app's, so the two members C# resolves against it — Path.GetFullPath(relative) and Path.GetRelativePath over a relative path — throw PlatformNotSupportedException (.NET's own shape for a member a platform does not have) and name the fix: say what the path is relative to with Path.GetFullPath(path, basePath). A path that is already rooted needs no current directory and normalizes exactly as in C#.

Examples#

Name an export inside the app's storage, keeping the upload's own extension:

/// "exports/2026-10/invoice-17.pdf"
string ExportName(string month, int number, string uploadedAs) =>
  Path.Combine("exports", month, "invoice-" + number + Path.GetExtension(uploadedAs));

/// "y.z" — the name without its LAST extension.
string Stem(string path) => Path.GetFileNameWithoutExtension(path);

/// "/app/b.txt" — `..` and `.` resolved against the base you name.
string Resolved(string path) => Path.GetFullPath(path, "/app/docs");

/// "../c/d"
string Between(string from, string to) => Path.GetRelativePath(from, to);
[Test]
void Every_separator_is_a_slash() {
  Assert.Equal('/', Path.DirectorySeparatorChar);
  Assert.Equal('/', Path.AltDirectorySeparatorChar);
  Assert.Equal('/', Path.VolumeSeparatorChar);
  Assert.Equal(':', Path.PathSeparator);
}

See also#

Related

File.WriteAllBytes(..., about: row)

Write a file by path ABOUT a row — a receipt for an order, a label for a shipment — and the row finds it again:…

Encoding — Base64, hex, text encodings, URL, HTML

Encode and decode text — Base64, URL percent-encoding, and HTML escaping — with the C#-faithful spellings…

Uri

Parse a URL into its parts with the C#-faithful `new Uri(url)` handle. Construct it from a URL string, then read…