blog

git clone https://git.ce9e.org/blog.git

commit
dc4e461d334f9f38a33dd1614feaab6ea8ea2969
parent
25b82f88cb7d2a89263b9a5b34762f7929f72b20
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2026-07-17 11:42
rework trap post

Diffstat

M _content/posts/2026-07-13-trap/index.md 47 +++++++++++++++++++++--------------------------

1 files changed, 21 insertions, 26 deletions


diff --git a/_content/posts/2026-07-13-trap/index.md b/_content/posts/2026-07-13-trap/index.md

@@ -2,7 +2,7 @@
    2     2 title: Using trap for cleanup in shell scripts
    3     3 date: 2026-07-13
    4     4 tags: [linux]
    5    -1 description: "trap … EXIT does not trigger if the script is terminated by a signal"
   -1     5 description: "trap … EXIT does not always trigger if the script is terminated by a signal"
    6     6 ---
    7     7 
    8     8 It is great when programming languages offer robust ways to cleanup resources.
@@ -36,7 +36,7 @@ There are quite a few signals that are supposed to terminate a process:
   36    36 SIGHUP, SIGINT, SIGQUIT, SIGKILL, and SIGTERM. The differences between them are
   37    37 [not completely obvious](https://stackoverflow.com/questions/4042201). For this
   38    38 article, I will concentrate on SIGINT and SIGTERM, because they are the most
   39    -1 common ones. SIGINT is the one that is sent when you press `Ctrl-C`.`
   -1    39 common ones. SIGINT is the one that is sent when you press `Ctrl-C`.
   40    40 
   41    41 trap's action argument is given as a string, which makes quoting a bit awkward.
   42    42 For example `trap "echo '$foo'" TERM` and `trap 'echo "$foo"' TERM` are
@@ -73,24 +73,22 @@ see how it behaves in practice:
   73    73 Bash is closest to what I expect: EXIT should mean *every* exit, including
   74    74 signals. So how can we get that behavior in all shells?
   75    75 
   76    -1 The proper solution is probably to have a separate trap for each relevant
   77    -1 signal that removes the trap, calls that cleanup code, and then re-raises the
   78    -1 signal. To avoid executing the cleanup code twice in bash we also need to reset
   79    -1 the EXIT handler:
   -1    76 A simple option is to convert the relevant signals to explicit `exit`:
   80    77 
   81    78 ```sh
   82    79 trap cleanup EXIT
   83    -1 trap 'cleanup; trap - INT; trap - EXIT; kill -INT $$'
   84    -1 trap 'cleanup; trap - TERM; trap - EXIT; kill -TERM $$'
   -1    80 trap 'exit 130' INT TERM
   85    81 ```
   86    82 
   87    -1 Another option is to convert the relevant signals to explicit `exit` with an
   88    -1 arbitrary exit code. While it loses out on any default behavior, it is much
   89    -1 simpler:
   -1    83 However, this fails to [communicate the signal to the parent
   -1    84 process](https://www.cons.org/cracauer/sigint.html). The proper solution is to
   -1    85 remove the handler, call that cleanup code, and then re-raises the signal. To
   -1    86 avoid executing the cleanup code twice in bash we also need to reset the EXIT
   -1    87 handler:
   90    88 
   91    89 ```sh
   92    90 trap cleanup EXIT
   93    -1 trap 'exit 130' INT TERM
   -1    91 trap 'trap - INT TERM EXIT; cleanup; kill -INT $$' INT TERM
   94    92 ```
   95    93 
   96    94 ## Multi-Stage Cleanup
@@ -101,29 +99,27 @@ a specific order, and unmount them in reverse order during cleanup. If any of
  101    99 these steps fail, we need to unmount only the locations that have already been
  102   100 mounted.
  103   101 
  104    -1 One approach I have used before is to have a mutable cleanup expression,
  105    -1 although I don't particularly like it because of the use of `eval`:
   -1   102 One approach I have used before (but don't particularly like it because of the
   -1   103 use of `eval`) is to have a mutable cleanup expression:
  106   104 
  107   105 ```sh
  108   106 cleanup='true'
  109   107 trap 'eval "$cleanup"' EXIT
  110    -1 trap 'exit 130' INT TERM
   -1   108 trap 'trap - EXIT INT TERM; eval "$cleanup"; kill -INT $$' INT TERM
  111   109 defer() {
  112   110     cleanup="$1; $cleanup"
  113   111 }
  114   112 ```
  115   113 
  116    -1 A flexible option that works consistently across the shells that I have tested
  117    -1 is using subshells:
   -1   114 A flexible option that works consistently across the shells that I tested is
   -1   115 using subshells:
  118   116 
  119   117 ```sh
  120   118 echo 'enter script'
  121   119 trap "echo 'exit script'" EXIT
  122    -1 trap 'exit 130' INT TERM
  123   120 (
  124   121     echo 'enter subshell'
  125   122     trap "echo 'exit subshell'" EXIT
  126    -1     trap 'exit 130' INT TERM
  127   123 )
  128   124 ```
  129   125 
@@ -133,11 +129,10 @@ support traps on the function level, so I didn't look into it any further.
  133   129 
  134   130 ## Conclusion
  135   131 
  136    -1 `trap … EXIT` is a very useful tool for doing cleanup in shell scripts.
  137    -1 Unfortunately, it is no universal “run on everything” hook.
   -1   132 `trap` is a very useful tool for doing cleanup in shell scripts. Unfortunately,
   -1   133 it does not provide a universal “run on everything” hook.
  138   134 
  139    -1 I do think that cleanup on signals such as SIGINT and SIGTERM is important.
  140    -1 Bash handles these cases as I would expect, but I do not want to restrict
  141    -1 myself to a single shell. On the other hand, I also do not want to
  142    -1 over-complicate things. `trap 'exit 130' INT TERM` seems to provide a good
  143    -1 balance of these different goals.
   -1   135 Cleanup on signals such as SIGINT and SIGTERM is important. Bash handles these
   -1   136 cases as I would expect, but I do not want to restrict myself to a single
   -1   137 shell. Implementing the correct behavior in a compatible way is much harder
   -1   138 than it should be.