Denoでパスの操作をする方法について解説します。
基本は URL API を使う
パス文字列を手動で操作する前に、まずはURL APIの使用を検討しましょう。
const pathToFoo = new URL("./foo.txt", import.meta.url);
URL APIはWeb APIの一つです。
URL APIを使うと
- Windowsにおけるドライブ文字の処理
- Windows/linuxにおけるパス区切り文字の差異の吸収(スラッシュとバックスラッシュ)
- パス同士の結合
を全てやってくれます。
特にドライブ文字の処理は手で書いてデバッグするの大変ですから、積極的にURL APIを使いましょう。
パスの結合
パスの結合にはURLコンストラクタを使用します。
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
を使うと、自分のファイルを基準にした相対パスが取れます。
// 自分のファイルが`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つは同じ値になる
ライブラリ等を https://deno.land/x に公開する場合、import.meta.url
とfetch
を使うと、ライブラリで読み込まれた時とローカルで実行した時の両方で動作するコードになります。
// このファイルをローカルで実行した場合、`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オブジェクトを引数に取ることができます。
Deno.readTextFile(new URL(...));
fetch(new URL(...));
標準ライブラリのpathモジュール
URL APIではできない特殊な操作については、標準ライブラリのpathモジュールを使います。
例えばDeno.cwd()
で得られるカレントディレクトリはパス文字列ですから、URLとして扱うことはできません。そのため、下のtoFileUrl
などを使ってURLに変換することになります。
fileURLとパス文字列の相互変換
まずはパス文字列とfileURLの変換方法です。標準ライブラリのfromFileUrl
/toFileUrl
を使います。
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
を使います。
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
を使います。
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
があります。
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
を使います。
import { normalize } from "https://deno.land/std@0.130.0/path/mod.ts";
console.log(normalize("./foo/bar/baz/../../")); // => "foo\\"
globを正規表現に変換
globToRegExp
を使います。
詳しい仕様は公式ドキュメントへ
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
関数は、オブジェクトをパス文字列に変換します。
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を持つオブジェクトです。
{
/** パスのルート('/' 又は 'c:\') */
root: string;
/** ディレクトリのフルパス */
dir: string;
/** ファイル名(拡張子付き) */
base: string;
/** 拡張子 */
ext: string;
/** ファイル名(拡張子なし) */
name: string;
}
絶対パスかどうかの判定
isAbsolute
を使うと絶対パスかどうか判定できます。
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の区切り文字と思われます。
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を使うことができます。
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を使う