WSLからWindows側にショートカットファイルを作る


TL;DR

WSLのUbuntu側でファイルを指定して、Windowsのデスクトップにショートカットファイルを作るシェルスクリプト関数wlnを作った。wln filenameとして実行すると、そのファイルのショートカットがWindowsデスクトップに作成される。

wln test.txt

実装は以下の通り。

function wln() {
  temp_ps1=`mktemp`.ps1
cat <<'EOD' > $temp_ps1
Param($targetfile)
$filename = [System.IO.Path]::GetFileName($targetfile)
$WshShell = New-Object -comObject WScript.Shell
$shortcutfile = "$Home\Desktop\" + $filename + ".lnk"
$Shortcut = $WshShell.CreateShortcut($shortcutfile)
$Shortcut.TargetPath = $targetfile
$Shortcut.Save()
EOD

targetfile=$(wslpath -w $1)
ps1file=$(wslpath -w $temp_ps1)
powershell.exe $ps1file $targetfile
rm -f $temp_ps1
}

Zshでしか確認していないが、Bash系なら動くと思う。

これを実行する前に、PowerShell側でスクリプトのセキュリティポリシーをBypassUnrestrictedにしておく必要がある。

Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope CurrentUser

もしくは

Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope CurrentUser

はじめに

Windowsを使っていて、ほぼ全ての作業をWSLのUbuntuで行っている人は多いのではないかと思う。特に、Ubuntu上のGitでファイルを管理していると、必要なファイルが全てWSL側にあることになる。そのファイルをWindows側で開きたい時、例えばopenコマンドを用意して開いても良いが、たまにショートカットファイルをデスクトップに作りたい時がある(例えばウェブ会議していて、複数のPDFファイルを順番に共有したいときとか)。というわけで、そんなコマンドwlnを作りたい。

ショートカットの作成

Windowsのショートカットファイルを作るのにはいくつか方法があるが、一番簡単なのはCOMでWScript.Shellオブジェクトを作ってCreateShortcutメソッドを叩くことだ。

例えばPowerShell上で以下を実行すると、$HOME\test.txtへのショートカットをデスクトップ上に作ることができる。

$WshShell = New-Object -comObject WScript.Shell
$Shortcut = $WshShell.CreateShortcut("$Home\Desktop\test.txt.lnk")
$Shortcut.TargetPath = "$Home\test.txt"
$Shortcut.Save()

順番に

  • COMを叩いてWScript.Shellオブジェクトを作り
  • ショートカットファイル名を指定してショートカットオブジェクトを作り
  • ショートカットオブジェクトにターゲットパスを指定して* 保存する

ということをしているだけだ。

スクリプトのセキュリティポリシー

上記はPowerShell上で一行ずつ実行したが、これを例えばtest.ps1というファイルに保存してスクリプトファイルとして実行すると怒られる。

