如何在Linux bash脚本中捕获错误?


13

我做了以下脚本:

# !/bin/bash

# OUTPUT-COLORING
red='\e[0;31m'
green='\e[0;32m'
NC='\e[0m' # No Color

# FUNCTIONS
# directoryExists - Does the directory exist?
function directoryExists {
    cd $1
    if [ $? = 0 ]
            then
                    echo -e "${green}$1${NC}"
            else
                    echo -e "${red}$1${NC}"
    fi
}

# EXE
directoryExists "~/foobar"
directoryExists "/www/html/drupal"

该脚本有效,但是在我的回声旁边,当

cd $1

执行失败。

testscripts//test_labo3: line 11: cd: ~/foobar: No such file or directory

有可能抓住这个吗?


只是一个FYI,您也可以更简单地完成此操作。test -d /path/to/directory(或[[ -d /path/to/directory ]]bash中的)会告诉您给定的目标是否为目录,它会悄悄地执行。
帕特里克

@Patrick,它只是测试它是否是目录,而不是是否可以cd进入它。
2013年

@StephaneChazelas是的。函数名称为directoryExists
帕特里克

在此处查看详细的答案:在Bash脚本中引发错误
codeforester

Answers:


8

您的脚本在运行时会更改目录,这意味着它将无法使用一系列相对路径名。然后,您稍后评论说,您只想检查目录是否存在,而不是要使用的能力 cd,因此答案完全不需要使用cd。修改。使用tput 和颜色man terminfo

#!/bin/bash -u
# OUTPUT-COLORING
red=$( tput setaf 1 )
green=$( tput setaf 2 )
NC=$( tput setaf 0 )      # or perhaps: tput sgr0

# FUNCTIONS
# directoryExists - Does the directory exist?
function directoryExists {
    # was: do the cd in a sub-shell so it doesn't change our own PWD
    # was: if errmsg=$( cd -- "$1" 2>&1 ) ; then
    if [ -d "$1" ] ; then
        # was: echo "${green}$1${NC}"
        printf "%s\n" "${green}$1${NC}"
    else
        # was: echo "${red}$1${NC}"
        printf "%s\n" "${red}$1${NC}"
        # was: optional: printf "%s\n" "${red}$1 -- $errmsg${NC}"
    fi
}

(经过编辑,使用了更强大的功能,printf而不是echo可能对文本中的转义序列起作用的问题 。)


这也解决了(除非打开xpg_echo)文件名包含反斜杠字符的问题。
斯特凡Chazelas

12

使用set -e到出口集上错误模式:如果一个简单的命令返回非零状态(表示失败),外壳退出。

要注意的是set -e并不总是一命呜呼命令测试位置都被允许失败(例如if failing_commandfailing_command || fallback)。子外壳程序中的命令仅导致退出子外壳程序,而不导致父级外壳程序:set -e; (false); echo foodisplay foo

另外,在bash(和ksh和zsh,但不是普通的sh)中,您可以指定一个命令,如果命令返回带有ERR陷阱的非零状态,则执行该命令,例如trap 'err=$?; echo >&2 "Exiting on error $err"; exit $err' ERR。请注意,在类似(false); …的情况下,ERR陷阱在子shell中执行,因此它不会导致父级退出。


最近,我做了一些试验,发现了一种修复||行为的简便方法,该方法无需使用陷阱即可轻松地进行正确的错误处理。看我的回答。您如何看待该方法?
skozin

@ sam.kozin我没有时间详细检查您的答案,原则上看起来不错。除了可移植性之外,与ksh / bash / zsh的ERR陷阱相比有什么好处?
吉尔(Gilles)“所以,别再邪恶了”

唯一的好处可能就是可组合性,因为您不必冒险覆盖函数运行之前设置的另一个陷阱。当您编写一些常用功能时,这是一个有用的功能,稍后您将从其他脚本中获取和使用这些功能。另一个好处可能是完全POSIX兼容性,尽管它并不重要,因为ERR在所有主要的shell中都支持伪信号。感谢您的评论!=)
skozin

@ sam.kozin我忘记在以前的评论中写:您可能希望将此内容发布在Code Review上,并在聊天室中发布链接。
吉尔斯(Gillles)“所以-别再邪恶了”

感谢您的建议,我将尽力遵循。不了解Code Review。
Skozin

6

扩展@Gilles的答案

的确,即使您在子shell中运行它们,set -e也不能在命令内部使用||运算符。例如,这行不通:

#!/bin/sh

# prints:
#
# --> outer
# --> inner
# ./so_1.sh: line 16: some_failed_command: command not found
# <-- inner
# <-- outer

set -e

outer() {
  echo '--> outer'
  (inner) || {
    exit_code=$?
    echo '--> cleanup'
    return $exit_code
  }
  echo '<-- outer'
}

inner() {
  set -e
  echo '--> inner'
  some_failed_command
  echo '<-- inner'
}

outer

但是||需要操作员来防止在清理之前从外部函数返回。

有一个小技巧可以解决此问题:在后台运行inner命令,然后立即等待它。该wait内建将返回内部命令的退出代码,现在你正在使用||wait,而不是内部功能,set -e工作正常后内:

#!/bin/sh

# prints:
#
# --> outer
# --> inner
# ./so_2.sh: line 27: some_failed_command: command not found
# --> cleanup

