できない.dev

相対パスを絶対パスにするには

相対パスを絶対パスへ変換する基本形を各言語で示す。
基準になるのはスクリプトの場所ではなく実行時のカレントディレクトリである点と、.. の除去やシンボリックリンクの解決まで行うかどうかが関数ごとに違う点を扱う。

公開:

各言語見出しの横のバッジは検証状態を表す。実行確認済みはコードを実際に実行して確認したもの、静的確認は構文と公式 API ドキュメントで確認したものである。

Python 実行確認済み

from pathlib import Path
import os
 
print(os.getcwd())                            # /work
print(Path("logs/../report.csv").resolve())   # /work/report.csv
print(Path("logs/../report.csv").absolute())  # /work/logs/../report.csv
 
# カレントディレクトリではなく、このスクリプトの場所を基準にする
print((Path(__file__).parent / "config.json").resolve())

resolve はカレントディレクトリを基準に絶対パスへ変換し、.. の除去とシンボリックリンクの解決まで行う。
absolute は前にカレントディレクトリを付けるだけで .. を残す。
相対パスはスクリプトの場所ではなく実行時のカレントディレクトリから解釈されるため、同梱ファイルを読むときは __file__ を基準にする。

JavaScript 実行確認済み

import path from "node:path";
 
console.log(process.cwd());                       // /work
console.log(path.resolve("logs/../report.csv"));  // /work/report.csv
console.log(path.resolve("/srv/app", "logs"));    // /srv/app/logs
 
// カレントディレクトリではなく、スクリプトの場所を基準にする
console.log(path.resolve(import.meta.dirname, "config.json"));

path.resolve は右側の要素から順に前へつなぎ、絶対パスになった時点で止め、最後まで絶対パスにならなければ process.cwd() を前に付ける。
ファイルが実在するかは確かめず、シンボリックリンクも解決しないので、実体のパスが必要なら fs.realpath を使う。

うまくいかない時: Node.js の ESM で「__dirname is not defined」が解決できない

TypeScript 実行確認済み

import { isAbsolute, resolve } from "node:path";
 
// 基準ディレクトリを引数で受け取れるようにしておくと、テストで cwd に左右されない
function toAbsolute(p: string, base: string = process.cwd()): string {
  return resolve(base, p);
}
 
console.log(toAbsolute("logs/app.log"));                 // /work/logs/app.log
console.log(toAbsolute("logs/../app.log", "/srv/app"));  // /srv/app/app.log
console.log(toAbsolute("/var/log/app.log", "/srv/app")); // /var/log/app.log
console.log(isAbsolute("logs/app.log"));                 // false

使い方は JavaScript と同じで、戻り値は string になる。
基準ディレクトリを引数で受け取る形にしておくと、呼び出し側のカレントディレクトリに左右されずテストしやすい。
引数が最初から絶対パスなら基準は無視され、そのパスが正規化されて返る。

Go 実行確認済み

package main
 
import (
	"fmt"
	"log"
	"os"
	"path/filepath"
)
 
func main() {
	wd, err := os.Getwd()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(wd) // /work
 
	abs, err := filepath.Abs("logs/../report.csv")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(abs)                               // /work/report.csv
	fmt.Println(filepath.IsAbs("logs/report.csv")) // false
}

filepath.Abs はカレントディレクトリを前に付けてから Clean するので、.. も取り除かれる。
ファイルが存在するかは確かめず、シンボリックリンクも解決しないため、実体のパスが必要なら続けて filepath.EvalSymlinks を呼ぶ。

Rust 実行確認済み

use std::env;
use std::fs;
use std::path;
 
fn main() -> std::io::Result<()> {
    println!("{}", env::current_dir()?.display()); // /work
 
    // path::absolute はファイルが無くても使えるが、.. は残る
    println!("{}", path::absolute("logs/../report.csv")?.display()); // /work/logs/../report.csv
 
    // canonicalize は .. とシンボリックリンクまで解決するが、実在しないとエラーになる
    match fs::canonicalize("logs/../report.csv") {
        Ok(p) => println!("{}", p.display()),
        Err(e) => println!("エラー: {e}"), // エラー: No such file or directory (os error 2)
    }
    Ok(())
}

fs::canonicalize は .. の除去とシンボリックリンクの解決まで行う代わりに、パスが実在しないとエラーを返す。
これから作るファイルの保存先を組み立てる段階では、Rust 1.79 で安定化した std::path::absolute を使う(POSIX では .. を残す)。

Java 実行確認済み

import java.io.IOException;
import java.nio.file.Path;
 
public class AbsolutePath {
    public static void main(String[] args) throws IOException {
        System.out.println(Path.of("").toAbsolutePath()); // /work
 
        Path p = Path.of("logs/../report.csv");
        System.out.println(p.toAbsolutePath());             // /work/logs/../report.csv
        System.out.println(p.toAbsolutePath().normalize()); // /work/report.csv
 
        // toRealPath はシンボリックリンクまで解決するが、実在しないと NoSuchFileException になる
        System.out.println(Path.of(".").toRealPath());      // /work
    }
}

toAbsolutePath はカレントディレクトリを前に付けるだけで .. を残すので、normalize を続けて呼んで取り除く。
シンボリックリンクまで解決した実体のパスは toRealPath で得られるが、こちらはファイルが実在しないと例外になる。

C# 実行確認済み

using System.IO;
 
Console.WriteLine(Directory.GetCurrentDirectory());               // /work
Console.WriteLine(Path.GetFullPath("logs/../report.csv"));        // /work/report.csv
// 基準ディレクトリを明示する 2 引数版
Console.WriteLine(Path.GetFullPath("logs/app.log", "/srv/app"));  // /srv/app/logs/app.log
Console.WriteLine(Path.IsPathRooted("logs/app.log"));             // False

Path.GetFullPath はカレントディレクトリを基準に .. や . を取り除いた絶対パスを返し、ファイルの存在は確かめない。
カレントディレクトリはプロセス全体で共有される状態なので、ライブラリの中では基準ディレクトリを渡す 2 引数版(.NET Core 2.1 以降)を使うほうが安全である。

つまずき

相対パスは、スクリプトの置き場所ではなく実行時のカレントディレクトリを基準に解釈される。
リポジトリのルートで python src/main.py と起動すると、main.py の中に書いた data/config.json は src/data ではなくルート直下の data を指す。
起動する場所で結果が変わるので、プログラムに同梱したファイルを読むときは、Python の __file__ や Node.js の import.meta.dirname を基準に組み立てる。

.. の除去とシンボリックリンクの解決は別の処理

絶対パスにする関数には、カレントディレクトリを前に付けるだけのもの(Python の absolute、Rust の path::absolute、Java の toAbsolutePath)、.. まで取り除くもの(Node.js の path.resolve、Go の filepath.Abs、C# の Path.GetFullPath)、シンボリックリンクまで解決するもの(Python の resolve、Rust の canonicalize、Java の toRealPath)がある。
最後の種類のうち Rust と Java はパスが実在しないとエラーになるので、これから作るファイルの保存先には使えない。

出力例について

コード中のコメントに書いた出力は、Linux でカレントディレクトリを /work にして実行したものである。
Windows では C:\work\report.csv のように、ドライブ名が付き区切り文字が \ になる。

この記事は役立ちましたか?