できない.dev

ディレクトリを作成するには

ディレクトリを新しく作る基本形を各言語で示す。
親ディレクトリが無いとき、既に存在するとき、同名のファイルがあるときの挙動が言語ごとに違うので、その分岐まで含めて扱う。

公開:

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

Python 実行確認済み

import os
from pathlib import Path
 
# 親が無いと mkdir 単体は失敗する
try:
    Path("build/logs").mkdir()
except FileNotFoundError:
    print("FileNotFoundError")  # FileNotFoundError
 
# parents=True で親もまとめて作り、exist_ok=True で既存でも成功にする
Path("build/logs").mkdir(parents=True, exist_ok=True)
Path("build/logs").mkdir(parents=True, exist_ok=True)  # 2 回目も成功
os.makedirs("build/cache/tmp", exist_ok=True)          # os 版でも同じ
print(Path("build/logs").is_dir())  # True
 
# exist_ok=True でも、同名のファイルがあれば失敗する
Path("build/report.txt").write_text("x")
try:
    Path("build/report.txt").mkdir(exist_ok=True)
except FileExistsError:
    print("FileExistsError")  # FileExistsError

Path.mkdir は親ディレクトリが無いと FileNotFoundError になるため、parents=True を付けて途中のディレクトリまでまとめて作る。
exist_ok=True を付けると既にあるときも成功になるので、何度実行しても同じ結果になる準備処理に向く。

JavaScript 実行確認済み

import path from "node:path";
import { mkdir, writeFile } from "node:fs/promises";
 
// 親が無いと recursive なしの mkdir は ENOENT で失敗する
try {
  await mkdir("build/logs");
} catch (err) {
  console.log(err.code); // ENOENT
}
 
// recursive: true なら親もまとめて作る。戻り値は最初に作ったディレクトリ
const first = await mkdir("build/logs", { recursive: true });
console.log(path.basename(first)); // build
console.log(await mkdir("build/logs", { recursive: true })); // undefined(既にあるので何も作らない)
 
// recursive なしだと、既にあるときは EEXIST で失敗する
try {
  await mkdir("build/logs");
} catch (err) {
  console.log(err.code); // EEXIST
}
 
// recursive: true でも、同名のファイルがあれば失敗する
await writeFile("build/report.txt", "x");
try {
  await mkdir("build/report.txt", { recursive: true });
} catch (err) {
  console.log(err.code); // EEXIST
}

fs の mkdir は recursive: true を付けると親までまとめて作り、既にあっても失敗しない。
戻り値は最初に新しく作ったディレクトリのパスで、すべて既にあったときは undefined になるため、新しく作ったかどうかの判定にも使える。

TypeScript 実行確認済み

import { mkdir } from "node:fs/promises";
 
// recursive: true の戻り値は string | undefined(最初に作ったパス。既にあれば undefined)
async function ensureDir(dir: string): Promise<boolean> {
  const first: string | undefined = await mkdir(dir, { recursive: true });
  return first !== undefined;
}
 
console.log(await ensureDir("build/logs")); // true(新しく作った)
console.log(await ensureDir("build/logs")); // false(既にあった)
 
// recursive なしなら、既にあるときは EEXIST。catch の err は unknown なので絞り込んでから読む
try {
  await mkdir("build/logs");
} catch (err) {
  console.log((err as NodeJS.ErrnoException).code); // EEXIST
}

recursive: true の mkdir は string | undefined を返すので、undefined かどうかで新しく作ったかを判定できる。
catch の err は unknown 型で、err.code と書くと TS18046 になるため、NodeJS.ErrnoException へ絞り込んでから code を読む。

うまくいかない時: TypeScript で catch の「Object is of type 'unknown'」が解決できない

Go 実行確認済み

package main
 
import (
	"errors"
	"fmt"
	"io/fs"
	"os"
)
 
func main() {
	// 親が無いと os.Mkdir は失敗する
	err := os.Mkdir("build/logs", 0o755)
	fmt.Println(errors.Is(err, fs.ErrNotExist)) // true
 
	// os.MkdirAll は親もまとめて作り、既にあっても nil を返す
	fmt.Println(os.MkdirAll("build/logs", 0o755)) // <nil>
	fmt.Println(os.MkdirAll("build/logs", 0o755)) // <nil>
 
	// os.Mkdir は既にあると失敗する
	err = os.Mkdir("build/logs", 0o755)
	fmt.Println(errors.Is(err, fs.ErrExist)) // true
 
	// 同名のファイルがあると os.MkdirAll も失敗する
	os.WriteFile("build/report.txt", []byte("x"), 0o644)
	fmt.Println(os.MkdirAll("build/report.txt", 0o755)) // mkdir build/report.txt: not a directory
}

os.Mkdir は親が無くても既にあっても失敗するので、準備処理には親までまとめて作る os.MkdirAll を使う。
MkdirAll は既にディレクトリがあれば nil を返すため何度呼んでも安全で、失敗の種類は errors.Is と fs.ErrNotExist などで見分ける。

Rust 実行確認済み

use std::fs;
 
