できない.dev

環境変数を真偽値として読むには

環境変数に入った "true" や "false" などの文字列を真偽値として読み取る基本形を各言語で示す。
文字列の "false" が真と評価される落とし穴と、標準関数が受け付ける表記の言語ごとの違い、未設定や不正な値の扱いまでを扱う。

公開:

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

Python 実行確認済み

import os
 
TRUE_VALUES = {"1", "true", "yes", "on"}
FALSE_VALUES = {"0", "false", "no", "off"}
 
 
def env_bool(name: str, default: bool = False) -> bool:
    raw = os.getenv(name)
    if raw is None or raw.strip() == "":
        return default  # 未設定・空文字は既定値
    value = raw.strip().lower()
    if value in TRUE_VALUES:
        return True
    if value in FALSE_VALUES:
        return False
    raise ValueError(f"{name} は真偽値として解釈できない: {raw!r}")
 
 
# DEBUG=false で実行した場合
print(bool(os.getenv("DEBUG")))   # -> True(空でない文字列はすべて真)
print(env_bool("DEBUG"))          # -> False
print(env_bool("VERBOSE", True))  # 未設定なら -> True

Python には環境変数の文字列を真偽値にする標準関数が無く、bool() で変換すると "false" や "0" も空でない文字列なので True になる。
以前使われた distutils.util.strtobool は Python 3.12 で distutils ごと削除され、import すると ModuleNotFoundError になるため、受け付ける値の集合を自前で定義して比較する。

うまくいかない時: Python import できない(ModuleNotFoundError)

JavaScript 実行確認済み

const TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
const FALSE_VALUES = new Set(["0", "false", "no", "off"]);
 
function envBool(name, defaultValue = false) {
  const raw = process.env[name];
  if (raw === undefined || raw.trim() === "") return defaultValue; // 未設定・空文字は既定値
  const value = raw.trim().toLowerCase();
  if (TRUE_VALUES.has(value)) return true;
  if (FALSE_VALUES.has(value)) return false;
  throw new Error(`${name} は真偽値として解釈できない: ${raw}`);
}
 
// DEBUG=false で実行した場合
console.log(Boolean(process.env.DEBUG)); // true(空でない文字列はすべて真)
console.log(envBool("DEBUG"));           // false
console.log(envBool("VERBOSE", true));   // 未設定なら true

Boolean() や !! で変換すると、"false" も空でない文字列なので true になる。
Node.js には文字列を真偽値に解釈する標準関数が無いため、受け付ける値を Set で決めて比較し、それ以外は例外にして設定ミスに気づけるようにする。

うまくいかない時: GitHub Actions の if 条件どおりにステップを実行できない

TypeScript 実行確認済み

const TRUE_VALUES: ReadonlySet<string> = new Set(["1", "true", "yes", "on"]);
const FALSE_VALUES: ReadonlySet<string> = new Set(["0", "false", "no", "off"]);
 
function envBool(name: string, defaultValue = false): boolean {
  const raw: string | undefined = process.env[name];
  if (raw === undefined || raw.trim() === "") return defaultValue; // 未設定・空文字は既定値
  const value = raw.trim().toLowerCase();
  if (TRUE_VALUES.has(value)) return true;
  if (FALSE_VALUES.has(value)) return false;
  throw new Error(`${name} は真偽値として解釈できない: ${raw}`);
}
 
// DEBUG=TRUE で実行した場合
console.log(process.env.DEBUG === "true"); // false(大文字の TRUE を取りこぼす)
const debug: boolean = envBool("DEBUG");
console.log(debug);                         // true
 
// const bad: boolean = process.env.DEBUG; // string | undefined は boolean に代入できない

process.env の値は string | undefined 型なので、そのまま boolean の変数に代入するとコンパイルエラーになる。
=== "true" だけで判定すると TRUE や 1 を黙って false にしてしまうため、正規化してから比較する関数を 1 つ用意し、戻り値を boolean にそろえる。

Go 実行確認済み

package main
 
import (
	"fmt"
	"os"
	"strconv"
	"strings"
)
 
func envBool(name string, def bool) (bool, error) {
	raw, ok := os.LookupEnv(name)
	if !ok || strings.TrimSpace(raw) == "" {
		return def, nil // 未設定・空文字は既定値
	}
	// ParseBool が受け付けるのは 1 t T TRUE true True 0 f F FALSE false False だけ
	v, err := strconv.ParseBool(strings.TrimSpace(raw))
	if err != nil {
		return def, fmt.Errorf("%s は真偽値として解釈できない: %w", name, err)
	}
	return v, nil
}
 
func main() {
	// DEBUG=false、ENABLE_CACHE=yes で実行した場合
	debug, err := envBool("DEBUG", false)
	fmt.Println(debug, err) // false <nil>
 
	cache, err := envBool("ENABLE_CACHE", false)
	fmt.Println(cache, err)
	// false ENABLE_CACHE は真偽値として解釈できない: strconv.ParseBool: parsing "yes": invalid syntax
}

strconv.ParseBool が受け付けるのは 1・t・T・TRUE・true・True と 0・f・F・FALSE・false・False だけで、yes や on、tRuE のような大文字小文字の混在はエラーになる。
前後の空白も許さないので、strings.TrimSpace で取り除いてから渡す。

Rust 実行確認済み

use std::env;
 
