Getting wrong $LINENO for a trapped function

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 '%s\n' 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)


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 '%s\n' 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 '%s\n' 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 of LINENO 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.

Tags:

Bash

Trap