This article provides a full stack analysis of all that happens when a simple bash script is run in order to teach us the Linux OS itself. We will look at system calls, exit statuses, and deep into the unseen machinery of Linux commands.
Bash scripting is often dismissed as a "simple" way to automate tasks. But beneath its straightforward syntax lies a vast, hidden world of system calls, file descriptors, and kernel interactions. Every command you run—whether it’s echo, cat, or ls, triggers a cascade of low-level operations that most users never see.
In this post, we’ll peel back the layers of Bash scripting to reveal how a simple script interacts with the Linux kernel - and why it’s more complex than you think. We’ll learn how to trace system calls using strace to uncover what’s…
This article provides a full stack analysis of all that happens when a simple bash script is run in order to teach us the Linux OS itself. We will look at system calls, exit statuses, and deep into the unseen machinery of Linux commands.
Bash scripting is often dismissed as a "simple" way to automate tasks. But beneath its straightforward syntax lies a vast, hidden world of system calls, file descriptors, and kernel interactions. Every command you run—whether it’s echo, cat, or ls, triggers a cascade of low-level operations that most users never see.
In this post, we’ll peel back the layers of Bash scripting to reveal how a simple script interacts with the Linux kernel - and why it’s more complex than you think. We’ll learn how to trace system calls using strace to uncover what’s really happening under the hood.
The Script: Writing to a File
Let’s start with a basic script that writes a greeting to a file and checks for errors:
#!/bin/bash
# Define a greeting
greeting="Hello, True Linux!"
# Write to a file
echo "$greeting" > output.txt
# Check if the operation succeeded
if [ $? -eq 0 ]; then
echo "Success! File written."
cat output.txt
else
echo "Error: Failed to write to file." >&2
exit 1
fi
Let’s look at all that’s happening when this script is run:
- Exit Statuses ($?): Every command in Bash returns an exit status: 0: Success. Non-zero: Failure (e.g., 1 for general errors). $? captures this status, allowing you to check if a command succeeded.
Without checking $?, your script is flying blind. Errors can (and will) happen — disk full, permission denied, file not found, etc. Your script needs to be able to handle them.
- Error Handling with if-else: The Safety Net The if statement checks $? to see if the previous command (echo) succeeded. If $? is 0, the script continues. If not, it prints an error to stderr (>&2) and exits with a failure status (exit 1).
Robust scripts anticipate failure. This is what separates toy scripts from production-ready tools.
- File Redirection (>, >&2): The Plumbing > redirects output to a file. >&2 redirects output to stderr (standard error), where error messages belong.
Proper redirection ensures your script’s output and errors go where they should—logs, terminals, or files.
Good practices to avoid common pitfalls:
| Pitfall | Example | Fix |
|---|---|---|
| Assuming commands succeed | echo "Hello" > file.txt | Check $? or use set -e. |
| Ignoring error streams | echo "Error!" | Redirect errors: >&2. |
| Hardcoding paths | echo "Hello" > /tmp/file.txt | Use variables: output_file="file.txt". |
Tracing System Calls with strace:
What Are System Calls?
System calls are the interface between user-space programs (like echo) and the Linux kernel. Every time your script writes to a file, lists a directory, or starts a process, it’s making system calls under the hood.
strace is a powerful tool that traces system calls and signals for a process. It lets you see exactly what a command is doing behind the scenes.
Tracing a Simple Command:
Run this command to see the system calls triggered by echo:
strace echo "Hello" > file.txt
You’ll see something like this:
openat(AT_FDCWD, "file.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
write(3, "Hello\n", 6) = 6
close(3) = 0
What Each System Call Does:
| System Call | Purpose | Return Value Example |
|---|---|---|
openat | Open a file for writing. | 3 (file descriptor) |
write | Write data to a file descriptor. | 6 (bytes written) |
close | Close a file descriptor. | 0 (success) |
Common Errors (and What They Mean):
| Pitfall | Example | Fix |
|---|---|---|
| Assuming commands succeed | echo "Hello" > file.txt | Check $? or use set -e. |
| Ignoring error streams | echo "Error!" | Redirect errors: >&2. |
| Hardcoding paths | echo "Hello" > /tmp/file.txt | Use variables: output_file="file.txt". |
Practical Exercises: Trace Everything
1 - Trace a Failing Command:
strace echo "Hello" > /root/file.txt
Observe the EACCES (Permission denied) error.
2 - Trace cat or ls:
strace cat file.txt
strace ls -l
Notice how these commands interact with the OS.
Bash Scripting - Going Forward:
Robustness: Always check exit statuses and handle errors. Debugging: Use strace to debug any command or script. Mastery: Understanding system calls and other ‘under the hood’ processes will make you into a Linux power user.
This knowledge applies to all Linux programming, not just Bash. You’re not just writing scripts — you’re orchestrating the OS.
Bash scripting isn’t just about automating tasks — it’s about understanding the machine and its operating system at low-level. By peeling back the layers for a deeper look, you’re not just writing scripts - you’re learning to master the Linux OS itself.
Ben Santora - January 2026