macOS 10.15 Catalina でターミナルのやつが zsh になった

2020年1月6日

デフォルトのシェルが bash から zsh というものになったそうだ。
 zsh は bash の上位互換だそうで、つまり良きものなのだろう。

zsh を使ってみる

Mojave から Catalina へのアップグレードした場合だと bash がまだ生き残っている。すぐには zsh にならないのだ。
 ターミナルを立ち上げると次のようなメッセージが表示される。

The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.

よくわかんねーながらもchsh -s /bin/zshってのをやれって書いてある気がするのでやってみる。
 と、そこはもう zsh。
 読み方もわかんねーような世界に来てしまった。雑種、みたいな発音で大丈夫だろうか。

zsh には入力補完というのがある。
 試しにcdの後ろに半角スペースを入力してからtabキーを押してみる。

tabキーをを押すたびに、カレントディレクトリのなかにある、移動先の候補を補完してくれる。これ、すごい便利だ。

 もうひとつcontrol+rキーを押すとイクリメンタル・サーチで過去の履歴を検索できちゃう。

control+rで後方検索、contro+sで前方検索になるとのこと。
 これもかっこいい。

ちなみに、ターミナルの「環境設定」>「一般」タブ>「開くシェル」の欄で、bash に戻すこともできる。「コマンド」を選択し、完全パスを/bin/bashにすれば OK。

bash との違い

そもそも bash との違いはあまりないみたいだ。
 筆者が使ってるだいたいのシェルスクリプトが動く、と思う。
 ただ、ひとつ大きく違うのは配列の開始番号だ。

zsh の配列は 0 ではなく 1 から始まるらしい。
 こちらの、Catalinaでデフォルトシェルが「zsh」に変わる、bashとの違いは? – 新・OS X ハッキング!(241) | マイナビニュースの記事を例文をそのまんまシェルスクリプトにしてみた。

📝 zshtest.sh

#!/bin/bash

array=(apple orange peach)
for i in `seq 0 3`; do
    echo "array[$i] = ${array[$i]}"
done;

一行目を#!/bin/bashにして試してみる。

$ zshtest.sh
array[0] = apple
array[1] = orange
array[2] = peach
array[3] = 

array の先頭は 0 番目になっている。
 次に一行目を#!/bin/zshにしてふたたび実行。

% zshtest.sh
array[0] = 
array[1] = apple
array[2] = orange
array[3] = peach

今度は先頭が 1 から開始されている。これは覚えておいたほうがよさげ。

zshrc などの設置

bash_profile とかで書いたものがあれば、それを zsh でもほぼ流用できる。
 こういう冴えたやり方が紹介されていた。

cat .bash_profile >> .zprofile

プロンプトの書き方だけは違う。

zsh の設定ファイルは、.zshenv > .zprofile > .zshrc > .zlogin の順番で読みこまれる。違いはよくわからないが、プロンプトの設定は zshrc に書かないと反映されなかった。

筆者はパスは .zshenv に書いた。

export PATH=$PATH:~/bin:/usr/local/opt

zshrc にプロンプトとかエイリアス、関数を書いた。

PROMPT='%K{cyan}%T%k %n@%m %F{yellow}%~%f
%F{green}[>_ ] >%f '

alias zrc='open -a CotEditor ~/.zshrc'

dict(){
    open dict://$1
}

プロンプトの書き方は下の記事が詳しい。
 参考にさせていただきました。ありがとうございます。

🔗 とりあえずZshを使えば良いんだろう? – Qiita

追記 19.12.27

もうひとつ、これは知らなかったんだけど、有名らしい違いがあった。
 bash で以下のようなシェルスクリプトを実行する。
 今いるディレクトリのファイルやらフォルダやらを、番号をつけて書き出すスクリプトだ。

#!/bin/bash

a=1
ls -1 | while read line
do
    echo $a "$line"
    a=`expr $a + 1`
done

ls -1 で一行ずつリストを出し、それを line という変数におさめている。a という変数はループが進むにつれて 1 ずつ加算されていく。
 実行結果は以下の通り。
 なかなかいい感じだ。ふつうに動作しているように見える。

1 Applications
2 Desktop
3 Documents
4 Downloads
5 Library
6 Movies
7 Music
8 Pictures
9 Public
10 bin

このループを終えた後、変数 a の数字はいくつになっているか。一見 10 のように思えるが、echo で出力したあと数字が足されるので答えは 11。これが正解と考えられる。
 ところがそうはならないのである。
 上記のシェルスクリプトの最後に、一行追加して確認してみる。

#!/bin/bash

a=1
ls -1 | while read line
do
    echo $a "$line"
    a=`expr $a + 1`
done

echo 'a = '$a

実行結果は以下の通り。

1 Applications
2 Desktop
3 Documents
4 Downloads
5 Library
6 Movies
7 Music
8 Pictures
9 Public
10 bin
a = 1

変数 a は 1 のまま変わらないのだ。
 なんでこんなことになるか、よくわからない。while はパイプを通すと、変数をいじれないのだそうで、これはこれで正しい挙動とも聞いたことがある。
 なんであれ、while で変数をどうこうしても、while の外には出せないのである。