$ ./test.ps1
./test.ps1 : このシステムではスクリプトの実行が無効になっているため、ファイル C:\Users\username\test.ps1 を読み込むこと
ができません。詳細については、「about_Execution_Policies」(https://go.microsoft.com/fwlink/?LinkID=135170) を参照してく
ださい。
発生場所 行:1 文字:1
+ ./test.ps1
+ ~~~~~~~~~~
    + CategoryInfo          : セキュリティ エラー: (: ) []、PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess

これは、デフォルトでPowerShellスクリプトの実行ポリシーが「Restricted(禁止)」になっているからだ。現在の実行ポリシーはGet-ExecutionPolicy -Listで得ることができる。

$ Get-ExecutionPolicy -List

        Scope ExecutionPolicy
        ----- ---------------
MachinePolicy       Undefined
   UserPolicy       Undefined
      Process       Undefined
  CurrentUser       Undefined
 LocalMachine       Undefined

過去に何も設定していなければUndefinedになっているはず。そしてUndefinedの場合はデフォルトでRestrictedが適用され、スクリプトの実行ができない。

セキュリティポリシーにはいくつか種類があるが、例えばRemoteSignedにすると、リモートのスクリプトは署名付きのみ、ローカルはそのまま実行できるようになる。

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

これで先ほどのスクリプトはそのまま実行できるようになる。

後はこれをWSLのUbuntu側でPowerShellに食わせて実行すればよいのだが、セキュリティポリシーRemoteSignedでは実行できない。

以下をUbuntu側で実行してみる。

$ echo "echo Hello" > test.ps1
$ powershell.exe ./test.ps1
./test.ps1 : ファイル \\wsl.localhost\Ubuntu\home\username\test.ps1 を読み込めません。ファイル \\wsl.localhost\Ubuntu\home\username\test.ps1 はデ
ジタル署名されていません。このスクリプトは現在のシステムでは実行できません。スクリプトの実行および実行ポリシーの設定の詳細については、「about_Exe
cution_Policies」(https://go.microsoft.com/fwlink/?LinkID=135170) を参照してください。
発生場所 行:1 文字:1
+ ./test.ps1
+ ~~~~~~~~~~
    + CategoryInfo          : セキュリティ エラー: (: ) []、PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess

これはWSLがリモート扱いだからだ。セキュリティポリシーをBypassにすると、全てのスクリプトを確認なしで実行できるようにする。

以下をPowerShell側で実行する。

Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope CurrentUser

すると、WSL側でPowerShellスクリプトを実行できるようになる。

$ powershell.exe ./test.ps1
Hello

セキュリティポリシーUnrestrictedにすると、毎回確認が入る。

$ powershell.exe ./test.ps1

セキュリティ警告
信頼するスクリプトのみを実行してください。インターネットから入手したスクリプトは便利ですが、コンピューターに危害を及ぼす可能性があります。このス
クリプトを信頼する場合は、この警告メッセージが表示されないように、Unblock-File
コマンドレットを使用して、スクリプトの実行を許可してください。\\wsl.localhost\Ubuntu\home\watanabe\test.ps1 を実行しますか?
[D] 実行しない(D)  [R] 一度だけ実行する(R)  [S] 中断(S)  [?] ヘルプ (既定値は "D"): r
Hello

こっちの方が安全。

ファイル名の処理

WSL側からPowerShellスクリプトが実行できるようになれば、あとはショートカットを作りたいファイルをPowerShellスクリプトに渡すだけだ。

使い方としては

wln filename

としよう。このfilenameはWSL側のパスになっているのでwslpath -wでWindows側のパスに修正して渡してやればよい。ショートカットファイル作成スクリプトをmakelnk.ps1とすると、wlnは内部で

powershell.exe makelnk.ps1 $(wslpath -w $1)

としてPowerShellを呼び出せば良い。

PowerShell側で引数の受け取り方はいくつかあるが、Paramで受け取るのが良いと思う。

Param($targetfile)

ショートカットファイル名を作るために、まずはフルパスからファイル名を分離する。

$filename = [System.IO.Path]::GetFileName($targetfile)

デスクトップにファイルを作りたいので、$HOME\Desktop\を追加。さらに、拡張子は.lnkでなくてはならないので、それも追加する。

$shortcutfile = "$Home\Desktop\" + $filename + ".lnk"

あとは先ほどの例と同様にショートカットオブジェクトを作って保存すればよい。以上から、引数を受け取ってデスクトップにショートカットファイルを作成するPowerShellスクリプトmakelnk.ps1はこうなる。

Param($targetfile)
$filename = [System.IO.Path]::GetFileName($targetfile)
$WshShell = New-Object -comObject WScript.Shell
$shortcutfile = "$Home\Desktop\" + $filename + ".lnk"
$Shortcut = $WshShell.CreateShortcut($shortcutfile)
$Shortcut.TargetPath = $targetfile
$Shortcut.Save()

このmakelnk.ps1をどこかパスが通った場所においても良いのだが、対して大きくないので毎回テンポラリファイルとして作ってしまおう。mktempでファイル名を作成し、拡張子.ps1をつけて保存する。

  temp_ps1=`mktemp`.ps1
cat <<'EOD' > $temp_ps1
Param($targetfile)
$filename = [System.IO.Path]::GetFileName($targetfile)
$WshShell = New-Object -comObject WScript.Shell
$shortcutfile = "$Home\Desktop\" + $filename + ".lnk"
$Shortcut = $WshShell.CreateShortcut($shortcutfile)
$Shortcut.TargetPath = $targetfile
$Shortcut.Save()
EOD

テンポラリファイル名はWSL側のパスなので、Windows側のパスに直して食わせる必要がある。ターゲットファイルも同様なので、

targetfile=$(wslpath -w $1)
ps1file=$(wslpath -w $temp_ps1)
powershell.exe $ps1file $targetfile
rm -f $temp_ps1

以上をまとめると冒頭のスクリプトになる。

まとめ

WSL側からPowerShellスクリプトを介してWindowsのデスクトップショートカットファイルを作る方法を紹介した。デフォルトではPowerShellスクリプトがそのまま実行できないので、セキュリティポリシーを変更する必要がある。他にもWindows側にRubyなりなんなりを入れて、そちらからCOMを叩くというのも考えたが、やりたいことのわりに面倒な気がする。

PowerShellスクリプトを使うなら、本当はちゃんと署名して、AllsignedもしくはRemoteSignedポリシーで実行できるようにしたほうが良いのだが、署名しようとしてなかなかうまくいかなかった(今後の課題)。wlnを実行するたびにスクリプトの許可の実行を求められるが、あまり利用頻度は高くないし、とりあえずUnrestrictedポリシーで運用しようと思う。