できない.dev

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

ログインしているユーザーのホームディレクトリのパスを各言語で取得する基本形を示す。
ファイル API は ~ を展開しない点と、環境変数 HOME を優先する言語・無視する言語・実在しない場所を指すと空文字を返す言語があることまでを扱う。

公開:

選んだ言語は、ほかのサンプルコードを開いたときも最初に表示する。

Python

import os
from pathlib import Path
 
print(Path.home())                      # /home/taro
print(Path.home() / ".config")          # /home/taro/.config
print(os.path.expanduser("~/.config"))  # /home/taro/.config
print(os.path.expanduser("~root"))      # /root
 
# ファイル API は ~ を展開しない
print(os.path.exists("~"))              # False
print(os.path.expanduser("a/~/b"))      # a/~/b(先頭の ~ だけが対象)
 
try:
    open("~/memo.txt", "w")
except FileNotFoundError as e:
    print(e)                            # [Errno 2] No such file or directory: '~/memo.txt'

Path.home() は環境変数 HOME を優先し、無ければ passwd のホームを返す。
文字列の ~ を展開するのは os.path.expanduser で、先頭の ~ だけが対象になり、a/~/b のように途中にある ~ は変えない。
open("~/memo.txt") のようにファイル API へ直接渡しても展開されず、FileNotFoundError になる。

JavaScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
 
console.log(os.homedir());                       // /home/taro
console.log(path.join(os.homedir(), ".config")); // /home/taro/.config
 
// ファイル API は ~ を展開しない
console.log(fs.existsSync("~"));                 // false
 
// 展開が必要なら、先頭の ~ だけを自前で置き換える
function expandHome(p) {
  if (p === "~" || p.startsWith("~/")) return path.join(os.homedir(), p.slice(1));
  return p;
}
console.log(expandHome("~/.config"));           // /home/taro/.config
console.log(expandHome("a/~/b"));               // a/~/b

os.homedir() は環境変数 HOME を優先し、無ければ passwd のホームを返す。
fs などのファイル API は ~ を展開しないので、展開が必要なら先頭の ~ だけを自前で置き換える。
p.startsWith("~/") で判定するため、a/~/b のように途中にある ~ は変わらない。

TypeScript

import os from "node:os";
import path from "node:path";
 
function expandHome(p: string, home: string = os.homedir()): string {
  if (p === "~") return home;
  if (p.startsWith("~/")) return path.join(home, p.slice(2));
  return p;
}
 
console.log(expandHome("~"));                      // /home/taro
console.log(expandHome("~/.config"));              // /home/taro/.config
console.log(expandHome("~root/.ssh"));             // ~root/.ssh(~ユーザー名 は展開しない)
console.log(expandHome("a/~/b"));                  // a/~/b
console.log(expandHome("~/.config", "/srv/app"));  // /srv/app/.config(home を渡せばテストしやすい)

展開する関数が home を引数に取り、既定値に os.homedir() を渡している。
テストでは別のホームを渡せるので、実行する環境のホームに結果が左右されない。
~root/.ssh のような ~ユーザー名 の形は展開せずそのまま返すので、必要になったら別に扱う。

Go

package main
 
import (
	"fmt"
	"os"
	"os/user"
	"path/filepath"
)
 
func main() {
	home, err := os.UserHomeDir()
	if err != nil {
		panic(err)
	}
	fmt.Println(home)                           // /home/taro
	fmt.Println(filepath.Join(home, ".config")) // /home/taro/.config
 
	// os/user は passwd を引くので、HOME とは別の経路で求める
	u, err := user.Current()
	if err != nil {
		panic(err)
	}
	fmt.Println(u.HomeDir) // /home/taro
 
	// ファイル API は ~ を展開しない
	_, err = os.Stat("~")
	fmt.Println(os.IsNotExist(err)) // true
}

os.UserHomeDir は Unix で環境変数 HOME だけを読み、未設定または空のときはエラーを返す。
passwd を引く user.Current() の HomeDir は HOME を見ないので、HOME を書き換えても /home/taro のままになる。
ファイル API の os.Stat("~") は展開せず、~ という名前を探して存在しないエラーを返す。

Rust

use std::env;
use std::path::Path;
 
fn main() {
    let home = env::home_dir().expect("home directory not found");
    println!("{}", home.display());                 // /home/taro
    println!("{}", home.join(".config").display()); // /home/taro/.config
 
    // ファイル API は ~ を展開しない
    println!("{}", Path::new("~").exists()); // false
}

env::home_dir() は HOME を優先し、無ければ passwd のホームを返す。
戻り値が Option<PathBuf> なので、サンプルでは取れなかったときに expect で止めている。
ファイル API の Path::exists は ~ を展開せず、~ という名前を探すだけなので false になる。

Java

import java.nio.file.Files;
import java.nio.file.Path;
 
