捕获函数的 $LINENO 错误

捕获函数的 $LINENO 错误

我正在为自己编写一个 Bash 脚本来学习脚本编写。在某些时候,我需要添加陷阱,以便在脚本被终止时清理不需要的目录和文件。但是,由于某种原因我不明白,clean_a()当脚本被杀死时,trap 调用清理函数,但$LINENO指向清理函数本身中的一行,而不是 int 函数 -archieve_it()当脚本被杀死时。

预期行为:

  1. 运行脚本
  2. Ctrl+C
  3. trap 缓存Ctrl+C并调用clean_a()函数
  4. clean_a()Ctrl函数会回显按下+的行号C。让它成为 中的第 10 行archieve_it()

实际发生的情况:

  1. 运行脚本
  2. Ctrl+C
  3. trap 缓存Ctrl+C并调用clean_a()函数
  4. clean_a()回显不相关的行号。比如说,clean_a()函数中的第 25 行。

这是我的脚本的一部分的示例:

archieve_it () {
  trap 'clean_a $LINENO $BASH_COMMAND'\
                SIGHUP SIGINT SIGTERM SIGQUIT
  for src in ${sources}; do
   mkdir -p "${dest}${today}${src}"
   if [[ "$?" -ne 0 ]] ; then
    error "Something!" 
   fi
   rsync "${options}" \
         --stats -i \
         --log-file="${dest}${rsync_log}" \
         "${excludes}" "${src}" "${dest}${today}${src}"
  done
}
clean_a () {
  error "something!
  line: $LINENO
  command: $BASH_COMMAND
  removing ${dest}${today}..."
  cd "${dest}"
  rm -rdf "${today}"
  exit "$1"
}

PS:原脚本可见这里。定义和变量名称为土耳其语。如果需要,我可以将任何内容翻译成英语。

编辑:我根据 @mikeserv 的解释尽可能地更改脚本,如下所示:

#!/bin/bash
PS4='DEBUG: $((LASTNO=$LINENO)) : '; set -x
archieve_it () {
  trap 'clean_a $LASTNO $LINENO $BASH_COMMAND'\
                SIGHUP SIGINT SIGTERM SIGQUIT
  ..
}
clean_a () {
  error " ...
  line: $LINENO $LASTNO
  ..."
}

现在,如果我运行脚本并用+set -x终止它,它会打印正确的行号,如下所示:CtrlC

 DDEBUG: 1 : clean_a 1 336 rsync '"${options}"' ...

然而,在clean_a()函数中, 的值$LASTNO被打印为 1。

 line: 462 1

它与 @Arkadiusz Drabczyk 显示的错误有关吗?

编辑2:我按照 @mikesrv 向我推荐的方式更改了脚本。但是当脚本终止时,$LASTNO 返回 1 作为该行的值(它应该是 337)。

#!/bin/bash
PS4='^MDEBUG: $((LASTNO=$LINENO)) : '; set -x
archieve_it () {
  trap 'clean_a $LASTNO $LINENO "$BASH_COMMAND"' \
                SIGHUP SIGINT SIGTERM SIGQUIT
  ...
} 2>/dev/null
clean_a () {
  error " ...
  line: $LASTNO $LINENO
  ..."
} 2>&1

如果我运行脚本并在 rsync 运行时用Ctrl+终止它,我会得到以下输出:C

^^MDEBUG: 1 : clean_a '337 1 rsync "${options}" --delete-during ...
...
line: 1 465

如您所见,$LASTNO 的值为 1。

当我试图找出问题所在时,我编写了另一个函数 -testing使用参数替换格式${parameter:-default}。所以脚本变成了这样:

#!/bin/bash
PS4='^MDEBUG: $((LASTNO=$LINENO)) : '; set -x
archieve_it () {
  trap 'testing "$LASTNO $LINENO $BASH_COMMAND"'\
                 SIGHUP SIGINT SIGTERM SIGQUIT
  ...
} 2>/dev/null
testing() {
  echo -e "${1:-Unknown error!}"
  exit 1
} 2>&1

现在,如果我运行脚本并按Ctrl+ C,我会得到以下输出:

^^MDEBUG: 1 : testing '337 1 rsync "${options}" --delete-during ...
337 1 rsync "${options}" --delete-during ... 

