Getting wrong $LINENO for a trapped function
I am writing a Bash script for myself to learn scripting. At some point, I need to add trap in order to clean unwanted directories and files if script is killed. However, for some reason I do not understand, trap calls the cleaning function – clean_a()
– when script killed but $LINENO
points to a line in the cleaning function itself, not int the function – archieve_it()
– when script is killed.
Expected behaviour:
- run script
- Press Ctrl+C
- trap caches Ctrl+C and calls
clean_a()
function clean_a()
function echoes the line number, which Ctrl+C is pressed. Let it be line 10 inarchieve_it()
.
What actually happens:
- run script
- Press Ctrl+C
- trap caches Ctrl+C and calls
clean_a()
function clean_a()
echoes an irrelevant line number. Say, line 25 inclean_a()
function.
Here is an example as a part of my script:
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"
}
P.S.: Original script can be seen here . Definitions and variable names are in Turkish. If it is required, I can translate anything to English.
EDIT: I change the script as best as I can do according to @mikeserv’s explanation like this:
#!/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
..."
}
Now, if I run script with set -x
and terminate it with Ctrl+C, it prints correct line number as can be seen below:
DDEBUG: 1 : clean_a 1 336 rsync '"${options}"' ...
However, in clean_a()
function, value of $LASTNO
is printed as 1.
line: 462 1
Does it have something to do with the bug that is shown by @Arkadiusz Drabczyk?
EDIT2: I changed script just like the way @mikesrv recommended to me. But $LASTNO returned 1 as the value of the line when script was terminated (it should have been 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
If I run script and terminate it with Ctrl+C while rsync was running, I get this output:
^^MDEBUG: 1 : clean_a '337 1 rsync "${options}" --delete-during ...
...
line: 1 465
As you can see, $LASTNO’s value is 1.
While I was trying to figure out what the problem is, I wrote another function – testing
– using parameter substitution format ${parameter:-default}
. So script turned out like this:
#!/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
Now, if I run script and press Ctrl+C, I get this output:
^^MDEBUG: 1 : testing '337 1 rsync "${options}" --delete-during ...
337 1 rsync "${options}" --delete-during ...
337 points out the line when I pressed Ctrl+C, while rsync was running.
For another test, I tried writing clear_a
funtion like this:
clear_a () {
echo -e " $LASTNO $LINENO"
}
and $LASTNO still returned 1.
So, this means that we can get correct line number when script terminated if we use parameter substitution?
EDIT3 It seems that I wrongly applied @mikeserv’s explanation in EDIT2. I corrected my mistake. Positional parameter "$1
should be replaced with $LASTNO in clear_a
funtion.
Here is the script which works how I want it to work:
#!/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
When the script is terminated, trap
evaluates $LASTNO
– first argument -, $LINENO
– second argument – and $BASH_COMMAND
-third argument -, then pass their values to the clear_a
function. Finally, we print $LASTNO with $1
as the line number which script is terminated.
I think the problem is that you’re expecting "$LINENO"
to give you the line of execution for the last command, which might almost work, but clean_a()
also gets its own $LINENO
and that you should do instead:
error "something!
line: $1
...
But even that probably wouldn’t work because I expect it will just print the line on which you set the trap
.
Here’s a little demo:
PS4='DEBUG: $LINENO : '
bash -x <<CMD
trap 'fn "$LINENO"' EXIT
fn() { printf %s\n "$LINENO" "$1"; }
echo "$LINENO"
CMD
OUTPUT
DEBUG: 1 : trap 'fn "$LINENO"' EXIT
DEBUG: 3 : echo 3
3
DEBUG: 1 : fn 1
DEBUG: 2 : printf '%sn' 2 1
2
1
So the trap
gets set, then, fn()
is defined, then echo
is executed. When the shell completes executing its input, the EXIT
trap is run and fn
is called. It is passed one argument – which is the trap
line’s $LINENO
. fn
prints first its own $LINENO
then its first argument.
I can think of one way you might get the behavior you expect, but it kinda screws up the shell’s stderr
:
PS4='DEBUG: $((LASTNO=$LINENO)) : '
bash -x <<CMD
trap 'fn "$LINENO" "$LASTNO"' EXIT
fn() { printf %s\n "$LINENO" "$LASTNO" "$@"; }
echo "$LINENO"
CMD
OUTPUT
DEBUG: 1 : trap 'fn "$LINENO" "$LASTNO"' EXIT
DEBUG: 3 : echo 3
3
DEBUG: 1 : fn 1 3
DEBUG: 2 : printf '%sn' 2 1 1 3
2
1
1
3
It uses the shell’s $PS4
debug prompt to define $LASTNO
on every line executed. It’s a current shell variable which you can access anywhere within the script. That means that no matter what line is currently being accessed, you can reference the most recent line of the script run in $LASTNO
. Of course, as you can see, it comes with debug output. You can push that to 2>/dev/null
for the majority of the script’s execution maybe, and then just 2>&1
in clean_a()
or something.
The reason you get 1
in $LASTNO
is because that is the last value to which $LASTNO
was set because that was the last $LINENO
value. You’ve got your trap
in the archieve_it()
function and so it gets its own $LINENO
as is noted in the spec below. Though it doesn’t appear that bash
does the right thing there anyway, so it may also be because the trap
has to re-exec the shell on INT
signal and $LINENO
is therefore reset. I’m a little fuzzy on that in this case – as is bash
, apparently.
You don’t want to evaluate $LASTNO
in clean_a()
, I think. Better would be to evaluate it in the trap
and pass the value trap
receives in $LASTNO
through to clean_a()
as an argument. Maybe like this:
#!/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
Try that – it should do what you want, I think. Oh – and note that in PS4=^M
the ^M
is a literal return – like CTRL+V ENTER.
From the POSIX shell spec:
Set by the shell to a decimal number representing the current sequential line number (numbered starting with 1) within a script or function before it executes each command. If the user unsets or resets
LINENO
, the variable may lose its special meaning for the life of the shell. If the shell is not currently executing a script or function, the value ofLINENO
is unspecified. This volume of IEEE Std 1003.1-2001 specifies the effects of the variable only for systems supporting the User Portability Utilities option.
mikeserv’s solution is good but the he’s incorrect in saying that fn
is passed the trap
line’s $LINENO
when the trap is executed. Insert a line before trap ...
and you will see that fn
is in fact always passed 1
, regardless where the trap was declared.
PS4='DEBUG: $LINENO : '
bash -x <<EOF
echo Foo
trap 'fn "$LINENO"' EXIT
fn() { printf %s\n "$LINENO" "$1"; }
echo "$LINENO"
exit
EOF
OUTPUT
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 '%sn' 3 1
3
1
Since the first argument to trap, fn "$LINENO"
, is put within single quotes, $LINENO
gets expanded, if and only when EXIT it triggered and should therefore expand to fn 5
. So why doesn’t it? In fact it did, up until bash-4.0 when it was deliberately changed so that $LINENO is reset to 1 when the trap is triggered, and therefore expands to fn 1
. [source] The original behavior is still maintained for ERR traps however, probably because how often something like trap 'echo "Error at line $LINENO"' ERR
is used.
#!/bin/bash
trap 'echo "exit at line $LINENO"' EXIT
trap 'echo "error at line $LINENO"' ERR
false
exit 0
OUTPUT
error at line 5
exit at line 1
tested with GNU bash, version 4.3.42(1)-release (x86_64-pc-linux-gnu)
Getting LINENO = 0
, instead of actual line number on script exit, can be fixed by trapping ERR
(instead of EXIT
).
In addition, add set -E
, to ensures that those ERR traps get inherited by functions, command substitutions, and subshell environments. For example, this is how to print the function name that that causes script error, and its line number:
set -eE
trap 'echo "Error in function $FUNCNAME at line $LINENO"' ERR
Credits to: https://citizen428.net/blog/bash-error-handling-with-trap/
this should do the job for the more recent versions of bash:
trap 'debug_line_old=$debug_line;debug_line=$LINENO' DEBUG
trap 'catch echo EXIT rc=$? line=$debug_line_old' EXIT
So, quite by accident, I stumbled upon a way to get bash to properly display the $LINENO variable (e.g., in a DEBUG or EXIT trap) and give the line number like you would expect. It is much simpler than the other posted methods and doesnt require hijacking PS4 and STDERR. Im not quite sure why this works, though would love to learn why if someone else can explain it.
Basically, wrap what you want to run in a dummy function (call it, say, _ff
), then run source <(declare -f _ff)
, and then run _ff
and any instances of $LINENO
in _ff
will actually give the line number.
EXAMPLE
_ff() (
trap 'echo "LINENO=$LINENO"' DEBUG;
echo a
echo b
echo c
echo d
echo e
echo f
echo g
)
source <(declare -f _ff)
Now, running _ff
will output
LINENO=2
a
LINENO=3
b
LINENO=4
c
LINENO=5
d
LINENO=6
e
LINENO=7
f
LINENO=8
g
Note: running source <(declare -f _ff)
is the crucial bit…if you dont then you will just get LINENO=1
for all the $LINENO
statements.
Side note: This question is old, but it is the only place I found basically anywhere on the internet to give a working workaround for showing the correct $LINENO outside of ERR traps. As such, it only seemed appropriate to post this here.