# Path

The `Path` service builds and inspects file system paths without hard-coding
separators or assumptions about the host OS. By depending on the abstract `Path`
service and providing a platform implementation (`NodePath.layer` on Node.js),
the same code produces POSIX-style paths on Linux and macOS and Windows-style
paths on Windows. Most methods are pure string operations; the two that can fail
(`fromFileUrl` and `toFileUrl`) return an `Effect` with a `BadArgument` error.

```ts
import { NodePath } from "@effect/platform-node"
import { Effect, Path } from "effect"

const program = Effect.gen(function*() {
  const path = yield* Path.Path

  // `join` concatenates segments using the platform separator and collapses
  // redundant slashes — never build paths with string templates.
  const config = path.join("home", "ada", "project", "effect.config.ts")
  // "home/ada/project/effect.config.ts" on POSIX

  // `resolve` produces an absolute path, resolving against the current
  // working directory and processing "." and ".." segments.
  const absolute = path.resolve("project", "..", "shared", "util.ts")

  // Decompose a path into its parts.
  yield* Effect.log(path.basename(config))   // "effect.config.ts"
  yield* Effect.log(path.dirname(config))    // "home/ada/project"
  yield* Effect.log(path.extname(config))    // ".ts"

  yield* Effect.log(`${config}\n${absolute}`)
}).pipe(
  // Provide the Node implementation of the Path service.
  Effect.provide(NodePath.layer)
)
```

## Building paths: `join` vs `resolve`

Both combine segments, but they answer different questions.

- **`join`** is pure concatenation: it glues segments with the separator,
  collapses redundant slashes, and normalizes `.`/`..` — but it never reaches
  outside the string you gave it. The result is absolute only if the first
  segment already is.
- **`resolve`** computes an **absolute** path. It processes segments
  right-to-left until it finds an absolute one; if none is absolute, it anchors
  the result to the current working directory (`process.cwd()` on Node).

```ts
import { Effect, Path } from "effect"

const program = Effect.gen(function*() {
  const path = yield* Path.Path

  // join: relative in, relative out.
  yield* Effect.log(path.join("a", "b", "..", "c")) // "a/c"
  yield* Effect.log(path.join("/var", "log", "app")) // "/var/log/app"

  // resolve: always absolute. Relative to cwd when no segment is rooted.
  yield* Effect.log(path.resolve("a", "b")) // e.g. "/current/working/dir/a/b"

  // The right-most absolute segment wins; earlier segments are discarded.
  yield* Effect.log(path.resolve("/etc", "/usr", "bin")) // "/usr/bin"
})
```

Use `join` to assemble a path under a known base. Use `resolve` when you need a
canonical absolute path (e.g. to compare two locations or pass to the file
system regardless of where the process started).

## Inspecting and decomposing paths

These pure helpers pull a path apart. `basename` optionally strips a known
suffix, and `parse` / `format` round-trip an entire path through a structured
object.

```ts
import { Effect, Path } from "effect"

const program = Effect.gen(function*() {
  const path = yield* Path.Path
  const file = "/home/ada/project/main.ts"

  yield* Effect.log(path.basename(file))         // "main.ts"
  yield* Effect.log(path.basename(file, ".ts"))  // "main"  (suffix stripped)
  yield* Effect.log(path.dirname(file))          // "/home/ada/project"
  yield* Effect.log(path.extname(file))          // ".ts"
  yield* Effect.log(path.isAbsolute(file))       // true
  yield* Effect.log(path.sep)                    // "/" (POSIX) or "\\" (win32)

  // parse -> Path.Parsed, format rebuilds the string. They are inverses.
  const parsed = path.parse(file)
  // { root: "/", dir: "/home/ada/project", base: "main.ts", ext: ".ts", name: "main" }
  yield* Effect.log(path.format(parsed))         // "/home/ada/project/main.ts"
})
```

## Parsing and formatting

`parse` turns a path string into a structured `Path.Parsed` object, and
`format` rebuilds a path from such an object. This pair is the cleanest way to
change one component of a path — for example, swapping a file extension:

