前回までに説明してきたように、MSYS2とWSL2はいずれも「LinuxのコマンドをWindowsで実行する」ことができるが、両者は採用している仕組みが全く異なり、その性能にも大きな違いがある。今回は、この2つの技術の違いとして、Windowsとの親和性について説明する。
MSYS2は基本的にLinuxのコマンドをWindowsでネイティブに実行している。次のようにMSYS2にインストールしたneofetchコマンドを実行してみよう。出力としては「Windows 11」が表示されている。あくまでも動いているのはWindowsカーネルの上であって、Linuxカーネルではないのだ。
同じコマンドをWSL2で動作しているUbuntuで実行すると次のようになる。
WSL2はHyper-V仮想環境でLinuxカーネルを動作させる技術だ。Linuxコマンドは当然ながらLinuxカーネルで実行される。実行結果は上記のようにLinuxが出力される。
WSL2はwslコマンドでWindows側となじむ
WSL2は、仮想環境のなかでLinuxを実行する技術だ。Visual StudioやVisual Studio Codeから使う場合は拡張機能でうまく補えるが、Windowsのネイティブな環境、例えばPowerShellからWSL2を利用しようとすると、どうしても一旦WSL2の環境へ入る必要がある。
しかし、WSL2は「wsl」というコマンドを介すことでLinuxのコマンドをWindows側からまるでネイティブなコマンドのように実行することができる。次のような感じだ。
インタラクティブシェルとしてPowerShell 7を使っているのであれば、この機能を使うことで、あたかも直接Linuxのコマンドを実行しているかのように見せかけることができる。Linuxコマンドと同名のPowerShell関数を作成し、その関数で「wsl コマンド」といったようにWSL2側のコマンドを呼び出す仕組みにすればよいのだ。
さらにいくつか細かい調整をすると、WSL2でもかなりMSYS2的な動きをさせることができる。今回は説明しないが、次のような関数をPowerShellに噛ませて、「linuxcmds」という関数を実行すればWSL2側のLinuxコマンドをWindowsネイティブなコマンド風に使うことができるようになる。
#========================================================================
# Linux commands integration mode
#========================================================================
function linuxcmds {
#----------------------------------------------------------------
# Definition of Linux commands used via wsl
#----------------------------------------------------------------
# Linux pagers
$_linux_pagers = @("less", "lv")
# Linux PATH and commands
Write-Host "checking linux commands:"
$_linux_path = (wsl echo '$PATH').Split(":") -NotMatch "/mnt"
$_linux_command_paths = (
wsl ls -d ($_linux_path[($_linux_path.Length - 1)..0] -replace
"$","/*")
) 2> $null
# Generate Linux commands functions
ForEach($n in $_linux_command_paths) {
$_n = (Split-Path -Leaf $n)
$_linux_functions += "
function global:$_n {
if (`$Input.Length) {
`$Input.Reset()
`$Input | wsl $n ([String]`$(_path_to_linux
`$Args)).Split(' ')
}
else {
wsl $n ([String]`$(_path_to_linux `$Args)).Split(' ')
}
}
Write-Host -NoNewline .
"
Write-Host -NoNewline '_'
}
# Generate Linux pagers functions
ForEach($_n in $_linux_pagers) {
$_linux_functions += "
function global:$_n {
if (`$Input.Length) {
`$Input.Reset();
# Prepare temporary file path
`$_temp = New-TemporaryFile
# Write data from pipeline to the temporary file
`$Input | Out-File `$_temp
# Do $_n
wsl $_n `$(_path_to_linux `$Args).Split(' ') ``
`$(_path_to_linux `$_temp.ToString()).Split(' ')
# Delete unnecessary temporary file and variable
Remove-Item `$_temp
Remove-Variable _temp
}
else {
wsl $_n `$(_path_to_linux `$Args).Split(' ')
}
}
Write-Host -NoNewline .
"
Write-Host -NoNewline '_'
}
# Function that converts Windows paths to Linux paths
$_linux_functions += @'
function global:_path_to_linux {
$linuxpath = @()
# Convert arguments to Linux path style
ForEach($winpath in $Args) {
if ($winpath -eq $null) {
Break
}
# Change drive path to mount path
if ($winpath -match '^[A-Z]:') {
$drive = $winpath.Substring(0,1).ToLower()
$linuxpath += "/mnt/" + $drive +
$winpath.Substring(2).Replace('\','/')
}
# Option is not converted
elseif ($winpath -match '^[-+]') {
$linuxpath += $winpath
}
# Other argument is converted
else {
$linuxpath += ([String]$winpath).Replace('\','/')
}
}
$linuxpath
}
Write-Host .
'@
Write-Host -NoNewline '_'
# Prepare temporary file path with extension .ps1
$_temp = New-TemporaryFile
$_temp_ps1 = $_temp.FullName + ".ps1"
Remove-Item $_temp
# Write function definition to temporary .ps1 file and parse
$_linux_functions | Out-File $_temp_ps1
Write-Host
Write-Host "functionizing linux commands:"
. $_temp_ps1
Remove-Item $_temp_ps1
# Delete unnecessary variables
Remove-Variable n
Remove-Variable _n
Remove-Variable _temp
Remove-Variable _temp_ps1
Remove-Variable _linux_pagers
Remove-Variable _linux_path
Remove-Variable _linux_command_paths
Remove-Variable _linux_functions
#----------------------------------------------------------------
# Individual Linux command function definitions
#----------------------------------------------------------------
# grep
function global:grep {
$pattern_exists = $False
$path_exists = $False
$skip = $False
$i = 0
ForEach($a in $Args) {
if ($skip) {
$skip = $False
$i++
continue
}
# Options without argumetn
if ($a -cmatch '^-[abcdDEFGHhIiJLlmnOopqRSsUVvwxZ]') {
}
# Options with argument
elseif ($a -cmatch '^-[ABC]') {
$skip = $True
}
# Pattern file specification option
elseif ($a -ceq '-f') {
$skip = $True
$pattern_exists = $True
$Args[$i+1] = _path_to_linux $Args[$i+1]
}
# Pattern specification option
elseif ($a -ceq '-e') {
$skip = $True
$pattern_exists = $True
}
# Pattern or file path
elseif ($a -cnotmatch '^-') {
if ($pattern_exists) {
$path_exists = $True
}
else {
$pattern_exists = $True
}
}
$i++
}
# Change file path
if ($path_exists) {
$Args[-1] = _path_to_linux $Args[-1]
}
$Input | wsl grep $Args
}
# ls
Get-Alias ls *> $null && Remove-Item alias:ls
function global:ls { wsl ls --color=auto $(_path_to_linux
$Args).Split(' ') }
function global:ll { ls -l $(_path_to_linux $Args).Split(' ') }
function global:la { ls -a $(_path_to_linux $Args).Split(' ') }
}
上記設定はなかなかよくできていると思うのだが、効果があるのはPowerShellだけだ。コマンドプロンプトでは使えないし、それ以外のシェルでも使えない。それぞれのシェルで似たようなギミックを実行する必要がある。
wslコマンドでうまくいかないケースもある
先ほどの方法でかなりのコマンドがWindowsネイティブ風に使えるようになるのだが、ターミナルから直接キー入力を取るようなタイプのコマンド、具体的にはページャがこの方法ではうまく扱えない。ちょっと見てみよう。
上記のコマンドは問題なく動作する。今度はこのコマンドをパイプでlessへつなげてみる。次のような感じだ。
MSYS2で実行すると、次のようにlessも機能すると、カーソルキーやショートカットキーで操作することもできる。
今度はこれをWSL2のlessへつなげてみよう。
一見すると動作するように見えるのだが、カーソルキーもショートカットキーも効かない。「Ctrl」+「C」でプログラムを終了しなければならない。
こんな感じでWSL2はあくまでも仮想環境で動作しているLinuxであり、ページャのようなコマンドをWindows側で使うといった操作がうまくできないのだ。
親和性のよさはMSYS2
やはり、Windowsとの親和性の高さという点ではWSL2よりもMSYS2のほうが高いように思える。Visual StudioやVisual Studio Codeのようにすでに用途が定まっており、拡張機能も整っている環境ではWSL2のほうが便利だが、LinuxコマンドをWindowsで使いたい場合はMSYS2のほうが無難なケースが多い。
これまで見てきて、メモリの消費量とパフォーマンスという点でMSYS2とWSL2には大きな違いがあることがわかったと思う。これら2つの技術は、競合するというよりは、苦手な部分をお互いに補い合う補完関係にあると捉えるほうが建設的だ。特性がわかれば使いこなし方も見えてくる。都合が良いように組み合わせて利用することで、より効果的な操作が可能だ。