【问题标题】:Is there a TRY CATCH command in BashBash 中是否有 TRY CATCH 命令
【发布时间】:2014-03-27 09:22:44
【问题描述】:

我正在编写一个 shell 脚本,需要检查是否已安装终端应用程序。除非有更简洁的方法,否则我想使用 TRY/CATCH 命令来执行此操作。

【问题讨论】:

  • 如果您能详细说明您要解决的问题可能会有所帮助。看来您在这里并不完全是新人,但您可能仍想访问Help Center 并查看有关如何提出好问题的帮助。
  • 也就是说,help test 似乎可以帮助您找到问题的解决方案。
  • try/catch/finally 块不是一个命令,它是一个构造
  • @LeeProbert :由于您在 bash 中没有异常,我想知道您想要捕获什么。最接近异常方向的东西是信号,其中大多数(不是全部)可以使用trap 命令捕获。

标签: bash shell error-handling


【解决方案1】:

Bash 中有 TRY CATCH 命令吗?

没有。

Bash 没有许多编程语言所能提供的那么多奢侈品。

bash 中没有try/catch;但是,使用&&|| 可以实现类似的行为。

使用||

如果command1 失败,则command2 运行如下

command1 || command2

同样,使用&&,如果command1 成功,command2 将运行

try/catch 的最接近近似如下

{ # try

    command1 &&
    #save your output

} || { # catch
    # save log for exception 
}

bash 也包含一些错误处理机制,以及

set -e

如果任何简单的命令失败,它会停止你的脚本。

还有为什么不if...else。它是你最好的朋友。

【讨论】:

  • 这样,您需要注意#save your output 的代码不会失败,否则“catch”块仍将执行。
  • 建议使用if...else 构造。这是否意味着 bash 命令如果运行成功则解析为“真”,如果失败则解析为“假”?
  • 对于这个线程的读者:似乎set -e 不一定是最好的做事方式;以下是一些反论点/特殊情况:mywiki.wooledge.org/BashFAQ/105
  • 我可以知道如何记录异常吗?通常在java代码中,我们可以使用system.out.log(e),但是在shell中呢?
  • 我将脚本粘贴在您的答案上,但我保留了您所写的换行符,当我这样做时,我得到了 syntax error near unexpected token } 错误
【解决方案2】:

bash 不会在检测到错误状态时中止正在运行的执行(除非您设置了-e 标志)。提供try/catch 的编程语言这样做是为了禁止由于这种特殊情况而“纾困”(因此通常称为“异常”)。

bash 中,只有相关命令会以大于 0 的退出代码退出,表示该错误状态。当然,您可以检查一下,但由于没有自动拯救任何东西,所以 try/catch 没有意义。只是缺少这种背景。

但是,您可以通过使用可以在您决定的点终止的子 shell 来模拟 救助

(
  echo "Do one thing"
  echo "Do another thing"
  if some_condition
  then
    exit 3  # <-- this is our simulated bailing out
  fi
  echo "Do yet another thing"
  echo "And do a last thing"
)   # <-- here we arrive after the simulated bailing out, and $? will be 3 (exit code)
if [ $? = 3 ]
then
  echo "Bail out detected"
fi

除了some_conditionif,您还可以尝试一个命令,如果它失败(退出代码大于0),请退出:

(
  echo "Do one thing"
  echo "Do another thing"
  some_command || exit 3
  echo "Do yet another thing"
  echo "And do a last thing"
)
...

不幸的是,使用这种技术,您只能使用 255 个不同的退出代码 (1..255),并且不能使用任何体面的异常对象。

如果您需要更多信息与模拟异常一起传递,您可以使用子外壳的标准输出,但这有点复杂,可能是另一个问题;-)

在 shell 中使用上面提到的 -e 标志,您甚至可以去掉显式的 exit 语句:

(
  set -e
  echo "Do one thing"
  echo "Do another thing"
  some_command
  echo "Do yet another thing"
  echo "And do a last thing"
)
...

【讨论】:

  • 这确实应该是公认的答案,因为它是最接近 shell 的 try/catch 逻辑。
  • 那不是真的。肯定有一些事情会导致 bash 立即退出,我敢打赌,这就是为什么这个问题的一半读者都在这里,因为他们正在寻找 try catch 的“尝试”部分,即;不要致命,而是自己处理错误,然后继续。例如a=$((0/0))||echo bad 不会回显。但是......你同样的 subshel​​l 解决方案确实可以解决这个问题:(a=$((0/0)))||echo bad 确实做了回声。
  • 我不确定我们在这里谈论的是相同的事情。点赞 a=$((0/0)) 不会使 shell 退出;它只是结束了对这一行的解析(这是一种奇怪的行为,我同意)。它确实将退出值设置为 1。如果将这两个命令分布在两行上,它会按预期工作:a=$((0/0)) 下一行:if [ $? = 1 ]; then ...
【解决方案3】:

你有陷阱http://www.tldp.org/LDP/Bash-Beginners-Guide/html/sect_12_02.html 不一样,但是你可以使用其他技术来达到这个目的

【讨论】:

  • 信号实际上只通过一个非常细的线程与异常和 try/catch 的概念相关,因为它们不是程序正常控制流的一部分。不过在这里提一下也没关系。
【解决方案4】:

根据我在这里找到的一些答案,我为自己制作了一个小帮助文件,用于为我的项目提供资源:

trycatch.sh

#!/bin/bash

function try()
{
    [[ $- = *e* ]]; SAVED_OPT_E=$?
    set +e
}

function throw()
{
    exit $1
}

function catch()
{
    export ex_code=$?
    (( $SAVED_OPT_E )) && set +e
    return $ex_code
}

function throwErrors()
{
    set -e
}

function ignoreErrors()
{
    set +e
}

这是一个使用示例:

#!/bin/bash
export AnException=100
export AnotherException=101

