zoukankan      html  css  js  c++  java
  • 使用 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
    "; exp_continue}
     8 "password:" { send "$passwd
    " }
     9 }
    10 
    11 expect "]# "
    12 send "touch a.txt
    " 
    13 send "exit
    "
    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 等命令,它们都是在内部开一个伪终端对,来实现对终端输入/输出的重定向能力的。与终端相关的内容,可以参考我之前写的这篇文章:[apue] 书中关于伪终端的一个纰漏

    参考

    [1]. Linux-expect命令详解

    [2]. expect用法

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

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

  • 相关阅读:
    20210108 android学习
    20210106 android学习
    20210105 android学习
    20210104 android学习
    httpClient请求转发个人理解
    CefSharp如何判断页面是否加载完
    C#/WPF 使用的Task线程程序缺依旧响应缓慢问题
    修改LiveChart的提示显示位置
    Wpf双击放大缩小界面
    WPF设置无边框CefSharp显示不出来问题
  • 原文地址:https://www.cnblogs.com/goodcitizen/p/using_expect_restart_git_pull_or_push.html
Copyright © 2011-2022 走看看