阿里云服务器ECS    
弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新 [咨询更多]
阿里云存储OSS
简单易用、多重冗余、数据备份高可靠、多层次安全防护安全性更强、低成本 [咨询更多]
阿里云数据库RDS
稳定可靠、可弹性伸缩、更拥有容灾、备份、恢复、监控、迁移等方面的全套解决方案 [咨询更多]
阿里云安全产品
DDoS高防IP、web应用防火墙、安骑士、sll证书、态势感知众多阿里云安全产品热销中 [咨询更多]
阿里云折扣优惠    
云服务器ECS、数据库、负载均衡等产品新购、续费、升级联系客服获取更多专属折扣 [咨询更多]
使用 expect 重启失败的 git pull/push 操作
2020-7-28    点击量:
  使用 expect 重启失败的 git pull/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 "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 脚本
  
  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 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 等命令,它们都是在内部开一个伪终端对,来实现对终端输入/输出的重定向能力的。
联系客服免费领取更多阿里云产品新购、续费升级折扣,叠加官网活动折上折更优惠