当我trap
在 Bash 中使用命令时,trap
给定信号的前一个将被替换。
有没有办法trap
为同一个信号产生不止一次的火灾?
从技术上讲,您不能为同一信号设置多个陷阱,但您可以添加到现有陷阱:
trap -p
这是执行上述操作的 bash 函数:
# note: printf is used instead of echo to avoid backslash
# processing and to properly handle values that begin with a '-'.
log() { printf '%s\n' "$*"; }
error() { log "ERROR: $*" >&2; }
fatal() { error "$@"; exit 1; }
# appends a command to a trap
#
# - 1st arg: code to add
# - remaining args: names of traps to modify
#
trap_add() {
trap_add_cmd=$1; shift || fatal "${FUNCNAME} usage error"
for trap_add_name in "$@"; do
trap -- "$(
# helper fn to get existing trap command from output
# of trap -p
extract_trap_cmd() { printf '%s\n' "$3"; }
# print existing trap command with newline
eval "extract_trap_cmd $(trap -p "${trap_add_name}")"
# print the new trap command
printf '%s\n' "${trap_add_cmd}"
)" "${trap_add_name}" \
|| fatal "unable to add to trap ${trap_add_name}"
done
}
# set the trace attribute for the above function. this is
# required to modify DEBUG or RETURN traps because functions don't
# inherit them unless the trace attribute is set
declare -f -t trap_add
示例用法:
trap_add 'echo "in trap DEBUG"' DEBUG
编辑:
看来我误读了这个问题。答案很简单:
handler1 () { do_something; }
handler2 () { do_something_else; }
handler3 () { handler1; handler2; }
trap handler3 SIGNAL1 SIGNAL2 ...
原来的:
只需在命令末尾列出多个信号:
trap function-name SIGNAL1 SIGNAL2 SIGNAL3 ...
您可以使用以下方法找到与特定信号关联的函数trap -p
:
trap -p SIGINT
请注意,即使它们由相同的函数处理,它也会单独列出每个信号。
您可以通过执行以下操作添加一个已知信号的附加信号:
eval "$(trap -p SIGUSR1) SIGUSR2"
即使同一功能正在处理其他附加信号,这也有效。换句话说,假设一个函数已经在处理三个信号 - 您可以通过引用一个现有信号并再附加两个信号来添加两个信号(其中只有一个显示在右引号内)。
如果你使用 Bash >= 3.2,你可以做这样的事情来提取给定信号的函数。请注意,它并不完全可靠,因为可能会出现其他单引号。
[[ $(trap -p SIGUSR1) =~ trap\ --\ \'([^\047]*)\'.* ]]
function_name=${BASH_REMATCH[1]}
然后,如果您需要使用函数名等,您可以从头开始重建您的陷阱命令。
您可以做的最好的事情是从一个trap
给定信号运行多个命令,但您不能为单个信号设置多个并发陷阱。例如:
$ trap "rm -f /tmp/xyz; exit 1" 2
$ trap
trap -- 'rm -f /tmp/xyz; exit 1' INT
$ trap 2
$ trap
$
第一行在信号 2 (SIGINT) 上设置了一个陷阱。第二行打印当前的陷阱——您必须从中捕获标准输出并解析它以获得您想要的信号。然后,您可以将您的代码添加到已经存在的代码中——注意前面的代码很可能包含一个“退出”操作。第三次调用陷阱清除 2/INT 上的陷阱。最后一个表明没有突出的陷阱。
您还可以使用trap -p INT
或trap -p 2
打印特定信号的陷阱。
我喜欢理查德汉森的回答,但我不关心嵌入式功能,所以替代方案是:
#===================================================================
# FUNCTION trap_add ()
#
# Purpose: appends a command to a trap
#
# - 1st arg: code to add
# - remaining args: names of traps to modify
#
# Example: trap_add 'echo "in trap DEBUG"' DEBUG
#
# See: http://stackoverflow.com/questions/3338030/multiple-bash-traps-for-the-same-signal
#===================================================================
trap_add() {
trap_add_cmd=$1; shift || fatal "${FUNCNAME} usage error"
new_cmd=
for trap_add_name in "$@"; do
# Grab the currently defined trap commands for this trap
existing_cmd=`trap -p "${trap_add_name}" | awk -F"'" '{print $2}'`
# Define default command
[ -z "${existing_cmd}" ] && existing_cmd="echo exiting @ `date`"
# Generate the new command
new_cmd="${existing_cmd};${trap_add_cmd}"
# Assign the test
trap "${new_cmd}" "${trap_add_name}" || \
fatal "unable to add to trap ${trap_add_name}"
done
}
这是另一种选择:
on_exit_acc () {
local next="$1"
eval "on_exit () {
local oldcmd='$(echo "$next" | sed -e s/\'/\'\\\\\'\'/g)'
local newcmd=\"\$oldcmd; \$1\"
trap -- \"\$newcmd\" 0
on_exit_acc \"\$newcmd\"
}"
}
on_exit_acc true
用法:
$ on_exit date
$ on_exit 'echo "Goodbye from '\''`uname`'\''!"'
$ exit
exit
Sat Jan 18 18:31:49 PST 2014
Goodbye from 'FreeBSD'!
tap#
我不喜欢玩这些在最好的时候令人困惑的字符串操作,所以我想出了这样的东西:
(显然你可以为其他信号修改它)
exit_trap_command=""
function cleanup {
eval "$exit_trap_command"
}
trap cleanup EXIT
function add_exit_trap {
local to_add=$1
if [[ -z "$exit_trap_command" ]]
then
exit_trap_command="$to_add"
else
exit_trap_command="$exit_trap_command; $to_add"
fi
}
我已经为自己编写了一组函数,以便以一种方便的方式解决这个任务。
更新:这里的实现已经过时,留在这里作为演示。新的实现更复杂,有依赖关系,支持更广泛的案例,而且相当大。
新实现:https ://sf.net/p/tacklelib/tacklelib/HEAD/tree/trunk/bash/tacklelib/traplib.sh
以下是新实现的功能列表:
优点:
RETURN
仅当堆栈中的所有函数都设置了它时,陷阱才会恢复。RETURN
信号陷阱可以支持其他信号陷阱来实现与其他语言一样的 RAII 模式。例如,在执行初始化代码时临时禁用中断处理并在函数结束时自动恢复它。RETURN
在信号陷阱的情况下防止调用不是来自函数上下文。RETURN
信号处理程序在一个bash进程中从底部到顶部一起调用,并按照与tkl_push_trap
函数调用相反的顺序执行它们RETURN
顺序调用单个函数tkl_push_trap
。EXIT
信号不会触发RETURN
信号陷阱处理程序,所以当信号陷阱处理程序第一次在 bash 进程中进行设置EXIT
时,信号陷阱处理程序会在每个 bash 进程中至少自动设置一次。RETURN
这包括所有 bash 进程,例如,表示为(...)
或$(...)
运算符。因此,EXIT
信号陷阱处理程序会在运行之前自动处理所有RETURN
陷阱处理程序。RETURN
信号陷阱处理程序仍然可以调用tkl_push_trap
和tkl_pop_trap
函数来处理非信号RETURN
陷阱RETURN
程序调用函数。如果从信号陷阱处理程序中调用,则陷阱处理程序将
在 bash 进程中的所有信号陷阱处理程序之后调用。如果从信号陷阱处理程序中调用,则陷阱处理程序将在调用最后一个信号陷阱处理程序后更改退出代码。tkl_set_trap_postponed_exit
EXIT
RETURN
RETURN
EXIT
RETURN
EXIT
EXIT
EXIT
(...)
or运算符。$(...)
source
忽略,因此对命令的所有调用都不会调用信号陷阱用户代码(在 Pros 中标记,因为信号陷阱处理程序必须首先在从函数返回后调用,而不是从脚本包含)。RETURN
source
RETURN
RETURN
缺点:
trap
在传递给tkl_push_trap
函数的处理程序中使用内置命令,因为tkl_*_trap
函数确实在内部使用它。exit
命令。否则,这将使其余的和信号陷阱处理程序不被执行。要更改处理程序的退出代码,您可以使用函数。EXIT
EXIT
RETURN
EXIT
EXIT
tkl_set_trap_postponed_exit
return
命令。否则,这将使其余的和信号陷阱处理程序不被执行。RETURN
RETURN
RETURN
EXIT
tkl_push_trap
和函数的所有调用都无效。tkl_pop_trap
trap
必须用函数替换嵌套或 3dparty 脚本中的所有内置命令。tkl_*_trap
source
忽略,因此对该命令的所有调用都不会调用信号陷阱用户代码(在 Cons 中标记,因为此处失去了向后兼容性)。RETURN
source
RETURN
旧实现:
陷阱库
#!/bin/bash
# Script can be ONLY included by "source" command.
if [[ -n "$BASH" && (-z "$BASH_LINENO" || ${BASH_LINENO[0]} -gt 0) ]] && (( ! ${#SOURCE_TRAPLIB_SH} )); then
SOURCE_TRAPLIB_SH=1 # including guard
function GetTrapCmdLine()
{
local IFS=$' \t\r\n'
GetTrapCmdLineImpl RETURN_VALUES "$@"
}
function GetTrapCmdLineImpl()
{
local out_var="$1"
shift
# drop return values
eval "$out_var=()"
local IFS
local trap_sig
local stack_var
local stack_arr
local trap_cmdline
local trap_prev_cmdline
local i
i=0
IFS=$' \t\r\n'; for trap_sig in "$@"; do
stack_var="_traplib_stack_${trap_sig}_cmdline"
declare -a "stack_arr=(\"\${$stack_var[@]}\")"
if (( ${#stack_arr[@]} )); then
for trap_cmdline in "${stack_arr[@]}"; do
declare -a "trap_prev_cmdline=(\"\${$out_var[i]}\")"
if [[ -n "$trap_prev_cmdline" ]]; then
eval "$out_var[i]=\"\$trap_cmdline; \$trap_prev_cmdline\"" # the last srored is the first executed
else
eval "$out_var[i]=\"\$trap_cmdline\""
fi
done
else
# use the signal current trap command line
declare -a "trap_cmdline=(`trap -p "$trap_sig"`)"
eval "$out_var[i]=\"\${trap_cmdline[2]}\""
fi
(( i++ ))
done
}
function PushTrap()
{
# drop return values
EXIT_CODES=()
RETURN_VALUES=()
local cmdline="$1"
[[ -z "$cmdline" ]] && return 0 # nothing to push
shift
local IFS
local trap_sig
local stack_var
local stack_arr
local trap_cmdline_size
local prev_cmdline
IFS=$' \t\r\n'; for trap_sig in "$@"; do
stack_var="_traplib_stack_${trap_sig}_cmdline"
declare -a "stack_arr=(\"\${$stack_var[@]}\")"
trap_cmdline_size=${#stack_arr[@]}
if (( trap_cmdline_size )); then
# append to the end is equal to push trap onto stack
eval "$stack_var[trap_cmdline_size]=\"\$cmdline\""
else
# first stack element is always the trap current command line if not empty
declare -a "prev_cmdline=(`trap -p $trap_sig`)"
if (( ${#prev_cmdline[2]} )); then
eval "$stack_var=(\"\${prev_cmdline[2]}\" \"\$cmdline\")"
else
eval "$stack_var=(\"\$cmdline\")"
fi
fi
# update the signal trap command line
GetTrapCmdLine "$trap_sig"
trap "${RETURN_VALUES[0]}" "$trap_sig"
EXIT_CODES[i++]=$?
done
}
function PopTrap()
{
# drop return values
EXIT_CODES=()
RETURN_VALUES=()
local IFS
local trap_sig
local stack_var
local stack_arr
local trap_cmdline_size
local trap_cmd_line
local i
i=0
IFS=$' \t\r\n'; for trap_sig in "$@"; do
stack_var="_traplib_stack_${trap_sig}_cmdline"
declare -a "stack_arr=(\"\${$stack_var[@]}\")"
trap_cmdline_size=${#stack_arr[@]}
if (( trap_cmdline_size )); then
(( trap_cmdline_size-- ))
RETURN_VALUES[i]="${stack_arr[trap_cmdline_size]}"
# unset the end
unset $stack_var[trap_cmdline_size]
(( !trap_cmdline_size )) && unset $stack_var
# update the signal trap command line
if (( trap_cmdline_size )); then
GetTrapCmdLineImpl trap_cmd_line "$trap_sig"
trap "${trap_cmd_line[0]}" "$trap_sig"
else
trap "" "$trap_sig" # just clear the trap
fi
EXIT_CODES[i]=$?
else
# nothing to pop
RETURN_VALUES[i]=""
fi
(( i++ ))
done
}
function PopExecTrap()
{
# drop exit codes
EXIT_CODES=()
local IFS=$' \t\r\n'
PopTrap "$@"
local cmdline
local i
i=0
IFS=$' \t\r\n'; for cmdline in "${RETURN_VALUES[@]}"; do
# execute as function and store exit code
eval "function _traplib_immediate_handler() { $cmdline; }"
_traplib_immediate_handler
EXIT_CODES[i++]=$?
unset _traplib_immediate_handler
done
}
fi
测试.sh
#/bin/bash
source ./traplib.sh
function Exit()
{
echo exitting...
exit $@
}
pushd ".." && {
PushTrap "echo popd; popd" EXIT
echo 111 || Exit
PopExecTrap EXIT
}
GetTrapCmdLine EXIT
echo -${RETURN_VALUES[@]}-
pushd ".." && {
PushTrap "echo popd; popd" EXIT
echo 222 && Exit
PopExecTrap EXIT
}
用法
cd ~/test
./test.sh
输出
~ ~/test
111
popd
~/test
--
~ ~/test
222
exitting...
popd
~/test
没有办法让同一个陷阱有多个处理程序,但是同一个处理程序可以做很多事情。
在做同样事情的各种其他答案中,我不喜欢的一件事是使用字符串操作来获取当前的陷阱函数。有两种简单的方法可以做到这一点:数组和参数。参数是最可靠的,但我将首先显示数组。
When using arrays, you rely on the fact that trap -p SIGNAL
returns trap -- ??? SIGNAL
, so whatever is the value of ???
, there are three more words in the array.
Therefore you can do this:
declare -a trapDecl
trapDecl=($(trap -p SIGNAL))
currentHandler="${trapDecl[@]:2:${#trapDecl[@]} - 3}"
eval "trap -- 'your handler;'${currentHandler} SIGNAL"
So let's explain this. First, variable trapDecl
is declared as an array. If you do this inside a function, it will also be local, which is convenient.
Next we assign the output of trap -p SIGNAL
to the array. To give an example, let's say you are running this after having sourced osht (unit testing for shell), and that the signal is EXIT
. The output of trap -p EXIT
will be trap -- '_osht_cleanup' EXIT
, so the trapDecl
assignment will be substituted like this:
trapDecl=(trap -- '_osht_cleanup' EXIT)
The parenthesis there are normal array assignment, so trapDecl
becomes an array with four elements: trap
, --
, '_osht_cleanup'
and EXIT
.
Next we extract the current handler -- that could be inlined in the next line, but for explanation's sake I assigned it to a variable first. Simplifying that line, I'm doing this: currentHandler="${array[@]:offset:length}"
, which is the syntax used by Bash to say pick length
elements starting at element offset
. Since it starts counting from 0
, number 2
will be '_osht_cleanup'
. Next, ${#trapDecl[@]}
is the number of elements inside trapDecl
, which will be 4 in the example. You subtract 3 because there are three elements you don't want: trap
, --
and EXIT
. I don't need to use $(...)
around that expression because arithmetic expansion is already performed on the offset
and length
arguments.
The final line performs an eval
, which is used so that the shell will interpret the quoting from the output of trap
. If we do parameter substitution on that line, it expands to the following in the example:
eval "trap -- 'your handler;''_osht_cleanup' EXIT"
Do not be confused by the double quote in the middle (''
). Bash simply concatenates two quotes strings if they are next to each other. For example, '1'"2"'3''4'
is expanded to 1234
by Bash. Or, to give a more interesting example, 1" "2
is the same thing as "1 2"
. So eval takes that string and evaluates it, which is equivalent to executing this:
trap -- 'your handler;''_osht_cleanup' EXIT
And that will handle the quoting correctly, turning everything between --
and EXIT
into a single parameter.
To give a more complex example, I'm prepending a directory clean up to the osht handler, so my EXIT
signal now has this:
trap -- 'rm -fr '\''/var/folders/7d/qthcbjz950775d6vn927lxwh0000gn/T/tmp.CmOubiwq'\'';_osht_cleanup' EXIT
If you assign that to trapDecl
, it will have size 6 because of the spaces on the handler. That is, 'rm
is one element, and so is -fr
, instead of 'rm -fr ...'
being a single element.
But currentHandler
will get all three elements (6 - 3 = 3), and the quoting will work out when eval
is run.
Arguments just skips all the array handling part and uses eval
up front to get the quoting right. The downside is that you replace the positional arguments on bash, so this is best done from a function. This is the code, though:
eval "set -- $(trap -p SIGNAL)"
trap -- "your handler${3:+;}${3}" SIGNAL
The first line will set the positional arguments to the output of trap -p SIGNAL
. Using the example from the Arrays section, $1
will be trap
, $2
will be --
, $3
will be _osht_cleanup
(no quotes!), and $4
will be EXIT
.
The next line is pretty straightforward, except for ${3:+;}
. The ${X:+Y}
syntax means "output Y
if the variable X
is unset or null". So it expands to ;
if $3
is set, or nothing otherwise (if there was no previous handler for SIGNAL
).
If all the signal handling functions are known at the same time, then the following is sufficient (has said by Jonathan):
trap 'handler1;handler2;handler3' EXIT
Else, if there is an existing handler(s) that should stay, then new handlers can easily be added like this:
trap "$( trap -p EXIT | cut -f2 -d \' );newHandler" EXIT
If you don't know if there are existing handlers but want to keep them in this case, do the following:
handlers="$( trap -p EXIT | cut -f2 -d \' )"
trap "${handlers}${handlers:+;}newHandler" EXIT
trap-add() {
local sig="${2:?Signal required}"
hdls="$( trap -p ${sig} | cut -f2 -d \' )";
trap "${hdls}${hdls:+;}${1:?Handler required}" "${sig}"
}
export -f trap-add
Usage:
trap-add 'echo "Bye bye"' EXIT
trap-add 'echo "See you next time"' EXIT
Remark : This works only as long as the handlers are function names, or simple instructions that did not contain any simple code (simple code conflicts with cut -f2 -d \'
).
I add a slightly more robust version of Laurent Simon's trap-add
script:
'
characterssed
instead of bash pattern substitution, but that would make it significantly slower.trap-add () {
local handler=$(trap -p "$2")
handler=${handler/trap -- \'/} # /- Strip `trap '...' SIGNAL` -> ...
handler=${handler%\'*} # \-
handler=${handler//\'\\\'\'/\'} # <- Unquote quoted quotes ('\'')
trap "${handler} $1;" "$2"
}
I would like to propose my solution of multiple trap functions for simple scripts
# Executes cleanup functions on exit
function on_exit {
for FUNCTION in $(declare -F); do
if [[ ${FUNCTION} == *"_on_exit" ]]; then
>&2 echo ${FUNCTION}
eval ${FUNCTION}
fi
done
}
trap on_exit EXIT
function remove_fifo_on_exit {
>&2 echo Removing FIFO...
}
function stop_daemon_on_exit {
>&2 echo Stopping daemon...
}
Just like to add my simple version as an example.
trap -- 'echo "Version 1";' EXIT;
function add_to_trap {
local -a TRAP;
# this will put parts of trap command into TRAP array
eval "TRAP=($(trap -p EXIT))";
# 3rd field is trap command. Concat strings.
trap -- 'echo "Version 2"; '"${TRAP[2]}" EXIT;
}
add_to_trap;
If this code is run, will print:
Version 2
Version 1
Here's how I usually do it. It's not much different from what other people have suggested here but my version seems dramatically simpler and so far it always worked as desired for me.
Somewhere in the code, set a trap:
trap "echo Hello" EXIT
and later on update it:
oldTrap=$(trap -p EXIT)
oldTrap=${oldTrap#"trap -- '"}
oldTrap=${oldTrap%"' EXIT"};
trap "$oldTrap; echo World" EXIT
finally, on exit
Hello
World
In this answer I implemented a simple solution. Here I implement another solution that is based on extracting of previous trap commands from trap -p
output. But I don't know how much it is portable because I'm not sure that trap -p
output is regulated. Maybe its format can be changed in future (but I doubt that).
trap_add()
{
local new="$1"
local sig="$2"
# Get raw trap output.
local old="$(trap -p "$sig")"
# Extract previous commands from raw trap output.
old="${old#*\'}" # Remove first ' and everything before it.
old="${old%\'*}" # Remove last ' and everything after it.
old="${old//\'\\\'\'/\'}" # Replace every '\'' (escaped ') to just '.
# Combine new and old commands. Separate them by newline.
trap -- "$new
$old" "$sig"
}
trap_add 'echo AAA' EXIT
trap_add '{ echo BBB; }' EXIT
But this solution doesn't work well with subshells. Unfortunately trap -p
prints commands of outer shell. And we execute them in subshell after extracting.
trap_add 'echo AAA' EXIT
( trap_add 'echo BBB' EXIT; )
In the above example echo AAA
is executed twice: first time in subshell and second time in outer shell.
We have to check whether we are in new subshell and if we are then we must not take commands from trap -p
.
trap_add()
{
local new="$1"
# Avoid inheriting trap commands from outer shell.
if [[ "${trap_subshell:-}" != "$BASH_SUBSHELL" ]]; then
# We are in new subshell, don't take commands from outer shell.
trap_subshell="$BASH_SUBSHELL"
local old=
else
# Get raw trap output.
local old="$(trap -p EXIT)"
# Extract previous commands from trap output.
old="${old#*\'}" # Remove first ' and everything before it.
old="${old%\'*}" # Remove last ' and everything after it.
old="${old//\'\\\'\'/\'}" # Replace every '\'' (escaped ') to just '.
fi
# Combine new and old commands. Separate them by newline.
trap -- "$new
$old" EXIT
}
Note that to avoid security issue you have to reset the trap_subshell
variable at script startup.
trap_subshell=
Unfortunately the solution above works only with EXIT signal now. A generic solution that works with any signal is below.
# Check if argument is number.
is_num()
{
[ -n "$1" ] && [ "$1" -eq "$1" ] 2>/dev/null
}
# Convert signal name to signal number.
to_sig_num()
{
if is_num "$1"; then
# Signal is already number.
kill -l "$1" >/dev/null # Check that signal number is valid.
echo "$1" # Return result.
else
# Convert to signal number.
kill -l "$1"
fi
}
trap_add()
{
local new="$1"
local sig="$2"
local sig_num
sig_num=$(to_sig_num "$sig")
# Avoid inheriting trap commands from outer shell.
if [[ "${trap_subshell[$sig_num]:-}" != "$BASH_SUBSHELL" ]]; then
# We are in new subshell, don't take commands from outer shell.
trap_subshell[$sig_num]="$BASH_SUBSHELL"
local old=
else
# Get raw trap output.
local old="$(trap -p "$sig")"
# Extract previous commands from trap output.
old="${old#*\'}" # Remove first ' and everything before it.
old="${old%\'*}" # Remove last ' and everything after it.
old="${old//\'\\\'\'/\'}" # Replace every '\'' (escaped ') to just '.
fi
# Combine new and old commands. Separate them by newline.
trap -- "$new
$old" "$sig"
}
trap_subshell=
trap_add 'echo AAA' EXIT
trap_add '{ echo BBB; }' 0 # The same as EXIT.
A special case of Richard Hansen's answer (great idea). I usually need it for EXIT traps. In such case:
extract_trap_cmd() { printf '%s\n' "${3-}"; }
get_exit_trap_cmd() {
eval "extract_trap_cmd $(trap -p EXIT)"
}
...
trap "echo '1 2'; $(get_exit_trap_cmd)" EXIT
...
trap "echo '3 4'; $(get_exit_trap_cmd)" EXIT
Or this way, if you will:
add_exit_trap_handler() {
trap "$1; $(get_exit_trap_cmd)" EXIT
}
...
add_exit_trap_handler "echo '5 6'"
...
add_exit_trap_handler "echo '7 8'"
Always assuming I remember to pass multiple code snippets in semi-colon delimited fashion (as per bash(1)s' requirement for multiple commands on a single line, it's rare that the following (or something similar to it) fails to fulfil my meagre requirements...
extend-trap() {
local sig=${1:?'Der, you forgot the sig!!!!'}
shift
local code s ; while IFS=\' read code s ; do
code="$code ; $*"
done < <(trap -p $sig)
trap "$code" $sig
}
My humble contrib:
#!/bin/bash
# global vars
TRAPS=()
# functions
function add_exit_trap() { TRAPS+=("$@") }
function execute_exit_traps() {
local I
local POSITIONS=${!TRAPS[@]} # retorna os índices válidos do array
for I in $POSITIONS
do
echo "executing TRAPS[$I]=${TRAPS[I]}"
eval ${TRAPS[I]}
done
}
# M A I N
LOCKFILE="/tmp/lock.me.1234567890"
touch $LOCKFILE
trap execute_exit_traps EXIT
add_exit_trap "rm -f $LOCKFILE && echo lock removed."
add_exit_trap "ls -l $LOCKFILE"
add_exit_trap "echo END"
echo "showing lockfile..."
ls -l $LOCKFILE
add_exit_trap()
keeps adding strings (commands) to a bash global array while
execute_exit_traps()
just loops thru that array and eval the commands
Executed script...
showing lockfile...
-rw-r--r--. 1 root root 0 Mar 24 10:08 /tmp/lock.me.1234567890
executing TRAPS[0]=rm -f /tmp/lock.me.1234567890 && echo lock removed.
lock removed.
executing TRAPS[1]=ls -l /tmp/lock.me.1234567890
ls: cannot access /tmp/lock.me.1234567890: No such file or directory
executing TRAPS[2]=echo END
END
Another solution, which executes all functions starting with the name trap_
:
trap 'eval $(declare -F | grep -oP "trap_[^ ]+" | tr "\n" ";")' EXIT
Now simply add as many trap functions as you like and they will be all executed:
# redirect output and errors to log file
exec 3>&1 4>&2
exec 1>> "/var/log/scripts/${0//\//_}.log" 2>&1
trap_reset_std() { exec 2>&4 1>&3; }
# make script race condition safe
[[ -d "/tmp/${0//\//_}" ]] || ! mkdir "/tmp/${0//\//_}" && echo "Script is already running!" && exit 1
trap_remove_lock() { rmdir "/tmp/${0//\//_}"; }
Note: trap_remove_lock()
in this example is not executed if condition exit 1
is met (which is what we want).
A simple solution is to save commands for trap to variable and, when adding new trap, to restore them from that variable.
trap_add()
{
# Combine new and old commands. Separate them by newline.
trap_cmds="$1
$trap_cmds"
trap -- "$trap_cmds" EXIT
}
trap_add 'echo AAA'
trap_add '{ echo BBB; }'
Unfortunately this solution does not work well with subshells, because subshell inherits outer shell variables and thus outer shell trap commands are executed in subshell.
trap_add 'echo AAA'
( trap_add 'echo BBB'; )
In the above example echo AAA
is executed twice: first time in subshell and second time in outer shell.
We have to check whether we are in new subshell and if we are then we must not take commands from the trap_cmds
variable.
trap_add()
{
# Avoid inheriting trap commands from outer shell.
if [[ "${trap_subshell:-}" != "$BASH_SUBSHELL" ]]; then
# We are in new subshell, don't take commands from outer shell.
trap_subshell="$BASH_SUBSHELL"
trap_cmds=
fi
# Combine new and old commands. Separate them by newline.
trap_cmds="$1
$trap_cmds"
trap -- "$trap_cmds" EXIT
}
Note that to avoid security issue you have to reset the used variables at script startup.
trap_subshell=
trap_cmds=
Otherwise someone who runs your script can inject their malicious commands via environment variables.
export trap_subshell=0
export trap_cmds='echo "I hacked you"'
./your_script
Generic version that works with arbitrary signals is below.
# Check if argument is number.
is_num()
{
[ -n "$1" ] && [ "$1" -eq "$1" ] 2>/dev/null
}
# Convert signal name to signal number.
to_sig_num()
{
if is_num "$1"; then
# Signal is already number.
kill -l "$1" >/dev/null # Check that signal number is valid.
echo "$1" # Return result.
else
# Convert to signal number.
kill -l "$1"
fi
}
trap_add()
{
local cmd sig sig_num
cmd="$1"
sig="$2"
sig_num=$(to_sig_num "$sig")
# Avoid inheriting trap commands from outer shell.
if [[ "${trap_subshell[$sig_num]:-}" != "$BASH_SUBSHELL" ]]; then
# We are in new subshell, don't take commands from outer shell.
trap_subshell[$sig_num]="$BASH_SUBSHELL"
trap_cmds[$sig_num]=
fi
# Combine new and old commands. Separate them by newline.
trap_cmds[$sig_num]="$cmd
${trap_cmds[$sig_num]}"
trap -- "${trap_cmds[$sig_num]}" $sig
}
trap_subshell=
trap_cmds=
trap_add 'echo AAA' EXIT
trap_add '{ echo BBB; }' 0 # The same as EXIT.
PS In this answer I implemented another solution that gets previous commands from trap -p
output.