Scripting in Bash with set -euo pipefail, trap and logging

Last update: 17/12/2025
Author Isaac
  • Enabling `set -euo pipefail` and safe IFS allows bash scripts to fail early and avoid silent errors when handling exit codes and undefined variables.
  • Using traps for ERR and signals like INT or TERM makes it easier to record faults, perform controlled cleaning, and keep the system in a consistent state.
  • A centralized logging system with levels and timestamps improves traceability, debugging, and script integration in DevOps and CI/CD environments.
  • The combination of defensive programming, input validation, and good debugging practices makes bash scripts reliable in production.

What are binary or proprietary blobs in Linux?

Automating tasks with Bash makes all the difference between a system that requires you to be constantly monitoring it and one that simply... It works on its own and alerts you when something goes wrong.In DevOps and systems administration environments, good scripting is not just about chaining together commandsIt's about avoiding silent failures, being able to debug quickly, and leaving clear traces in logs to find out what happened and when.

When you start working seriously with scripts, you discover that Bash, by default, is quite permissive: if something goes wrong, the script usually to carry on as if nothing happened, using empty variables or incomplete resultsThat's where horror stories like truncated backups come from (for which it's advisable Sync with rsync on Linux), mass deletions due to poorly constructed routes or deployments that seemed correct but failed halfway through. That's why learning to use it properly makes so much sense. set -euo pipefail, trap, and a decent logging system.

Why Bash remains key in DevOps

In the day-to-day work of systems and DevOps, Bash remains the tool that is always there: on servers Linuxcontainers, CI/CD machines, maintenance scripts… Its biggest advantage is that You don't depend on installing runtimes or external librariesWith the shell that the system already includes, you can automate everything from health checks to complete deployments.

That power comes with a catch: with poorly written scripts, it's too easy for a command to fail and The rest of the script continues as if everything went perfectly.If you add critical tasks to that (backups, log rotation, file deletion, scheduled executions), the risk is not theoretical: sooner or later something will break without you even noticing.

That's why it's so important that, in addition to automating, you include it from the very beginning. error handling, debugging, and structured loggingAnd that's where three pieces come into play that are repeated in all good examples of scripting in modern Bash: set -euo pipefail, trap and a consistent logging system.

Practical example: monitoring script with strict error handling

A very useful pattern is to create a monitoring script that collects key system information (CPU, memory, disk, network, processes, recent logs…) and that also fail safely, leaving clear traces in a log fileThis type of script can be used manually, from cron, or to support CI/CD pipelines.

general idea The general idea is to have a script with:

  • Strict mode activated Use `set -euo pipefail` to fail early and not continue running on inconsistent states.
  • trap on ERR and signals like INT or TERM to record errors and clean up before leaving.
  • Centralized logging function that writes to console a file with timestamp and level (INFO, WARNING, ERROR).
  • Modular checkups CPU, memory, disk, network, and system logs.
  • Optional installation of dependencies such as sysstat, lm-sensors or net-tools, adapted to Debian/Ubuntu or RHEL/CentOS/Fedora.

usual In a script of this type, it is common to see something like:

  • Color variables to highlight warnings and errors in the terminal (RED, GREEN, YELLOW, BLUE, NC).
  • Routes like /var/log/system-monitor.log for general logs and /var/log/system-metrics.log for periodic metrics.
  • Configurable thresholds alert: for example, ALERT_CPU_THRESHOLD=80, ALERT_MEM_THRESHOLD=80, ALERT_DISK_THRESHOLD=85.
  • Un monitoring interval MONITOR_INTERVAL to repeat the checks every X seconds if you run the script in a loop.

Based on that foundation, a robust script is built that not only collects information, but also It reacts correctly to failures, leaves clear evidence, and can be integrated into larger processes..

Activate strict mode: set -euo pipefail and secure IFS

Most "weird" problems in Bash stem from the fact that the shell, by default, It does not consider it serious that a command fails or that a variable does not exist.To force safer behavior, what many people call "strict Bash mode" is often activated.

  Oregon 10 Linux: What it is, versions, requirements, and real benefits

The basic components of that strict mode are:

  • set -e: causes the script to terminate as soon as a command returns a non-0 exit code, except in some special contexts (if, while, &&, ||…).
  • set -u o set -o nounset: causes an error when you try to use a variable that is not defined, instead of treating it as an empty string.
  • set -o pipefail: makes the exit code in a pipeline that is the code of the first command that fails, not just the last one.
  • IFS=$'\n\t': limits the internal field separator to line break and tab to prevent spaces from breaking for loops over lists of files or similar.

Combining everything results in something very common in the header of serious scripts:

set -euo pipefail
IFS=$'\n\t'
secure headboard

