【Deno】パスやURLの操作方法まとめ2022

Denoでパスの操作をする方法について解説します。

## 基本は [URL API](https://developer.mozilla.org/ja/docs/Web/API/URL) を使う

パス文字列を手動で操作する前に、まずは[URL API](https://developer.mozilla.org/ja/docs/Web/API/URL)の使用を検討しましょう。

```ts
const pathToFoo = new URL("./foo.txt", import.meta.url);
```

URL APIはWeb APIの一つです。
URL APIを使うと

- Windowsにおけるドライブ文字の処理
- Windows/linuxにおけるパス区切り文字の差異の吸収（スラッシュとバックスラッシュ）
- パス同士の結合

を全てやってくれます。
特にドライブ文字の処理は手で書いてデバッグするの大変ですから、積極的にURL APIを使いましょう。

#### パスの結合

パスの結合にはURLコンストラクタを使用します。

```ts
const pathA = "./foo.txt";
const pathB = "/path/to/dir/";
const path = new URL(pathA, pathB); // pathBにpathAを結合したもの
```

URLコンストラクタの左と右に文字列を渡すと両者が結合されたURLが返ります。

#### 自ファイル基準の相対パス

`import.meta.url`や`import.meta.resolve`を使うと、自分のファイルを基準にした相対パスが取れます。

```ts
// 自分のファイルが`file:///a/b/c.ts`のとき、`file:///a/b/foo.txt`
// 自分のファイルが`https://example.com/a/b/c.ts`のとき、`https://example.com/a/b/foo.txt`
const relativePath1 = new URL("./foo.txt", import.meta.url);
const relativePath2 = new URL(import.meta.resolve("./foo.txt"));
// 上の2つは同じ値になる
```

:::note
ライブラリ等を https://deno.land/x に公開する場合、`import.meta.url`と`fetch`を使うと、ライブラリで読み込まれた時とローカルで実行した時の両方で動作するコードになります。

```ts
// このファイルをローカルで実行した場合、`file:///...`
// このファイルが https://deno.land/x からimportされた場合、`https://deno.land/x/...`
const url = new URL("./foo.wasm", import.meta.url);

// fetchは`https://...`と`file:///...`の両方のURLを読むことができる。
// ローカルで実行した場合はローカルファイルが、
// deno.land/x からimportされた場合は deno.land/x からファイルが読み込まれる。
const res = await fetch(url);
console.log(await res.text());
```

この挙動はwasmファイルの読み込みなどに便利です。

ここで例えば`import.meta.url`の代わりにカレントディレクトリを使ったり、`fetch`の代わりに`Deno.readTextFile`を使ったりしてしまうと動きません。
:::

#### 他のAPIと組み合わせて使う

Denoのファイル操作系APIや、`fetch`などのWeb APIは、URLオブジェクトを引数に取ることができます。

```ts
Deno.readTextFile(new URL(...));
fetch(new URL(...));
```

## 標準ライブラリのpathモジュール

URL APIではできない特殊な操作については、標準ライブラリのpathモジュールを使います。

https://deno.land/std@0.130.0/path

例えば`Deno.cwd()`で得られるカレントディレクトリはパス文字列ですから、URLとして扱うことはできません。そのため、下の`toFileUrl`などを使ってURLに変換することになります。

#### fileURLとパス文字列の相互変換

まずはパス文字列とfileURLの変換方法です。標準ライブラリの`fromFileUrl`/`toFileUrl`を使います。

```ts
import { fromFileUrl, toFileUrl } from "https://deno.land/std@0.130.0/path/mod.ts";

// パス文字列→fileURLの変換
console.log(toFileUrl("/foo/bar.txt")); // => URL { href: "file:///foo/bar.txt" }
console.log(toFileUrl(`${Deno.cwd()}/`)) // => URL { href: "file:///C:/Users/path/to/cwd/"}

// fileURL→パス文字列の変換
console.log(fromFileUrl("file:///foo/bar.txt")); // => "\foo\bar.txt"
console.log(fromFileUrl(new URL("file:///foo/bar.txt"))); // => "\foo\bar.txt"
```

#### パス文字列からファイル名・ディレクトリ・拡張子の取得

それぞれ`basename`/`dirname`/`extname`を使います。

```ts
import { basename, dirname, extname } from "https://deno.land/std@0.130.0/path/mod.ts";

console.log(basename("/foo/bar.txt")); // => "bar.txt"
console.log(dirname("/foo/bar.txt")); // => "/foo"
console.log(extname("/foo/bar.txt")); // => ".txt"
```

#### パスの結合

パス文字列の結合には`join`か`resolve`を使います。

```ts
import { join, resolve } from "https://deno.land/std@0.130.0/path/mod.ts";

console.log(join("./foo/", "./bar/", "../baz.txt")); // => "foo\baz.txt"
console.log(resolve("./foo/", "./bar/", "../baz.txt")); // => "C:\Users\azusa\work\deno\eki\foo\baz.txt"
```

`join`が単純に複数のパス文字列を結合するのに対し、`resolve`はカレントディレクトリを考慮した上でフルパスを計算してくれます。

おそらく普通は`join`を使えばいいと思います。

#### 2つのパスの比較

パスAからパスBへの相対パスを計算する`relative`と、パス同士の共通部分を計算する`common`があります。

```ts
import { common, relative } from "https://deno.land/std@0.130.0/path/mod.ts";

// "./foo/"から"./bar/"への相対パスを計算する
console.log(relative("./foo/", "./bar/")); // => "..\\bar"

// ".\\foo\\bar\\a.txt"と".\\foo\\bar\\b.txt"の共通部分を計算する
console.log(common([".\\foo\\bar\\a.txt", ".\\foo\\bar\\b.txt"])); // => ".\\foo\\bar\\"
```

#### パスの正規化

`normalize`を使います。

```ts
import { normalize } from "https://deno.land/std@0.130.0/path/mod.ts";

console.log(normalize("./foo/bar/baz/../../")); // => "foo\\"
```

#### globを正規表現に変換

`globToRegExp`を使います。
詳しい仕様は[公式ドキュメント](https://doc.deno.land/https://deno.land/std@0.130.0/path/mod.ts/~/globToRegExp)へ

```ts
import { globToRegExp } from "https://deno.land/std@0.130.0/path/mod.ts";

const re = globToRegExp("/foo/*.txt");

console.log(re); // => /^(?:\\|\/)+foo(?:\\|\/)+[^\\/]*\.txt(?:\\|\/)*$/
console.log(re.test("/foo/bar.txt")); // => true
console.log(re.test("/bar/")); // => false
```

#### パス文字列のパース

`parse`関数は、パス文字列をオブジェクトへ変換します。
`format`関数は、オブジェクトをパス文字列に変換します。

```ts
import { format, parse } from "https://deno.land/std@0.130.0/path/mod.ts";

// パス文字列→オブジェクト
const parsed = parse("\\foo\\bar\\baz.txt");
console.log(parsed); // => { root: "\\", dir: "\\foo\\bar", base: "baz.txt", ext: ".txt", name: "baz" }

// オブジェクト→パス文字列
console.log(format(parsed)); // => "\\foo\\bar\\baz.txt"
```

`parse`関数の返り値は以下のようなinterfaceを持つオブジェクトです。

```ts
{
  /** パスのルート（'/' 又は 'c:\'） */
  root: string;
  /** ディレクトリのフルパス */
  dir: string;
  /** ファイル名（拡張子付き） */
  base: string;
  /** 拡張子 */
  ext: string;
  /** ファイル名（拡張子なし） */
  name: string;
}
```

#### 絶対パスかどうかの判定

`isAbsolute`を使うと絶対パスかどうか判定できます。

```ts
import { isAbsolute } from "https://deno.land/std@0.130.0/path/mod.ts";

console.log(isAbsolute("/foo")); // => true
console.log(isAbsolute("c:/foo")); // => true
console.log(isAbsolute("cat:/foo")); // => false
console.log(isAbsolute("./foo")); // => false
```

#### パス区切り文字

`sep`と`SEP`はパスの区切り文字です。
`SEP_PATTERN`はパスの区切り文字にマッチする正規表現です。
`delimiter`は環境変数PATHの区切り文字と思われます。

```ts
import { SEP, sep, SEP_PATTERN, delimiter } from "https://deno.land/std@0.130.0/path/mod.ts";

console.log(sep); // => "\\" (windowsとposixで異なる)
console.log(SEP); // => "\\" (windowsとposixで異なる)
console.log(SEP_PATTERN); // => /[\\/]+/
console.log(delimiter) // => ";" (windowsとposixで異なる)
```

## URLのパターンマッチ

URLのパターンマッチには[URLPattern API](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern)を使うことができます。

```ts
const pattern = new URLPattern({ pathname: "/foo/:id.txt" });

console.log(pattern.test({ pathname: "/foo/bar.txt" })); // => true
console.log(pattern.exec({ pathname: "/foo/bar.txt" })); // => { pathname: { input: "/foo/bar.txt", groups: { id: "bar" } }, ...}
```

`pattern.test()`でマッチしたかどうか（trueかfalse）、`pattern.exec()`でマッチ結果を得ることができます。詳しい使用方はMDNを見てください。

## まとめ

- パス文字列を直接操作することは避け、極力URL APIを使うのがオススメ
- 特殊なパス文字列の操作には標準ライブラリのpathモジュールを使う
- URLへのパターンマッチにはURLPattern APIを使う