1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
|
# Problem: Repeated printing of (parts of) the stack trace
## Gist
* With subshells, several stack traces may be printed.
* All but the first stack trace can be ignored.
## Problematic behaviour
* For each subshell in the current stack of subshells (including the root
shell), we get a stack trace.
* The `ERR` trap is caught for each subshell.
* Reason: The trap on `ERR` returns non-zero, and so does the subshell.
* If it returned zero, the error would be ignored on the upper level,
and program execution continue, which is undesired.
* More precisely, each subshell gives a stack trace on the stack from itself
up to the root shell.
* Reason: Subshells inherit the knowledge of its ancestors, but do not
know of its descendant subshells.
* Thus, the closer to the root of the stack, the more often we get the
(same) information printed; while the information on where the error
originally occurred is printed only once.
## Desired behaviour
* The whole stack trace is printed once, and nothing more.
* This would be achieved if only the subshell where the error originally
occurred were to print a stack trace.
## Considerations on fixes / improvements
### Only print the stack trace for the root shell.
* This is easy, just check for `[[ $BASH_SUBSHELL -eq 0 ]]`.
* This would mean that on error within a subshell, we do not get
information below where the first subshell was invoked.
* This may be deemed acceptable if subshells are rarely used and/or only
with short local code within.
* Short local code would be, e.g., `$(head -n 1 FILE)`.
* Short local code would not be, e.g., `$(local_nontrivial_function)`.
### Reserve a special exit code.
* Let `$SPECIAL_EXIT_CODE` be some exit code distinct from `0` and `1`.
* Change `ERR` trap to `trap 'basic::on_error 0' ERR`, and define `on_error()`
as follows:
```
function basic::on_error()
{
[[ $? -eq $SPECIAL_EXIT_CODE ]] && exit 1
local -ri offset="$1"
basic::print_stacktrace $((offset + 1))
exit $SPECIAL_EXIT_CODE
}
```
* The idea is that `on_error()` only prints the stack trace in the lowest
subshell, which has the full stack trace.
* Naturally, this does not work properly if the original actual error had
`$SPECIAL_EXIT_CODE` as exit code.
* That is, we'd need an exit code that cannot occur anywhere else.
* This should be impossible in the general case.
### Use a temporary file
* Use a temporary file to indicate whether `print_stacktrace()` was already
called in a subshell.
* This feels somewhat evil.
### Inspect `$BASH_COMMAND`
* The `$BASH_COMMAND` variable contains the command executed that caused the
trap.
* We'd have to identify whether the command spawned a subshell (and the error
came from there).
* Note that `(` is a valid command name---but `$BASH_COMMAND` maintains any
necessary quoting.
* It might suffice to check for `(.*` and `${varname_regex}=\$(.*`, given the
[other self-imposed restrictions](../../error-handling.md#Subshell).
|