问题的提出
最近使用 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 登录的例子:
#!/usr/bin/expect -f
set ipaddr "localhost"
set passwd "iforgot" spawn ssh root@$ipaddr
expect {
"yes/no" { send "yes\r"; exp_continue}
"password:" { send "$passwd\r" }
} expect "]# "
send "touch a.txt\r"
send "exit\r"
expect eof
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 脚本
#! /usr/bin/expect -f
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 }
}
这段脚本使用了 expect 循环,最多尝试 10 次,如果仍然拉取不成功,则可能是其它原因导致的,此时退出循环。
#! /usr/bin/expect -f
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 }
}
与 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,可以看到它的定义:
$ cat ~/.bashrc
# .bashrc # Source global definitions
if [ -f /etc/bashrc ]; then
. /etc/bashrc
fi # User specific aliases and functions
cdls() {
cd "${1}";
ls;
} alias cd='cdls'
其实就是一个 shell function,里面组合调用了 cd 与 ls 命令,达到切换到新目录同时列出目录内容的功能。看到这里,类比着去实现一个 gpull / gpush 应该不难了吧:
git_pull() {
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 } }'
} git_push() {
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 } }'
} alias gpull='git_pull'
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用法