Skip to content

Latest commit

 

History

History
160 lines (110 loc) · 5.93 KB

retry-demo-aso.md

File metadata and controls

160 lines (110 loc) · 5.93 KB

在 SHell 上做命令执行失败后重试

demo

  1. 只是要提示的话

    curl xxxx && echo :ok xxx || echo :err xxx

    (上面的冒号 : 没有特别作用只是字符串而已)

    如果想这个提示进入标准错误,对应片段像这样就行:

    echo xxx >&2
  2. 如果想要它自动重做的话(由于要用 declare 所以 sh 就不行了这里是 bash 代码)

    # def your work
    works ()
    {
        sleep 1 ; cd xxx ;
    } ;
    
    # redo if fail
    retry ()
    {
        works_def_name="$1" tried="${2:-0}" &&
        
        "$works_def_name" &&
        { echo :ok "$works_def_name" ... "$tried" ; } ||
        { echo :err "$works_def_name" ... "$tried" ;
          exec bash -c "$(declare -f "$works_def_name") ; $(declare -f retry) ; retry '$works_def_name' $((tried+1))" ; } ;
    } ;

    使用:

    (retry works)

    上面的代码会不停地在等待后尝试 cd xxx 这个命令,如果没这个目录就会失败,输出的信息里会带有已经尝试了几次的计数。

    也可以用这样一个更简单的例子理解:

    cd_retring ()
    {
        n="${1:-0}" &&
        cd "$n" &&
        { echo :succ cd ... "${1:-0}" ; } ||
        { echo :fail cd ... "${1:-0}" ; exec bash -c "$(declare -f cd_retring) ; cd_retring '$((n+1))'" ; } ;
    } ;

    使用:

    (cd_retring) #or (cd_retring 114514) will let num begin with 114514

    原理就是把定义交给 exec bash -c 从而在子进程里也保持定义。这个方法也可以用于远程执行本地定义的函数。

  3. 如果不是用函数而是把 works 里的代码写进脚本文件,对应的 retry 里就不需写 exec bash -c "$(declare -f works)" 了,只需要 exec bash works.sh 就好,而且由于不用 declare 了所以应该也能用 sh 了,但这就导致要对文件系统内的内容产生影响。

    关于 exec

    如果不是 exec bash 而是 bash 的话,就可能会让函数调用的类似于【压栈】的操作只增不减,从而在某个时候溢出,现象就是卡死然后报个错,然后结束。不过,比较新版本的 bash 不会卡死或者结束,而是在【压栈】够多后给个警告。

    exec 的作用就是,哪怕 exec bash 这句后面还有语句,也会被忽略掉,也就是强行丢弃外一层的函数中的一切,用新启动的进程把外层原本要做的事全覆盖掉。

这个玩法是我自己因为不想写循环的嵌套然后试出来的。后来又了解了 lambda 演算还有 Y 组合子什么的,不知道有没有联系……这里面相当于有递归了,函数在错误时用新的参数(新的计数)调用了自身。而 exec 相当于在 sh 上强制造成一种函数式的尾递归的效果。

code

上面的代码都加了必要的 ; 还有别的各种符号。

我归纳了一些符号,管它们叫命令的 结尾符

  • ; :表示 如果上一条执行完就执行下一条
  • & :表示 上一条作为后台进程执行(它仍然是当前 SHell 进程的子进程)
  • && :表示 如果前一句执行成功则执行后一句否则其后一句不执行并且其后一句直接算执行失败
  • || :表示 如果前一句执行失败则执行后一句否则其后一句不执行并且其后一句直接算执行成功
  • | :表示 前一句的向前一句的标准输出中写入的信息*写入到后一句的标准输入里*

其中,前两个 ; & 我又叫它 条尾符 ,而 一条 命令就是 在后面打回车的话就会让它被执行(即导致创建进程) 和那种命令。

后面三个,我叫它们 句尾符 ,它们后面紧挨着的无数个回车或其它空白符都等价于单个空格

还有括号对(这只是一部分):

  • { }
  • ( )
  • ' '
  • " "
  • do done
  • case esac

它们各有自己的功能,但共同的效果就是:

  • 它们的内部,可以写入 命令;对外,则整体地等同于 命令。

另外切记 SHell 上一切都是字符串(基本上吧)。比如:

echo foo\ \ foo
echo foo'  'foo
echo 'foo  foo'
'e'"c"\ho 'foo  foo'

这里四行是完全等价的。

在 SHell 上, 裸词 本身就是字面量,或者不是字面量,但如果不是的话也可以转义成字面量,而引号和反斜线都是转义的手段,其中双引号支持字符串插值写法仅此而已。

——当然了由于空白符在 SHell 上一般是特殊符号,所以如果想让一个通过引用(不管是变量还是子进程标准输出还是别的啥)得到的字符串保持原模原样的话,就只能用引号包裹才行(下列情况为啥用双引号或单引号自己测试与思考):

str="${something:-default vals xxx}"
str="$(somecmd someargs)"
str="$(sh -c "echo '${something:-default vals xxx}'")"
str="$(sh -c "echo '$(somecmd someargs)'")"

(提示:第三行第四行是引用了当前环境而不是子进程环境的变量。)

或者试试下面得到的每个变量,再试试如果去掉一些引号的话会有啥效果。(就是说:通过拿走必要的东西会帮你明白它为什么必要——所以尽管拆吧!

str0="${HAHAHAHA:-$(cat /etc/hosts)}"
str1="$str0"
str2="$(sh -c "echo '$str0'")"
str3="$(sh -c "echo '$(cat /etc/hosts)'")"

(当然你如果有更好的例子则用它来尝试更好🙃)

使用时记得要这样:

echo "$str0"
echo "$str1"
echo "$str2"
echo "$str3"

(也可以试试这里去掉双引号)

思考题:

如果上面被 cat 的不是 /etc/hosts 而是 /etc/profile ,就是说:它的内容里有单引号存在,
那,那四个变量的赋值又分别会是什么效果呢?🦥