This prevents situations where an intermediate command in a pipeline fails, but the script continues because the last command returns 0. It also prevents... misspelled or undefined variables silently become empty strings, which is often the origin of incorrect paths or logical conditions that are never met.

The traps of set -e: why it's not magic and can break your script

Even though it is very useful, set -e is not a silver bulletIts behavior has nuances that can be startling if you don't understand how it works. For example, some constructions like:

  • Commands within if, while o until.
  • Expressions with && y || for flow control.
  • Certain arithmetic expansions.

They don't trigger the "immediate exit" you might expect. A classic example is that of arithmetic expansion with post-increment:

((contador++))

Arithmetic expansion returns a value that Bash interprets as an exit code. In the case of post-increment, when the expression returns 0 it is considered a success, but at other times it may return 1, which with set -e makes the script end right thereSomething as innocent as a counter inside a loop can cause your script to exit prematurely.

In contrast, constructions like ((++i)) or ((i+=1)) They behave differently and don't trigger the same problem. This shows that With set -e you need to know the constructs you're using wellbecause there are many borderline cases.

Therefore, several authors and experienced administrators recommend using set -e especially during testing...as an aggressive way to find weaknesses, but in production they prefer a more explicit approach: manually checking output codes and decide exactly when and how the script is aborted.

trap: capture errors and signals to clean and log properly

The command systematic trap: every time It's the other major player when we talk about defensive scripting in Bash. Basically, it lets you specify which commands or functions you want to execute when the shell receives a sign or a special event (such as ERR or EXIT).

typical uses Typical uses of trap in robust scripts are:

  • trap 'error_handling_function' ERR: execute error handling logic right when a command fails.
  • trap 'cleaning_function' INT TERM: react to signals such as CTRL+C (SIGINT) or stop requests (SIGTERM).
  • trap 'finalization_function' EXIT: execute a block upon exiting the script, regardless of whether it went well or not.

Imagine a script that, upon receiving CTRL+C, instead of abruptly stopping, calls a function cleaning that deletes temporary files, closes connections, or leaves a clear message in the logs:

trap 'limpieza' TERM INT
function limpieza(){
echo "Ejecutando limpieza, el usuario uso CTRL + C"
# ... lógica de limpieza ...
}
preventive action

This pattern is especially useful in long processes (backups, ETL, deployments) where you are interested to be able to abort in a controlled manner without leaving the system in an inconsistent state.

What you can and can't capture with a trap ERR

common example Many guides suggest using something like:

trap 'echo "Error en línea $LINENO" >&2' ERR capture line

to register faults with your line number. That's fine, but it's advisable understand your limitsTrap can:

  • Execute additional code when an ERR is triggeredFor example, writing to a log with date, script, and line.
  • Access the exit code of the last command with $?.
  • DISCOVER the script line with $LINENO, very useful for debugging.
  A complete guide to getting the most out of GitHub Copilot on the command line

As it can not do The ERR trap is used to "rescue" the standard error output (stderr) of a command that has already run, unless you have previously redirected it to a file or descriptor that you can then read. The error itself is a text stream, not a single value.

Some patterns involve redirecting the stderr of the entire script to a temporary log and, in the trap, Add a header with date, file, and line. This allows you to correlate that block of errors with the point in the script where the failure occurred. It's more work, but when something crashes in production, you appreciate having context.

Logging in Bash: recording only what's necessary, formatted, and without cluttering things up

If you automate even minimally critical tasks, you need to know what happened, when, and with what resultThat's why almost all examples of serious scripting end up including a centralized logging function that unifies the format.

typical function A typical function might look like this:

log_message() {
local level="$1"
local message="$2"
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
echo -e "${timestamp} ${message}" | tee -a "$LOG_FILE"
}
unified format

examples With something like this you can call:

  • log_message "INFO" "Starting CPU check"
  • log_message "WARNING" "Disk usage above 85%"
  • log_message "ERROR" "Could not connect to the database"

and always use the same format, with the date and level clearly marked. tee-a It also allows you to view the message on screen and save it to a log file at the same time, which is very convenient for both interactive use and later analysis.

Another good practice is to separate activity logs (what the script does) and metrics or results logs (CPU, RAM, disk, etc. values) in different files. For example:

  • /var/log/system-monitor.log for events and messages.
  • /var/log/system-metrics.log for periodic status records.

facilitates integration This then makes it easier to feed external tools, parsers, or even observability solutions without mixing human messages with machine data.

System status check: CPU, memory, disk, network, and logs

