できない.dev

親ディレクトリのパスを取得するには

ファイルやディレクトリのパスから、1 つ上の親ディレクトリのパスを取り出す基本形を各言語で示す。
末尾に / が付いたパスで結果が 1 階層ずれるかどうかと、ディレクトリ部分が無いパスやルートで何が返るかが言語ごとに違うので、その違いまで含めて扱う。

公開:

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

Python 実行確認済み

from pathlib import Path
import os
 
p = Path("data/logs/app.log")
print(p.parent)                    # data/logs
print(p.parents[1])                # data
print(Path("report.csv").parent)   # .
 
# 末尾に / があると、os.path.dirname はそのディレクトリ自身を返す
print(Path("data/logs/").parent)      # data
print(os.path.dirname("data/logs/"))  # data/logs
 
# このスクリプトが置かれたディレクトリ
print(Path(__file__).resolve().parent)

Path.parent は最後の要素を取り除いたパスを返し、2 階層以上さかのぼるときは parents[n] を使う。
os.path.dirname は最後の区切り文字より前を返すだけなので、data/logs/ のように末尾に / が付いたパスではそのディレクトリ自身が返る。
スクリプトと同じ場所のファイルを扱うときは、__file__ を resolve してから parent を取る。

JavaScript 実行確認済み

import path from "node:path";
import { fileURLToPath } from "node:url";
 
console.log(path.dirname("data/logs/app.log"));                // data/logs
console.log(path.dirname(path.dirname("data/logs/app.log")));  // data
console.log(path.dirname("data/logs/"));                       // data
console.log(path.dirname("report.csv"));                       // .
 
// ESM でこのファイルのディレクトリを得る
const here = path.dirname(fileURLToPath(import.meta.url));
console.log(here === import.meta.dirname);                     // true

path.dirname は最後の要素を取り除いたパスを返し、末尾の区切り文字は無視する。
ESM では __dirname が定義されないため、import.meta.url を fileURLToPath でファイルパスに直してから dirname に渡す。
Node.js 20.11 以降なら import.meta.dirname で同じ値が得られる。

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

TypeScript 実行確認済み

import { dirname } from "node:path";
 
// 親ディレクトリを近い順に並べる(設定ファイルを上の階層へ探すときの形)
function ancestors(p: string): string[] {
  const result: string[] = [];
  let current: string = p;
  // ルートの / と、ディレクトリ部分の無い . は dirname しても変わらない
  while (dirname(current) !== current) {
    current = dirname(current);
    result.push(current);
  }
  return result;
}
 
console.log(ancestors("/srv/app/src/index.ts")); // [ '/srv/app/src', '/srv/app', '/srv', '/' ]
console.log(ancestors("src/index.ts"));          // [ 'src', '.' ]

使い方は JavaScript と同じで、dirname の戻り値は string になる。
ルートの / とディレクトリ部分の無い . は dirname しても同じ値が返るので、戻り値が変わらなくなった時点でループを止めれば、上の階層へ設定ファイルを探す処理を無限ループにせずに書ける。

Go 実行確認済み

package main
 
import (
	"fmt"
	"path/filepath"
)
 
func main() {
	p := "data/logs/app.log"
	fmt.Println(filepath.Dir(p))               // data/logs
	fmt.Println(filepath.Dir(filepath.Dir(p))) // data
	fmt.Println(filepath.Dir("report.csv"))    // .
	fmt.Println(filepath.Dir("/"))             // /
 
	// 末尾に / があると、そのディレクトリ自身が返る
	fmt.Println(filepath.Dir("data/logs/"))                 // data/logs
	fmt.Println(filepath.Dir(filepath.Clean("data/logs/"))) // data
}

filepath.Dir は最後の区切り文字より後ろを取り除き、残りを Clean して返す。
data/logs/ のように末尾に / が付いたパスでは最後の要素が空とみなされてそのディレクトリ自身が返るので、ディレクトリのパスを受け取る場面では先に filepath.Clean で末尾の区切り文字を落とす。

Rust 実行確認済み

use std::path::Path;
 
fn main() {
    let p = Path::new("data/logs/app.log");
    println!("{:?}", p.parent()); // Some("data/logs")
    println!("{:?}", p.ancestors().nth(2)); // Some("data")
    // 末尾の / は無視される
    println!("{:?}", Path::new("data/logs/").parent()); // Some("data")
 
    // ルートでは None、ディレクトリ部分が無いと空のパスが返る
    println!("{:?}", Path::new("/").parent()); // None
    println!("{:?}", Path::new("report.csv").parent()); // Some("")
 
    let dir = match Path::new("report.csv").parent() {
        Some(d) if !d.as_os_str().is_empty() => d,
        _ => Path::new("."),
    };
    println!("{}", dir.display()); // .
}

