Help us understand the problem. What is going on with this article?

Windows向けにdotfilesセットアップスクリプトを作る

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です。 image.png

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"としても良いです。

こんな感じ

image.png

特定のパスが存在するか調べる

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"となってしまうイメージ)。

↓よくない例

image.png

パッケージのインストール

パッケージマネージャはシンボリックリンクを駆使してパッケージのバージョン管理をしてくれるscoopがお勧めです。
scoopの基本的な使い方はQiita上にもたくさんありますので各自ググってみてください。

さて、インストール作業の全てを自動化する場合、環境によってはインストール時に自動で依存パッケージ(パッケージの解凍ツール等)をダウンロードする機能が正常に動かなかったりします(特にWindows Sandboxなどの不安定な環境の場合)。

それを防ぐために先に依存パッケージを明示的にインストールします

  • aria2
    • proxy対策です(後述)
  • 7zip, lessmsi, dark
    • 基本的にscoopはzip, tar.gz, msiやexeを良い感じに解凍してくれます。PowerShellの機能で解凍できるファイルはzipのみなので、必要に応じて7zip, lessmsi, darkをインストールしてくれます・・・、が時々動かない :thinking:
  • 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 installscoop 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のある環境では導入のタイミングだけ一部手動で妥協しています :innocent:

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を使いました。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away