#!/bin/bash

a=1
str='Documents ないよ'
ls -1 | while read line
do
    if test "$line" = 'Documents'; then
        str='Documents 見つけた! '`echo $a`'番目に。'
    fi
    a=`expr $a + 1`
done

echo 'a = '$a
echo 'str = '"$str"

今いるディレクトリの中身を ls でリストにして while で順番に調べ、「Documents」というフォルダなり、ファイルがあるかどうかを調べるスクリプト。
 これも実行結果はむなしい。

a = 1
str = Documents ないよ

しょうがないので、かつてはIFS=$'\n'みたいなことをして for を使っていた。
 しかし zsh においては、この構文の挙動が bash と違うのだ。
 while の外に、変更した変数を持ち出せるのである。
 上記のスクリプトの一行目を#!/bin/bash から #!/bin/zshに書きかえてみる。
 再度実行した結果は、

a = 11
str = Documents 見つけた! 3番目に。

ちゃんと想定した通りに動いてくれる。
 これは、非常にグッジョブといえるのではないかと思う。

read コマンドが違う

bash でread -p '朝ごはんなに食べた? >' gohan などとやると、以下のようになる。

$ read -p '朝ごはんなに食べた? >' gohan
朝ごはんなに食べた >目玉焼き
$ echo $gohan
目玉焼き

zsh で同じことをやると怒られてしまう。

% read -p '朝ごはんなに食べた? >' gohan
read: -p: no coprocess

bash と zsh では read のオプションが違うのだ。
 上記のコードを zsh でやる場合、以下のように書くらしい。

% read gohan\?'朝ごはんなに食べた? >'
朝ごはんなに食べた >トースト
% echo $gohan
トースト

read 変数\?'プロンプト'という具合に、はてなをバックスラッシュでエスケープして、プロンプトとなる文字列を指定する。

違いは他にもある。
 bash なら-aで、配列に入力できるようになる。
 が、zsh の場合は-Aで配列変数を指定する。

% read -A michibata
karen jessica angelica
% echo $michibata[3]
angelica

bash では-nで入力文字数を指定する。
 zsh では -kだ。

% read -k 1 yesno
y%
% echo $yesno
y

また zsh には-qというオプションがある。yes か no かを尋ねる専門のオプションみたいで、y を入力した場合は 0 が返され、n なら 1 が返ってくる。
 以下のように使うらしい。

% echo 'てか、朝ごはん食べた?(y/n)'; if read -q;
then
    echo 'え、食べたの?'
else
    echo '駄目じゃん'
fi

てか、朝ごはん食べた?(y/n)
yえ、食べたの?

下記の記事が詳しい。
🔗 bash, zshでyes/no判定をするワンライナー – Qiita

この部分は 20.01.06 に追記しました。

変数展開みたいなの

f='/Users/username/Pictures/photo.jpg'
 みたいな、ファイルパスが入った変数があったとして、ファイル名やファイルの拡張子、あるいはファイルのあるディレクトリを取り出したいとき、よく変数展開と呼ばれる手法を使う。

echo ${f%.*}    拡張子を取り除く photo.jpg > …/Pictures/photo
echo ${f##*.}    拡張子を取り出す photo.jpg > jpg
echo ${f##*/}    ファイル名取得 basename /Pictures/photo.jpg > photo.jpg
echo ${f%/*}    ディレクトリ取得 dirname /Pictures/photo.jpg > /Pictures

変数の後ろにあるマークが「 # 」なら頭から文字を見ていって、例えば「.*」のようなパターンを探し、見つけるやいなや、頭からその場所までを削除する。「 ## 」という具合に二重になっている場合は、一番最後に見つかったパターンから頭までを削除。
「 ##*. 」なら最後に見つかった「 文字列. 」までが削除。残るのは拡張子だけだ。
 変数の後ろにあるマークが「 % 」なら、尻から文字を見ていってパターンを探す。
「 %% 」だと最後に見つかったパターンから右を削除。

これは bash でも zsh でも同様に使える。
 しかし zsh の場合はもっと簡単な方法がある。

echo $f:e    拡張子を取り出す /Pictures/photo.jpg > jpg
echo $f:h    ディレクトリを取り出す /Pictures/photo.jpg > /Pictures
echo $f:r    拡張子を取り除く /Pictures/photo.jpg > /Pictures/photo
echo $f:t    ファイル名を取り出す /Pictures/photo.jpg > photo.jpg

嬉しいことに、このオプションは複数指定できる。
 拡張子もパスも必要ない、ファイル名だけが必要な場合は以下のようにすればいい。

echo $f:r:t        /Pictures/photo.jpg > photo

オプションは他にもある。
 下記の記事が詳しい。勉強させていただきました。ありがとうございます。

zshのfor文は強かった – Qiita

この部分は 20.01.06 に加筆しました。

余談だけど変数展開で文字列の置換も出来る。
${変数名//検索文字列/置換文字列}
 という感じ。

% baka='馬鹿! 馬鹿! 大馬鹿!'
% echo ${baka//馬鹿/好き}
好き! 好き! 大好き!