337 指出当 rsync 运行时我按下Ctrl+时的行。C

对于另一个测试,我尝试编写clear_a如下函数:

clear_a () {
  echo -e " $LASTNO $LINENO"
}

并且 $LASTNO 仍然返回 1。

那么,这意味着如果我们使用参数替换,我们可以在脚本终止时获得正确的行号?

编辑3看来我错误地应用了 EDIT2 中@mikeserv 的解释。我纠正了我的错误。函数中的位置参数"$1应替换为 $LASTNO 。clear_a

这是按照我希望的方式工作的脚本:

#!/bin/bash
PS4='^MDEBUG: $((LASTNO=$LINENO)) : '; set -x
archieve_it () {
  trap 'clean_a $LASTNO $LINENO "$BASH_COMMAND"' \
                SIGHUP SIGINT SIGTERM SIGQUIT
  ...
} 2>/dev/null
clean_a () {
  error " ...
  line: $1
  ..."
} 2>&1

当脚本终止时,trap计算$LASTNO- 第一个参数 -、$LINENO- 第二个参数 - 和$BASH_COMMAND- 第三个参数 -,然后将它们的值传递给函数clear_a。最后,我们打印 $LASTNO$1作为脚本终止的行号。

答案1

mikeserv 的解决方案很好,但他说在执行陷阱时fn已通过该trap行是不正确的。$LINENO在前面插入一行trap ...,您将看到fn实际上总是通过1,无论陷阱是在哪里声明的。

PS4='DEBUG: $LINENO : ' \
bash -x <<\EOF
    echo Foo
    trap 'fn "$LINENO"' EXIT             
    fn() { printf %s\\n "$LINENO" "$1"; }
    echo "$LINENO"
    exit
EOF

输出

DEBUG: 1 : echo Foo
Foo
DEBUG: 2 : trap 'fn "$LINENO"' EXIT
DEBUG: 4 : echo 4
4
DEBUG: 5 : exit
DEBUG: 1 : fn 1
DEBUG: 3 : printf '%s\n' 3 1
3
1

由于 trap 的第一个参数 ,fn "$LINENO"被放在单身的引用,$LINENO获取扩大,当且仅当它触发了 EXIT,因此应该扩展到fn 5.那么为什么不呢?事实上它确实如此,直到 bash-4.0 为止,它被故意更改,以便在触发陷阱时 $LINENO 重置为 1,因此扩展为fn 1.[来源]然而,ERR 陷阱的原始行为仍然保留,可能是因为类似的东西经常trap 'echo "Error at line $LINENO"' ERR被使用。

#!/bin/bash

trap 'echo "exit at line $LINENO"' EXIT
trap 'echo "error at line $LINENO"' ERR
false
exit 0

输出

error at line 5
exit at line 1

使用 GNU bash 版本 4.3.42(1)-release (x86_64-pc-linux-gnu) 进行测试

答案2

我认为问题在于您期望"$LINENO"为您提供最后一个命令的执行行,这可能几乎可以工作,但是clean_a() 有它自己的$LINENO,你应该这样做:

error "something!
line: $1
...

但即使这样也可能行不通,因为我希望它只会打印您设置trap.

这是一个小演示:

PS4='DEBUG: $LINENO : ' \
bash -x <<\CMD          
    trap 'fn "$LINENO"' EXIT             
    fn() { printf %s\\n "$LINENO" "$1"; }
    echo "$LINENO"
CMD

输出

DEBUG: 1 : trap 'fn "$LINENO"' EXIT
DEBUG: 3 : echo 3
3
DEBUG: 1 : fn 1
DEBUG: 2 : printf '%s\n' 2 1
2
1

因此,trap获取设置,然后fn()定义,然后echo执行。当 shell 完成执行其输入时,EXIT陷阱将运行并被fn调用。它传递一个参数 - 这是该trap行的$LINENO.fn首先打印它自己的内容$LINENO,然后打印它的第一个参数。

我可以想到一种方法,你可能会得到你期望的行为,但它有点搞砸了 shell 的stderr

PS4='DEBUG: $((LASTNO=$LINENO)) : ' \
bash -x <<\CMD
    trap 'fn "$LINENO" "$LASTNO"' EXIT
    fn() { printf %s\\n "$LINENO" "$LASTNO" "$@"; }
    echo "$LINENO"