parent は最後の要素を取り除いたパスを Option<&Path> で返し、ルートでは None になる。
report.csv のようにディレクトリ部分が無いときは None ではなく空のパスが返るので、カレントディレクトリとして使うなら空かどうかを確かめて . に置き換える。
2 階層以上さかのぼるときは、自分自身から順に親をたどる ancestors を使う。

Java 実行確認済み

import java.nio.file.Path;
 
public class ParentDirectory {
    public static void main(String[] args) {
        Path p = Path.of("data/logs/app.log");
        System.out.println(p.getParent());                     // data/logs
        System.out.println(p.getParent().getParent());         // data
        System.out.println(Path.of("data/logs/").getParent()); // data
 
        // ディレクトリ部分が無いときとルートでは null が返る
        System.out.println(Path.of("report.csv").getParent()); // null
        System.out.println(Path.of("/").getParent());          // null
 
        // 相対パスでも親を得たいときは、先に絶対パスにする
        System.out.println(Path.of("report.csv").toAbsolutePath().getParent()); // /work
    }
}

getParent は最後の要素を取り除いたパスを返すが、ディレクトリ部分が無い report.csv やルートでは null を返すため、戻り値にそのままメソッドを呼ぶと NullPointerException になる。
相対パスでも親を確実に得たいときは、先に toAbsolutePath で絶対パスにしてから getParent を呼ぶ。

C# 実行確認済み

using System.IO;
 
Console.WriteLine(Path.GetDirectoryName("data/logs/app.log")); // data/logs
// 末尾に / があると、そのディレクトリ自身が返る
Console.WriteLine(Path.GetDirectoryName("data/logs/"));        // data/logs
Console.WriteLine(Path.GetDirectoryName(Path.TrimEndingDirectorySeparator("data/logs/"))); // data
 
Console.WriteLine(Path.GetDirectoryName("report.csv") == "");  // True
Console.WriteLine(Path.GetDirectoryName("/") is null);         // True
 
// Directory.GetParent はカレントディレクトリを基準にした絶対パスで返す
Console.WriteLine(Directory.GetParent("data/logs/app.log")?.FullName); // /work/data/logs

Path.GetDirectoryName は最後の区切り文字より前を返す文字列操作で、末尾に / が付いたパスではそのディレクトリ自身が返り、ルートでは null を返す。
末尾の区切り文字は Path.TrimEndingDirectorySeparator(.NET Core 3.0 以降)で落としてから渡す。
Directory.GetParent は相対パスをカレントディレクトリ基準で解釈し、絶対パスの DirectoryInfo を返す点が異なる。

つまずき

ディレクトリのパスの末尾に / が付いているときの結果が言語で割れる。
Python の pathlib、Node.js の path.dirname、Rust の parent、Java の getParent は末尾の区切り文字を無視して data/logs/ の親を data とするが、Python の os.path.dirname、Go の filepath.Dir、C# の Path.GetDirectoryName は最後の区切り文字より前をそのまま返すので data/logs になる。
設定値やコマンドライン引数で受け取ったディレクトリの親を求めると、末尾に / を付けたかどうかで結果が 1 階層ずれる。
後者の関数を使うときは、先に os.path.normpath・filepath.Clean・Path.TrimEndingDirectorySeparator で末尾の区切り文字を取り除く。

ディレクトリ部分が無いパスとルートで返る値

report.csv のようにディレクトリ部分が無いパスでは、Python の pathlib・Node.js・Go は . を返し、Python の os.path.dirname と C# は空文字、Rust は空のパス、Java は null を返す。
ルートの / では、Python・Node.js・Go は / 自身を返し、Rust は None、Java と C# は null を返す。
親を順にさかのぼるループは、戻り値が変わらなくなった時点で止めるのか、None や null で止めるのかを言語に合わせて書き分ける。

パスの文字列を削るだけで、ファイルシステムは見ない

どの関数もパスの文字列から最後の要素を取り除くだけで、実際のディレクトリ構成は確かめない。
data/logs/.. は data と同じ場所を指すが、その親は data/logs として返る。
シンボリックリンクもたどらないので、リンクの親ではなく実体のディレクトリの親が必要なときは、Python の resolve や Node.js の fs.realpathSync のように .. とリンクを解決してから親を求める。

出力例と Windows 形式のパス

コード中のコメントに書いた出力は、Linux でカレントディレクトリを /work にして実行したものである。
Windows では区切り文字が \ になる。
逆に Linux 上では \ が区切り文字として扱われないため、C:\data\app.log のような Windows 形式のパス文字列は 1 つのファイル名とみなされ、Node.js の path.dirname は . を返す。
Windows のパスを別の OS で処理するなら、Node.js の path.win32 や Python の PureWindowsPath のように Windows の規則を明示して使う。

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