zsh のプロンプトを Windows コマンドプロンプト風にするいたずら

# TL;DR

macOS がデフォルト指定した zsh の man を見てたら変数展開[^1]があまりにも多機能だったので、パスを Windows 風に表記できないか遊んでみました。

[^1]: `man zshexpn` の "PARAMETER EXPANSION" 節 (2019年9月11日閲覧)

```plaintext:本物のcmd
Microsoft Windows [Version 10.0.18363.1082]
(c) 2019 Microsoft Corporation. All rights reserved.

C:\Users\me>
```

```zsh:出来上がった~/.zshrc
setopt PROMPT_SUBST
PS1=$'\n''C:${${PWD//\//\\}//\%/%%}>'
```

# やったこと

シンプルにカレントディレクトリの "/" を "\\" に置換して "(改行)C:" と "\>" で挟みました。

## 実装のポイント

### プロンプトシーケンスで変数展開を有効化する

```zsh
setopt PROMPT_SUBST
```

プロンプトシーケンスの展開に関するヘルプ[^2]を見たところ、残念ながらこの文法では文字置換できないことが判明しました。
代替機能として「プロンプトシーケンス内で変数展開を有効にするおぷしょん」が提供されていました。それを ON にするのが上述の1行です。

[^2]: `man zshmisc` の "EXPANSION OF PROMPT SEQUENCES" 節 (2019年9月11日閲覧)

### 改行

cmd はコマンド実行後に改行します。この挙動を zsh で再現するためプロンプト先頭に改行を入れました。

zsh で改行文字を変数の値に使う方法は複数ありますが、今回はその中でもインデントに強い `$'\n'` を使いました。

### 変数 $PWD の値を2回加工する

```zsh
PS1=$'\n''C:${${PWD//\//\\}//\%/%%}>'
```
&uarr; の `${${PWD//\//\\}//\%/%%}` という式です。

前々節のコマンドで変数展開を有効化しましたので、 \$PS1 の中で変数展開を使用することができます。

また zsh では変数の文字置換を行うために専用の文法が用意されています。
man に3通り載っています[^1]が、今回はそのうち `${name//pattern/repl}` を利用いたしました。

これを使い "/"&rarr;"\\" と "%"&rarr;"%%" の2通りの置換を行いました。

#### % を %% に変える理由

/ を \\ に置換するのは Windows 風にするためとして、 % を %% に置換する理由は何でしょうか?

そもそも*プロンプトシーケンスの中では % を使って様々な特殊な値を表現*できます[^2]。
例えば \$PS1 の既定値 `%m%# ` において `%m` はホスト名（略称）、 `%#` は管理者権限の有無に応じて `#` または `%` になります。

しかし変数展開の方が先に実施されるため、 **カレントディレクトリが % 文字を含んでいると挙動がおかしくなってしまいます。**

これを防ぐため、カレントディレクトリ中の % をエスケープする必要があります。

#### 変数展開で2回置換する方法

`${}` 構文において変数名の部分に `${}` 構文を使うと、その内側の変数展開された値を多重に加工できます。

例えば \$hoge の値が `fuga` だったとして `${${hoge}}` とすると評価結果は **\$hoge の値 fuga** になります。
内側から展開されて `${fuga}` と解釈されて \$fuga の値に…はならないわけです。

この挙動は変数展開のヘルプ[^1]にしれっと以下のように載っていました。