CMD

输出

DEBUG: 1 : trap 'fn "$LINENO" "$LASTNO"' EXIT
DEBUG: 3 : echo 3
3
DEBUG: 1 : fn 1 3
DEBUG: 2 : printf '%s\n' 2 1 1 3
2
1
1
3

它使用 shell 的$PS4调试提示符来定义$LASTNO执行的每一行。它是当前的 shell 变量,您可以在脚本中的任何位置访问它。这意味着无论当前正在访问哪一行,您都可以引用 中运行的脚本的最新行$LASTNO。当然,正如您所看到的,它带有调试输出。您可以将其推到2>/dev/null脚本执行的大部分时间,然后再2>&1执行clean_a()其他操作。

1你进来的原因$LASTNO是因为最后设置的值,$LASTNO因为那是最后一个$LINENO值。你已经在函数trap中得到了你的archieve_it(),所以它有自己的,$LINENO如下面的规范中所述。尽管看起来bash无论如何都没有做正确的事情,所以也可能是因为必须根据信号trap重新执行 shell ,因此被重置。在这种情况下,我对此有点模糊——显然,就是这样。INT$LINENObash

我认为你不想$LASTNO在 中进行评估clean_a()。更好的方法是在 中对其进行评估,并将接收到的trap值作为参数传递给 to 。也许是这样的:trap$LASTNOclean_a()

#!/bin/bash
PS4='^MDEBUG: $((LASTNO=$LINENO)) : '; set -x
archieve_it () {
    trap 'clean_a $LASTNO $LINENO "$BASH_COMMAND"' \
        SIGHUP SIGINT SIGTERM SIGQUIT
    while :; do sleep 1; done
} 2>/dev/null
clean_a () { : "$@" ; } 2>&1

尝试一下 - 我认为它应该可以满足你的要求。哦 - 请注意,其中PS4=^M^M字面返回 - 就像 CTRL+V ENTER 一样。

来自POSIX shell 规范:

在执行每个命令之前,由 shell 设置为一个十进制数字,表示脚本或函数中当前的连续行号(编号从 1 开始)。如果用户取消设置或重置LINENO,该变量可能会失去其在 shell 生命周期中的特殊含义。如果 shell 当前未执行脚本或函数,则 的值LINENO未指定。本卷 IEEE Std 1003.1-2001 仅指定变量对支持“用户可移植性实用程序”选项的系统的影响。

答案3

获取LINENO = 0,而不是脚本退出时的实际行号,可以通过捕获ERR(而不是EXIT)来修复。

此外,添加set -E, 以确保这些 ERR 陷阱由函数、命令替换和子 shell 环境继承。例如,以下是如何打印导致脚本错误的函数名称及其行号:

set -eE
trap 'echo "Error in function $FUNCNAME at line $LINENO"' ERR

致谢:https://citizen428.net/blog/bash-error-handling-with-trap/

答案4

因此,非常偶然的是,我偶然发现了一种方法,可以让 bash 正确显示 $LINENO 变量(例如,在 DEBUG 或 EXIT 陷阱中)并给出您所期望的行号。它比其他发布的方法简单得多,并且不需要劫持 PS4 和 STDERR。我不太确定为什么这是可行的,但如果其他人可以解释的话,我很想知道为什么。

基本上,将您想要运行的内容包装在一个虚拟函数中(例如,将其称为_ff),然后运行source <(declare -f _ff),然后运行,并且in_ff的任何实例实际上都会给出行号。$LINENO_ff

例子

    _ff() (
    trap 'echo "LINENO=$LINENO"' DEBUG;
    echo a
    echo b
    echo c
    echo d
    echo e
    echo f
    echo g
    )
    source <(declare -f _ff)

现在,运行_ff将输出

LINENO=2
a
LINENO=3
b
LINENO=4
c
LINENO=5
d
LINENO=6
e
LINENO=7
f
LINENO=8
g

注意:运行source <(declare -f _ff)是关键点...如果你不运行,那么你将得到LINENO=1所有的$LINENO语句。

旁注:这个问题很老了,但这是我在互联网上基本上找到的唯一一个提供工作解决方法的地方,用于在 ERR 陷阱之外显示正确的 $LINENO 。因此,将其发布在这里似乎是合适的。

相关内容