zsh的字符串替换引起的卡顿

我的mac系统每次启动后第一次打开iterm2的时候,oh-my-zsh的启动总是明显的卡顿一下,而之后退出iterm2重启动则不会有这个卡顿,也就是只在第一次启动iterm2的时候发生。对启动的zsh增加了-xv参数后观察,发现这个卡顿发生在git_compare_version函数的第4行:

找到这个函数后,发现第4行的操作并不是git等网络操作,而是一个字符串替换的操作,它使用zsh内置的字符串替换功能:INSTALLED_GIT_VERSION=(${(s/./)INSTALLED_GIT_VERSION[3]})

非常的不符合直觉(直觉上以为卡顿是因为网络阻塞引起的),模拟一下,在一个脚本里使用这个字符串替换操作,看看具体的耗时情况:

$ cat zsh-test.sh
#!/usr/bin/env zsh -xv
export PS4=$'%D{%M%S%.} %N:%i> '

INSTALLED_GIT_VERSION=($(command git --version 2>/dev/null));
INSTALLED_GIT_VERSION=(${(s/./)INSTALLED_GIT_VERSION[3]});
echo "$INSTALLED_GIT_VERSION"

然后再启动时调用这个zsh脚本:

$ cat run.sh
#!/usr/bin/env zsh -xv
export PS4=$'%D{%M%S%.} %N:%i> '
./zsh-test.sh

重启系统,启动后在bash下执行run.sh脚本:

INSTALLED_GIT_VERSION=($(command git --version 2>/dev/null));
5649865 ./zsh-test.sh:4> INSTALLED_GIT_VERSION=5649867 ./zsh-test.sh:4> git --version
5649865 ./zsh-test.sh:4> INSTALLED_GIT_VERSION=( git version 2.8.4 '(Apple' 'Git-73)' ) 
INSTALLED_GIT_VERSION=(${(s/./)INSTALLED_GIT_VERSION[3]});
5650999 ./zsh-test.sh:5> INSTALLED_GIT_VERSION=( 2 8 4 )

看到zsh-test.sh里的第5行字符串替换的操作耗时用了1秒多时间,如果再次执行的话会降到几个毫秒。这真是个蹊跷的问题,发邮件给 zsh-works@zsh.org 好几周也没有人回复,先在博客里记录一下这个问题,以后再追踪。zsh版本是:5.2 (x86_64-apple-darwin16.0.0)。

shell前边的连字符含义

在一个脚本里,要获取其父shell时,使用了下面的方式:

#!/bin/bash 
ps -ocomm= -p $(ps -oppid= $$)

它在某些环境下,父shell会显示为 “/usr/local/bin/zsh” 或者 “bash” ,而某些环境下却会显示为”-bash”或”-zsh”;这个开头多出来的连字符是怎么回事?查了一下,原来是表示的是”login shell”。

linux下可以在”/etc/passwd”里看到用户的login shell,而在mac下要确认当前用户的login shell,要通过下面的命令:

$ dscl . read /users/$USER UserShell
UserShell: /bin/bash

在mac下,当打开终端程序(Terminal.app)时,终端shell是login进程的子进程(不管你配置那种command):

$ pstree
 ...
 |-+= 01272 hongjiang /Applications/Utilities/Terminal.app/Contents/MacOS/Terminal
 | \-+= 01275 root login -pf hongjiang
 |   \-+= 01276 hongjiang -bash
 ...

$ ps -ef | grep login
0  1275  1272   0  2:05PM ttys000    0:00.05 login -pf hongjiang 

# 把Terminal的启动命令修改为zsh也一样

 |-+= 01988 hongjiang /Applications/Utilities/Terminal.app/Contents/MacOS/Terminal
     | \-+= 01991 root login -pf hongjiang /bin/zsh  |   \-+= 01992 hongjiang -zsh

而在mac的 iTerm.app 下,不管如果你配置的command是”Login shell”还是修改为其他shell,启动后shell没有再挂在login进程下:

$ pstree
 |-+= 00574 hongjiang /Volumes/Data/program/iTerm.app/Contents/MacOS/iTerm
 | \-+= 02177 hongjiang /usr/local/bin/zsh  

但在iTerm下如果使用的是“Login shell”显示的名称前边却是有连字符的,而其他shell则没有

$ ps $$
  PID   TT  STAT      TIME COMMAND
 2220 s001  Ss     0:00.01 -/bin/bash

# 修改启动shell为zsh

➜  ps $$
  PID   TT  STAT      TIME COMMAND
 2524 s000  Ss     0:00.16 /usr/local/bin/zsh

