summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/error-handling.md112
-rw-r--r--docs/error-handling/subshell/repeated-printing.md84
-rw-r--r--docs/subshell.md8
3 files changed, 204 insertions, 0 deletions
diff --git a/docs/error-handling.md b/docs/error-handling.md
new file mode 100644
index 0000000..483123f
--- /dev/null
+++ b/docs/error-handling.md
@@ -0,0 +1,112 @@
+# Error handling
+
+* A central feature of `basic.bash` is to exit (non-zero) on error and provide
+ a useful error message, namely a stack trace.
+ * In many cases, the failing command will additionally print an error
+ message on its own, which should be printed above the stack trace.
+* Manual error handling is still best, but often too time consuming when
+ calling many external commands and bash builtins that may fail, as is common
+ with shell scripts (unless they are very small).
+
+
+## Limitations
+
+### Backgrounded process
+
+* An error in a backgrounded process (e.g., `false &`) is ignored by the
+ calling shell.
+* Failures within, e.g., `{ false; } &` will cause a stack trace to be
+ printed, but the calling shell will not terminate.
+* It may be a good idea to use `wait` to check on the return status.
+* It may further be a good idea to redefine the trap on `ERR` within a
+ backgrounded process to not print a stack trace.
+
+
+### Subshell
+
+* Some errors in [subshells](subshell.md) are not caught (properly):
+ * `declare var=$(false)`
+ * Instead, write: `declare var; var=$(false)`
+ * `true $(false)`
+ * Workaround: Use an intermediate variable.
+ * `true <(false)`, `true >(false)`
+ * Workaround: Use a pipe where possible.
+ * See also: [Pipes](#Pipes)
+ * Workaround: Use a temporary file.
+ * See also: [Backgrounded process](#Backgrounded_process)
+
+
+### Unset variables
+
+* Unset arrays (when accessed with subscript `@` or `*`) are never treated as
+ an error.
+ * To manually check for a *non-empty* array: `test -v var[@]`
+ * To manually check for an unset array, `declare -p var` and/or `${var@a}`
+ may be helpful.
+ * To manually check whether a variable is declared as an array,
+ `declare -p var` and/or `${var@a}` may be helpful.
+ * The latter will fail (due to `set -o nounset`) if `$var` is empty
+ or unset.
+ * `${var[@]@a}` and `${var[*]@a}` behave weirdly:
+ * If `$var` is undeclared, evaluates to the empty array/string.
+ * If `$var` is declared as an array, but unset, evaluates to `a`.
+ * If `$var` is set to the empty array, evaluates to the empty
+ array/string.
+ * If `$var` is set to a non-empty array, evaluates to the array
+ (or space-separated list in case of `${var[*]@a}`) of as many
+ `a` as the array is long.
+* An error on an unset variable (due to `set -o nounset`) does not cause a
+ stack trace to be printed in the current [(sub-)](subshell.md)shell, but
+ only a simple message mentioning the file and line number.
+ * If there is no subshell involved, no stack trace is printed at all.
+ * Note that unset variable errors are also caught in
+ `declare var=$undef_var` (compare [Subshell section](#Subshell)).
+ * This is arguably not a big problem; such errors should mostly be quickly
+ spotted by simple testing (or some static analysis tool).
+ * Exceptions: Usage of `declare -n` and `eval` with dynamic variable
+ names.
+
+
+### Pipes
+
+* A pipeline is considered an error iff the last command returns non-zero.
+* A different behaviour can be achieved by `set -o pipefail`.
+ * `basic.bash` deliberately does not set this. See there for details.
+* To catch errors in a non-last command of a pipeline, one should either
+ consult the `${PIPESTATUS[@]}` array variable, or `set -o pipefail` in a
+ subshell.
+* Oftentimes, avoiding pipes may be the best option.
+
+
+### Conditionals
+
+* A command that is evaluated as a condition (e.g., `if cmd; then ...; fi`),
+ is never considered an error as the return code is instead used as a boolean
+ condition.
+* In some cases it may be necessary for proper error reporting to do something
+ like the following:
+ ```
+ if cmd
+ ...
+ else
+ then
+ if [[ $? -ne 1 ]]
+ then
+ return 1
+ fi
+ ...
+ fi
+ ```
+* See also: [Pipes](#Pipes)
+
+
+### Other use of `stderr`
+
+* The `basic.bash` library assumes that `stderr` is never redirected, except
+ directly from external commands or shell builtins (e.g., `var=$(cmd 2>&1)`).
+
+
+## Annoyances
+
+* With [subshells](subshell.md), there may be
+ [multiple stack traces printed](error-handling/subshell/repeated-printing.md).
diff --git a/docs/error-handling/subshell/repeated-printing.md b/docs/error-handling/subshell/repeated-printing.md
new file mode 100644
index 0000000..fa94a1b
--- /dev/null
+++ b/docs/error-handling/subshell/repeated-printing.md
@@ -0,0 +1,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).
diff --git a/docs/subshell.md b/docs/subshell.md
new file mode 100644
index 0000000..319e629
--- /dev/null
+++ b/docs/subshell.md
@@ -0,0 +1,8 @@
+# Subshell
+
+* A subshell may be created by any of the following:
+ * `(.)`
+ * command substitution: `$(.)`, `` `.` ``
+ * process substitution: `<(.)`, `>(.)`
+ * `bash(1)` does not talk of a subshell here, but this seems to work
+ similarly to `$(.)`.