こんにちは、おうどんです🍜
PATH に自分のツールのフォルダを足しました。setx で!
お、えらい。で、動いた?
『内部コマンドまたは外部コマンド、操作可能なプログラムまたはバッチ ファイルとして認識されていません』って言われました
長い。断り文句が長い
同じ窓で試しましたか? setx は、今開いている窓には届かないんです
(会話は説明用の架空の場面です)
環境変数は、Windows で開発をしていると毎日のように出てくるのに、ちゃんと習う機会があまりない言葉です。
PATH を通す、JAVA_HOME を設定する、バッチの中で set する。どれも環境変数の話です。
今日は、うどん屋の「のれん分け」でたとえます。
師匠の店(親のプロセス)には、秘伝のレシピ帳があります。
弟子が独立して店を出すとき(子のプロセスが起動するとき)、レシピ帳のコピーを1冊もらっていく。
弟子が自分の店でレシピを書き換えても、師匠の帳面は変わりません。
読者「師匠、心が広い」
心が広いというより、コピーなので、そもそも届かないんです。この「コピー」が今日の主役です。
今日のチェックリスト(初心者はここだけでも)
setや$env:で作った環境変数は、その窓(とそこから起動したもの)だけで有効setxや設定画面で変えた環境変数は、新しく開いた窓から有効。今の窓には届かない- PATH を変えたら、窓を開き直してから確かめる
環境変数は、プロセスが持ち歩く「名前=値」のメモ
レシピ帳の中身は、「名前=値」の行が並んだだけのものです。
秘伝のレシピなのに、そんなに単純なんですか
だし=こんぶ、麺=さぬき、店主=おうどん。以上
秘伝というより、ただの名札の束です。でも、その名札をプログラムが毎回見に来ます。
用語を先に2つ。
- プロセス:動いているプログラム1つ分のこと。コマンドプロンプトの窓1つも、そこから起動した python も、それぞれ1つのプロセスです
- 環境変数:プロセスが1冊ずつ持っている「名前=値」のメモ。プログラムは、ここから設定を読みます(PATH、TEMP、USERNAME など)
Microsoft の Win32 のドキュメントには、どのプロセスも環境ブロック(環境変数の一覧を入れる場所)を持っていて、環境変数にはユーザーごとのものと、全員に共通のシステムのものの2種類がある、と書かれています。
コマンドプロンプト(cmd)で作って、読んで、消してみます。
@echo off
set UDON=kitsune
echo UDON is %UDON%
echo lower is %udon%
set ud
set UDON=
echo after clear: [%UDON%]
set UDON
echo errorlevel=%errorlevel%
UDON is kitsune
lower is kitsune
UDON=kitsune
after clear: []
errorlevel=1
最後の set UDON のところでは、画面にこのメッセージ(標準エラー出力)も出ました。
環境変数 UDON が定義されていません
(今回の検証環境:Windows 10 の cmd.exe、Microsoft Windows [Version 10.0.19045.7807]。標準出力と標準エラー出力を分けて受け取ったので、2つのブロックに分けて載せています)
読み取れることは4つです。
set 名前=値で作り、%名前%で読む%udon%と小文字で書いても読めた。今回の検証では、名前の大文字・小文字を区別していませんset udは、ud で始まる変数を全部出す(set のドキュメントにも、値を付けて set を実行すると、その文字で始まる変数を表示するとあります)set UDON=と、値を書かずに=で終えると消える。消えた変数をset UDONで探すと、終了コード(ERRORLEVEL)は 1 でした
名前の大文字小文字を区別しないなら、Udon でも uDoN でも同じですか
同じ。ただ、uDoN と書いた人とは、あまり一緒に働きたくない
読めるからといって、書き方をばらばらにしていいわけではありません。チームでそろえましょう。
set で作った変数は、その窓だけのもの
ここで、のれん分けの話が出てきます。
親のバッチ(師匠)から、子のバッチ(弟子)を2通りの方法で呼んでみます。
親(e3_parent.bat):
@echo off
set UDON=kake
echo parent before: %UDON%
cmd /d /c "%~dp0e3_child.bat"
echo parent after cmd /c: %UDON%
call "%~dp0e3_child.bat"
echo parent after call: %UDON%
子(e3_child.bat):
@echo off
echo child sees: %UDON%
set UDON=tempura
echo child set: %UDON%
parent before: kake
child sees: kake
child set: tempura
parent after cmd /c: kake
child sees: kake
child set: tempura
parent after call: tempura
(%~dp0 は、このバッチファイルが置いてあるフォルダのことです)
cmd /cで呼んだ子は、新しい cmd のプロセス。親の UDON(kake)をコピーでもらい、自分の帳面を tempura に書き換えた。でも、親は kake のままcallで呼んだ子は、同じ cmd の中で動く。だから子の書き換えが、そのまま親に残った
Win32 のドキュメントにも、子のプロセスは、既定では親のプロセスの環境変数を引き継ぐ、コマンドプロセッサ(cmd)から起動したプログラムは cmd の環境変数を引き継ぐ、と書かれています。引き継ぐのは親から子への一方通行です。
じゃあ、子から親に値を返したいときは
弟子が師匠の店に夜中に忍び込んで、帳面を書き換える
忍び込めません。子から親へは、ファイルに書いて親が読むか、終了コードで返すのが普通です(call で同じ cmd の中で動かすなら、さっきのとおり残ります)。
PowerShell でも同じ
PowerShell では、$env:名前 で環境変数を読み書きします。
$env:UDON = 'kake'
"PS: $env:UDON"
"lower: $env:udon"
cmd /d /c "echo cmd child sees: %UDON%"
cmd /d /c "set UDON=tempura"
"PS after child: $env:UDON"
Get-ChildItem Env:UDON
"User scope: [" + [Environment]::GetEnvironmentVariable('UDON','User') + "]"
$env:UDON = ''
"exists after empty: " + (Test-Path Env:UDON)
PS: kake
lower: kake
cmd child sees: kake
PS after child: kake
Name Value
---- -----
UDON kake
User scope: []
exists after empty: False
(今回の検証環境:Windows PowerShell 5.1.19041.7802。表の行末の空白は省いています)
- 子の cmd には kake が届いた。子が tempura にしても、PowerShell 側は kake のまま
Get-ChildItem Env:で一覧が見られる(Env: は環境変数をドライブのように見せる仕組み)[Environment]::GetEnvironmentVariable('UDON','User')は空。$env:で作った変数は、ユーザーの設定(レジストリ)には書かれていない$env:UDON = ''で、変数そのものが消えた
PowerShell の about_Environment_Variablesにも、PowerShell で環境変数を変えても影響は今のセッションだけで、cmd の set と似た動きだ、と書かれています。空の文字列か $null を入れると、その変数は消えます。
空にしたつもりが消えている。PowerShell では「空の環境変数」は持てない、と覚えておくと、Test-Path で引っかかりません。
空っぽの丼を置いておきたかっただけなのに、丼ごと片づけられました
PowerShell の店員さんは、きれい好きなんです。空の丼は、置いた瞬間に下げます。
PATH は、コマンドを探しに行く棚のリスト
PATH は、たくさんある環境変数のうち、いちばんお世話になるものです。
コマンドの名前だけを打ったとき、Windows はどこを探せばいいかを PATH で知ります。PowerShell のドキュメントでも、PATH は実行ファイルを探すフォルダの一覧で、Windows では ;(セミコロン)で区切る、とされています。
うどん屋でいえば、道具を探しに行く棚のリストです。リストにない棚の道具は、目の前にあっても見つかりません。
tools フォルダに、udonya.bat(「udonya: irasshai!」と出すだけのバッチ)を置いて試します。
@echo off
call udonya
set "PATH=%~dp0tools;%PATH%"
call udonya
where udonya
udonya: irasshai!
C:\work\tools\udonya.bat
1回目の call udonya では、標準エラー出力にこう出ました。
'udonya' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。
(パスの一部を伏せています。標準出力と標準エラー出力は分けて載せています)
- 1回目は、tools が PATH にないので見つからない
set "PATH=%~dp0tools;%PATH%"で、今の PATH の先頭に tools を足した- 2回目は見つかった。
where udonyaで、どのファイルが使われるかも確かめられる
冒頭のエラー、これでした
棚のリストに、棚を書き足すのを忘れていたんだね
読者「いっそ C:\ を丸ごと PATH に入れれば、全部見つかるのでは」
倉庫の入口だけをリストに書いても、店員さんは倉庫の奥の棚までは探しに行きません。今回の検証でも、tools2\sub に置いたバッチは、tools2 を PATH に足しただけでは同じ「認識されていません」で、tools2\sub を足したら見つかりました。PATH には、ツールが入っているフォルダそのものを書きます。
ちなみにバッチからバッチを呼ぶときに call を付けたのは、付けないと呼んだ先に処理が移って、元のバッチに戻ってこないからです。今日の本題とは別の話なので、ここでは「バッチからバッチは call」とだけ。
set "PATH=..." のように、set の後ろから値の最後までを " でくくっているのにも理由があります。次の章の落とし穴で出てきます。
🔰 ここまで読めば今日から困らない
環境変数で困ったら、まずこの3つです。
set(cmd)や$env:(PowerShell)で作った値は、その窓と、そこから起動したプログラムだけのもの。窓を閉じたら消える- ずっと残したい値は、設定画面(システムのプロパティ → 環境変数)か
setxで書く。ただし、効くのは新しく開いた窓から - 「設定したのに反映されない」ときは、まず窓を開き直す。PATH なら
where コマンド名(cmd)で、どこが見つかるかを確かめる
窓を開き直すだけで直るなら、最初に教えてほしかったです
最初に教えると、ありがたみがないから
ありがたみより、新人さんの午前中のほうが大事です。
窓を20個くらい開いたまま暮らしている人は、どうするんですか
20個とも開き直す。または、引っ越す
引っ越さなくても、新しい設定を試すときは、新しい窓を1つ開けば済みます。古い窓は、古い帳面のまま働き続けるだけです。
窓を開き直してもだめなら、そもそも書いた場所(ユーザーかシステムか)や、値のつづりを疑います。そこから先が中級者向けの話です。
ここから先は中級者向け:環境変数の落とし穴
落とし穴1:set の = のまわりに空白を入れると、名前に空白が付く
@echo off
set UDON = kake
echo [%UDON%]
echo [%UDON %]
set "MEN=kake udon"
echo [%MEN%]
set SOBA="kake soba"
echo [%SOBA%]
[]
[ kake]
[kake udon]
["kake soba"]
set UDON = kake と書くと、UDON(後ろに空白付き)という名前の変数に、 kake(前に空白付き)という値が入ります。%UDON% では読めず、%UDON % なら読めました。
見た目、完全に同じなのに
のれんの文字は同じなのに、看板の後ろに空白が1文字掛かっている
そんな看板、誰も気づきません。だから = のまわりに空白は入れない、が鉄則です。
もう1つ。set SOBA="kake soba" は、" まで値に入りました。set のドキュメントにも、特別な文字を含む値を " でくくると、" も値の一部になる、とあります。
空白入りの値や、値の後ろの見えない空白を防ぎたいときは、set "MEN=kake udon" のように、名前から値の最後までをまとめてくくります。今回の結果では、こちらは " が値に入りませんでした。
落とし穴2:setlocal の中で変えた値は、endlocal で戻る
@echo off
set UDON=kake
setlocal
set UDON=tempura
echo inside setlocal: %UDON%
endlocal
echo after endlocal: %UDON%
inside setlocal: tempura
after endlocal: kake
setlocal から endlocal までは、帳面のコピーの上で作業している、と考えると分かりやすいです。バッチの中で PATH を書き換えて「戻し忘れ」が怖いときは、setlocal で囲んでおけば、終わったときに元に戻ります。
読者「setlocal、前にも見たな」
バッチファイルの for や if の中で変数が変わらない話で出てきた setlocal です。実践編として、あわせてどうぞ。
落とし穴3:setx は、今の窓に効かない
setx のドキュメントには、setx はレジストリの環境変数に書き込み、setx で設定した変数は、これから開くコマンドの窓でだけ使えて、今の窓では使えない、と書かれています。/m を付けると、ユーザーではなくシステムの環境変数に書きます。
rem ユーザーの環境変数に書く(今の窓には効かない)
setx UDON kake
rem 新しく開いた窓で確かめる
echo %UDON%
(上は説明用の書き方で、今回の検証では実行していません。setx はユーザーの設定を書き換えるためです)
冒頭の新人さんは、ここにはまりました。setx が書いたのは本部のレシピ帳(レジストリ)で、今開いている店(窓)は、開店したときにもらったコピーで営業を続けています。
じゃあ、setx のあとは、パソコンを再起動ですか
再起動は、店を全部たたんでから開き直すくらいの大工事
大工事の前に、まずは窓を開き直すところからです。
Win32 のドキュメントでは、システムの環境変数をプログラムから変えるときは、レジストリに書いたあとに WM_SETTINGCHANGE(設定が変わったことをアプリに知らせるメッセージ)を送ると、シェルなどのアプリが変更を拾える、と説明されています。逆にいえば、知らせを受け取らないアプリや、すでに動いているプログラムには届かないことがあります。エディターの中のターミナルなど、親のアプリから開く窓で反映されないときは、親のアプリごと開き直すのが確実です(どのアプリが知らせを拾うかは、アプリしだいです)。
落とし穴4:setx PATH "%PATH%;..." は、PATH を壊すことがある
ネットでよく見る、この書き方です。
rem おすすめしない書き方(実行していません)
setx PATH "%PATH%;C:\tools"
setx のドキュメントには、気をつけることが2つ書かれています。
- setx で入れられる内容は 1024 文字まで。超えた分は切られて、切られた値が入る。既存の変数に入れると、もとの値を失うことがある
- 既存の変数に setx を使うと、中の
%JAVADIR%のような参照は、展開した値に置き換えられる。そのあと JAVADIR を変えても、PATH には反映されない
今回の検証環境で、PowerShell の窓の PATH の長さを数えてみました。
"PATH length: " + $env:Path.Length
"PATH entries: " + ($env:Path -split ';').Count
PATH length: 1564
PATH entries: 42
1564 文字。ドキュメントどおりなら、この窓で setx PATH "%PATH%;C:\tools" をすると、1024 文字で切られた値がユーザーの PATH に入ることになります。
それに、窓の %PATH% は、システムの PATH とユーザーの PATH から作られたものです(PowerShell のドキュメントでも、プロセスの環境変数は、親から引き継がれ、マシンとユーザーの環境変数から作られる、とされています)。それをまるごとユーザーの PATH に書くと、システムの分がユーザーの側にも重なって入ります。
1564 文字の巻物を、1024 文字の箱に詰めたら
巻物の後ろ540文字が、箱の外に落ちる。しかも、落ちたほうに大事な棚が書いてある
PATH を永続的に足したいときは、設定画面の「環境変数」から、ユーザーの Path を選んで1行足すのがいちばん安全です。
PowerShell でやるなら、ユーザーの PATH だけを読んで足す形にします。
# 変更前:ユーザーの PATH をファイルに控える(戻し方の材料)
$old = [Environment]::GetEnvironmentVariable('Path', 'User')
$old | Out-File -FilePath "$HOME\path_user_backup.txt"
# 変更:ユーザーの PATH だけに C:\tools を足す
[Environment]::SetEnvironmentVariable('Path', $old + ';C:\tools', 'User')
# 確認:ユーザーの PATH を読み直す(今の窓の $env:Path は変わらない)
[Environment]::GetEnvironmentVariable('Path', 'User')
# 戻し方:控えた値を書き戻す
# [Environment]::SetEnvironmentVariable('Path', (Get-Content "$HOME\path_user_backup.txt" -Raw).TrimEnd(), 'User')
(ユーザーの設定を書き換えるので、今回の検証では実行していません。コードはドキュメントの SetEnvironmentVariable() の使い方に沿って書いたものです)
- 前提:ユーザーの PATH を変える。管理者の権限は要らない。システムの PATH(
'Machine')を変えるなら権限が要る(PowerShell のドキュメントより) - 影響:これから開く窓・プログラムの PATH が変わる。今の窓は変わらない
- 反映:新しく開いた窓から
- 確認:新しい窓で
where コマンド名(cmd)やGet-Command コマンド名(PowerShell) - 戻し方:控えた値を書き戻すか、設定画面で消す
ユーザーの PATH がまだ空の人は、先頭に ; が付いてしまうので、$old が空かどうかを見てから足してください。
ここから先は上級者向け。読み飛ばしてもOKです:環境ブロックの正体と、Python の os.environ
環境ブロックは、「名前=値」を \0 でつないだもの
Win32 のドキュメントでは、環境ブロックの形はこう説明されています。
Var1=Value1\0Var2=Value2\0Var3=Value3\0 ...VarN=ValueN\0\0
(ドキュメントの記載をそのまま写したもので、実行結果ではありません)
- 「名前=値」を、\0(値が0の文字)で区切って並べ、最後は \0 が2つ
- だから、環境変数の名前には
=を入れられない(ドキュメントにも明記されています) - ユーザーが決める環境変数1つの最大は 32,767 文字。ブロック全体の大きさには技術的な上限はないが、使う方法ごとに現実の上限がある。たとえばバッチファイルは、コマンドラインの最大の長さより長い変数は作れない
レシピ帳の正体、1本の長いレシートだった
改行の代わりに \0 で区切ってるんですね
最後に \0 が2つ並ぶところが、レシートの「以上です」の線です。
子のプロセスは、既定では親のこの中身を引き継ぎます(コピーなので、あとから親が変えても届かないことは、すぐ下の Python で確かめます)。CreateProcess という関数に、別の環境ブロックを渡すこともできる、と同じドキュメントにあります。のれん分けのとき、師匠のレシピ帳ではなく、特別に作った別冊を持たせることもできる、ということです。
Python の os.environ は、起動したときの写し
Python で、同じのれん分けを確かめます。
import os, subprocess, sys, time
os.environ["udon"] = "kake"
print("keys:", [k for k in os.environ if k.upper() == "UDON"])
code = 'import os; print(" child sees:", os.environ.get("UDON")); os.environ["UDON"] = "tempura"; print(" child set:", os.environ["UDON"])'
subprocess.run([sys.executable, "-c", code])
print("parent after child:", os.environ["UDON"])
os.putenv("NABE", "yaki")
print("NABE in os.environ:", "NABE" in os.environ)
subprocess.run([sys.executable, "-c", 'import os; print(" child NABE:", os.environ.get("NABE"))'])
subprocess.run([sys.executable, "-c", 'import os; print(" child with env=:", os.environ.get("UDON"))'], env={**os.environ, "UDON": "zaru"})
print("parent still:", os.environ["UDON"])
p = subprocess.Popen([sys.executable, "-c", 'import os, time; time.sleep(1.5); print(" late child sees:", os.environ.get("UDON"))'])
os.environ["UDON"] = "nabeyaki"
print("parent changed to:", os.environ["UDON"])
p.wait()
keys: ['UDON']
child sees: kake
child set: tempura
parent after child: kake
NABE in os.environ: False
child NABE: yaki
child with env=: zaru
parent still: kake
parent changed to: nabeyaki
late child sees: kake
(今回の検証環境:Python 3.13.1、Windows。出力の順番をそろえるため、python -X utf8 -u で実行しました)
Python の os モジュールのドキュメントと並べると、こうなります。
udonと小文字で入れたのに、キーはUDON。ドキュメントにも、Windows ではキーが大文字に変換される、とあります- 子が tempura にしても、親は kake のまま。cmd や PowerShell と同じ一方通行
os.putenv()で入れた NABE は、os.environには出てこないのに、子には届いた。ドキュメントにも、putenv を直接呼んでも os.environ は変わらないので、os.environ を変えるほうがよい、と書かれていますenv=を渡すと、その子にだけ別の値(zaru)を持たせられる。CreateProcess に別冊を渡すのと同じ発想です- 子を起動したあとに親が nabeyaki に変えても、1.5 秒後の子は kake のまま。子がもらうのは、起動した瞬間のコピー
読者「起動したあとに親が変えても届かない。setx が今の窓に効かないのと同じだ」
そうなんです。setx の件は、レジストリと窓の間で同じことが起きているだけです。
NABE は帳面に書かずに、弟子の荷物にこっそり入れた鍋焼き
師匠の帳面には載ってないのに、弟子の店では出てくる。怖い
ドキュメントには、os.environ は os モジュールを最初に import したときに取り込まれ、そのあとの環境の変化は、os.environ を直接変えた場合を除いて反映されない、ともあります。Python の中で環境変数を変えるなら、os.environ を通す。これで帳面と荷物が食い違いません。
3つの置き場所と、どれが勝つか
Windows の環境変数の置き場所を、PowerShell のドキュメントの言葉で整理します。
| 置き場所 | 中身 | 変え方の例 | 効く範囲 |
|---|---|---|---|
| Machine(システム) | 全ユーザー共通 | 設定画面、setx /m、SetEnvironmentVariable(..., 'Machine') | 新しく起動するプロセスから。変えるには権限が要る |
| User(ユーザー) | そのユーザーだけ | 設定画面、setx、SetEnvironmentVariable(..., 'User') | 新しく起動するプロセスから |
| Process(プロセス) | 今のプロセスの帳面 | set、$env:、os.environ | そのプロセスと、そこから起動する子だけ |
Process の中身は、親から引き継がれ、Machine と User の環境変数から作られる、とドキュメントにあります。同じ名前が Machine と User の両方にあるときにどちらが使われるかは、今回の資料では確かめられなかったので、ここでは書きません。ただ PATH については、窓の中で見える値は2つから作られたもの、というのが落とし穴4の前提でした。
表にすると、急にまじめな記事に見えますね
まじめな記事です。最初から
冒頭の新人さんに、もう一度
冒頭では、setx で PATH を足したのに、同じ窓で「認識されていません」と言われていました。
今なら、こう説明できます。
setx はレジストリに書くだけで、今の窓は開いたときのコピーで動いてるんですね。窓を開き直したら動きました。あと、setx で %PATH% ごと書き直すと1024文字で切られるかもしれないので、設定画面から足し直しました
コピーの話まで分かったなら、もう環境変数で迷子にはなりませんね。おうどんさんより説明が上手でした
師匠の帳面より、弟子の帳面のほうが新しくなってる
それは、師匠が自分の窓を開き直していないだけです。
今日から、おうどんも窓を開き直す人になります
開き直すのは窓だけで大丈夫です。開き直りすぎないでください。
環境変数は、プロセスごとに持っている帳面のコピー。変えた場所と、読んでいる窓が同じかを確かめる。
反映されないときに見るのは、設定の中身より先に「いつ、どこで開いた窓か」です。
まずは今の窓で set path(cmd)か $env:Path(PowerShell)を打って、自分の棚のリストを一度眺めてみてください🍜
参考資料
- Microsoft Learn:Environment Variables(Win32) — 環境ブロック、ユーザーとシステムの2種類、子は親の環境変数を引き継ぐ、ブロックの形と名前に = を使えない、32,767 文字、WM_SETTINGCHANGE
- Microsoft Learn:set — set の表示・作成・削除、先頭の文字で一覧、特別な文字と引用符が値に入ること、PATH に足す例
- Microsoft Learn:setx — レジストリに書き今の窓には効かない、/m でシステム、1024 文字で切られる、参照が展開される、set は今の窓だけ
- Microsoft Learn:about_Environment_Variables(PowerShell 5.1) — $env: と Env:、変更は今のセッションだけ、空の文字列で削除、Machine・User・Process の3つ、SetEnvironmentVariable と権限、PATH の区切り
- Python ドキュメント:os(os.environ・os.putenv・os.getenv) — import したときの写し、Windows ではキーを大文字に、putenv は os.environ を変えない、putenv の変更は子に届く
Windows 10・PowerShell 5.1・Python 3.13 と公式ドキュメントで確かめた、2026年11月時点の内容です。








コメント