# Path — the text of a file path

> 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.

<!-- id: stdlib-path · area: stdlib · stability: stable · html: https://osysharp.com/reference/stdlib/path/ -->

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

```osy syntax
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      {#signature}
```osy syntax
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    {#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)](https://osysharp.com/reference/storage/file-about/) (`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       {#examples}

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

```osy title="building and taking apart a path" test app=stdlib-path
/// "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);
```

```osy title="the separators, pinned" run app=stdlib-path-separators
[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       {#see-also}
- [File.WriteAllBytes(..., about: row)](https://osysharp.com/reference/storage/file-about/) — read and write the files these paths name
- [Uri](https://osysharp.com/reference/stdlib/uri/) — take a URL apart (`new Uri(url).AbsolutePath`)
- [Encoding — Base64, hex, text encodings, URL, HTML](https://osysharp.com/reference/stdlib/encoding/) — text ↔ bytes, for a file's contents