```ts
import { Effect, Path } from "effect"

// Replace the extension of a path, e.g. ".ts" -> ".js".
const changeExtension = Effect.fn("changeExtension")(function*(
  file: string,
  ext: string
) {
  const path = yield* Path.Path

  const parsed = path.parse(file)
  // `parsed` is { root, dir, base, ext, name }. When `base` is set it wins,
  // so clear it and let `format` rebuild from `name` + `ext`.
  return path.format({ dir: parsed.dir, name: parsed.name, ext })
})

const program = Effect.gen(function*() {
  const result = yield* changeExtension("src/index.ts", ".js")
  yield* Effect.log(result) // "src/index.js"
})
```

:::note[format precedence]
`format` prefers `base` over `name` + `ext`, and `dir` over `root`. If you set
`base`, the `name` and `ext` fields are ignored. To rebuild from `name`/`ext`,
omit `base` entirely (as the example above does).
:::

## Relative and normalized paths

`relative` computes the path from one location to another, `isAbsolute` tells
you whether a path is already rooted, and `normalize` collapses `.`/`..`
segments and redundant separators. These are useful when displaying paths
relative to a project root or cleaning up user-supplied input:

```ts
import { Effect, Path } from "effect"

const program = Effect.gen(function*() {
  const path = yield* Path.Path

  const from = "/home/ada/project"
  const to = "/home/ada/project/src/main.ts"

  yield* Effect.log(path.relative(from, to)) // "src/main.ts"
  yield* Effect.log(path.relative(to, from)) // "../.."
  yield* Effect.log(path.isAbsolute(to))     // true
  yield* Effect.log(path.normalize("a/./b/../c")) // "a/c"

  // toNamespacedPath is a no-op on POSIX; on Windows it adds the \\?\ prefix.
  yield* Effect.log(path.toNamespacedPath("/home/ada")) // "/home/ada" (POSIX)
})
```

## File URLs

Converting between paths and `file://` URLs is the one place `Path` can fail —
an invalid URL or non-`file:` scheme yields a `BadArgument` (a
`PlatformError`). Because these return effects, the failure is typed and
handled like any other:

```ts
import { Effect, Path } from "effect"

const program = Effect.gen(function*() {
  const path = yield* Path.Path

  // `import.meta.url` is a file:// URL; convert it to a filesystem path.
  const filePath = yield* path.fromFileUrl(new URL("file:///home/ada/app.ts"))
  yield* Effect.log(filePath) // "/home/ada/app.ts"

  const url = yield* path.toFileUrl("/home/ada/app.ts")
  yield* Effect.log(url.href) // "file:///home/ada/app.ts"
}).pipe(
  Effect.catchTag("BadArgument", (error) =>
    Effect.logError(`invalid path or URL: ${error.message}`)
  )
)
```

`fromFileUrl` rejects URLs whose scheme is not `file:`, that carry a hostname,
or that contain percent-encoded path separators (`%2F`). Every other method on
the service is a pure, synchronous string operation that cannot fail.

## Choosing a separator style

