できない.dev

パスを連結するには

ディレクトリ名とファイル名をつないで 1 本のパスを組み立てる基本形を各言語で示す。
区切り文字を OS に任せる書き方に加え、途中に絶対パスが混ざったとき前の部分が捨てられるかどうかが言語ごとに違う点までを扱う。

公開:

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

Python 実行確認済み

from pathlib import Path
import os
 
base = Path("data")
print(base / "2026" / "report.csv")            # data/2026/report.csv
print(os.path.join("data", "2026", "report.csv"))
 
# 途中に絶対パスがあると、それより前は捨てられる
print(base / "/tmp/report.csv")                # /tmp/report.csv

pathlib.Path は / 演算子でつなげるので、区切り文字を文字列で書かずに済み、Windows では自動で \ になる。
os.path.join も同じ規則でつなぐが戻り値が str なので、続けて拡張子や親ディレクトリを扱うなら Path のまま持つほうが扱いやすい。

JavaScript 実行確認済み

import path from "node:path";
 
console.log(path.join("data", "2026", "report.csv"));       // data/2026/report.csv
// 余分な区切り文字や .. は正規化される。/ で始まる要素も普通につなぐだけ
console.log(path.join("data/", "/2026", "../report.csv"));  // data/report.csv
 
// ESM でスクリプト自身の場所を基準にするなら import.meta.dirname を使う
console.log(path.join(import.meta.dirname, "config.json"));

path.join は要素をつないだあとで余分な区切り文字や .. を正規化する。
ESM では __dirname が使えないため、スクリプトの場所を基準にしたいときは Node.js 20.11 以降で使える import.meta.dirname を先頭に置く。

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

TypeScript 実行確認済み

import { join, resolve } from "node:path";
 
const dir: string = "data";
const file: string = join(dir, "2026", "report.csv");
console.log(file);                                // data/2026/report.csv
 
console.log(join(dir, "/tmp", "report.csv"));     // data/tmp/report.csv
// 後ろの絶対パスを優先させたいときは join ではなく resolve を使う
console.log(resolve(dir, "/tmp", "report.csv"));  // /tmp/report.csv

使い方は JavaScript と同じで、戻り値は string 型になる。
join は / で始まる要素も単につなぐだけなので、途中の絶対パスを起点にしたい場面では resolve を選ぶ。

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

Go 実行確認済み

package main
 
import (
	"fmt"
	"path"
	"path/filepath"
)
 
func main() {
	fmt.Println(filepath.Join("data", "2026", "report.csv")) // data/2026/report.csv
	fmt.Println(filepath.Join("data", "/tmp", "report.csv")) // data/tmp/report.csv
 
	// URL のパスのように常に / で区切るものは path.Join を使う
	fmt.Println(path.Join("api", "v1", "users")) // api/v1/users
}

ファイルシステムのパスには、OS の区切り文字を使う filepath.Join を選ぶ。
path パッケージは常に / で区切るので URL などに向き、Windows のファイルパスに使うと区切り文字が混ざる。
filepath.Join は途中の絶対パスも普通の要素としてつなぎ、結果を Clean してから返す。

Rust 実行確認済み

use std::path::{Path, PathBuf};
 
fn main() {
    let p: PathBuf = Path::new("data").join("2026").join("report.csv");
    println!("{}", p.display()); // data/2026/report.csv
 
    // join に絶対パスを渡すと、それまでのパスは置き換えられる
    println!("{}", Path::new("data").join("/tmp/report.csv").display()); // /tmp/report.csv
 
    let mut buf = PathBuf::from("data");
    buf.push("2026");
    buf.push("report.csv");
    println!("{}", buf.display()); // data/2026/report.csv
}

Path::join は新しい PathBuf を返し、PathBuf::push は自分自身を書き換える。
どちらも追加する側が絶対パスだと、それまでのパスを丸ごと置き換える。
Path は Display を実装していないので、表示するときは display() を通す。

Java 実行確認済み

import java.nio.file.Path;
 
public class JoinPath {
    public static void main(String[] args) {
        Path p = Path.of("data", "2026", "report.csv");
        System.out.println(p); // data/2026/report.csv
 
        Path base = Path.of("data");
        System.out.println(base.resolve("2026").resolve("report.csv")); // data/2026/report.csv
        // resolve に絶対パスを渡すと、その絶対パスがそのまま返る
        System.out.println(base.resolve("/tmp/report.csv")); // /tmp/report.csv
    }
}

Path.of は可変長引数で要素をまとめてつなぎ、既存の Path に足していくときは resolve を使う。
resolve は引数が絶対パスならそれをそのまま返すので、外から受け取った名前をつなぐときは、結果が基準ディレクトリの下に収まっているかを確かめる。

C# 実行確認済み

using System.IO;
 
Console.WriteLine(Path.Combine("data", "2026", "report.csv")); // data/2026/report.csv
 
// Combine は途中に絶対パスがあると、それより前を捨てる
Console.WriteLine(Path.Combine("data", "/tmp", "report.csv")); // /tmp/report.csv
// Join は区切り文字を補ってつなぐだけで、前の部分を捨てない
Console.WriteLine(Path.Join("data", "/tmp", "report.csv"));    // data/tmp/report.csv

Path.Combine は途中の要素が絶対パスだとそこから組み立て直し、Path.Join(.NET Core 3.0 以降)は区切り文字を補ってつなぐだけで前の部分を残す。
外から受け取った名前をつなぐ場面では、どちらの挙動を期待しているのかを決めてから選ぶ。

つまずき

途中の要素が絶対パスだったときの扱いが言語で割れる。
Python の pathlib と os.path.join、Rust の Path::join、Java の resolve、C# の Path.Combine は、その絶対パスより前を捨てて組み立て直す。
Node.js の path.join、Go の filepath.Join、C# の Path.Join は、普通の要素としてつなぐだけである。
設定値やユーザー入力のように / で始まるかもしれない文字列をつなぐと、前者では基準ディレクトリの外を指すパスがそのまま出来上がる。

区切り文字を文字列で足さない

"data" + "/" + name のように手で連結すると、Windows では / と \ が混ざったパスになり、name の先頭に区切り文字があれば // が生じる。
どの言語にも OS に合わせてつなぐ関数があるので、それを使う。
コード中のコメントに書いた出力は Linux で実行したもので、Windows では区切り文字が \ になる。

外から受け取った名前は基準の外に出ないか確かめる

アップロードされたファイル名などをそのままつなぐと、../../etc/passwd のような値で基準ディレクトリの外を指せてしまう。
連結の関数だけでは防げないので、絶対パスに変換して正規化したうえで、結果が基準ディレクトリの下にあるかを確かめてから使う。

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