# start with a try
try
(   # open a subshell !!!
    echo "do something"
    [ someErrorCondition ] && throw $AnException

    echo "do something more"
    executeCommandThatMightFail || throw $AnotherException

    throwErrors # automaticatly end the try block, if command-result is non-null
    echo "now on to something completely different"
    executeCommandThatMightFail

    echo "it's a wonder we came so far"
    executeCommandThatFailsForSure || true # ignore a single failing command

    ignoreErrors # ignore failures of commands until further notice
    executeCommand1ThatFailsForSure
    local result = $(executeCommand2ThatFailsForSure)
    [ result != "expected error" ] && throw $AnException # ok, if it's not an expected error, we want to bail out!
    executeCommand3ThatFailsForSure

    # make sure to clear $ex_code, otherwise catch * will run
    # echo "finished" does the trick for this example
    echo "finished"
)
# directly after closing the subshell you need to connect a group to the catch using ||
catch || {
    # now you can handle
    case $ex_code in
        $AnException)
            echo "AnException was thrown"
        ;;
        $AnotherException)
            echo "AnotherException was thrown"
        ;;
        *)
            echo "An unexpected exception was thrown"
            throw $ex_code # you can rethrow the "exception" causing the script to exit if not caught
        ;;
    esac
}

【讨论】:

  • 你能告诉你如何将 try catch 函数导入到另一个例子中吗? (我假设它们在单独的文件中)
  • @kilianc:我只是像这样获取它:source inc/trycatch.sh。
  • @MathiasHenze 谢谢伙计,你的代码太酷了。但是为什么在 catch 之后和 {} 块之前需要一个 || 呢?我本来以为是&amp;&amp;
  • (发现此问题的任何人的最新答案)本质上,错误案例是if False or run_if_failed(),这意味着短路 OR 尝试了第一个没有返回 true 的语句,现在正在继续下一个语句. &amp;&amp; 将不起作用,因为第一个语句 (try) 产生了错误,这意味着重言式规则 false&amp;any equals false 不需要 catch 语句。只有非短路 AND/OR 才能同时执行。
【解决方案5】:

正如大家所说,bash 没有适当的语言支持的 try/catch 语法。您可以使用 -e 参数启动 bash 或在脚本中使用 set -e 如果任何命令具有非零退出代码,则中止整个 bash 进程。 (您也可以set +e 暂时允许失败的命令。)

因此,模拟 try/catch 块的一种技术是启动子进程以在启用 -e 的情况下完成工作。然后在主进程中,查看子进程的返回码。

Bash 支持 heredoc 字符串,因此您不必编写两个单独的文件来处理它。在下面的示例中,TRY heredoc 将在单独的 bash 实例中运行,并启用 -e,因此如果任何命令返回非零退出代码,子进程将崩溃。然后,回到主进程,我们可以检查返回码来处理一个 catch 块。

#!/bin/bash

set +e
bash -e <<TRY
  echo hello
  cd /does/not/exist
  echo world
TRY
if [ $? -ne 0 ]; then
  echo caught exception
fi

这不是一个适当的语言支持的 try/catch 块,但它可能会为您带来类似的痒。