`NodePath.layer` follows the host operating system, but you can force a specific
style with `NodePath.layerPosix` (always `/`) or `NodePath.layerWin32` (always
`\`). This is handy when generating paths for a different target than the
machine you are running on:

```ts
import { NodePath } from "@effect/platform-node"
import { Effect, Path } from "effect"

const program = Effect.gen(function*() {
  const path = yield* Path.Path
  yield* Effect.log(path.join("a", "b", "c"))
}).pipe(
  // Always emit POSIX paths regardless of the host OS.
  Effect.provide(NodePath.layerPosix)
)
```

The core `effect` package also ships a built-in POSIX implementation as
`Path.layer`, so code that does not depend on `@effect/platform-node` can still
satisfy the `Path` requirement (with `/` separators and POSIX semantics):

```ts
import { Effect, Path } from "effect"

const program = Effect.gen(function*() {
  const path = yield* Path.Path
  yield* Effect.log(path.join("a", "b", "c")) // "a/b/c"
}).pipe(Effect.provide(Path.layer))
```

Paths produced here are exactly what the [`FileSystem`](https://effect.plants.sh/platform/file-system/)
service expects, so the two services compose naturally.

## API reference

The `Path` service is accessed through the `Path.Path` tag. Every method below
is a member of the `Path` interface. All are pure synchronous string operations
except `fromFileUrl` and `toFileUrl`, which return an `Effect`.

### `Path.Path`

The `Context.Service` tag used to access the current path implementation from
the environment. Provide it with `Path.layer`, `NodePath.layer`, or a custom
implementation.

```ts
import { Effect, Path } from "effect"

const program = Effect.gen(function*() {
  const path = yield* Path.Path // resolve the service
  return path.join("a", "b")
}) // => Effect<string, never, Path.Path>
```

### `Path.layer`

A `Layer` providing the built-in POSIX implementation from the core `effect`
package. Separator is `/`; semantics are POSIX.

```ts
import { Effect, Path } from "effect"

const run = Effect.gen(function*() {
  const path = yield* Path.Path
  return path.sep // => "/"
}).pipe(Effect.provide(Path.layer))
```

### `sep`

The platform path-segment separator as a string.

```ts
path.sep // => "/" on POSIX, "\\" on Windows
```

### `basename`

Returns the last portion of a path. An optional second argument is a suffix to
strip from the result.

```ts
path.basename("/foo/bar/baz.html")        // => "baz.html"
path.basename("/foo/bar/baz.html", ".html") // => "baz"
path.basename("/foo/bar/")                 // => "bar"
```

### `dirname`

Returns the directory portion of a path (everything before the last segment).

```ts
path.dirname("/foo/bar/baz.html") // => "/foo/bar"
path.dirname("file.txt")          // => "."
path.dirname("/foo")              // => "/"
```

### `extname`

Returns the extension of the path — the substring from the last `.` to the end
of the last segment — or an empty string when there is none.

```ts
path.extname("index.html") // => ".html"
path.extname("index.")     // => "."
path.extname("index")      // => ""
path.extname(".gitignore") // => "" (leading dot is not an extension)
```

### `format`

Builds a path string from a partial `Path.Parsed` object. `base` takes
precedence over `name` + `ext`, and `dir` takes precedence over `root`.

```ts
path.format({ dir: "/home/ada", name: "file", ext: ".ts" }) // => "/home/ada/file.ts"
path.format({ root: "/", base: "file.txt" })                // => "/file.txt"
path.format({ name: "file", ext: ".txt" })                  // => "file.txt"
```

### `fromFileUrl`

Converts a `file:` `URL` into a filesystem path. Fails with `BadArgument` when
the scheme is not `file:`, a hostname is present, or the path contains an
encoded separator (`%2F`). Returns `Effect<string, BadArgument>`.

```ts
import { Effect, Path } from "effect"

Effect.gen(function*() {
  const path = yield* Path.Path
  return yield* path.fromFileUrl(new URL("file:///home/ada/app.ts"))
  // => "/home/ada/app.ts"
})
```

### `isAbsolute`

Returns whether the path is absolute (rooted).

```ts
path.isAbsolute("/foo/bar") // => true
path.isAbsolute("foo/bar")  // => false
path.isAbsolute("")         // => false
```

### `join`

Joins all segments with the platform separator, then normalizes the result.
Empty segments are ignored; with no segments it returns `"."`.

```ts
path.join("/foo", "bar", "baz/asdf", "quux", "..") // => "/foo/bar/baz/asdf"
path.join("a", "", "b")                            // => "a/b"
path.join()                                        // => "."
```

### `normalize`

Resolves `.` and `..` segments and collapses redundant separators. Does not
make the path absolute and preserves a trailing separator if present.

```ts
path.normalize("/foo/bar//baz/asdf/quux/..") // => "/foo/bar/baz/asdf"
path.normalize("a/./b/../c")                 // => "a/c"
path.normalize("")                           // => "."
```

### `parse`

Decomposes a path into a `Path.Parsed` object: `{ root, dir, base, ext, name }`.
The inverse of `format`.

```ts
path.parse("/home/ada/file.txt")
// => { root: "/", dir: "/home/ada", base: "file.txt", ext: ".txt", name: "file" }

path.parse("file.txt")
// => { root: "", dir: "", base: "file.txt", ext: ".txt", name: "file" }
```

### `relative`

Returns the relative path from `from` to `to`. Both arguments are resolved to
absolute paths first; equal paths yield an empty string.

```ts
path.relative("/data/orandea/test/aaa", "/data/orandea/impl/bbb")
// => "../../impl/bbb"
path.relative("/home/ada/project", "/home/ada/project/src/main.ts")
// => "src/main.ts"
```

### `resolve`

Resolves segments right-to-left into an absolute path, anchoring to the current
working directory if no absolute segment is found. Processes `.` and `..`.

```ts
path.resolve("/foo/bar", "./baz")    // => "/foo/bar/baz"
path.resolve("/foo/bar", "/tmp/file") // => "/tmp/file"
path.resolve("wwwroot", "static_files/png/", "../gif/image.gif")
// => "<cwd>/wwwroot/static_files/gif/image.gif"
```

### `toFileUrl`

Converts a filesystem path into a `file:` `URL`. The path is resolved to
absolute first and special characters are percent-encoded. Returns
`Effect<URL, BadArgument>`.

```ts
import { Effect, Path } from "effect"

Effect.gen(function*() {
  const path = yield* Path.Path
  const url = yield* path.toFileUrl("/home/ada/app.ts")
  return url.href // => "file:///home/ada/app.ts"
})
```

### `toNamespacedPath`

Returns the namespace-prefixed path. A no-op on POSIX; on Windows it prefixes
absolute paths with `\\?\` (the extended-length namespace).

```ts
// POSIX implementation:
path.toNamespacedPath("/home/ada") // => "/home/ada"
// Windows implementation:
// path.toNamespacedPath("C:\\foo") // => "\\\\?\\C:\\foo"
```

### `Path.Parsed`

The structured object produced by `parse` and consumed by `format`. All fields
are strings.

```ts
interface Parsed {
  readonly root: string // filesystem root, e.g. "/"  (or "" if relative)
  readonly dir: string  // full directory, e.g. "/home/ada"
  readonly base: string // file name with extension, e.g. "file.txt"
  readonly ext: string  // extension including the dot, e.g. ".txt"
  readonly name: string // file name without extension, e.g. "file"
}
```

For a worked example, parse a path and rebuild it after changing one field —
see [Parsing and formatting](#parsing-and-formatting) above.

### Providing a custom implementation

`Path.Path` is an ordinary service tag, so you can supply your own
implementation (for tests, virtual file systems, or alternate platforms) with
`Layer.succeed`. The implementation must include the `Path.TypeId` marker:

```ts
import { Effect, Layer, Path } from "effect"

const fakePath = Path.Path.of({
  [Path.TypeId]: Path.TypeId,
  sep: "/",
  basename: (p) => p.split("/").pop() ?? "",
  dirname: (p) => p.split("/").slice(0, -1).join("/") || "/",
  extname: (p) => p.match(/\.[^.]*$/)?.[0] ?? "",
  format: ({ dir, name, ext }) => `${dir ?? ""}/${name ?? ""}${ext ?? ""}`,
  fromFileUrl: (url) => Effect.succeed(url.pathname),
  isAbsolute: (p) => p.startsWith("/"),
  join: (...ps) => ps.join("/"),
  normalize: (p) => p.replace(/\/+/g, "/"),
  parse: (p) => ({ root: "", dir: "", base: p, ext: "", name: p }),
  relative: (from, to) => to.replace(from, ""),
  resolve: (...ps) => ps.join("/"),
  toFileUrl: (p) => Effect.succeed(new URL(`file://${p}`)),
  toNamespacedPath: (p) => p
})

const testLayer = Layer.succeed(Path.Path)(fakePath)
```