要想在iTerm下保持跟Terminal一致也用login来启动,应该在配置里修改启动命令为:

login -pf $username /usr/local/bin/zsh   

在linux下,从tty登录shell也是一样由login进程启动的

$ ps -ef | grep bash
 hongjia+  2241   467  0 14:09 tty1     00:00:00 -bash

$ pstree 
systemd─┬─NetworkManager─┬─2*[dhclient]
    │                └─3*[{NetworkManager}]
    ├─...
    ├─login───bash
    ...   

$ ps -ef | grep login
root       467  0.0  0.2  84584  2328 ?        Ss   14:08   0:00 login -- hongjiang    

使用su命令切换到一个用户shell下,默认情况这个shell并不是”login shell”不会去执行/etc/profile和home目录下相关配置:

$ sudo su hongjiang

$ ps -ef | grep $$
hongjia+  2796  2795  0 14:57 pts/0    00:00:00 bash

要以”login shell”方式启动,需要对su指定一个参数“-“

$ sudo su - hongjiang

$ ps -ef | grep $$
hongjia+  3188  3187  0 15:35 pts/0    00:00:00 -bash

从bash文档里可以看到,要以”login shell”方式启动一个shell,要么第一个参数给一个特定的连字符“-”,要么显式的对bash设定”–login”参数。

命令输入完,发现需要先执行另一条命令

比如, cd /tmp/dd,刚输入完,发现需要先创建这个目录。以往的做法是 Ctrl+a 移到这一行开头,
然后输入 mkdir /tmp/dd && cd /tmp/dd

还有个简单的方式是,Ctrl+u 删除此行命令,然后执行其他命令。之后,再 Ctrl+y 粘贴之前的命令

这里Ctrl+u 相当于 Ctrl+a , Ctrl+k

在zsh下,还可以省一步,用 alt+q 可以替代上面的两步 ctrl+u , ctrl+y

注: Esc-qAlt-q 清除当前命令,执行另一个命令结束后,再插入此命令。

搜索历史命令

很久之前,我是这样对历史搜索增强的,参考我的.zshrc文件

bash 中绑定 up 和 down 在匹配的条件中选择

bind '"\e[A": history-search-backward'
bind '"\e[B": history-search-forward'

zsh中的绑定:

bindkey "^[[A" history-search-backward
bindkey "^[[B" history-search-forward

这样 ctrl-r 后搜索 mvn 然后可以用 up/down 选择所有mvn的历史命令。
不过这样如果你刚好使用过“history | grep mvn” 这样的命令时,再通过ctrl-r搜索mvn会把这条也匹配到,并且再继续用方向键时,列出的都是“history | grep”开头的命令;这点有些不爽。

还好有一种稍微好点的方式:敲入一个命令时,通过快捷键列出所有这个命令开头的,并给出序号来选择。

把下面几行放到 .zshrc文件里:

autoload -Uz history-beginning-search-menu
zle -N history-beginning-search-menu
bindkey '^X^X' history-beginning-search-menu

然后在终端,当我敲入 mvn 后,按ctrl-x两次会列出所有mvn开头的命令,然后可以通过输入序号来执行那一次的命令了。

Enter digits:
01 mvn                                                     13 mvn dependency:sources
02 mvn assembly                                            14 mvn dependency:soureces
03 mvn assembly:assembly                                   15 mvn dependency:tree | tee /data/tmp/hsf-dep-tree
04 mvn assembly:assembly -Dmaven.test.skip                 16 mvn eclipse:eclipse
05 mvn clean                                               17 mvn hi /data
06 mvn clean compile                                       18 mvn install
07 mvn clean install -Dmaven.test.skip                     19 mvn package
08 mvn clean package assembly:assembly -Dmaven.test.skip   20 mvn package assembly:assembly
09 mvn compile                                             21 mvn package assembly:assembly -Dmaven.test.skip
10 mvn compile:compile                                     22 mvn3
11 mvn copile                                              23 mvn3 compile
12 mvn dependency:sources                                  24 mvn3 eclipse:eclipse

不过这招仍有些问题,当历史命令里带有管道时,输入序号不起作用了,比如:

hongjiang@whj-mbp ~ % ps #两次ctrl-x
Enter digits:
01 ps -aux | head -2                           12 ps -ef | grep mplayer 
02 ps -aux | head 2                            13 ps -ef | grep fmd 
03 ps -ef                                      14 ps -ef | grep java                            

这时候输入12并不能自动补齐这条历史命令。先配合着与ctr-r一同来用吧。