Blog
Mert Özen's blog posts on software development, backend, frontend, and DevOps.
Your First Bash Script: Shebang, Execute Permission, Exit Codes and When Not to Write a Script
A Bash script picks its interpreter with the shebang line, runs thanks to execute permission and talks to its caller through the exit code. With a 10-line script that checks the Bookstore API health endpoint we show the shebang and the dash difference, ./script.sh vs bash vs source, exit codes, the PATH, a script skeleton, the CRLF trap and when a job belongs to Python, PowerShell or a .NET 10 file-based app.
Variables, Quoting and Expansion: Parameter Expansion, Command Substitution, Arrays and the Word Splitting Trap
In Bash you should always use a variable inside double quotes, because an unquoted value goes through word splitting and globbing, and a file name with a space can get the wrong file deleted. Using Bookstore's health script we show the expansion order, parameter expansion, command substitution and arithmetic, indexed and associative arrays, export and ShellCheck, and explain the rm -rf with an empty variable and macOS Bash 3.2 traps with real output.
Conditionals and Loops: if, [[ ]], case, for, while read and Looping Over Files Safely
In Bash, if looks at a command's exit code rather than a condition; bash scripts use [[ ]], numbers use (( )), reading line by line uses while IFS= read -r, and file names use a glob or find -print0. While writing a bookstore-ctl.sh with subcommands and a cleanup script that keeps the last five releases on the Bookstore server, we show how glob sorting can delete the live release, the file created by > inside [ ] and the counter lost by a while loop in a pipe.
Functions, Arguments and Parsing Options: $@, shift, getopts and a Good --help
A Bash function returns data on stdout and success through its exit code; variables inside it are global unless declared local, and scope is dynamic. We collect shared functions in a sourced library and use positional parameters, shift, getopts and a while/case pattern for long options to build Bookstore's bookstore-deploy.sh -e staging -v 1.4.2 --dry-run interface, a good --help text and a dry run based on printf %q.
Data Flow in Scripts: Pipes, Redirection, Here-docs, Process Substitution and xargs
In a script, stdout should carry only data and stderr the messages; a pipeline's exit code is its last command's by default and becomes visible with PIPESTATUS and pipefail. We add a log file to Bookstore's deploy script with exec and tee and a RELEASE file with a here-doc, recover the variable a pipe-fed loop loses with < <(...), run work in parallel with xargs -0 -P and produce a JSON report with jq.
Error Handling: How set -euo pipefail Really Behaves, trap, Cleanup, Retry and Locking
set -e does not act on commands whose result is used as a condition, nor inside functions called in such conditions; local x=$(...), command substitution and (( count++ )) surprise too. We add set -Eeuo pipefail, an ERR trap that logs a call stack, an EXIT trap that leaves no half-done folder, retries, timeout, a single-deploy lock with flock and a flow that is safe to run twice to Bookstore's deploy script.
A Real Script: Writing a Deploy and Database Backup Script for Bookstore
A reliable deploy script verifies the artifact's checksum, prepares the release outside current, switches current atomically with mv -T instead of ln -sfn and returns to the previous release if the health check fails. We write that deploy for Bookstore, read secrets from env files instead of arguments, and add a verified, rotating SQL Server backup script scheduled with a systemd timer.
Testing Scripts and Making Them Portable: ShellCheck, shfmt, bats-core, CI and From Bash 3.2 on macOS to Bash 5.3
Bash scripts are linted with ShellCheck, formatted with shfmt and tested with bats-core using stubs placed on PATH, and all three should run in CI on every pull request. We write tests for Bookstore's deploy script and library, wire them into GitHub Actions, and run the same tests on Bash 3.2 on macOS, on Bash 5.3 and on Ubuntu 26.04's Rust coreutils to show where portability breaks.