我正在为自己编写一个 Bash 脚本来学习脚本编写。在某些时候,我需要添加陷阱,以便在脚本被终止时清理不需要的目录和文件。但是,由于某种原因我不明白,clean_a()
当脚本被杀死时,trap 调用清理函数,但$LINENO
指向清理函数本身中的一行,而不是 int 函数 -archieve_it()
当脚本被杀死时。
预期行为:
- 运行脚本
- 按Ctrl+C
- trap 缓存Ctrl+C并调用
clean_a()
函数 clean_a()
Ctrl函数会回显按下+的行号C。让它成为 中的第 10 行archieve_it()
。
实际发生的情况:
- 运行脚本
- 按Ctrl+C
- trap 缓存Ctrl+C并调用
clean_a()
函数 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
$LINENO
bash
我认为你不想$LASTNO
在 中进行评估clean_a()
。更好的方法是在 中对其进行评估,并将接收到的trap
值作为参数传递给 to 。也许是这样的:trap
$LASTNO
clean_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 一样。
在执行每个命令之前,由 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 。因此,将其发布在这里似乎是合适的。