使用 expect 重启失败的 git pull/push 操作

  • A+
所属分类:linux技术
摘要

最近使用 github 上传、下载项目代码时,经常会卡很久,有时候在命令行打了 git push 然后就去上厕所了,结果等我回来的时候,发现 push 早已经失败了,还得重新提交一下。如果有一个工具,可以不停的重启失败的 git push 直到它成功才退出,那就好了。


问题的提出

最近使用 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 "yesr"; exp_continue}  8 "password:" { send "$passwdr" }  9 } 10  11 expect "]# " 12 send "touch a.txtr"  13 send "exitr" 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 [email protected]:goodpaperman/apue.git    87748c7..08e3a1e  master -> master

 

已经是最新时:

$ git push Everything up-to-date

 

于是很自然的想到了一个解决方案:一直 spawn git pull / push 直到 expect 到我们想要的输出 "xxx up-to-date."

重启失败的操作

利用上面的思路,写出了下面的 expect 脚本

pull.exp

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 次,如果仍然拉取不成功,则可能是其它原因导致的,此时退出循环。

push.exp

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 [email protected]: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用法

[3]. expect语法基础: while、for 循环、if 语句的用法示例

[4]. expect(spawn) 自动化git提交和scp拷贝---centos(linux)