A very common use of Bash in Linux environments is to implement a "quick system check" script that gathers all the necessary information at once. basic information for diagnosing problemsThis type of script usually includes several blocks:

  • CPU load: wearing mpstat if it is available (thanks to the sysstat package), or by resorting to top in batch mode to get a summary of CPU usage.
  • Top processes per CPU: With ps aux --sort=-%cpu | head -n N to see what is consuming the most resources.
  • Disk Space: With df -h, paying particular attention to the root partition and any application-critical volumes or databases (see how free up space in Linux).
  • RAM and SWAP memory: through free -hto see how much is being used and if excessive swap usage is being used.
  • Network status: checking external connectivity with a ping to a known IP (typically 8.8.8.8) and listing active interfaces with ip addr showand using tools such as netcat (nc/ncat) for more advanced testing.
  • Recent system logs: drawing the last lines with journalctl -n or equivalent, to detect service errors just before the script runs.

The result is usually saved in a log file or displayed in a formatted format on the screen, serving as Quick snapshot of system health which you can review yourself or integrate into monitoring tools.

Automatic dependency installation and privilege checking

preconditions An important detail of many administration scripts is that, before doing anything serious, they verify that two conditions are met:

  • The script is running as root (or via south).
  • The necessary tools are installed depending on the operating system.

root verification Root verification is usually done with $EUID:

check_root() {
if ; then
log_message "ERROR" "Este script necesita privilegios de root para instalar paquetes."
log_message "WARNING" "Por favor, ejecute con sudo."
exit 1
fi
}
It fails if you don't have root.

This way you avoid subsequent errors due to insufficient permissions and send a clear message to the user from the beginning.

  Change the 24 hour clock to a 12 hour clock in Dwelling home windows 10

detect distro The installation of dependencies is usually supported by /etc/os-release to detect the distribution family:

  • En Debian / Ubuntu it is used apt-get to install packages such as sysstat, lm-sensors, net-tools.
  • En CentOS / RHEL / Fedora would be pulled dnf o yum with equivalent packages (lm_sensors, net-tools, etc.).

Alerts, cron, and log export: taking the script to production

Once you have a reasonably robust checking or automation script, the next logical step is integrate it into daily life:

  • Alerts via email, Slack, or other channelsFor example, if disk usage exceeds 90%, you send an email using mailx or a Slack webhook. The typical pattern is to check the value with df/awk/sed and, if it exceeds a certain threshold, trigger the notification.
  • Periodic execution with cron: add an entry like this 0 8 * * * /ruta/a/script.sh so that it runs every day at a specific time.
  • Redirecting output to log files: execute ./script.sh > /var/log/system-check.log 2>&1 if you want to log all standard and error output to a single file.

It's important to understand that if your script is designed to stop at the first mistake Thanks to `set -euo pipefail`, this means that the cron or CI/CD pipeline that calls it will clearly see a non-zero exit code when there is a failure, allowing you to trigger recovery flows, mark builds as failed, etc.

Debugging in Bash: set -x, -v, -ny trace to file

Bash doesn't have a debugger like some other languages, but it does provide several very powerful debugging options which, when used correctly, save many hours of "why on earth is this happening?":

  • set -x o set -o xtrace: displays each command that is executed with all its parameters already expanded. Ideal for seeing what actual values ​​the variables are taking.
  • set -v o set -o verbose: Prints the script lines as they are read, even before expansion. Useful for understanding the flow.
  • set -n o set -o noexec: analyzes the syntax without executing the code, perfect for catching missing quotes, braces or brackets.
  • set -uAs already mentioned, it fails when undefined variables are used, which is very useful during the debugging of logical errors.

Additionally, you can limit the effect of these options to a specific fragment of the script wrapping it with set -x / set +x, for example:

set -x
# bloque problemático
read -p "Pass Dir name : " D_OBJECT
read -p "Pass File name : " F_OBJECT
set +x
time trace

#!/bin/bash
exec 6> salida_debug.log
BASH_XTRACEFD="6"
set -x
# ... resto del script ...
redirect trace

Thus, all debugging information is stored in debug_output.logAnd you can continue to see only the "normal" output of the script in the terminal.

Typical example of a bug with undefined variables

A very common problem when you don't use set -u is invoking variables that do not exist without you realizing it. Imagine a script that asks for an object name and then checks if it's a file or directory, but it gets the variable name wrong:

#!/bin/bash
read -p "Nombre del Objeto : " OBJECT
if ]; then
echo "$OBJECT es un archivo"
elif ]; then
echo "$OBJECT es un directorio"
fi
variable error

Just like $OBJECT1 is not definedBash expands it to an empty string, the -fy -d tests do not give an error, they are simply not met, and the script exits with code 0. Nothing "happens", but logically the result is not the expected one.

If instead you run the script with bash -u scriptname or active set -uYou'll immediately get an "Unbound variable" error, which leads you directly to a type fault. Combined with bash -x scriptname Or, by using `set -x`, you'll also see which specific line and values ​​are being used, making debugging much easier.

Creating Lightweight Containers with Podman on Linux
Related article:
Lightweight Containers with Podman on Linux: A Practical Guide