问题的提出
最近使用 github 上传、下载项目代码时,经常会卡很久,有时候在命令行打了 git push 然后就去上厕所了,结果等我回来的时候,发现 push 早已经失败了,还得重新提交一下。如果有一个工具,可以不停的重启失败的 git push 直到它成功才退出,那就好了。
什么是 expect
在介绍使用 expect 重启 git 操作之前,先简单说明一下这个命令。其实它并不是一个新潮的东西,在很早以前就存在了,以至于现在一些系统默认都不带这个命令了,需要自己手工安装下:
$ sudo yum install expect
$ expect -v
expect version 5.44.1.15
简单的说,expect 就是完成一些需要与用户交互的任务,例如 telnet、ftp、ssh 远程登录机器的时候,这些命令会要求用户输入用户名、密码等相关信息,而这些,是无法通过 shell 脚本来完成的。这是因为这些命令是从控制终端而不是标准输入上读取的,所以无法事先将信息重定向到标准输入从而实现自动化运行。而 expect 就是用来解决这类问题的,下面是一个使用 expect 进行 ssh 登录的例子:
1 #!/usr/bin/expect -f 2 set ipaddr "localhost" 3 set passwd "iforgot" 4 5 spawn ssh root@$ipaddr 6 expect { 7 "yes/no" { send "yes\r"; exp_continue} 8 "password:" { send "$passwd\r" } 9 } 10 11 expect "]# " 12 send "touch a.txt\r" 13 send "exit\r" 14 expect eof 15 exit
expect 脚本里有这么几个关键动作:
- spawn :启动需要执行的命令;
- expect :解析命令输出,并根据下面的匹配语句进入子控制块;
- send :向命令发送信息,这些信息相当于是命令从控制终端读取的;
- interact :继续命令与控制终端的交互,此时用户可以正常向命令输入信息(本例未展示)。
- ……
好了,熟悉了 expect 的用法后,有人可能有疑问了,这个 git pull/push 操作也不涉及密码,用它做什么呢?这就是因人而异了,有些人是因为密码的关系用它,而我只看中了它的 expect 动作。
失败日志与正常日志
以 git pull 为例,失败时,它的输出如下:
$ git pull ssh: connect to host github.com port 22: Connection refused fatal: The remote end hung up unexpectedly
成功时,它的输出是这样的:
$ git pull remote: Enumerating objects: 38, done. remote: Counting objects: 100% (38/38), done. remote: Compressing objects: 100% (24/24), done. remote: Total 36 (delta 24), reused 24 (delta 12), pack-reused 0 Unpacking objects: 100% (36/36), done. From github.com:goodpaperman/apue 86b80d3..e0cc835 master -> origin/master Updating 386fd43..e0cc835 Fast-forward apue.c | 10 ++++++++++ 1 files changed, 10 insertions(+), 0 deletions(-)
如果已经没有更新的内容可以拉取,它的输出是这样的:
$ git pull Already up-to-date.
对于 git push 而言也是大同小异,失败时:
$ git push Connection reset by 13.229.188.59 port 22 fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists.
成功时:
$ git push Counting objects: 16, done. Compressing objects: 100% (10/10), done. Writing objects: 100% (10/10), 1.05 KiB, done. Total 10 (delta 7), reused 0 (delta 0) remote: Resolving deltas: 100% (7/7), completed with 6 local objects. To git@github.com:goodpaperman/apue.git 87748c7..08e3a1e master -> master
已经是最新时:
$ git push Everything up-to-date
于是很自然的想到了一个解决方案:一直 spawn git pull / push 直到 expect 到我们想要的输出 "xxx up-to-date."
重启失败的操作
利用上面的思路,写出了下面的 expect 脚本
1 #! /usr/bin/expect -f 2 set timeout 30 3 for {set i 0} {$i<=10} {incr i} { 4 puts "start pulling git $i" 5 spawn git pull 6 expect "Already up-to-date." { puts "pulling ok"; exit } 7 }
这段脚本使用了 expect 循环,最多尝试 10 次,如果仍然拉取不成功,则可能是其它原因导致的,此时退出循环。
1 #! /usr/bin/expect -f 2 set timeout 30 3 for {set i 0} {$i<=10} {incr i} { 4 puts "start pushing git $i" 5 spawn git push 6 expect "Everything up-to-date" { puts "pushing ok"; exit } 7 }
与 pull 类似,只是 expect 的特征串不同,这里使用 “Everything up-to-date” 代替 “Already up-to-date.”
但是这样写有个缺点,就是如果这个做成脚本放在某个目录下的话,我需要明确指定对应的路径才可以调用它。有没有什么办法可以像命令一样随时随地的调用这个脚本呢?
使用 alias
在你的系统上输入 alias 可以查看当前开启的命令别名。
$ alias alias cd='cdls' alias l.='ls -d .* --color=auto' alias ll='ls -l --color=auto' alias ls='ls --color=auto' alias vi='vim' alias which='alias | /usr/bin/which --tty-only --read-alias --show-dot --show-tilde'
可以看到我的机器上 cd 命令被重定义为 cdls,这又是个什么神奇的东东呢,打开 ~/.bashrc,可以看到它的定义:
1 $ cat ~/.bashrc 2 # .bashrc 3 4 # Source global definitions 5 if [ -f /etc/bashrc ]; then 6 . /etc/bashrc 7 fi 8 9 # User specific aliases and functions 10 cdls() { 11 cd "${1}"; 12 ls; 13 } 14 15 alias cd='cdls'
其实就是一个 shell function,里面组合调用了 cd 与 ls 命令,达到切换到新目录同时列出目录内容的功能。看到这里,类比着去实现一个 gpull / gpush 应该不难了吧:
1 git_pull() { 2 expect -c 'set timeout 30; for {set i 0} {$i<=10} {incr i} { puts "start pulling git $i"; spawn git pull; expect "Already up-to-date." { puts "pulling ok"; exit } }' 3 } 4 5 git_push() { 6 expect -c 'set timeout 30; for {set i 0} {$i<=10} {incr i} { puts "start pushing git $i"; spawn git push; expect "Everything up-to-date" { puts "pushing ok"; exit } }' 7 } 8 9 alias gpull='git_pull' 10 alias gpush='git_push'
这里使用 expect 的 -c 选项来在一行内输入所有脚本语句,各个语句之间使用分号分隔。在 ~/.bashrc 中加入上面的内容,然后执行以下命令重载 bashrc 文件
$ . ~/.bashrc
就可以使刚加入的 gpull 与 gpush 别名生效啦!当然,这样做了以后,只对当前用户生效,其它用户登录后是无法使用的。可以将这个别名定义在 /etc/bashrc 中,这样所有用户就都可以使用啦~ 下面是执行的效果:
$gpull start pulling git 0 spawn git pull remote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. remote: Compressing objects: 100% (1/1), done. remote: Total 3 (delta 2), reused 3 (delta 2), pack-reused 0 Unpacking objects: 100% (3/3), done. From github.com:goodpaperman/apue 65d83a6..8560ad0 master -> origin/master Updating 65d83a6..8560ad0 Fast-forward apue.c | 11 +++++------ 1 files changed, 5 insertions(+), 6 deletions(-) start pulling git 1 spawn git pull Already up-to-date. pulling ok $gpush start pushing git 0 spawn git push Counting objects: 5, done. Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 316 bytes, done. Total 3 (delta 2), reused 0 (delta 0) remote: Resolving deltas: 100% (2/2), completed with 2 local objects. To git@github.com:goodpaperman/apue.git 8560ad0..0d3c3c7 master -> master start pushing git 1 spawn git push Everything up-to-date pushing ok
从上面的输出可以看到一个问题,就是第一次实际上已经 pull / push 成功了,但是由于没有得到我们想要的输出,操作又被重启了一次,直到它输出 xxxx up-to-date 为止。可见我们的 expect 也不是非常智能啊,有关于这个的改进就留给各位看官了……
结语
其实我们这里只是用了 expect 的脚本语法,并没有用到它更高深的部分:终端控制,其实与 expect 类似的还有 script 和 tee 等命令,它们都是在内部开一个伪终端对,来实现对终端输入/输出的重定向能力的。与终端相关的内容,可以参考我之前写的这篇文章:[apue] 书中关于伪终端的一个纰漏
参考
[1]. Linux-expect命令详解
[2]. expect用法