【讨论】:

    【解决方案6】:

    我在 bash 中开发了一个几乎完美的 try & catch 实现,它允许您编写如下代码:

    try 
        echo 'Hello'
        false
        echo 'This will not be displayed'
    
    catch 
        echo "Error in $__EXCEPTION_SOURCE__ at line: $__EXCEPTION_LINE__!"
    

    您甚至可以将 try-catch 块嵌套在自身内部!

    try {
        echo 'Hello'
    
        try {
            echo 'Nested Hello'
            false
            echo 'This will not execute'
        } catch {
            echo "Nested Caught (@ $__EXCEPTION_LINE__)"
        }
    
        false
        echo 'This will not execute too'
    
    } catch {
        echo "Error in $__EXCEPTION_SOURCE__ at line: $__EXCEPTION_LINE__!"
    }
    

    代码是我bash boilerplate/framework 的一部分。它进一步扩展了 try & catch 的想法,例如使用回溯和异常进行错误处理(以及其他一些不错的功能)。

    这是只负责 try & catch 的代码:

    set -o pipefail
    shopt -s expand_aliases
    declare -ig __oo__insideTryCatch=0
    
    # if try-catch is nested, then set +e before so the parent handler doesn't catch us
    alias try="[[ \$__oo__insideTryCatch -gt 0 ]] && set +e;
               __oo__insideTryCatch+=1; ( set -e;
               trap \"Exception.Capture \${LINENO}; \" ERR;"
    alias catch=" ); Exception.Extract \$? || "
    
    Exception.Capture() {
        local script="${BASH_SOURCE[1]#./}"
    
        if [[ ! -f /tmp/stored_exception_source ]]; then
            echo "$script" > /tmp/stored_exception_source
        fi
        if [[ ! -f /tmp/stored_exception_line ]]; then
            echo "$1" > /tmp/stored_exception_line
        fi
        return 0
    }
    
    Exception.Extract() {
        if [[ $__oo__insideTryCatch -gt 1 ]]
        then
            set -e
        fi
    
        __oo__insideTryCatch+=-1
    
        __EXCEPTION_CATCH__=( $(Exception.GetLastException) )
    
        local retVal=$1
        if [[ $retVal -gt 0 ]]
        then
            # BACKWARDS COMPATIBILE WAY:
            # export __EXCEPTION_SOURCE__="${__EXCEPTION_CATCH__[(${#__EXCEPTION_CATCH__[@]}-1)]}"
            # export __EXCEPTION_LINE__="${__EXCEPTION_CATCH__[(${#__EXCEPTION_CATCH__[@]}-2)]}"
            export __EXCEPTION_SOURCE__="${__EXCEPTION_CATCH__[-1]}"
            export __EXCEPTION_LINE__="${__EXCEPTION_CATCH__[-2]}"
            export __EXCEPTION__="${__EXCEPTION_CATCH__[@]:0:(${#__EXCEPTION_CATCH__[@]} - 2)}"
            return 1 # so that we may continue with a "catch"
        fi
    }
    
    Exception.GetLastException() {
        if [[ -f /tmp/stored_exception ]] && [[ -f /tmp/stored_exception_line ]] && [[ -f /tmp/stored_exception_source ]]
        then
            cat /tmp/stored_exception
            cat /tmp/stored_exception_line
            cat /tmp/stored_exception_source
        else
            echo -e " \n${BASH_LINENO[1]}\n${BASH_SOURCE[2]#./}"
        fi
    
        rm -f /tmp/stored_exception /tmp/stored_exception_line /tmp/stored_exception_source
        return 0
    }
    

    请随意使用、分叉和贡献 - 它位于 GitHub

    【讨论】:

    • @erm3nda 很高兴听到这个消息!我想在我发布这个之后我已经解决了一些错误,所以请查看 GitHub 以获取更新(您需要包括 03_exception.sh 和 04_try_catch.sh)。据我所知,当前版本几乎是防弹的。
    • 非常好!我将在我的项目中使用。我在 5 分钟内开始工作,我的 centos 已经使用 bash 4.2.46
    • 这里有个根本的问题:如果你在try块里改变了一个变量,它在外面是看不到的,因为它是在一个子shell中运行的。
    • @KanLi 正确。如果您关心 try/catch 的输出,您可以像这样捕获它:my_output=$(try { code...; } catch { code...; })
    • 在最新版本中,EXCEPTION_LINE 好像改名为 BACKTRACE_LINE github.com/niieani/bash-oo-framework#using-try--catch
    【解决方案7】:

    你可以使用trap:

    try { block A } catch { block B } finally { block C }

    翻译为:

    (
      set -Ee
      function _catch {
        block B
        exit 0  # optional; use if you don't want to propagate (rethrow) error to outer shell
      }
      function _finally {
        block C
      }
      trap _catch ERR
      trap _finally EXIT
      block A
    )
    

    【讨论】:

    • 我认为您还需要-E 标志,因此陷阱会传播到函数
    【解决方案8】:

    有很多类似的解决方案可能有效。下面是实现 try/catch 的一种简单有效的方法,在 cmets 中有说明。

    #!/bin/bash
    
    function a() {
      # do some stuff here
    }
    function b() {
      # do more stuff here
    }
    
    # this subshell is a scope of try
    # try
    (
      # this flag will make to exit from current subshell on any error
      # inside it (all functions run inside will also break on any error)
      set -e
      a
      b
      # do more stuff here
    )
    # and here we catch errors
    # catch
    errorCode=$?
    if [ $errorCode -ne 0 ]; then
      echo "We have an error"
      # We exit the all script with the same error, if you don't want to
      # exit it and continue, just delete this line.
      exit $errorCode
    fi
    

    【讨论】:

    • 在此线程中提供最佳解决方案。简单但有效!可以很容易地集成到现有脚本中。谢谢分享!!
    【解决方案9】:

    我用的一个很简单的东西:

    try() {
        "$@" || (e=$?; echo "$@" > /dev/stderr; exit $e)
    }
    

    【讨论】:

    • 由于|| 的右侧在() 中,因此它将在子shell 中运行并退出,而不会导致主shell 退出。请改用{ } 分组。
    【解决方案10】:

    你可以这样做:

    #!/bin/bash
    if <command> ; then # TRY
        <do-whatever-you-want>
    else # CATCH
        echo 'Exception'
        <do-whatever-you-want>
    fi
    

    【讨论】:

      【解决方案11】:

      以下是在 bash 中实现 try/catch/finally 的脚本示例。

      与此问题的其他答案一样,必须在退出子进程后捕获异常。

      示例脚本首先创建一个匿名fifo,用于将字符串消息从command exceptionthrow 传递到最近的try 块的末尾。在这里,消息从 fifo 中删除并放置在数组变量中。状态通过returnexit 命令返回并放置在不同的变量中。要输入catch 块,此状态不得为零。输入catch 块的其他要求作为参数传递。如果到达catch 块的末尾,则将状态设置为零。如果到达finally 块的末尾并且状态仍然非零,则执行包含消息和状态的隐式抛出。该脚本需要调用函数trycatchfinally,其中包含一个未处理的异常处理程序。

      trycatchfinally 命令的语法如下。

      trycatchfinally [-cde] [-h ERR_handler] [-k] [-o debug_file] [-u unhandled_handler] [-v variable] fifo function
      

      -c 选项将调用堆栈添加到异常消息中。
      -d 选项启用调试输出。
      -e 选项启用命令异常。
      -h 选项允许用户替换他们自己的命令异常处理程序。
      -k 选项将调用堆栈添加到调试输出中。
      -o 选项替换默认输出文件 /dev/fd/2
      -u 选项允许用户替换他们自己的未处理异常处理程序。
      -v 选项允许用户选择通过使用命令替换来传回值。
      fifo 是 fifo 文件名。
      函数functiontrycatchfinally 作为子进程调用。

      注意:cdko 选项已被删除以简化脚本。

      catch 命令的语法如下。

      catch [[-enoprt] list ...] ...
      

      选项定义如下。第一个列表的值是状态。后续值是消息。如果消息多于 列表,则忽略其余消息。

      -e 表示[[ $value == "$string" ]](该值必须与列表中的至少一个字符串匹配)
      -n 表示[[ $value != "$string" ]](该值不能与列表中的任何字符串匹配)
      -o 表示[[ $value != $pattern ]](该值不能与列表中的任何模式匹配)
      -p 表示[[ $value == $pattern ]](该值必须与列表中的至少一个模式匹配)
      @ 987654359@ 表示[[ $value =~ $regex ]](该值必须至少匹配列表中的一个扩展正则表达式)
      -t 表示[[ ! $value =~ $regex ]](该值不能匹配列表中任何一个扩展正则表达式)

      try/catch/finally 脚本如下所示。为了简化此答案的脚本,删除了大部分错误检查。这将大小减少了 64%。可以在我的other answer 找到此脚本的完整副本。

      shopt -s expand_aliases
      alias try='{ common.Try'
      alias yrt='EchoExitStatus; common.yrT; }'
      alias catch='{ while common.Catch'
      alias hctac='common.hctaC; done; }'
      alias finally='{ common.Finally'
      alias yllanif='common.yllaniF; }'
      
      DefaultErrHandler() {
          echo "Orginal Status: $common_status"
          echo "Exception Type: ERR"
      }
      
      exception() {
          let "common_status = 10#$1"
          shift
          common_messages=()
          for message in "$@"; do
              common_messages+=("$message")
          done
      }
      
      throw() {
          local "message"
          if [[ $# -gt 0 ]]; then
              let "common_status = 10#$1"
              shift
              for message in "$@"; do
                  echo "$message" >"$common_fifo"
              done
          elif [[ ${#common_messages[@]} -gt 0 ]]; then
              for message in "${common_messages[@]}"; do
                  echo "$message" >"$common_fifo"
              done
          fi
          chmod "0400" "$common_fifo"
          exit "$common_status"
      }
      
      common.ErrHandler() {
          common_status=$?
          trap ERR
          if [[ -w "$common_fifo" ]]; then
              if [[ $common_options != *e* ]]; then
                  common_status="0"
                  return
              fi
              eval "${common_errHandler:-} \"${BASH_LINENO[0]}\" \"${BASH_SOURCE[1]}\" \"${FUNCNAME[1]}\" >$common_fifo <$common_fifo"
              chmod "0400" "$common_fifo"
          fi
          if [[ common_trySubshell -eq BASH_SUBSHELL ]]; then
              return   
          else
              exit "$common_status"
          fi
      }
      
      common.Try() {
          common_status="0"
          common_subshell="$common_trySubshell"
          common_trySubshell="$BASH_SUBSHELL"
          common_messages=()
      }
      
      common.yrT() {
          local "status=$?"
          if [[ common_status -ne 0 ]]; then    
              local "message=" "eof=TRY_CATCH_FINALLY_END_OF_MESSAGES_$RANDOM"
              chmod "0600" "$common_fifo"
              echo "$eof" >"$common_fifo"
              common_messages=()
              while read "message"; do
                  [[ $message != *$eof ]] || break
                  common_messages+=("$message")
              done <"$common_fifo"
          fi
          common_trySubshell="$common_subshell"
      }
      
      common.Catch() {
          [[ common_status -ne 0 ]] || return "1"
          local "parameter" "pattern" "value"
          local "toggle=true" "compare=p" "options=$-"
          local -i "i=-1" "status=0"
          set -f
          for parameter in "$@"; do
              if "$toggle"; then
                  toggle="false"
                  if [[ $parameter =~ ^-[notepr]$ ]]; then
                      compare="${parameter#-}"
                      continue 
                  fi
              fi
              toggle="true"
              while "true"; do
                  eval local "patterns=($parameter)"
                  if [[ ${#patterns[@]} -gt 0 ]]; then
                      for pattern in "${patterns[@]}"; do
                          [[ i -lt ${#common_messages[@]} ]] || break
                          if [[ i -lt 0 ]]; then
                              value="$common_status"
                          else
                              value="${common_messages[i]}"
                          fi
                          case $compare in
                          [ne]) [[ ! $value == "$pattern" ]] || break 2;;
                          [op]) [[ ! $value == $pattern ]] || break 2;;
                          [tr]) [[ ! $value =~ $pattern ]] || break 2;;
                          esac
                      done
                  fi
                  if [[ $compare == [not] ]]; then
                      let "++i,1"
                      continue 2
                  else
                      status="1"
                      break 2
                  fi
              done
              if [[ $compare == [not] ]]; then
                  status="1"
                  break
              else
                  let "++i,1"
              fi
          done
          [[ $options == *f* ]] || set +f
          return "$status"
      } 
      
      common.hctaC() {
          common_status="0"
      }
      
      common.Finally() {
          :
      }
      
      common.yllaniF() {
          [[ common_status -eq 0 ]] || throw
      }
      
      caught() {
          [[ common_status -eq 0 ]] || return 1
      }
      
      EchoExitStatus() {
          return "${1:-$?}"
      }
      
      EnableThrowOnError() {
          [[ $common_options == *e* ]] || common_options+="e"
      }
      
      DisableThrowOnError() {
          common_options="${common_options/e}"
      }
      
      GetStatus() {
          echo "$common_status"
      }
      
      SetStatus() {
          let "common_status = 10#$1"
      }
      
      GetMessage() {
          echo "${common_messages[$1]}"
      }
      
      MessageCount() {
          echo "${#common_messages[@]}"
      }
      
      CopyMessages() {
          if [[ ${#common_messages} -gt 0 ]]; then
              eval "$1=(\"\${common_messages[@]}\")"
          else
              eval "$1=()"
          fi
      }
      
      common.GetOptions() {
          local "opt"
          let "OPTIND = 1"
          let "OPTERR = 0"
          while getopts ":cdeh:ko:u:v:" opt "$@"; do
              case $opt in
              e)  [[ $common_options == *e* ]] || common_options+="e";;
              h)  common_errHandler="$OPTARG";;
              u)  common_unhandled="$OPTARG";;
              v)  common_command="$OPTARG";;
              esac
          done
          shift "$((OPTIND - 1))"
          common_fifo="$1"
          shift
          common_function="$1"
          chmod "0600" "$common_fifo"
      }
      
      DefaultUnhandled() {
          local -i "i"
          echo "-------------------------------------------------"
          echo "TryCatchFinally: Unhandeled exception occurred"
          echo "Status: $(GetStatus)"
          echo "Messages:"
          for ((i=0; i<$(MessageCount); i++)); do
              echo "$(GetMessage "$i")"
          done
          echo "-------------------------------------------------"
      }
      
      TryCatchFinally() {
          local "common_errHandler=DefaultErrHandler"
          local "common_unhandled=DefaultUnhandled"
          local "common_options="
          local "common_fifo="
          local "common_function="
          local "common_flags=$-"
          local "common_trySubshell=-1"
          local "common_subshell"
          local "common_status=0"
          local "common_command="
          local "common_messages=()"
          local "common_handler=$(trap -p ERR)"
          [[ -n $common_handler ]] || common_handler="trap ERR"
          common.GetOptions "$@"
          shift "$((OPTIND + 1))"
          [[ -z $common_command ]] || common_command+="=$"
          common_command+='("$common_function" "$@")'
          set -E
          set +e
          trap "common.ErrHandler" ERR
          try
              eval "$common_command"
          yrt
          catch; do
              "$common_unhandled" >&2
          hctac
          [[ $common_flags == *E* ]] || set +E
          [[ $common_flags != *e* ]] || set -e
          [[ $common_flags != *f* || $- == *f* ]] || set -f
          [[ $common_flags == *f* || $- != *f* ]] || set +f
          eval "$common_handler"
      }
      

      以下是一个示例,假设上述脚本存储在名为simple 的文件中。 makefifo 文件包含this answer 中描述的脚本。假设名为4444kkkkk 的文件不存在,因此导致发生异常。 ls 4444kkkkk 命令的错误消息输出会被自动抑制,直到出现在适当的 catch 块内。

      #!/bin/bash
      #
      
      if [[ $0 != ${BASH_SOURCE[0]} ]]; then
          bash "${BASH_SOURCE[0]}" "$@"
          return
      fi
      
      source simple
      source makefifo
      
      MyFunction3() {
          echo "entered MyFunction3" >&4
          echo "This is from MyFunction3"
          ls 4444kkkkk
          echo "leaving MyFunction3" >&4
      }
      
      MyFunction2() {
          echo "entered MyFunction2" >&4
          value="$(MyFunction3)"
          echo "leaving MyFunction2" >&4
      }
      
      MyFunction1() {
          echo "entered MyFunction1" >&4
          local "flag=false"
          try 
          (
              echo "start of try" >&4
              MyFunction2
              echo "end of try" >&4
          )
          yrt
          catch "[1-3]" "*" "Exception\ Type:\ ERR"; do
              echo 'start of catch "[1-3]" "*" "Exception\ Type:\ ERR"'
              local -i "i"
              echo "-------------------------------------------------"
              echo "Status: $(GetStatus)"
              echo "Messages:"
              for ((i=0; i<$(MessageCount); i++)); do
                  echo "$(GetMessage "$i")"
              done
              echo "-------------------------------------------------"
              break
              echo 'end of catch "[1-3]" "*" "Exception\ Type:\ ERR"'
          hctac >&4
          catch "1 3 5" "*" -n "Exception\ Type:\ ERR"; do
              echo 'start of catch "1 3 5" "*" -n "Exception\ Type:\ ERR"'
              echo "-------------------------------------------------"
              echo "Status: $(GetStatus)"
              [[ $(MessageCount) -le 1 ]] || echo "$(GetMessage "1")"
              echo "-------------------------------------------------"
              break
              echo 'end of catch "1 3 5" "*" -n "Exception\ Type:\ ERR"'
          hctac >&4
          catch; do
              echo 'start of catch' >&4
              echo "failure"
              flag="true"
              echo 'end of catch' >&4
          hctac
          finally
              echo "in finally"
          yllanif >&4
          "$flag" || echo "success"
          echo "leaving MyFunction1" >&4
      } 2>&6
      
      ErrHandler() {
          echo "EOF"
          DefaultErrHandler "$@"
          echo "Function: $3"
          while read; do
              [[ $REPLY != *EOF ]] || break
              echo "$REPLY"
          done
      }
      
      set -u
      echo "starting" >&2
      MakeFIFO "6"
      TryCatchFinally -e -h ErrHandler -o /dev/fd/4 -v result /dev/fd/6 MyFunction1 4>&2
      echo "result=$result"
      exec >&6-
      

      上面的脚本是使用GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17) 测试的。运行此脚本的输出如下所示。

      starting
      entered MyFunction1
      start of try
      entered MyFunction2
      entered MyFunction3
      start of catch "[1-3]" "*" "Exception\ Type:\ ERR"
      -------------------------------------------------
      Status: 1
      Messages:
      Orginal Status: 1
      Exception Type: ERR
      Function: MyFunction3
      ls: 4444kkkkk: No such file or directory
      -------------------------------------------------
      start of catch
      end of catch
      in finally
      leaving MyFunction1
      result=failure
      

      另一个使用throw 的示例可以通过将函数MyFunction3 替换为如下所示的脚本来创建。

      MyFunction3() {
          echo "entered MyFunction3" >&4
          echo "This is from MyFunction3"
          throw "3" "Orginal Status: 3" "Exception Type: throw"
          echo "leaving MyFunction3" >&4
      }
      

      throw 命令的语法如下所示。如果不存在参数,则使用存储在变量中的状态和消息。

      throw [status] [message ...]
      

      执行修改后的脚本的输出如下所示。

      starting
      entered MyFunction1
      start of try
      entered MyFunction2
      entered MyFunction3
      start of catch "1 3 5" "*" -n "Exception\ Type:\ ERR"
      -------------------------------------------------
      Status: 3
      Exception Type: throw
      -------------------------------------------------
      start of catch
      end of catch
      in finally
      leaving MyFunction1
      result=failure
      

      【讨论】:

        【解决方案12】:

        以下是我的other answer 中使用的简化脚本的完整副本。除了额外的错误检查之外,还有一个别名允许用户更改现有别名的名称。语法如下。如果new_alias 参数被省略,则别名被删除。

        ChangeAlias old_alias [new_alias]
        

        完整的脚本如下。

        common.GetAlias() {
            local "oldname=${1:-0}"
            if [[ $oldname =~ ^[0-9]+$ && oldname+1 -lt ${#FUNCNAME[@]} ]]; then
                oldname="${FUNCNAME[oldname + 1]}"
            fi
            name="common_${oldname#common.}"
            echo "${!name:-$oldname}"
        }
        
        common.Alias() {
            if [[ $# -ne 2 || -z $1 || -z $2 ]]; then
                echo "$(common.GetAlias): The must be only two parameters of nonzero length" >&2
                return 1;
            fi
            eval "alias $1='$2'"
            local "f=${2##*common.}"
            f="${f%%;*}"
            local "v=common_$f"
            f="common.$f"
            if [[ -n ${!v:-} ]]; then
                echo "$(common.GetAlias): $1: Function \`$f' already paired with name \`${!v}'" >&2
                return 1;
            fi
            shopt -s expand_aliases
            eval "$v=\"$1\""
        }
        
        common.ChangeAlias() {
            if [[ $# -lt 1 || $# -gt 2 ]]; then
                echo "usage: $(common.GetAlias) old_name [new_name]" >&2
                return "1"
            elif ! alias "$1" &>"/dev/null"; then
                echo "$(common.GetAlias): $1: Name not found" >&2
                return 1;
            fi
            local "s=$(alias "$1")" 
            s="${s#alias $1=\'}"
            s="${s%\'}"
            local "f=${s##*common.}"
            f="${f%%;*}"
            local "v=common_$f"
            f="common.$f"
            if [[ ${!v:-} != "$1" ]]; then
                echo "$(common.GetAlias): $1: Name not paired with a function \`$f'" >&2
                return 1;
            elif [[ $# -gt 1 ]]; then
                eval "alias $2='$s'"
                eval "$v=\"$2\""
            else
                unset "$v"
            fi
            unalias "$1"
        }
        
        common.Alias exception             'common.Exception'
        common.Alias throw                 'common.Throw'
        common.Alias try                   '{ if common.Try; then'
        common.Alias yrt                   'common.EchoExitStatus; fi; common.yrT; }'
        common.Alias catch                 '{ while common.Catch'
        common.Alias hctac                 'common.hctaC -r; done; common.hctaC; }'
        common.Alias finally               '{ if common.Finally; then'
        common.Alias yllanif               'fi; common.yllaniF; }'
        common.Alias caught                'common.Caught'
        common.Alias EchoExitStatus        'common.EchoExitStatus'
        common.Alias EnableThrowOnError    'common.EnableThrowOnError'
        common.Alias DisableThrowOnError   'common.DisableThrowOnError'
        common.Alias GetStatus             'common.GetStatus'
        common.Alias SetStatus             'common.SetStatus'
        common.Alias GetMessage            'common.GetMessage'
        common.Alias MessageCount          'common.MessageCount'
        common.Alias CopyMessages          'common.CopyMessages'
        common.Alias TryCatchFinally       'common.TryCatchFinally'
        common.Alias DefaultErrHandler     'common.DefaultErrHandler'
        common.Alias DefaultUnhandled      'common.DefaultUnhandled'
        common.Alias CallStack             'common.CallStack'
        common.Alias ChangeAlias           'common.ChangeAlias'
        common.Alias TryCatchFinallyAlias  'common.Alias'
        
        common.CallStack() {
            local -i "i" "j" "k" "subshell=${2:-0}" "wi" "wl" "wn"
            local "format= %*s  %*s  %-*s  %s\n" "name"
            eval local "lineno=('' ${BASH_LINENO[@]})"
            for (( i=${1:-0},j=wi=wl=wn=0; i<${#FUNCNAME[@]}; ++i,++j )); do  
                name="$(common.GetAlias "$i")"
                let "wi = ${#j} > wi ? wi = ${#j} : wi"
                let "wl = ${#lineno[i]} > wl ? wl = ${#lineno[i]} : wl"
                let "wn = ${#name} > wn ? wn = ${#name} : wn"
            done
            for (( i=${1:-0},j=0; i<${#FUNCNAME[@]}; ++i,++j )); do
                ! let "k = ${#FUNCNAME[@]} - i - 1"
                name="$(common.GetAlias "$i")"
                printf "$format" "$wi" "$j" "$wl" "${lineno[i]}" "$wn" "$name" "${BASH_SOURCE[i]}"
            done
        }
        
        common.Echo() {
            [[ $common_options != *d* ]] || echo "$@" >"$common_file"
        }
        
        common.DefaultErrHandler() {
            echo "Orginal Status: $common_status"
            echo "Exception Type: ERR"
        }
        
        common.Exception() {
            common.TryCatchFinallyVerify || return
            if [[ $# -eq 0 ]]; then
                echo "$(common.GetAlias): At least one parameter is required" >&2
                return "1"         
            elif [[ ${#1} -gt 16 || -n ${1%%[0-9]*} || 10#$1 -lt 1 || 10#$1 -gt 255 ]]; then
                echo "$(common.GetAlias): $1: First parameter was not an integer between 1 and 255" >&2
                return "1"
            fi
            let "common_status = 10#$1"
            shift
            common_messages=()
            for message in "$@"; do
                common_messages+=("$message")
            done
            if [[ $common_options == *c* ]]; then
                echo "Call Stack:" >"$common_fifo"
                common.CallStack "2" >"$common_fifo"
            fi
        }
        
        common.Throw() {
            common.TryCatchFinallyVerify || return
            local "message"
            if ! common.TryCatchFinallyExists; then
                echo "$(common.GetAlias): No Try-Catch-Finally exists" >&2
                return "1"        
            elif [[ $# -eq 0 && common_status -eq 0 ]]; then
                echo "$(common.GetAlias): No previous unhandled exception" >&2 
                return "1"
            elif [[ $# -gt 0 && ( ${#1} -gt 16 || -n ${1%%[0-9]*} || 10#$1 -lt 1 || 10#$1 -gt 255 ) ]]; then
                echo "$(common.GetAlias): $1: First parameter was not an integer between 1 and 255" >&2
                return "1"
            fi
            common.Echo -n "In Throw ?=$common_status "
            common.Echo "try=$common_trySubshell subshell=$BASH_SUBSHELL #=$#"
            if [[ $common_options == *k* ]]; then
                common.CallStack "2" >"$common_file"
            fi
            if [[ $# -gt 0 ]]; then
                let "common_status = 10#$1"
                shift
                for message in "$@"; do
                    echo "$message" >"$common_fifo"
                done
                if [[ $common_options == *c* ]]; then
                    echo "Call Stack:" >"$common_fifo"
                    common.CallStack "2" >"$common_fifo"
                fi
            elif [[ ${#common_messages[@]} -gt 0 ]]; then
                for message in "${common_messages[@]}"; do
                    echo "$message" >"$common_fifo"
                done
            fi
            chmod "0400" "$common_fifo"
            common.Echo "Still in Throw $=$common_status subshell=$BASH_SUBSHELL #=$# -=$-"
            exit "$common_status"
        }
        
        common.ErrHandler() {
            common_status=$?
            trap ERR
            common.Echo -n "In ErrHandler ?=$common_status debug=$common_options "
            common.Echo "try=$common_trySubshell subshell=$BASH_SUBSHELL order=$common_order"
            if [[ -w "$common_fifo" ]]; then
                if [[ $common_options != *e* ]]; then
                    common.Echo "ErrHandler is ignoring"
                    common_status="0"
                    return "$common_status" # value is ignored
                fi
                if [[ $common_options == *k* ]]; then
                    common.CallStack "2" >"$common_file"
                fi
                common.Echo "Calling ${common_errHandler:-}"
                eval "${common_errHandler:-} \"${BASH_LINENO[0]}\" \"${BASH_SOURCE[1]}\" \"${FUNCNAME[1]}\" >$common_fifo <$common_fifo"
                if [[ $common_options == *c* ]]; then
                    echo "Call Stack:" >"$common_fifo"
                    common.CallStack "2" >"$common_fifo"
                fi
                chmod "0400" "$common_fifo"
            fi
            common.Echo "Still in ErrHandler $=$common_status subshell=$BASH_SUBSHELL -=$-"
            if [[ common_trySubshell -eq BASH_SUBSHELL ]]; then
                return "$common_status" # value is ignored   
            else
                exit "$common_status"
            fi
        }
        
        common.Token() {
            local "name"
            case $1 in
            b) name="before";;
            t) name="$common_Try";;
            y) name="$common_yrT";;
            c) name="$common_Catch";;
            h) name="$common_hctaC";;
            f) name="$common_yllaniF";;
            l) name="$common_Finally";;
            *) name="unknown";;
            esac
            echo "$name"
        }
        
        common.TryCatchFinallyNext() {
            common.ShellInit
            local "previous=$common_order" "errmsg"
            common_order="$2"
            if [[ $previous != $1 ]]; then
                errmsg="${BASH_SOURCE[2]}: line ${BASH_LINENO[1]}: syntax error_near unexpected token \`$(common.Token "$2")'"
                echo "$errmsg" >&2
                [[ /dev/fd/2 -ef $common_file ]] || echo "$errmsg" >"$common_file"
                kill -s INT 0
                return "1"        
            fi
        }
        
        common.ShellInit() {
            if [[ common_initSubshell -ne BASH_SUBSHELL ]]; then
                common_initSubshell="$BASH_SUBSHELL"
                common_order="b"
            fi
        }
        
        common.Try() {
            common.TryCatchFinallyVerify || return
            common.TryCatchFinallyNext "[byhl]" "t" || return 
            common_status="0"
            common_subshell="$common_trySubshell"
            common_trySubshell="$BASH_SUBSHELL"
            common_messages=()
            common.Echo "-------------> Setting try=$common_trySubshell at subshell=$BASH_SUBSHELL"
        }
        
        common.yrT() {
            local "status=$?"
            common.TryCatchFinallyVerify || return
            common.TryCatchFinallyNext "[t]" "y" || return 
            common.Echo -n "Entered yrT ?=$status status=$common_status "
            common.Echo "try=$common_trySubshell subshell=$BASH_SUBSHELL"
            if [[ common_status -ne 0 ]]; then    
        
                common.Echo "Build message array. ?=$common_status, subshell=$BASH_SUBSHELL"
                local "message=" "eof=TRY_CATCH_FINALLY_END_OF_MESSAGES_$RANDOM"
                chmod "0600" "$common_fifo"
                echo "$eof" >"$common_fifo"
                common_messages=()
                while read "message"; do
        
                    common.Echo "----> $message"
        
                    [[ $message != *$eof ]] || break
                    common_messages+=("$message")
                done <"$common_fifo"
            fi
        
            common.Echo "In ytT status=$common_status"
            common_trySubshell="$common_subshell"
        }
        
        common.Catch() {
            common.TryCatchFinallyVerify || return
            common.TryCatchFinallyNext "[yh]" "c" || return 
            [[ common_status -ne 0 ]] || return "1"
            local "parameter" "pattern" "value"
            local "toggle=true" "compare=p" "options=$-"
            local -i "i=-1" "status=0"
            set -f
            for parameter in "$@"; do
                if "$toggle"; then
                    toggle="false"
                    if [[ $parameter =~ ^-[notepr]$ ]]; then
                        compare="${parameter#-}"
                        continue 
                    fi
                fi
                toggle="true"
                while "true"; do
                    eval local "patterns=($parameter)"
                    if [[ ${#patterns[@]} -gt 0 ]]; then
                        for pattern in "${patterns[@]}"; do
                            [[ i -lt ${#common_messages[@]} ]] || break
                            if [[ i -lt 0 ]]; then
                                value="$common_status"
                            else
                                value="${common_messages[i]}"
                            fi
                            case $compare in
                            [ne]) [[ ! $value == "$pattern" ]] || break 2;;
                            [op]) [[ ! $value == $pattern ]] || break 2;;
                            [tr]) [[ ! $value =~ $pattern ]] || break 2;;
                            esac
                        done
                    fi
                    if [[ $compare == [not] ]]; then
                        let "++i,1"
                        continue 2
                    else
                        status="1"
                        break 2
                    fi
                done
                if [[ $compare == [not] ]]; then
                    status="1"
                    break
                else
                    let "++i,1"
                fi
            done
            [[ $options == *f* ]] || set +f
            return "$status"
        } 
        
        common.hctaC() {
            common.TryCatchFinallyVerify || return
            common.TryCatchFinallyNext "[c]" "h" || return 
            [[ $# -ne 1 || $1 != -r ]] || common_status="0"
        }
        
        common.Finally() {
            common.TryCatchFinallyVerify || return
            common.TryCatchFinallyNext "[ych]" "f" || return 
        }
        
        common.yllaniF() {
            common.TryCatchFinallyVerify || return
            common.TryCatchFinallyNext "[f]" "l" || return 
            [[ common_status -eq 0 ]] || common.Throw
        }
        
        common.Caught() {
            common.TryCatchFinallyVerify || return
            [[ common_status -eq 0 ]] || return 1
        }
        
        common.EchoExitStatus() {
            return "${1:-$?}"
        }
        
        common.EnableThrowOnError() {
            common.TryCatchFinallyVerify || return
            [[ $common_options == *e* ]] || common_options+="e"
        }
        
        common.DisableThrowOnError() {
            common.TryCatchFinallyVerify || return
            common_options="${common_options/e}"
        }
        
        common.GetStatus() {
            common.TryCatchFinallyVerify || return
            echo "$common_status"
        }
        
        common.SetStatus() {
            common.TryCatchFinallyVerify || return
            if [[ $# -ne 1 ]]; then
                echo "$(common.GetAlias): $#: Wrong number of parameters" >&2
                return "1"         
            elif [[ ${#1} -gt 16 || -n ${1%%[0-9]*} || 10#$1 -lt 1 || 10#$1 -gt 255 ]]; then
                echo "$(common.GetAlias): $1: First parameter was not an integer between 1 and 255" >&2
                return "1"
            fi
            let "common_status = 10#$1"
        }
        
        common.GetMessage() {
            common.TryCatchFinallyVerify || return
            local "upper=${#common_messages[@]}"
            if [[ upper -eq 0 ]]; then
                echo "$(common.GetAlias): $1: There are no messages" >&2
                return "1"
            elif [[ $# -ne 1 ]]; then
                echo "$(common.GetAlias): $#: Wrong number of parameters" >&2
                return "1"         
            elif [[ ${#1} -gt 16 || -n ${1%%[0-9]*} || 10#$1 -ge upper ]]; then
                echo "$(common.GetAlias): $1: First parameter was an invalid index" >&2
                return "1"
            fi
            echo "${common_messages[$1]}"
        }
        
        common.MessageCount() {
            common.TryCatchFinallyVerify || return
            echo "${#common_messages[@]}"
        }
        
        common.CopyMessages() {
            common.TryCatchFinallyVerify || return
            if [[ $# -ne 1 ]]; then
                echo "$(common.GetAlias): $#: Wrong number of parameters" >&2
                return "1"         
            elif [[ ${#common_messages} -gt 0 ]]; then
                eval "$1=(\"\${common_messages[@]}\")"
            else
                eval "$1=()"
            fi
        }
        
        common.TryCatchFinallyExists() {
            [[ ${common_fifo:-u} != u ]]
        }
        
        common.TryCatchFinallyVerify() {
            local "name"
            if ! common.TryCatchFinallyExists; then
                echo "$(common.GetAlias "1"): No Try-Catch-Finally exists" >&2
                return "2"        
            fi
        }
        
        common.GetOptions() {
            local "opt"
            local "name=$(common.GetAlias "1")"
            if common.TryCatchFinallyExists; then
                echo "$name: A Try-Catch-Finally already exists" >&2
                return "1"        
            fi
            let "OPTIND = 1"
            let "OPTERR = 0"
            while getopts ":cdeh:ko:u:v:" opt "$@"; do
                case $opt in
                c)  [[ $common_options == *c* ]] || common_options+="c";;
                d)  [[ $common_options == *d* ]] || common_options+="d";;
                e)  [[ $common_options == *e* ]] || common_options+="e";;
                h)  common_errHandler="$OPTARG";;
                k)  [[ $common_options == *k* ]] || common_options+="k";;
                o)  common_file="$OPTARG";;
                u)  common_unhandled="$OPTARG";;
                v)  common_command="$OPTARG";;
                \?) #echo "Invalid option: -$OPTARG" >&2
                    echo "$name: Illegal option: $OPTARG" >&2
                    return "1";;
                :)  echo "$name: Option requires an argument: $OPTARG" >&2
                    return "1";;
                *)  echo "$name: An error occurred while parsing options." >&2
                    return "1";;
                esac
            done
        
            shift "$((OPTIND - 1))"
            if [[ $# -lt 1 ]]; then
                echo "$name: The fifo_file parameter is missing" >&2
                return "1"
            fi
            common_fifo="$1"
            if [[ ! -p $common_fifo ]]; then
                echo "$name: $1: The fifo_file is not an open FIFO" >&2
                return "1"  
            fi
        
            shift
            if [[ $# -lt 1 ]]; then
                echo "$name: The function parameter is missing" >&2
                return "1"
            fi
            common_function="$1"
            if ! chmod "0600" "$common_fifo"; then
                echo "$name: $common_fifo: Can not change file mode to 0600" >&2
                return "1"
            fi
        
            local "message=" "eof=TRY_CATCH_FINALLY_END_OF_FILE_$RANDOM"
            { echo "$eof" >"$common_fifo"; } 2>"/dev/null"
            if [[ $? -ne 0 ]]; then
                echo "$name: $common_fifo: Can not write" >&2
                return "1"
            fi   
            { while [[ $message != *$eof ]]; do
                read "message"
            done <"$common_fifo"; } 2>"/dev/null"
            if [[ $? -ne 0 ]]; then
                echo "$name: $common_fifo: Can not read" >&2
                return "1"
            fi   
        
            return "0"
        }
        
        common.DefaultUnhandled() {
            local -i "i"
            echo "-------------------------------------------------"
            echo "$(common.GetAlias "common.TryCatchFinally"): Unhandeled exception occurred"
            echo "Status: $(GetStatus)"
            echo "Messages:"
            for ((i=0; i<$(MessageCount); i++)); do
                echo "$(GetMessage "$i")"
            done
            echo "-------------------------------------------------"
        }
        
        common.TryCatchFinally() {
            local "common_file=/dev/fd/2"
            local "common_errHandler=common.DefaultErrHandler"
            local "common_unhandled=common.DefaultUnhandled"
            local "common_options="
            local "common_fifo="
            local "common_function="
            local "common_flags=$-"
            local "common_trySubshell=-1"
            local "common_initSubshell=-1"
            local "common_subshell"
            local "common_status=0"
            local "common_order=b"
            local "common_command="
            local "common_messages=()"
            local "common_handler=$(trap -p ERR)"
            [[ -n $common_handler ]] || common_handler="trap ERR"
        
            common.GetOptions "$@" || return "$?"
            shift "$((OPTIND + 1))"
        
            [[ -z $common_command ]] || common_command+="=$"
            common_command+='("$common_function" "$@")'
        
            set -E
            set +e
            trap "common.ErrHandler" ERR
            if true; then
                common.Try 
                eval "$common_command"
                common.EchoExitStatus
                common.yrT
            fi
            while common.Catch; do
                "$common_unhandled" >&2
                break
                common.hctaC -r
            done
            common.hctaC
            [[ $common_flags == *E* ]] || set +E
            [[ $common_flags != *e* ]] || set -e
            [[ $common_flags != *f* || $- == *f* ]] || set -f
            [[ $common_flags == *f* || $- != *f* ]] || set +f
            eval "$common_handler"
            return "$((common_status?2:0))"
        }
        

        【讨论】:

          【解决方案13】:

          我可以在“bash -ue”模式下推荐这个:

          set -ue
             
          false && RET=$? || RET=$? 
          echo "expecting 1, got ${RET}"
          true && RET=$? || RET=$? 
          echo "expecting 0, got ${RET}"
          
          echo "test try...catch"
          false && RET=$? || RET=$? 
          if [ ${RET} -ne 0 ]; then
            echo "caught error ${RET}"
          fi
          
          echo "beware, using '||' before '&&' fails"
          echo "  -> memory aid: [A]nd before [O]r in the alphabet"
          false || RET=$? && RET=$? 
          echo "expecting 1, got ${RET}"
          true || RET=$? && RET=$? 
          echo "expecting 0, got ${RET}"
          

          【讨论】:

            【解决方案14】:

            假设有问题的终端应用程序名为“app”(并且您希望它位于您的 $PATH 中):

            if [[ ! `which app` ]]; then
                # run code if app not installed
            else
                # run code if app is installed
            fi
            

            【讨论】:

              【解决方案15】:

              我使用这样的东西:

              YOUR COMMAND HERE
              EXITCODE=$?
              if [ "$EXITCODE" -ne "0" ]; then
                  #this is the catch part
                  echo "uh oh"
                  exit $EXITCODE
              fi
              

              这只是检查命令的退出代码并将其与零进行比较 (表示命令运行成功)

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2015-12-07
                • 1970-01-01
                • 2021-11-13
                • 1970-01-01
                • 2016-02-14
                • 1970-01-01
                • 1970-01-01
                • 2018-09-28
                相关资源
                最近更新 更多