Shell Scripting Advanced — SIGTERM Trap Leaves Temp Files
Kubernetes sends SIGTERM on pod shutdown, not SIGINT — failing to trap it leaves /tmp littered.
20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.
- ✓Production DevOps experience
- ✓Deep understanding of the tool's internals
- ✓Experience debugging distributed systems
- Process substitution (<(cmd)) feeds command output as a file without temp files
- Signal traps (trap 'handler' SIGTERM) catch OS signals to run cleanup
- Subshells ((cmd)) run commands in isolated environments — variables don't leak
- File descriptor management (exec 3>file) prevents descriptor leaks and race conditions
- Performance insight: process substitution avoids disk I/O, but forks per substitution
- Production insight: missing trap leaves zombie processes and broken locks on container restart
- Biggest mistake: assuming wait in a trap works — it deadlocks in signal context
Imagine your shell script is a factory floor manager. Basic scripts just shout instructions one at a time. Advanced scripting is like giving that manager a walkie-talkie, a panic button, a set of private offices for side conversations, and a logbook that writes itself — all at once. Process substitution lets two workers share data without leaving paper on the floor. Signal traps are the emergency stop button. Subshells are the private offices where experiments happen without disturbing the main floor.
Every DevOps engineer has hit the same wall: a shell script that works beautifully on a laptop but silently corrupts data in production, leaves zombie processes behind after a Kubernetes pod restart, or races itself when two cron jobs fire at the same millisecond. That wall is not a Bash limitation — it's the gap between scripting and engineering. The difference is understanding what the shell is actually doing beneath the syntax.
Shell scripts fail in production for three predictable reasons: they don't handle signals (so cleanup never runs when a container dies), they mismanage file descriptors (so logs get garbled or pipes deadlock), and they make assumptions about subshell variable scope (so a loop that 'obviously' increments a counter does nothing). These aren't beginner mistakes — senior engineers hit them too, because they only surface under specific timing conditions or OS configurations.
By the end of this article you'll be able to write scripts that trap and handle SIGTERM gracefully, use process substitution to diff two live command outputs without temp files, manage file descriptors explicitly to prevent descriptor leaks, implement advisory locking to prevent concurrent runs, and structure a production-grade script with a proper exit framework. These are the patterns that make the difference between a script you trust at 3 AM and one you babysit.
What Shell Scripting Advanced Really Means
Advanced shell scripting is the discipline of writing robust, production-grade Bash programs that handle signals, manage resources, and compose complex workflows without leaking state. The core mechanic is explicit control over process lifecycle — trapping SIGTERM, SIGINT, and EXIT to clean up temporary files, release locks, or roll back partial operations. Without these traps, a script killed mid-flight leaves corrupted data and orphaned resources.
In practice, advanced scripting relies on three properties: idempotent cleanup routines, atomic file operations (e.g., mv over write), and strict error handling via set -euo pipefail. A trap on EXIT guarantees cleanup even on unexpected termination, but only if the trap handler itself is idempotent and fast — a slow trap can delay process shutdown and cause cascading failures in orchestrated environments like Kubernetes.
Use advanced patterns when your script manages state beyond its own process — creating temp files, acquiring locks, or modifying shared filesystems. In CI/CD pipelines, cron jobs, or container entrypoints, a missing trap is the difference between a clean retry and a silent corruption that surfaces hours later. This is not about elegance; it's about survival under real-world conditions.
Process Substitution: How It Works and Where It Breaks
Process substitution (<(command)) lets you pass the output of a command as if it were a file. It's syntactic sugar for a temporary named pipe managed by the shell. Use it when you need to feed the result of a command into something that expects a file argument — like diff, comm, or paste.
The shell creates a file descriptor backed by a pipe, then substitutes the path /dev/fd/N in the command line. The command reads from that descriptor. No temp file hits disk, no separate process to manage.
But process substitution only works in bash, zsh, and ksh — not in dash or sh. Portable scripts must fall back to temp files or explicit pipes. Also, each substitution forks a child process. Heavy use can exhaust process limits.
<(cmd) as a temporary garden hose that connects the output of one command to the input hole of another — no buckets (temp files) needed.- The shell creates a pipe, forks a child, and writes child's stdout to one end.
- It substitutes the path
/dev/fd/Nat the command line — ordinary file operations apply. - The command runs concurrently with the parent — no buffering until read completes.
- The descriptor is automatically cleaned when both sides finish.
- Unlike
|, process substitution works in argument positions — not just stdin.
Signal Traps: The Cleanup That Never Runs
trap registers a command or function to run when the shell receives a signal or exits. The most common production mistake is trapping only SIGINT and forgetting SIGTERM. Kubernetes, Docker, and systemd all send SIGTERM by default when they want a process to stop gracefully.
An even subtler trap: trapping a signal inside a function scope. The trap is global — once set, it applies to the entire shell session. If multiple scripts source a shared library, traps can collide. Always reset traps at the start of your function and restore them afterward.
Another trap: using wait inside a trap handler. Signals are blocked while the trap runs, so wait may return immediately without waiting for children. Use a polling loop instead.