最近、『プログラマーの自己修養』という本を読んでいますが、この本のコード例は基本的にUNIXのものです。しかしすでに最高のCmderに慣れた自分はまたあの黒いBashを使わせるのは多分できかねないでしょう。
そしてこの記事が生まれました!

Cygwin、MinGW+MSYS、MinGW-w64、MSYS2 どっち?

もうタイトルでバレてました(笑)。
MSYS2はモダンなCygwinとMinGW-w64に基づいて発展してきたプロジェクトであり、その核心の部分はMSYSの書き換えたバーションというものです。そして、最も重要なことは、fullバージョンのCmderが含まれるGit for Windows 2.x自身はMSYS2のフォークです。ならばなぜGit for Windowsを直接使わないのですか?その原因は、オリジナルのMSYS2はArch LinuxのPacman(パッケージ管理システム)を移植されます。Pacmanがあれば、gitやgccツールチェーンなどのパッケージを簡単にインストールし、管理することが可能になります。

ちなみに、この節の見出しに列挙されたプロジェクトの関係はちょっと複雑すぎで、検討する価値があります。
私は最初調べる時、GCCのホームページによってMinGWのインストーラをダウンロードしましたが、そのインストーラはMinGWだけでなく、MSYSもインストールして、もう一つのGNU Coreutilesが導入されてしまいました(一つはMSYSのもの、一つはGit for Windowsのもの)。幸いすぐにより良いソリューションを見つけました・・・そう!MinGW-64です。MinGW-64はMinGWの64bitに非対応と両方の食い違いで始められたプロジェクトですが、64bitしか対応しないわけではありません、MinGW-64は実際32bitと64bitをサポートしています。
一方、MSYS2プロジェクトの目標は、最新のCygwinをきっちりと追跡することです。それに対して、MSYSは現在Cygwinに遅れずについていけることができません。こうして見ると、MSYS2がMinGW-w64を選んだのは明らかでしょう。

CmderでMSYS2を起動する方法?

まず、MSYS2を正常に起動する方法を見てみましょう。
MSYS2のインストール先にはmsys2_shell.cmdという実行ファイルが存在し、同じフォルダでの3つの.exeファイルはそれぞれのオプションを持つmsys2_shell.cmdのラッパーです。

1
2
3
msys2.exe   --> msys2_shell.cmd -msys
mingw64.exe --> msys2_shell.cmd -mingw64
mingw32.exe --> msys2_shell.cmd -mingw32

続いて、msys2_shell.cmdの肝心な部分はオプションに応じて環境変数MSYSTEMを設定することです。その変数は後続のsource全プロセスに直接影響を与え、/usr/bin内一部の実行ファイルのロジックさえも影響します。MSYSTEMには、MSYSMINGW64MINGW32の三つの値があり、それぞれの値は独立なサブシステムを対応されています。MSYSは”mostly-POSIX-compliant”なエミュレートされた環境を提供するためのサブシステムです。同時に提供されたmsys-2.0.dllはCygwinのcygwin1.dllのようなランタイムライブラリーです。それに対して、MINGW64MINGW32サブシステムは上述のMinGW-w64の環境と見なすといい。その上、MINGW64はmingw-w64-x86_64-toolchainを、MINGW32はmingw-w64-i686-toolchainを利用し、コンパイルしたネイティブのWindowsプログラムを提供しています。

Cmderは所詮ConEmuの上にいくつかの機能を加えて構築されたプロジェクトですから、MSYS2の起動問題は「どうやってConEmuで新しいtaskを追加する」になります。msys2_shell.cmdはユーザーの渡されたオプションを判断して特定ターミナルエミュレーター(ConEmu、Mintty、ConsoleZなど)を起動するがありますが、幸いにConEmuに関するのロジックは特に複雑ではありません、核心コマンドはbash --login -iだけです。それを踏まえて、以下のコマンドを別々Settings > Startup > Task タブの”Command”フィールドに書き込んで三つの新しいtaskを作りましょう。

1
2
3
set MSYSTEM=MSYS & cmd /c "%MSYS2_ROOT%\usr\bin\bash --login -i" -new_console:d:%USERPROFILE%
set MSYSTEM=MINGW64 & cmd /c "%MSYS2_ROOT%\usr\bin\bash --login -i" -new_console:d:%USERPROFILE%
set MSYSTEM=MINGW32 & cmd /c "%MSYS2_ROOT%\usr\bin\bash --login -i" -new_console:d:%USERPROFILE%

上述の通り、各コマンドは一つのサブシステムを対応されています。%MSYS2_ROOT%はMSYS2のインストール先ですが、その環境変数はない場合OSにマニュアルで追加する必要があります。%MSYS2_ROOT%を絶対パスに置き替えるのもおkです。
デフォルトTaskの設定とTask名前の指定も忘れないでね。

“Cmder Here”ショートカットが壊れた?

CmderレポジトリのReadmeにより、Cmder.exeを格納するフォルダで.\cmder.exe /REGISTER ALLを実行するとWindowsのコンテキストメニューに”Cmder Here”ショートカットを追加することになります。実はこのショートカットで起動とCmder.exeをクリックで起動を比較すれば、ただ「当セッションで環境変数CMDER_STARTが作成される」だけの違いです。本当のcd操作は%CMDER_ROOT%/vendor/init.batのコードで実現させます。

1
2
3
4
5
:: This is either a env variable set by the user or the result of
:: cmder.exe setting this variable due to a commandline argument or a "cmder here"
if defined CMDER_START (
cd /d "%CMDER_START%"
)

