summaryrefslogtreecommitdiff
path: root/docs/error-handling/subshell/repeated-printing.md
blob: b0a38fe4808b4e2436045404a8992287219ba524 (plain)
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
85
86
# 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).
    * Note, though, that there may be several assignments on a single line,
      and thus within `$BASH_COMMAND`.