# Windows, dotnetの環境変数のバグ
---
## 皆さんこんにちは!
:wave:
---
<!--
初めましての方も多いと思いますので自己紹介させて
いただきます。
フリーランスのバックエンドエンジニアをやっております
音川といいます。
-->
My Profile
Otogawa Katsutoshi
freelance backend enginner
interesting
golang, python, and more...
twitter account[https://twitter.com/k_otogawa]
---
<!-- Setting 舞台 -->
## 突然ですがみなさん!Windowsは好きですか!
:sunglasses:
---
## 私はたまに気が狂いそうになります😇
<!-- msの中でも一貫性なかったりね。 -->
---
<!-- 今回私が何しに来たか?というと -->
## 今回何しに来たか?というと
WindowsはCui, Gui, プログラミング言語由来含めて色んな環境変数の設定方法があります。
しかし、それぞれ実際は違う値が設定されたり、上書きされたりします。
## 要するに
---
### windowsの環境変数の~~限りなくバグに近い~~仕様があります。

これをマスターしたら君も立派なdotnet, windowsユーザー
---
## windowsユーザーの地獄の門
windowsユーザーは~~地獄~~天国の門を開いたと思って、聞いて頂けたら幸いです。

---
<!--
ではそもそも、Windowsの環境変数とは?
Linux, macとどう違うのかという説明をしていきます。
-->
## windowsの環境変数とは?

---
## windowsの環境変数にはスコープがある。
以下の3種類のスコープがある。
1. Process -> 現在のプロセスの環境変数。(printenvで見れるやつ)
2. User -> ユーザーの環境変数。(~/.bash_profile)
3. LocalMachine -> rootの環境変数に相当(/root/.bash_profile)
<!--
UserとLoaclMachineの環境変数については
-->
---
## UserとLoacalMachineの環境変数
最終的に環境変数はレジストリの値に設定される。

<!--
つまり
-->
---
### 環境変数を設定するということはレジストリに値を設定するということ

---
## ここで疑問が生じる

<!--
さっきのレジストリの画像に不思議なものがあったと思うんですよ。
-->
---
## ここにあるTypeってなんや?!
環境変数に型?!

<!-- って思われる型も多いと思います。 -->
---
<!--
このTypeはレジストリとプログラミング言語で違う名前で呼ばれています。
-->
## レジストリのアプリとプログラミング言語で違う名前
レジストリのアプリ -> Type
プログラミング言語(csharp) -> RegistryValueKind
<!--
つまり、レジストリには型があります。
-->
---
## RegistryValueKindとは?
以下のどれかの型を表します。
1. String -> ただの文字列
2. ExpandString -> 展開可能(中に変数を埋め込める)
3. MultiString -> Stringの配列
4. DWORD -> 数字
5. Binary -> バイナリ
<!--
ExpandStringっていうのは展開可能な文字列で、
MultiStringっていうのは、Stringの配列です。
Dwordは数字です。
今聞いた話で皆さん思ったと思います。
-->
---
### 環境変数を設定するときに型設定したことあったけ?

---
<!-- という事で一度 -->
### windowsの環境変数の設定方法
<!-- をおさらいしましょう。 -->
以下の三種類が一般的
1. Environment::SetEnvironement ->(dotnetの関数を実行する)
2. Edit environment variable (アプリから変更)
3. setx (コマンド)

<!--
この3種類の方法が一般的です。
これを聞いて
-->
---
<!--
あっここから型を指定するんじゃないか
-->
## RegistryValueKindを指定できる?
ここからRegistryValueKindを指定して
設定できるんでしょ?

<!--
そう思われるかと思います。
-->
---
## まさかのすべて指定することができません!
どれもRegistryValueKindを引数に取らない。
1. SetEnvironemnt -> 引数にない。
2. Edit environment variable (項目にない)
3. setx (引数にない)
---
### つまりどういう事?

---
## 特定の条件下で自動的に上書き、設定されます!
1. SetEnvironment -> 常にStringで上書き
2. Edit environment variable -> 環境変数内に%があったらExpandStringで上書き、%が無かったら、Stringで上書き。
3. setx -> 環境変数が%%で囲まれていたらExpandStringで上書き。%に囲まれていなかったらStringで上書き。
<!--
ここの設定を詳しく説明したいんですが、
その前にwindowsの変数の展開について説明します。
-->
---
## windows変数の展開
変数は下記の書き方で展開できる。
<!-- bashでいうecho $varっていう展開で、
%で囲んだら変数の展開を意味します。
-->
```cmd
@rem bashで言うecho $var
echo %var%
```
つまり%変数%という書式を見つけて、ExpandStringにするなら
指定できなくてもまだマシな処理。
ベストは指定しない場合は、型を変更しないだが...
<!--
ということで、各処理を見ていきます。
-->
---
## setx
コマンドは環境変数が%%で囲まれていたらExpandString型で上書き。%に囲まれていなかったらString型で上書き。
```cmd
@rem ExpandStringでレジストリに登録
setx ENVNAME %var%
@rem %で囲まれていなかったらStringでレジストリに登録
setx ENVNAME %var
@rem 普通の値だとStringでレジストリに登録
setx ENVNAME var
@rem 環境変数の長さが1024文字以上だと切り捨て
setx ENVNAME "too long value"
```
<!--
%っていうのはコマンドプロンプトでwindowsの環境変数を展開するときに
使うシンボルでこれで囲っていたら、展開を意味するっていうのは先ほど説明したとおりです。
なので型を指定できないけど、展開される変数を見てちゃんと適切な型を設定しているので、これは許される処理かなと。
-->
---
## SetEnvironemnt
dotnetの関数は常にString型で上書き。
%のパースもしないし、*なんも考えてない*。
```powershell
#ex) 下記はすべてnameという名前でString型でレジストリに登録
[Environment]::SetEnvironment("ENVNAME", "%Value%","User")
[Environment]::SetEnvironment("ENVNAME", "%Value", "User")
[Environment]::SetEnvironment("ENVNAME", "Value", "User")
```
<!--
だから、展開されるべき変数も普通の値もすべてString型の環境変数として上書きされます。
結構ヤバい作りです。
-->
---
## Edit environment variable
GUIでは環境変数内に%があったらExpandStringで上書き、%が無かったら、Stringで上書き。*%%で囲わなくてもExpandString扱い*。
*2048文字を超えていたら勝手に切り捨て*
| 環境変数名 | 値 | RegistryValueKind |
| -------- | -------- | -------- |
| ENVNAME | value | String |
| ENVNAME | %value | ExpandString |
| ENVNAME | %value% | ExpandString |
| ENVNAME | 2048文字以上 | 容赦なく切り捨て |
---
## 変数が展開されるかどうか?は何も考えてない
%が一個でもあったらそれは変数の展開やろと、ExpandString扱いするのでプログラムとしてはかなりお行儀が悪い。
普通だったら、実際パースしてみて囲っていなかったら、エラーにするかRegistryValueKindを設定させるか、どちらかにするのが普通の設計。
<!--
だから変数が展開されるかどうか?は何も考えてなくて、
%が一個でもあったらそれは変数の展開やろと、ExpandString扱いするのでプログラムとしてはかなりお行儀が悪い。
普通だったら、実際パースしてみて囲っていなかったら、エラーにするかRegistryValueKindを設定させるか、どちらかにするのが普通の設計。
-->
<!--
ここまで3つ説明して
-->
---
## まさかの動作に一貫性が無い...
この中ではsetxが一番マシ。
<!--
でヤバすぎるのが
-->
---
## ただ、setxには~~バグみてぇな~~厳しい仕様がある
環境変数の長さが1024文字を超えていたら、警告を出して問答無用で勝手に切り捨て。
普通にwindowsで開発していたら超える。
---
## 警告したからバグじゃないよ。もう消したけどな。
<!-- windows serverとか、多分サーバー用とで使っているところは環境変数追加することはあっても不要なやつバシバシ消す運用になっていないと思うので、世界中で痛い目見ているかなと。
-->

---
## 特にヤバいのはSetEnvironmentVariable
例えばPATHに%UserProfile%を追加した後に
SetEnvironmentVariableで変更すると
変数展開ができなくなって、パスが通らなくなる。
```cmd
@rem ユーザーディレクトリ配下のbinにパスを通す
setx PATH %PATH%;%UserProfile%\bin
```
```powershell
# ExpandStringがStringで上書きされる
$value = [Environment]::GetEnvironment("PATH", "User")
[Environment]::SetEnvironment("PATH", $value, "User")
```
<!--
これを聞いてみなさんはこう思うと思うんですよ。
-->
---
## そんな壊れている処理、MSは放置しないでしょ?
代替案でカバーするっしょ?

<!--
そんな事ないです!
-->
---
## MSの色々なアプリ、ソースコードで使われています!
1. githubのdotnet/runtimeのソースコード
2. visual studio のインストーラー
<!--
なので、
-->
---
## MSの環境変数の操作は基本的に壊れている
Windowsとdotnetの環境変数の扱いは**壊れている**。

---
## できてるっぽいし、リリースしようぜ!
MSの*とりあえず*リリースしたいという強い意志が感じられる

<!--
せめてdotnetの環境変数のバグだけでも治らないの?
と思うと思うんですが、
-->
---
## dotnetの環境変数のバグ今後直らないの?
dotnetのissue見ても、pwshのissue見てもたびたび議論に上がるが、MSは問題と思っていないので無理。
---
## MSのバグは闇が深い...
このバグ自体は.net framework時代から存在していて、最新のdotnet 7.0でも残っている
公式が直す可能性も望みが**薄い**
<!--
後、これはdotnetのissueで書いてあったんですが、
-->
---
## そもそも一般ユーザーが考える問題じゃなくない?
そうだよ!
:angry:
<!--
ただ、私もプログラマーなんでプルリク出そうぜと思って、
powershellで環境変数を正しく操作できる処理を作りました。
dotnetのチームだと話進まないんで、powershellのチームなら
話通じるかなと思って、
-->
---
## powershellプルリク出したけど
Powershellチーム、PMとしては
powershellからdotnet由来の方法で設定できるから問題無いとのこと。
```powershell=
[Environment]::GetEnvironment("ENVNAME")
[Environment]::SetEnvironment("ENVNAME", "VALUE")
```
コマンドレットを提供するとしても、この関数のラッパーになるから、作るとしてもPSGalleryに追加が妥当との事
<!--
PSGallerytとはPowershellのライブラリをまとめているサイトです。
これはその時だけこういう対応したとかでなくて、
Powershellから環境変数いじるならSetEnvironment使えるっていうのは、度々いろんなMSの人間がこの回答をしています。
-->
---
## いや、そもそもこの関数にバグがあるんやけど

もちろんそういう反論したし、いままで皆さんに説明したのと同じ説明しました。しかし、リジェクトされました。
<!--
よって、
-->
---
### 今後公式として環境変数を正しく触れる処理は提供されません!
さすが、俺達のMS!俺達にできないことを平然とやってのける!

---
## ちなみにPSGalleryは
PSGalleryで環境変数いじるライブラリはあるが、
すべてSetEnvironmentVariableを使っているため、
*すべて処理が壊れています。*
*使っちゃダメです。*

<!--
ダメダメな状態を説明するだけだと
よくないので、解決策も述べます。
-->
---
## 今の範囲でできることは?
という事で今の我々にできる事を述べていきます。
---
## 安全に変更する方法はレジストリを操作するしかない。
この2つのみ
1. GUIのレジストリエディタ
2. pwshのSet-ItemProperty
ただし、MSが環境変数であることを想定していない
DWORD, Binaryなども設定できてしまう。
---
## powershellのレジストリ操作
Set-ItemPropertyなら下記のように安全に指定できる
```powershell
# Set-ItemPropertyは自動的に型を上書きしない。String型ならStringのまま。
Set-ItemProperty "HKCU:\Environment\"
-Name ENVNAME
-Value "%VALUE%"
# Set-ItemProperty型を上書きしたい場合は指定する
Set-ItemProperty "HKCU:\Environment\"
-Name ENVNAME
-Value "%VALUE%"
-Type ExpandString
```
<!--
特に指定が無い場合はString型ならString型のまま設定されますし、
指定したいなら、Typeを指定できます。
-->
---
<!--
Powershellでなくて、csharpで安全に書く場合も
同様にレジストリを操作することになりますが、ソースコードは
長くなります。
というわけで
-->
## まとめ
<!-- に入らせて頂きますと、 -->
1. Windowsの環境変数を安全に操作するならレジストリを触る必要がある。
<!-- 今回プレゼンした問題はUserとLocalMachineの環境変数のみの問題なので -->
2. 現在のプロセスの環境変数の変更ならSetEnvironmentでもよい。
<!--
という事だけ覚えて頂けたら幸いです。
ご清聴ありがとうございました。
-->

<!-- -->
---
{"metaMigratedAt":"2023-06-17T11:02:25.529Z","metaMigratedFrom":"YAML","breaks":true,"slideOptions":"{\"controls\":false,\"slideNumber\":false,\"progress\":true}","title":"Windows, dotnetの環境変数を正しく操作できるライブラリを作った","contributors":"[{\"id\":\"13bbf9e7-416d-4813-946c-591c31f51efc\",\"add\":20985,\"del\":12612}]","description":":wave:"}