set -e

outer() {
  echo '--> outer'
  inner &
  wait $! || {
    exit_code=$?
    echo '--> cleanup'
    return $exit_code
  }
  echo '<-- outer'
}

inner() {
  set -e
  echo '--> inner'
  some_failed_command
  echo '<-- inner'
}

outer

这是基于此思想的通用功能。如果删除local关键字,则它应该在所有POSIX兼容的shell中都可以工作,即local x=y用just 替换所有关键字x=y

# [CLEANUP=cleanup_cmd] run cmd [args...]
#
# `cmd` and `args...` A command to run and its arguments.
#
# `cleanup_cmd` A command that is called after cmd has exited,
# and gets passed the same arguments as cmd. Additionally, the
# following environment variables are available to that command:
#
# - `RUN_CMD` contains the `cmd` that was passed to `run`;
# - `RUN_EXIT_CODE` contains the exit code of the command.
#
# If `cleanup_cmd` is set, `run` will return the exit code of that
# command. Otherwise, it will return the exit code of `cmd`.
#
run() {
  local cmd="$1"; shift
  local exit_code=0

  local e_was_set=1; if ! is_shell_attribute_set e; then
    set -e
    e_was_set=0
  fi

  "$cmd" "$@" &

  wait $! || {
    exit_code=$?
  }

  if [ "$e_was_set" = 0 ] && is_shell_attribute_set e; then
    set +e
  fi

  if [ -n "$CLEANUP" ]; then
    RUN_CMD="$cmd" RUN_EXIT_CODE="$exit_code" "$CLEANUP" "$@"
    return $?
  fi

  return $exit_code
}


is_shell_attribute_set() { # attribute, like "x"
  case "$-" in
    *"$1"*) return 0 ;;
    *)    return 1 ;;
  esac
}

用法示例:

#!/bin/sh
set -e

# Source the file with the definition of `run` (previous code snippet).
# Alternatively, you may paste that code directly here and comment the next line.
. ./utils.sh


main() {
  echo "--> main: $@"
  CLEANUP=cleanup run inner "$@"
  echo "<-- main"
}


inner() {
  echo "--> inner: $@"
  sleep 0.5; if [ "$1" = 'fail' ]; then
    oh_my_god_look_at_this
  fi
  echo "<-- inner"
}


cleanup() {
  echo "--> cleanup: $@"
  echo "    RUN_CMD = '$RUN_CMD'"
  echo "    RUN_EXIT_CODE = $RUN_EXIT_CODE"
  sleep 0.3
  echo '<-- cleanup'
  return $RUN_EXIT_CODE
}

main "$@"

运行示例:

$ ./so_3 fail; echo "exit code: $?"

--> main: fail
--> inner: fail
./so_3: line 15: oh_my_god_look_at_this: command not found
--> cleanup: fail
    RUN_CMD = 'inner'
    RUN_EXIT_CODE = 127
<-- cleanup
exit code: 127

$ ./so_3 pass; echo "exit code: $?"

--> main: pass
--> inner: pass
<-- inner
--> cleanup: pass
    RUN_CMD = 'inner'
    RUN_EXIT_CODE = 0
<-- cleanup
<-- main
exit code: 0

使用此方法时,您唯一需要了解的是,由于传递给命令的Shell变量的所有修改run都不会传播到调用函数,因为该命令在子shell中运行。


2

你没有说出你到底是什么意思catch---报告并继续;中止进一步处理?

由于cd失败时返回非零状态,因此您可以执行以下操作:

cd -- "$1" && echo OK || echo NOT_OK

您可以在失败时退出:

cd -- "$1" || exit 1

或者,回显您自己的消息并退出:

cd -- "$1" || { echo NOT_OK; exit 1; }

和/或抑制cd失败时提供的错误:

cd -- "$1" 2>/dev/null || exit 1

按照标准,命令应将错误消息放在STDERR(文件描述符2)上。这样2>/dev/null说,将STDERR重定向到已知的“位桶” /dev/null

(不要忘了引用变量并标记的选项结尾cd)。


@Stephane Chazelas的报价和信号终止选项的观点很恰当。感谢您的编辑。
JRFerguson

1

实际上,对于您的情况,我会说逻辑可以改进。

代替cd,然后检查它是否存在,检查它是否存在,然后进入目录。

if [ -d "$1" ]
then
     printf "${green}${NC}\\n" "$1"
     cd -- "$1"
else 
     printf "${red}${NC}\\n" "$1"
fi  

但是,如果您的目的是使可能的错误静音,那么cd -- "$1" 2>/dev/null,这将使您以后的调试更加困难。您可以在以下位置检查if测试标记:Bash if documentation


该答案无法用引号引起来,$1并且如果该变量包含空格或其他外壳元字符,则将失败。它还无法检查用户是否具有cd进入权限。
伊恩·艾伦

我实际上是在尝试检查某个目录是否存在,而不一定是CD。但是由于我并不了解,所以我想尝试对其进行裁谈会导致错误(如果不存在),那么为什么不赶上它呢?我不知道if [-d $ 1]是否正是我所需要的。因此,非常感谢!(我习惯于编写Java语言,并在if语句中检查目录在Java中并不完全常见)
Thomas De Wilde
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.