現在ではだいぶ減ってきているが、PowerShellとWSLを組み合わせて使っていると、ときどき文字化けが発生する。ASCIIの範囲内でしか使わない英語圏の方などはまるで問題にならないのだが、日本語の場合はそうもいかない。では、文字化けする例を見てみよう。

次のスクリーンショットはdirコマンドの出力結果だ。ファイル名に日本語が含まれている。

dirコマンドの実行例 - 日本語のファイル名が表示されている

これをWSLのlsコマンドで実行すると次のようになる。これもうまくいっている。 

lsコマンドの実行例 - 日本語のファイル名が表示されている

では、上記の出力をパイプラインでWSLのコマンドにつなげてみよう。ここでは「wsl cat」に接続してみる。dirコマンドではこのケースでもうまくいく。

「dir | wsl cat」はうまく動作する

lsをcatにパイプラインで接続すると、次のように日本語名が化けていることがわかる(わかりやすいようにLinuxコマンドには明示的にwslを指定して、「wsl ls | wsl cat」と実行している)。

「wsl ls | wsl cat」では文字化けが発生する

Windows 10では文字化けが発生しないように結構無難な設定が行われている。しかし、wslのコマンドからwslのコマンドへパイプラインで接続するケースというのは想定していないようだ。

Shift-JIS (CP932)とUTF-8

Windowsの日本語はもともとエンコーディングとしてShift-JIS (CP932)を採用していた。UNIX系では日本語エンコーディングとしてEUC-JPが使われることが多かった。Linuxでは、UEC-JPのほかShift-JISがデフォルト設定されることもあった。デフォルトエンコーディングが異なるので、異なるOSを使うときにはしょっちゅう文字化けが発生していた。

LinuxなどのUNIX系OSはその後、エンコーディングとしてUTF-8をデフォルト化する方向で動いていく。macOSもユーザーが利用する視点から見るとUTF-8にしておけば問題が出ることが少ない。Windowsは後方互換性を重要視する傾向が強いため従来のエンコーディングが使われることが多いが、それでも徐々にUTF-8をデフォルト化する方向へ進んでいるように見える。WebもデフォルトエンコーディングはほぼUTF-8という方向にあり、今後エンコーディングはUTF-8にしておけば問題が出にくい、という状況になっている。

しかしWindows 10ではまだ調整が必要だ。まず、PowerShellでエンコーディングがどう設定されているのかを確認してみよう。次のスクリーンショットで確認できる。

PowerShellで設定されているエンコーディング

$OutputEncodingはUTF-8に設定されているのだが、システムコンソールのOutputEncodingはShift-JIS (CP932)に設定されている。今回はこの設定が問題として現れたことになる。Windowsの視点からすれば、過去のソフトウエアとの互換性を確保しつつ、現在のソフトウエアと連携して動作することを考えると、この設定は悪くない。むしろ問題が起きないことのほうが多いと思う。

しかし、WSLからWSLへパイプするという用途になると、この設定だと文字化けが発生してしまう。そこで、悩ましいのだが、今後のことを考え、システムコンソールのOutputEncodingをUTF-8に設定することにする。

次の処理でUTF-8エンコーディングのオブジェクトを生成できる。

UTF-8エンコーディングのオブジェクトを生成

したがって、次のようにするとシステムコンソールのOutputEncodingをUTF-8に設定することができる。

とシステムコンソールのOutputEncodingをUTF-8に設定

動作を確認してみよう。まず、ちゃんと動作していた「dir | wsl cat」の動作を確認する。こちらは問題なく動作している。

「dir | wsl cat」は問題なく動作

文字化けしていた「wsl ls | wsl cat」の動作を確認すると、次のように文字化けしていた部分が化けずに表示されることを確認できる。

「wsl ls | wsl cat」で文字化けがなくなったことを確認

設定方法としては、次のように$OutputEncodingの値を直接システムコンソールのOutputEncodingに代入してもよい。この書き方だとUTF-8を設定するという情報はわかりにくくなるが、エンコーディングを揃えるというニュアンスは出るようになる。

$OutputEncodingの値を直接システムコンソールのOutputEncodingに代入しても同じ

どちらでもよいし、明確にするために両方とも書いておいてもよいと思う。

なぜ設定したのかを覚えておこう

ここでは[System.Console]::OutputEncodingと$OutputEncodingをUTF-8に設定することで問題を解決したわけだが、これまでのMicrosoftの取り組みを見てみると、この問題は自動的に回避されるように挙動が変更される可能性がある。そうなると、逆にこの設定をしておくことで既存のDOSコマンドとの連携がうまくいかなくなるなど、逆の面が問題として出てくる可能性がある。

設定とは必ずしも絶対的なものではなく、状況が変われば都度、書き換えるものだ。だが、時間が経つと「そもそも、なぜその設定をしたのか」を忘れがちである。忘れてしまうと、書き換えたくなっても、手を入れづらくなる。そのため、コメントには「何を設定しているのか」ではなく「なぜその設定を行ったのか」を書いておくとよい。そうすれば、後で読み返したときに理由がわかり、ためらわずに書き換えられるようになる。設定は状況に応じて書き換えるものであり、書き換えるために未来の自分に対して「なぜ」をメモしておくことは大切なことなのだ。

付録: $PROFILE

本連載時点での$PROFILEを次に掲載しておく。

# Copyright (c) 2020 Daichi GOTO <daichi@ongs.co.jp>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
# THE POSSIBILITY OF SUCH DAMAGE.

# author: Daichi GOTO (daichi@ongs.co.jp)
# first edition: Mon Jun 22 18:20:36 JST 2020

#========================================================================
# Set encoding to UTF-8
#========================================================================
# [System.Console]::OutputEncoding is set to local encoding, so character
# corruption occurs when piped from WSL to WSL. Therefore, set
# [System.Console]::OutputEncoding and $OutputEncoding to UTF-8 to avoid
# the problem.
$OutputEncoding = [System.Console]::OutputEncoding =
    [System.Text.UTF8Encoding]::new()

#========================================================================
# Definition of Linux commands used via wsl
#========================================================================
$_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 command functions
ForEach($n in $_linux_command_paths) {
    $_n = (Split-Path -Leaf $n)
    $_linux_functions += "
        function $_n {
            if (`$Input.Length) {
                `$Input.Reset()
                `$Input | wsl $n `$(_path_to_linux `$Args)
            }
            else {
                wsl $n `$(_path_to_linux `$Args)
            }
        }"
}
Remove-Variable n
Remove-Variable _n

$_linux_functions += @'
    function _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
    }
'@

# 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
. $_temp_ps1
Remove-Item $_temp_ps1

# Delete unnecessary variables
Remove-Variable _temp
Remove-Variable _temp_ps1
Remove-Variable _linux_path
Remove-Variable _linux_command_paths
Remove-Variable _linux_functions

#========================================================================
# Individual Linux command function definitions
#========================================================================
# grep
function 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
}
#less
function less {
    if ($Input.Length) {
        $Input.Reset()
        $Input | Out-Host -Paging
    }
    else {
        wsl less $(_path_to_linux $Args)
    }
}

# ls
Get-Alias ls *> $null && Remove-Item alias:ls
function ls { wsl ls --color=auto $Args }
function ll { ls -l }
function la { ls -a }

#========================================================================
# Alias definition
#========================================================================
Set-Alias -Name open -Value explorer
Set-Alias -Name edge -Value "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
Set-Alias -Name chrome -Value "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"

Get-Alias man *> $null && Remove-Item alias:man