fn main() -> std::io::Result<()> {
    // 親が無いと create_dir は失敗する
    let e = fs::create_dir("build/logs").unwrap_err();
    println!("{:?}", e.kind()); // NotFound
 
    // create_dir_all は親もまとめて作り、既にあっても Ok を返す
    fs::create_dir_all("build/logs")?;
    fs::create_dir_all("build/logs")?;
 
    // create_dir は既にあると失敗する
    let e = fs::create_dir("build/logs").unwrap_err();
    println!("{:?}", e.kind()); // AlreadyExists
 
    // 同名のファイルがあると create_dir_all も失敗する
    fs::write("build/report.txt", "x")?;
    let e = fs::create_dir_all("build/report.txt").unwrap_err();
    println!("{:?}", e.kind()); // AlreadyExists
    Ok(())
}

fs::create_dir は親が無いと NotFound、既にあると AlreadyExists で失敗する。
通常は親までまとめて作り、既にディレクトリがあれば Ok を返す fs::create_dir_all を使い、失敗の種類は io::Error の kind() で見分ける。

Java 実行確認済み

import java.io.IOException;
import java.nio.file.*;
 
public class MakeDir {
    public static void main(String[] args) throws IOException {
        Path dir = Path.of("build/logs");
 
        // 親が無いと createDirectory は失敗する
        try {
            Files.createDirectory(dir);
        } catch (NoSuchFileException e) {
            System.out.println("NoSuchFileException"); // NoSuchFileException
        }
 
        // createDirectories は親もまとめて作り、既にディレクトリがあれば何もしない
        Files.createDirectories(dir);
        Files.createDirectories(dir);
        System.out.println(Files.isDirectory(dir)); // true
 
        // createDirectory は既にあると失敗する
        try {
            Files.createDirectory(dir);
        } catch (FileAlreadyExistsException e) {
            System.out.println("FileAlreadyExistsException"); // FileAlreadyExistsException
        }
 
        // 同名のファイルがあると createDirectories も失敗する
        Path file = Path.of("build/report.txt");
        Files.writeString(file, "x");
        try {
            Files.createDirectories(file);
        } catch (FileAlreadyExistsException e) {
            System.out.println("FileAlreadyExistsException"); // FileAlreadyExistsException
        }
    }
}

Files.createDirectory は親が無いと NoSuchFileException、既にあると FileAlreadyExistsException になる。
準備処理には、親までまとめて作り、既にディレクトリがあれば何もしない Files.createDirectories を使う。

C# 実行確認済み

using System.IO;
 
// 親が無くても、CreateDirectory は途中のディレクトリをまとめて作る
DirectoryInfo made = Directory.CreateDirectory("build/logs");
Console.WriteLine(made.Name); // logs
 
// 既にあっても例外にならない
Directory.CreateDirectory("build/logs");
Console.WriteLine(Directory.Exists("build/logs")); // True
 
// 同名のファイルがあると IOException になる
File.WriteAllText("build/report.txt", "x");
try
{
    Directory.CreateDirectory("build/report.txt");
}
catch (IOException)
{
    Console.WriteLine("IOException"); // IOException
}

Directory.CreateDirectory は途中のディレクトリを含めてまとめて作り、既にあっても例外にならない。
親が無いと失敗する単体版や、既にあると失敗する版が無いので、そうしたい場面では先に Directory.Exists で調べる。

つまずき

ディレクトリを 1 つだけ作る関数は、親ディレクトリが無いと失敗する。
Python の Path.mkdir は FileNotFoundError、Node.js の mkdir は ENOENT、Go の os.Mkdir は fs.ErrNotExist、Rust の create_dir は NotFound、Java の createDirectory は NoSuchFileException と、名前は違っても理由は同じである。
どの言語にも親までまとめて作る形(parents=True・recursive: true・MkdirAll・create_dir_all・createDirectories)があり、C# の CreateDirectory は最初からそう動くので、普段使いはこちらを選べばよい。

既にあるときの扱いが形によって違う

親までまとめて作る形は、既にディレクトリがあっても成功する(Python は exist_ok=True を付けたときだけ)。
これに対して 1 つだけ作る形は、Python の FileExistsError、Node.js の EEXIST、Go の fs.ErrExist、Rust の AlreadyExists、Java の FileAlreadyExistsException のように、既にあることを失敗として返す。
どちらの動きになるかは使う関数で決まるので、準備処理には「あれば何もしない」形を、排他的に新規作成したいときには「あれば失敗」の形を選ぶ。

同名のファイルがあると成功する形でも失敗する

「既にあっても成功する」のは、あるものがディレクトリのときだけである。
同じ名前のファイルがあると、Python は exist_ok=True でも FileExistsError、Node.js は recursive: true でも EEXIST、Go の MkdirAll は not a directory、Rust は AlreadyExists、Java は FileAlreadyExistsException、C# は IOException で失敗する。
何かが既にあれば成功する、という意味ではないので、作れないときは出力先と同じ名前のファイルが残っていないかも疑う。

権限(mode)の効き方

mode を渡せる言語でも、実際の権限は umask で削られる。
umask 022 の Linux コンテナで測ると、mode に 0o777 を指定しても作られたディレクトリは 0o755 になった。
さらに Python の os.makedirs は mode を最後のディレクトリにだけ適用し、途中の親は既定の権限で作る(0o700 を指定すると、最後が 0o700 で親は 0o755 になった)。
Node.js の recursive: true と Go の os.MkdirAll は、新しく作ったすべてのディレクトリに mode を適用する(0o700 を指定するとすべて 0o700 になった)。

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