Windows向けにdotfilesセットアップスクリプトを作る
というのがテーマです。
会社ではLinux, Windows, Macで、自宅ではLinux, Windowsで開発をしています。
元々はLinux向けとしてdotfilesとして管理していましたが、scoopの出現によりモチベが急上昇、Windows環境もdotfilesとして管理できるよう頑張ってみました(あとMacも)。
本稿ではLinux向けにbashで書いたセットアップスクリプトを元にWindows向けにPowerShellで同じようなものを作った際に得たナレッジを紹介しています。
見せて
これをiex(new-object net.webclient).downloadstring($url)
のように実行すると環境が自動でマイグレートされます(注: そのまま実行すると一部のパスがWindows Defender管理外になりセキュリティリスクが上昇します)。
Windows 10 + (Windows PowerShell 5.1 | PowerShell Core 6)での動作を確認をしています。
環境変数
例えばzshなら.zshenv
のシンボリックリンクを$HOME
に作って終了ですがWindowsにそのような機能はないのでコマンドラインから設定します(PowerShellのみの反映で良いなら後述のprofile.ps1
に書いても良いと思います)。
$newPath = @(
"$env:USERPROFILE\bin"
"$env:USERPROFILE\.dotnet\tools"
"$env:USERPROFILE\.local\bin"
"$env:USERPROFILE\scoop\shims"
"$env:USERPROFILE\scoop\apps\python\current"
"$env:USERPROFILE\scoop\apps\python\current\Scripts"
"$env:USERPROFILE\scoop\apps\python27\current\scripts"
"$env:USERPROFILE\scoop\apps\nodejs-lts\current\bin"
"$env:USERPROFILE\scoop\apps\nodejs-lts\current"
"$env:USERPROFILE\scoop\apps\ruby\current\gems\bin"
"$env:USERPROFILE\scoop\apps\ruby\current\bin"
"$env:USERPROFILE\scoop\apps\git\current\usr\bin"
"$env:USERPROFILE\scoop\apps\git\current\mingw64\bin"
"$env:USERPROFILE\scoop\apps\git\current\mingw64\libexec\git-core"
"$env:USERPROFILE\AppData\Local\Programs\Python\Launcher"
"$env:USERPROFILE\AppData\Local\Microsoft\WindowsApps"
) -join ";"
# カレントプロセスへの反映(bashでいうexport))
$env:PATH = $newPath + ';' + $env:PATH
# 永続化
[System.Environment]::SetEnvironmentVariable("PATH", $newPath, "User")
他tips
- Powershell上でHOMEディレクトリ(
C:\Users\<UserName>
)を指定したい場合、$env:UserProfile
が正式な?表記ですが~
や$HOME
でも認識してくれます。 - bashの
PATH
の区切り文字は:
ですがPowerShellのそれは;
です。 - bashの場合、環境変数はCaseSensitiveですがPowerShellの場合CaseInsensitiveです。
RunControlファイル
bashやzshのRunControl fileは$HOME/.bashrc
や$HOME/.zshrc
ですが、Powershellの場合は環境に応じて次の場所に置きます。
- Windows PowerShell ->
$env:USERPROFILE\Documents\WindowsPowerShell\profile.ps1
- PowerShell Core ->
$env:USERPROFILE\Documents\PowerShell\profile.ps1
また、shellのプロンプトを変えたい場合、bashやzshは$PS1
を変更しますが、PowerShellの場合profile.ps1
内のPrompt
関数が評価・実行されます(公式doc)。
特定のコマンドが使えるか調べる
bashの場合
if type $cmd > /dev/null 2>&1; then
# $cmdがある
fi
if !(type $cmd > /dev/null 2>&1); then
# $cmdがない
fi
PowerShellの場合
if (Get-Command * | Where-Object { $_.Name -match $cmd }) {
# $cmdがある
}
if (!(Get-Command * | Where-object { $_.Name -match $cmd })) {
# $cmdがない
}
PowerShellで注意しなければいけないのはGet-Command
の中身をWhere-object
する場合、-eq
を使ってしまうと完全一致となってしまいマッチしません(例えばコマンド上からgo
を使う場合PowerShellは内部で勝手にgo.exe
に変換してくれている)。実行可能な拡張子は.exe
のみならず.bat
, .ps1
等あるので-match
句を使い部分一致をします(実際には正規表現が使えます)。
また、try...catch...finallyを使う方法もあります。
try {
Get-Command -Name $cmd -ErrorAction Stop
} catch [System.Management.Automation.CommandNotFoundException] {
# 例えばインストールスクリプト実行
# catchする型は省略しても良いです
} finally {
$cmd
}
なお-ErrorAction Stop
の代わりに$ErrorActionPreference = "Stop"
としても良いです。
こんな感じ
特定のパスが存在するか調べる
bashの場合
if [ -e $target ]; then
# 何かある
fi
if [ -f $target ]; then
# ファイルがある
fi
if [ -d $target ]; then
# ディレクトリがある
fi
PowerShellの場合(Test-Path)
if (Test-Path $target) {
# 何かある
}
if (Test-Path $target -PathType Leaf) {
# ファイルがある
}
if (Test-Path $target -PathType Container) {
# フォルダがある
}
配列の展開
パッケージをインストール際はよく配列にして1行に1パッケージインストール等をすることが多いと思います。
bashの場合
pkgs=(
"pip"
"pipx"
)
pip install --upgrade $(IFS=' '; echo ${pkgs[*]})
PowerShellの場合
$pkgs=@(
"pip"
"pipx"
)
pip install --upgrade $pkgs
注意するべきなのはbashとPowerShellで展開の仕方が異なることです。bashの場合、コマンドに配列を渡す際はjoin
っぽいことをする必要がありますが、PowerShellの場合joinをすると逆に1つの文字列として解釈しまいます(pip install "pip pipx"
となってしまうイメージ)。
↓よくない例
パッケージのインストール
パッケージマネージャはシンボリックリンクを駆使してパッケージのバージョン管理をしてくれるscoopがお勧めです。
scoopの基本的な使い方はQiita上にもたくさんありますので各自ググってみてください。
さて、インストール作業の全てを自動化する場合、環境によってはインストール時に自動で依存パッケージ(パッケージの解凍ツール等)をダウンロードする機能が正常に動かなかったりします(特にWindows Sandboxなどの不安定な環境の場合)。
それを防ぐために先に依存パッケージを明示的にインストールします
- aria2
- proxy対策です(後述)
- 7zip, lessmsi, dark
- 基本的にscoopはzip, tar.gz, msiやexeを良い感じに解凍してくれます。PowerShellの機能で解凍できるファイルはzipのみなので、必要に応じて7zip, lessmsi, darkをインストールしてくれます・・・、が時々動かない
- git
-
scoop bucket add
時に必要です
-
docker
WSL2を使ったdockerは現状docker desktop for windowsでないと使えません。そういった事情がある場合scoop経由ではなく手動で入れたほうが良いです。
ちなみにdocker desktop for windowsにはdocker-compose.exeがついてくるのでpipxで入れる必要もありません。
scoopとproxy
proxyに関しては公式のドキュメントにも記載されています。
ただ、少し気を付けなければならないのは、Windows特有の仕様により場所によって使われるproxy情報が異なることです。
-
scoopのインストールをする場合
[net.webrequest]::defaultwebproxy = new-object net.webproxy "http://proxy.example.local:3128" [net.webrequest]::defaultwebproxy.credentials = new-object net.networkcredential 'kentac55@example.local', 'P4SSW0RD' iex (new-object net.webclient).downloadstring('https://get.scoop.sh')
-
scoopでパッケージをインストールする場合
scoop config proxy "kentac55\@example.local:P4SSW0RD@proxy.example.local:3128" scoop install pkg
-
scoop updateをする場合
-
netsh winhttp show proxy
の中身が使われます
-
特にscoop install
とscoop update
で使われるproxy情報が違う点が曲者です。混乱を避けるためにも普段使い慣れているhttp_proxy
環境変数を読むaria2c
の導入をおすすめします。
[net.webrequest]::defaultwebproxy = new-object net.webproxy "http://proxy.example.local:3128"
[net.webrequest]::defaultwebproxy.credentials = new-object net.networkcredential 'kentac55@example.local', 'P4SSW0RD'
iex (new-object net.webclient).downloadstring('https://get.scoop.sh')
scoop config proxy "kentac55\@example.local:P4SSW0RD@proxy.example.local:3128"
scoop install aria2
scoop config rm proxy
# ここから$env:http_proxyが読まれる
scoop install git ...
proxyが絡むとセットアップスクリプトでの全自動化は難しくなってくると思います。私もproxyのある環境では導入のタイミングだけ一部手動で妥協しています
pythonの切り替えについて
Windowsではbashで書かれたpyenvは使えません。しかしscoopで似たようなことをできます。
scoop install python
python --version
Python 3.8.0
scoop install python@3.7.0
python --version
Python 3.7.0
scoop reset python
python --version
Python 3.8.0
scoop reset python@3.7.0
python --version
Python 3.7.0
ところでpython
コマンドで2系と3系どちらが起動すべきかについてはUnix環境向けにはPEP394が、Windows環境向けにはPEP397がそれぞれ提供されています。PEP397にはpy.exe
でハンドリングできるよと書いてありますが、scoop経由で入れるとpy.exe -3
をしてもpython3を見つけてくれません。
また、LinuxとWindowsでいちいちコマンドを変えるのも面倒です。幸いにも2019/06にPEP394が改訂され、python
コマンドでpython3起動させても良いよとなりました。併せてWindows側もpythonコマンドでpython3を立ち上げるようにするのが良いと思います(scoop reset python
)。
なおpython2系を入れる度にpython
へのシンボリックリンクがpython2へ向いてしまうので都度scoop reset python
することをお勧めします。
nightlyビルドについて
visual studio code insider版のようなnightlyビルドはscoop update
をしても自動でアップデートされません。引数に--force
をつけることで解決できます。
パス中の空文字の扱い
VisualStudioCodeのOSS版やInsiders版を使うとconfigのパス中に空白文字が現れます。
Linuxの場合はエスケープに\
を使いますがPowerShellの場合は `(バッククォート) を使います。
mkdir -p $HOME/.config/Code\ -\ Insiders/User
New-Item $env:APPDATA\Code` -` Insiders\User -Force -ItemType Directory
シンボリックリンク
Linux + bashの場合
ln -sf $HOME/.dotfiles/.vimrc $HOME/.vimrc
Windows + PowerShellの場合
New-Item -Force -ItemType SymbolicLink -Value $env:USERPROFILE\.dotfiles\.vimrc -Path $env:USERPROFILE\.vimrc
Linuxの場合は通常ユーザー権限で作れますがWindowsの場合は管理者権限が必要です。
UACを切っていればそんなこと気にしなくても良いのですがUACのプロンプトはホッとさせる一面があるためなかなか切れません。
とはいえUACのプロンプトが出てきてセットアップが中断するのもイケていません。
そこで管理者権限が必要なコマンドを別のファイルに集約し、管理者権限用の別プロセスを立ち上げてそこで処理を行わせるようにします。
Start-Process PowerShell.exe ("-NoProfile -Command cd " + $env:USERPROFILE + "\.dotfiles; .\runas.ps1") -Verb runas
こうすることでUAC画面が出てくる一方でメインのセットアップスクリプトは動き続けることが可能です。
余談ですがOneDrive上ではシンボリックリンクファイルは通常ファイルに変換されます。
終わりに
以上Windows環境をdotfile風に管理する方法を紹介してみました。
Linux向けのセットアップスクリプトを作るときはvagrantやdockerを使って挙動を確認していましたが、Windows向けに作った際はWindows SandboxとHyper-V上のWindows 10 dev environmentを活用しました。
特にWindows Sandboxはwsbファイルを作ってダブルクリックすれば一発で挙動を確認できるのである意味最強なのですが、微妙に挙動が怪しい部分があります。そのため、最終的な確認はWindows 10 dev environmentを使いました。