fn env_bool(name: &str, default: bool) -> Result<bool, String> {
    match env::var(name) {
        Err(env::VarError::NotPresent) => Ok(default), // 未設定は既定値
        Err(e) => Err(format!("{name}: {e}")),
        Ok(raw) => match raw.trim().to_ascii_lowercase().as_str() {
            "" => Ok(default),
            "1" | "true" | "yes" | "on" => Ok(true),
            "0" | "false" | "no" | "off" => Ok(false),
            _ => Err(format!("{name} は真偽値として解釈できない: {raw:?}")),
        },
    }
}
 
fn main() {
    // str::parse::<bool> が受け付けるのは小文字の "true" と "false" だけ
    println!("{:?}", "True".parse::<bool>()); // Err(ParseBoolError)
 
    // DEBUG=False で実行した場合
    println!("{:?}", env_bool("DEBUG", true)); // Ok(false)
    println!("{:?}", env_bool("VERBOSE", true)); // 未設定なら Ok(true)
}

str の parse::<bool> が受け付けるのは小文字の "true" と "false" だけで、True や 1 は Err になる。
環境変数では表記が揺れるため、to_ascii_lowercase で正規化してから match で受け付ける値を列挙し、未設定(VarError::NotPresent)と不正な値を区別して扱う。

Java 実行確認済み

import java.util.Locale;
import java.util.Set;
 
public class Main {
    static final Set<String> TRUE_VALUES = Set.of("1", "true", "yes", "on");
    static final Set<String> FALSE_VALUES = Set.of("0", "false", "no", "off");
 
    static boolean envBool(String name, boolean defaultValue) {
        String raw = System.getenv(name);
        if (raw == null || raw.isBlank()) return defaultValue; // 未設定・空文字は既定値
        String value = raw.strip().toLowerCase(Locale.ROOT);
        if (TRUE_VALUES.contains(value)) return true;
        if (FALSE_VALUES.contains(value)) return false;
        throw new IllegalArgumentException(name + " は真偽値として解釈できない: " + raw);
    }
 
    public static void main(String[] args) {
        // Boolean.parseBoolean は "true"(大文字小文字は無視)以外をすべて false にする
        System.out.println(Boolean.parseBoolean("TRUE")); // true
        System.out.println(Boolean.parseBoolean("yes"));  // false(例外にならない)
        System.out.println(Boolean.parseBoolean(null));   // false
 
        // DEBUG=yes で実行した場合
        System.out.println(envBool("DEBUG", false)); // true
    }
}

Boolean.parseBoolean は例外を投げず、大文字小文字を無視した "true" 以外をすべて false にするため、yes や 1、タイプミスの ture が黙って false になる。
自前で判定するときは、実行環境のロケールで結果が変わらないよう toLowerCase に Locale.ROOT を渡す。

C# 実行確認済み

using System;
 
static bool EnvBool(string name, bool defaultValue)
{
    string? raw = Environment.GetEnvironmentVariable(name);
    if (string.IsNullOrWhiteSpace(raw)) return defaultValue; // 未設定・空文字は既定値
 
    // bool.TryParse が受け付けるのは "true" と "false"(大文字小文字と前後の空白は無視)
    if (bool.TryParse(raw, out bool value)) return value;
    return raw.Trim().ToLowerInvariant() switch
    {
        "1" or "yes" or "on" => true,
        "0" or "no" or "off" => false,
        _ => throw new FormatException($"{name} は真偽値として解釈できない: {raw}"),
    };
}
 
Console.WriteLine(bool.TryParse("1", out _)); // False("1" は受け付けない)
 
// DEBUG=" False " で実行した場合
Console.WriteLine(EnvBool("DEBUG", true));   // False
Console.WriteLine(EnvBool("VERBOSE", true)); // 未設定なら True

bool.TryParse は大文字小文字と前後の空白を無視して "true" と "false" を受け付けるが、"1" や "yes" では false を返して失敗する。
TryParse で解釈できなかった値だけを switch で追加判定すると、標準の表記と慣用の表記を両方扱える。

つまずき

環境変数の値は常に文字列で渡るため、DEBUG=false と設定しても受け取る側では "false" という 5 文字の文字列になる。
Python の bool() や JavaScript の Boolean() は空でない文字列をすべて真と評価するので、if os.getenv("DEBUG"): のような書き方では false を指定してもデバッグモードが有効になる。
"0" も同じ理由で真になる。
GitHub Actions の式やシェルスクリプトから渡した値も文字列なので、真偽値として使う箇所では必ず明示的に変換する。

受け付ける値を決め、それ以外はエラーにする

true と false に加えて 1 と 0、yes と no、on と off のどれを受け付けるかはプロジェクトで決め、1 つの関数にまとめる。
受け付けない値を黙って false に倒すと、ture のようなタイプミスで機能が無効になっても気づけないため、例外やエラーで起動時に止めるほうが安全である。
未設定と空文字をどちらも既定値に倒しておくと、DEBUG= のように値を空にして渡した場合も未設定と同じ扱いになる。

標準関数が受け付ける表記は言語ごとに違う

標準で用意された変換関数は、受け付ける表記が言語ごとに異なる。
Go の strconv.ParseBool は 1 や t、TRUE まで受け付けるが yes や on は受け付けず、Rust の parse::<bool> は小文字の true と false しか受け付けない。
C# の bool.TryParse は大文字小文字と前後の空白を無視するが 1 は受け付けず、Java の Boolean.parseBoolean は true 以外をすべて false にして例外を投げない。
Python と JavaScript には相当する標準関数が無く、Python にあった distutils.util.strtobool は 3.12 で削除された。
同じ値を複数の言語のサービスで共有するなら、どの言語でも解釈が一致する小文字の true と false にそろえておくと食い違いが起きない。

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