错误的 $LINENO 被困函数

num*_*and 7 bash trap

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

预期行为:

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

实际发生的情况:

  1. 运行脚本
  2. Ctrl+C
  3. 陷阱缓存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"
}
Run Code Online (Sandbox Code Playgroud)

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
  ..."
}
Run Code Online (Sandbox Code Playgroud)

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

 DDEBUG: 1 : clean_a 1 336 rsync '"${options}"' ...
Run Code Online (Sandbox Code Playgroud)

但是,在clean_a()函数中,值的$LASTNO打印为 1。

 line: 462 1
Run Code Online (Sandbox Code Playgroud)

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

EDIT2:我按照@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
Run Code Online (Sandbox Code Playgroud)

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

^^MDEBUG: 1 : clean_a '337 1 rsync "${options}" --delete-during ...
...
line: 1 465
Run Code Online (Sandbox Code Playgroud)

如您所见,$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
Run Code Online (Sandbox Code Playgroud)

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

^^MDEBUG: 1 : testing '337 1 rsync "${options}" --delete-during ...
337 1 rsync "${options}" --delete-during ... 
Run Code Online (Sandbox Code Playgroud)

当我按下Ctrl+时 337 指出该行C,而 rsync 正在运行。

对于另一个测试,我尝试clear_a像这样编写函数:

clear_a () {
  echo -e " $LASTNO $LINENO"
}
Run Code Online (Sandbox Code Playgroud)

并且 $LASTNO 仍然返回 1。

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

EDIT3似乎我在 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
Run Code Online (Sandbox Code Playgroud)

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

Nik*_*olm 9

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
Run Code Online (Sandbox Code Playgroud)

输出

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
Run Code Online (Sandbox Code Playgroud)

由于 trap 的第一个参数fn "$LINENO", 被放在引号内,当且仅当EXIT 触发时,才会$LINENO扩展,因此应该扩展为。那为什么不呢?事实上它确实如此,直到 bash-4.0 故意更改它以便在触发陷阱时将 $LINENO 重置为 1,因此扩展为. [来源]然而,ERR 陷阱仍然保持原始行为,可能是因为使用类似的频率。fn 5fn 1trap '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
Run Code Online (Sandbox Code Playgroud)

输出

error at line 5
exit at line 1
Run Code Online (Sandbox Code Playgroud)

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


mik*_*erv 4

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

error "something!
line: $1
...
Run Code Online (Sandbox Code Playgroud)

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

这是一个小演示:

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

输出

DEBUG: 1 : trap 'fn "$LINENO"' EXIT
DEBUG: 3 : echo 3
3
DEBUG: 1 : fn 1
DEBUG: 2 : printf '%s\n' 2 1
2
1
Run Code Online (Sandbox Code Playgroud)

因此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
Run Code Online (Sandbox Code Playgroud)

输出

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
Run Code Online (Sandbox Code Playgroud)

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

1您进入的原因$LASTNO是因为这最后设置的值,$LASTNO因为这是最后一个$LINENO值。你已经traparchieve_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
Run Code Online (Sandbox Code Playgroud)

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

来自POSIX shell 规范

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