public class Main {
    public static void main(String[] args) {
        Path home = Path.of(System.getProperty("user.home"));
        System.out.println(home);                    // /home/taro
        System.out.println(home.resolve(".config")); // /home/taro/.config
 
        // ファイル API は ~ を展開しない
        System.out.println(Files.exists(Path.of("~"))); // false
    }
}

System.getProperty("user.home") は環境変数 HOME を見ず、passwd のホームを返す。
HOME=/tmp/other を付けて実行しても /home/taro のままだった。
差し替えたいときは -Duser.home=/tmp/other のようにシステムプロパティで渡す。
Files.exists(Path.of("~")) は ~ を展開せず false になる。

C#

using System;
using System.IO;
 
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
Console.WriteLine(home);                          // /home/taro
Console.WriteLine(Path.Combine(home, ".config")); // /home/taro/.config
 
// 既定では、フォルダが実在しないと空文字が返る。パスだけが欲しいときは DoNotVerify を渡す
string raw = Environment.GetFolderPath(
    Environment.SpecialFolder.UserProfile, Environment.SpecialFolderOption.DoNotVerify);
Console.WriteLine(raw == home); // True
 
// ファイル API は ~ を展開しない
Console.WriteLine(Directory.Exists("~")); // False

Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) は Linux で HOME を優先し、無ければ passwd のホームを返す。
既定ではそのフォルダが実在しないと空文字を返すので、パスだけが欲しいときは SpecialFolderOption.DoNotVerify を渡す。
ファイル API の Directory.Exists("~") は展開せず False になる。

~ を展開するのはシェルで、ファイル API ではない

~ を「ホームディレクトリ」として扱うのはシェルの機能であり、ファイル API は ~ をただの文字として受け取る。
Python で open("~/memo.txt", "w") を実行すると、~ という名前のディレクトリが無いので FileNotFoundError になる。

作る側はもっと厄介で、Python の os.makedirs("~/data") や Node.js の fs.mkdirSync("~/data", { recursive: true }) は、エラーにならずカレントディレクトリの下に ~ という名前のディレクトリを作る(どちらも実行して確かめた)。
設定ファイルや引数で受け取った ~/data のような文字列は、ファイル API に渡す前に展開する。

Python には展開用の os.path.expanduser があり、先頭の ~ だけが対象になる。
JavaScript と TypeScript のサンプルは、ホームを求めたうえで先頭の ~ を自前で置き換えている。

環境変数 HOME を見る言語と見ない言語

Linux で、passwd 上のホームが /home/taro のユーザーに HOME=/tmp/other を付けて実行し、ホームを求める 1 行の結果を比べた(/tmp/other は実在しないディレクトリである)。

  • Python の Path.home(): /tmp/other
  • Node.js の os.homedir(): /tmp/other
  • Go の os.UserHomeDir(): /tmp/other(user.Current().HomeDir は /home/taro のまま)
  • Rust の env::home_dir(): /tmp/other
  • Java の user.home: /home/taro(HOME を見ない)
  • C# の GetFolderPath: 空文字(SpecialFolderOption.DoNotVerify を渡すと /tmp/other)

Java だけが HOME を見ずに passwd のホームを返すので、HOME を一時ディレクトリに差し替えて試すテストでは、Java だけが本物のホームを読み書きしてしまう。
差し替えたいときは -Duser.home=/tmp/other のようにシステムプロパティで渡す。

C# は HOME を優先するものの、既定ではそのフォルダが実在しないと空文字を返す。
空文字が返る場合に備えて、DoNotVerify を付けるか、空文字を弾いてから使う。

HOME が未設定のとき・空のとき

env -u HOME で HOME を未設定にして同じ 1 行を実行すると、Python・Node.js・Rust・Java・C# は passwd のホームである /home/taro を返す。
Go の os.UserHomeDir() だけは $HOME is not defined というエラーを返す。

HOME= で空文字にすると結果が割れる。
Python の Path.home() は /、Node.js の os.homedir() は空文字、Go は未設定のときと同じエラーになり、Rust・Java・C# は /home/taro を返す。

空文字や / を受け取ったまま .config を結合すると、意図しない場所を指すパスができる。
Go では err を、Node.js では空文字を確認してから使う。

Windows では USERPROFILE を見る

Windows 11 で HOME に別のパスを設定して試したところ、Python の Path.home() と Node.js の os.homedir() はどちらも USERPROFILE の値を返し、HOME は見なかった。
Linux の感覚で HOME を書き換えてホームを差し替えるテストは、Windows では効かない。

Go・Rust・Java・C# の Windows での挙動は確かめていないので、Windows で使うときは各言語のドキュメントで確認してほしい。

出力例について

コード中のコメントは、Docker の公式イメージ(Python 3.14、Node.js 22、Go 1.27、Rust 1.99、OpenJDK 21、.NET SDK 8.0)の Linux で、ホームが /home/taro の一般ユーザーとして 2026-10-19 に実行した結果である。
root で実行するとホームは /root になる。
TypeScript は Node.js 22 上で tsx を使って実行した。

このコードで動いた?