Linux Basic — Writing Files in Bash
Writing Files
Writing a file looks trivial — echo "hi" > file — and that is exactly why most production bugs around file writing happen. Partial writes during crashes, lost newlines from echo, broken redirection of stderr, and accidental overwrites are all preventable with a few simple habits.
The Two Redirection Operators
echo "first" > out.txt # OVERWRITE — truncates and writes
echo "second" >> out.txt # APPEND — adds to end (creates if missing)
cat out.txt
# first
# second
>truncates the file first (any existing content is lost)>>appends (always safe for adding)- Both create the file if it does not exist
Prefer printf Over echo in Scripts
echo is portable in name only — its behaviour around -e, -n, and backslash escapes varies between shells, builtins, and /bin/echo. printf is consistent everywhere.
# ❌ echo is unpredictable across systems
echo -e "first\nsecond"
# On Bash: first
# second
# On dash: -e first\nsecond ← prints the -e!
# ✅ printf is reliable
printf '%s\n' "first" "second"
# first
# second
printf cheat sheet:
| Format | Effect |
|---|---|
%s |
string |
%d |
integer |
%f |
float (use %.2f for two decimals) |
%x |
hex |
%q |
shell-quoted (escape for re-execution) |
\n |
newline |
\t |
tab |
%b |
string with backslash interpretation |
printf '%-10s %5d\n' "apples" 42
printf '%-10s %5d\n' "bananas" 7
# apples 42
# bananas 7
Atomic Writes — Never Leave a Partial File
If a script crashes mid-write, you can be left with a half-written file that downstream tools then process as if it were complete. The cure is atomic write: write to a temporary file first, then rename it. On a POSIX filesystem, mv within the same directory is atomic.
# ❌ Not atomic — if the second line never runs, file is half-written
generate_report > /var/lib/app/report.json
# ✅ Atomic
tmp=$(mktemp /var/lib/app/.report.XXXXXX)
generate_report > "$tmp" && mv "$tmp" /var/lib/app/report.json
Why this works:
- Any reader sees either the old complete file or the new complete file
- A crash during
generate_reportleaves only$tmp(which you can clean up) - No reader ever sees a partially-written file
Tip: keep the tempfile in the same directory as the target — cross-directory mv can fall back to copy+delete (no longer atomic).
Overwrite Protection — noclobber
set -o noclobber # or: set -C
echo "x" > existing.txt # bash: existing.txt: cannot overwrite existing file
echo "x" >| existing.txt # force — overrides noclobber for this one redirection
echo "x" >> existing.txt # >> is unaffected — safe to append
noclobber is a great default at the top of a script: it stops you from accidentally overwriting an existing file with >. Use >| when you genuinely want to overwrite.
Redirecting stdout, stderr, and Both
Every process has three open streams: 0 (stdin), 1 (stdout), 2 (stderr).
# Run a command that writes to both streams
ls / missing 2>/dev/null
# /bin /etc ... (only the success output — errors went to /dev/null)
ls / missing 1>/dev/null
# ls: cannot access 'missing': No such file or directory
# (only the errors — success went to /dev/null)
Both streams to one file
# Modern Bash shorthand
build.sh &> build.log
# Portable form — write stdout, then merge stderr into it
build.sh > build.log 2>&1
# WRONG — order matters: this sends stderr to the screen
build.sh 2>&1 > build.log
The first form merges streams at the destination. The second sends stderr to wherever stdout was pointing at the time — which was still the terminal — then redirects stdout.
Separate stdout and stderr
build.sh > build.out 2> build.err
Swap them (advanced)
# Make stdout into stderr and stderr into stdout
cmd 3>&1 1>&2 2>&3
Writing Multiple Lines
# A heredoc is the cleanest way to write a multi-line file
cat > /etc/myapp.conf <<'EOF'
listen = 0.0.0.0:8080
workers = 4
debug = false
EOF
# Quoted EOF (single quotes) means: do NOT expand variables inside
# Without quotes, $vars and `cmds` ARE expanded
NAME="webapp"
cat > /etc/myapp.conf <<EOF
name = $NAME
EOF
cat /etc/myapp.conf
# name = webapp
Heredocs are covered in detail in Advanced Scripting → Here-Documents.
Common Patterns
Write a JSON file from values
name="webapp"
version="1.4.2"
cat > app.json <<EOF
{
"name": "$name",
"version": "$version"
}
EOF
Write only if the content differs
new=$(generate_config)
if ! diff -q <(printf '%s' "$new") /etc/app.conf >/dev/null 2>&1; then
printf '%s' "$new" > /etc/app.conf
systemctl reload app
fi
Truncate without deleting
: > big.log # truncate to zero bytes (the file stays — same inode)
> big.log # equivalent
Useful when other processes have the file open: they keep their FD; only the contents are wiped.
Common Mistakes
| ❌ Wrong | ✅ Right | Issue |
|---|---|---|
echo -e "\t" |
printf '\t' |
echo -e not portable |
cmd > f 2>&1 \| less |
cmd 2>&1 \| less |
first form sends stderr to f, then to less |
cmd 2>&1 > f |
cmd > f 2>&1 |
order matters for redirection |
generate > /var/data/x.json |
tempfile + atomic mv |
partial writes on crash |
echo "$big" > f in a hot loop |
build in a variable then write once | thousands of redirects = thousands of syscalls |
Next → Appending to Files