> If  a `${...}` type parameter expression or a `$(...)` type command substi‐
>        tution is used in place of name above, it is  expanded  first  and  the
>        result is used as if it were the value of name.  Thus it is possible to
>        perform nested operations:  `${${foo#head}%tail}` substitutes  the  value
>        of  `$foo`  with both \`head' and \`tail' deleted.  The form with `$(...)` is
>        often useful in combination with the  flags  described  next;  see  the
>        examples  below.   Each  name or nested `${...}` in a parameter expansion
>        may also be followed by a subscript expression as  described  in  Array
>        Parameters in zshparam(1).

今回の式 `${${PWD//\//\\}//\%/%%}` に戻ってみると、内側の `${}` で /&rarr;\\ の置換を、外側の `${}` で %&rarr;%% の置換をそれぞれ行っています。

## 出来上がった $PS1 (再掲)

さあ皆さんもこれを使って、不正ログインしてきたクラッカーを混乱させてやりましょう! (白目)

```zsh:~/.zshrc
setopt PROMPT_SUBST
PS1=$'\n''C:${${PWD//\//\\}//\%/%%}>'
```

おまけ: /home を C:\\Users として表示する \$PS1

```zsh:~/.zshrc
setopt PROMPT_SUBST
PS1=$'\n''C:${${${PWD:/#%\/home(\/*|)/${PWD/#\/home//Users}}//\//\\}//\%/%%}>'
```

# 応用: WSL において Windows のファイルシステムに移動したときだけ Windows 風にする

今度はいたずらではなく比較的実用的な機能。

WSL では Windows のディレクトリが /mnt ディレクトリ以下にマウントされますが、ここにいるときだけ作業ディレクトリを Windows 風にします。

## 出来上がった $PS1

```zsh:~/.zshrc
setopt PROMPT_SUBST
PS1=$'\n''${${PWD:/#%\/mnt\/[a-z](\/*|)/${(U)PWD:5:1}:\\${${PWD:7}//\//\\}}//\%/%%}>'
```

```plaintext:実行サンプル
/home/felith>cd /mnt/c

C:\>pwd
/mnt/c
```

## 詳細

WSL においては、 Windows から見える「ドライブ」が /mnt 以下にマウントされます。
作業ディレクトリがこれ以下の時だけ、前節の置換を行うようにしてみました。

```zsh
PS1=$'\n''${${PWD:/#%\/mnt\/[a-z](\/*|)/${(U)PWD:5:1}:\\${${PWD:7}//\//\\}}//\%/%%}>'
```

今度はファイル名展開を駆使し、作業ディレクトリが /mnt/(英小文字1字) 以下の時だけマッチするようにしてます。

また、今度は前回と少し異なる `${name:/pattern/repl}` という構文を使いました。

これの評価結果は \$name の値が pattern にマッチすれば repl になり、マッチしなければ \$name になるというものです。

### 置換対象 (pattern)

まず **完全一致** にするためパターンの前に `#%` というおまじないを挿入しています。(デフォルトは部分一致です。)

パターン文字列は `/mnt/[a-z](/*|)` です。
要点だけ説明すると以下のようになっています。

- `[a-z]`: 半角英小文字
- `(/*|)`: "/" に続く任意の文字列、または空文字列。 "/\*" というパターンと "" というパターンを \| で区切っています。

ちなみに前方一致もできますが、前方一致にしない理由は /mnt/abc のようなパスでは変換してほしくないからです。

### 置換後の文字列

`${(U)PWD:5:1}:\\${${PWD:7}//\//\\}` というのが置換後の文字列なのですが、先頭から解説します。

まず `${(U)PWD:5:1}` の `(U)PWD` というのは「フラグ」という記法を使って、 \$PWD を大文字にしています。
続く `:5:1` というのは、先頭から6文字目の文字という意味です。 (1文字目は0)
実際の \$PWD は /mnt/**c**/Users/... というパスになりますので、ドライブ文字に相当するのは6文字目です。

この展開の後 `:\\` を追加して "C:\\" にしてます。

さらにそのあとに続く `${${PWD:7}//\//\\}` は、 \$PWD から先頭7文字を削除したうえでディレクトリ区切りを置換しています。
ドライブ文字の続きは **/mnt/c/**Users/... と8文字目以降になるので、先頭7文字を削ればそのパスが得られます。

## 実演

```
/home/me>cd C:
cd: no such file or directory: C:

/home/me>cd /mnt/c

C:\>pwd
/mnt/c

C:\>cd Users

C:\Users>pwd
/mnt/c/Users

C:\Users>cd

/home/me>
```

完璧ですね!

# License

記事本文中のコードスニペットのうち "~/.zshrc" とラベルがついているものはすべて CC0 (public domain) です。

# 参考文献

脚注にまとめました。