上述のinit.batファイルはネイティブWindowsターミナルcmder.exeのすべてを初期化するとユーザー体験を増加するためですから、MSYS2を利用するために作り出したtask達はそのbatファイルを実行しないのは当然のことです。だから次のステップは上述のコードのロジックをMSYS2のbashの初期化ところへ運ぶことです。
以下のコードをMSYS2のホームディレクトリにある.bashrcファイルに追加すれば、壊れた”Cmder Here”ショートカットは直せる。

1
2
3
4
5
# For "Cmder Here"
if [[ -n ${CMDER_START} ]]; then
echo "Changing into directory: ${CMDER_START}"
cd "${CMDER_START}"
fi

Cmder.exeをクリックで起動やタスクバーのショートカットで起動の場合は、CMDER_STARTはセットしないので、ここのNullチェックは不可欠です。こうすれば/Startオプションをショートカットに付加するなどのことが避けられます。しかし、この方法はまだバグがあります。未知の原因で、パーティションのルートディレクトリ直下で”Cmder Here”を実行すると、CMDER_STARTが持つパスの末尾はいつも余計のダブルクォートがついています。これはCmder自身の問題のようですから、しばらくおいておきます。

Cmder側のインジェクション

Cmderにはcmder_exinitファイルがあります。ディレクトリは%CMDER_ROOT%/vendor/。用法はこのファイルを適切な拡張子をつけて、/etc/profile.d/に移動することです。そうすると、MSYS2のシェルはこのファイルを”source”られる。
以下はcmder_exinitの主な機能です。

  1. 環境変数CMDER_ROOTを作ってCmderに関するディレクトリを環境変数PATHに追加する。
  2. %CMDER_ROOT%/config/profile.d/内すべてのファイルと%CMDER_ROOT%/config/user-profile.shを”source”する。

そして、%CMDER_ROOT%/bin/内のバイナリをbashに利用することができる。

もしCmderの”ポータブル”の開発理念に従って、先の節でカスタマイズコードを追加すべき場所は.bashrcではなく、user-profile.shになります。そうしないと、Cmder全体を別のマシンに移行する時にカスタマイズ内容はなくなります。もちろん、どっちは違うとは言えません、個人的な好みだけです。

もう一つ、git-prompt.shファイルも/etc/profile.d/へ移動する必要があります。このファイルはGit for Windowsのもので、Gitのブランチ名を表示するとコマンドを補完するためです。Cmderはfull-versionならば、ファイルは%CMDER_ROOT%/vendor/git-for-windows/etc/profile.d/にある。Cmderはmini-versoinの際、ファイルはまだGit for WindowsのGitHubレポジトリからダウンロードできる。ちなみに、いま言及しているgit-prompt.shは本体ではありません。機能を実際に動作させるファイルはgitインストール先でのgit-prompt.shgit-completion.bashです。上述のgit-prompt.shは主に環境変数PS1をセットし、git-prompt.shgit-completion.bashを”source”します。

git-prompt.shのロジックは以下の通り。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PS1='\[\033]0;$MSYSTEM:${PWD//[^[:ascii:]]/?}\007\]' # set window title
PS1="$PS1"'\[\033[32m\]' # change to green
PS1="$PS1"'\u@\h ' # user@host<space>
PS1="$PS1"'\[\033[33m\]' # change to brownish yellow
PS1="$PS1"'\w' # current working directory
if test -z "$WINELOADERNOEXEC"
then
GIT_EXEC_PATH="$(git --exec-path 2>/dev/null)"
COMPLETION_PATH="${GIT_EXEC_PATH%/libexec/git-core}"
COMPLETION_PATH="${COMPLETION_PATH%/lib/git-core}"
COMPLETION_PATH="$COMPLETION_PATH/share/git/completion"
if test -f "$COMPLETION_PATH/git-prompt.sh"
then
. "$COMPLETION_PATH/git-completion.bash"
. "$COMPLETION_PATH/git-prompt.sh"
PS1="$PS1"'\[\033[36m\]' # change color to cyan
PS1="$PS1"'`__git_ps1`' # bash function
fi
fi
PS1="$PS1"'\[\033[0m\]' # change color
PS1="$PS1"'\n' # new line
PS1="$PS1"'λ ' # prompt: always λ

AliasとPATHをカスタマイズ

Cmder自身は特製のalias機能をcmd.exeに提供しています(詳細はCmderのconfigフォルダでのuser-aliases.cmdを参考にしてください)。同様に、その特製aliasプリセットを利用したいならコードのロジックを.bashrcuser-profile.shへ運ぶしかないです。なお、WindowsとLinuxのディレクトリ記述方法やスクリプト文法は違うので、特別扱いの必要があります。

自分が使っているコードは以下の通り。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
alias ll='ls -alh --time-style=long-iso --show-control-chars -F --color $*'
alias e.='explorer .'
alias showpath=" tr ':' '\n' <<< \"\$PATH\" "

# Include Extra Paths
ExtraPaths=(
"${NODE_PATH}"
"${APPDATA}\npm"
)
for currPath in ${ExtraPaths[@]}
do
# Convert path from Windows to Unix format
if [ "$currPath" != "" ] ; then
case "$currPath" in *\\*) currPath="$(cygpath -u "$currPath")";; esac
fi
if [ "$currPath" != "" ] ; then
# Remove any trailing '/'
currPath=$(echo $currPath | sed 's:/*$::')

echo "Adding extra path \"${currPath}\"."
PATH=${currPath}:${PATH}
fi
export PATH
done
unset ExtraPaths