The printf builtin formats and prints data with precise control
over spacing, alignment, padding, and numeric representation. It is the go-to
tool whenever echo isn't enough — tabular output, fixed-width
fields, zero-padded numbers, hex conversion, and writing to variables rather
than stdout.
Unlike echo, printf does
not add a newline automatically, does not interpret escape sequences by default
in arguments, and behaves consistently across all platforms. For serious scripting,
printf is nearly always the better choice.
| Specifier | Type | Example | Output |
|---|---|---|---|
| %s | String | printf "%s" "hello" | hello |
| %d | Decimal integer | printf "%d" 42 | 42 |
| %i | Integer (same as %d) | printf "%i" 42 | 42 |
| %f | Floating point | printf "%f" 3.14 | 3.140000 |
| %e | Scientific notation | printf "%e" 12345 | 1.234500e+04 |
| %o | Octal | printf "%o" 8 | 10 |
| %x | Hexadecimal (lower) | printf "%x" 255 | ff |
| %X | Hexadecimal (upper) | printf "%X" 255 | FF |
| %c | Single character | printf "%c" 65 | A |
| %b | String with backslash escapes | printf "%b" "a\tb" | a b |
| %% | Literal percent sign | printf "100%%" | 100% |
| Modifier | Meaning | Example | Output |
|---|---|---|---|
| %10s | Right-align in 10-char field | printf "%10s" "hi" | hi |
| %-10s | Left-align in 10-char field | printf "%-10s" "hi" | hi |
| %05d | Zero-pad to 5 digits | printf "%05d" 42 | 00042 |
| %+d | Always show sign | printf "%+d" 42 | +42 |
| %.2f | 2 decimal places | printf "%.2f" 3.14159 | 3.14 |
| %8.2f | Width 8, 2 decimal places | printf "%8.2f" 3.14159 | 3.14 |
| %*s | Width from argument | printf "%*s" 10 "hi" | hi |
The fundamentals — printf vs echo:
# echo adds newline automatically; printf does not echo "Hello World" printf "Hello World\n" # equivalent # Multiple arguments — format string repeats printf "%s\n" "alpha" "beta" "gamma" # String and integer together printf "Name: %s Age: %d\n" "Craig" 42 # echo -e vs printf %b for escape sequences printf "Column1\tColumn2\tColumn3\n" printf "%b" "Line one\nLine two\nLine three\n"Output:
printf "%s\n" a b c prints each on its own line — very useful
for processing lists.
The most common sysadmin use — clean aligned columns:
#!/bin/bash
# Print a service status table
printf "%-15s %-10s %8s\n" "SERVICE" "STATUS" "PID"
printf "%-15s %-10s %8s\n" "-------" "------" "---"
services=("nginx" "sshd" "postgresql" "redis" "crond")
for svc in "${services[@]}"; do
if systemctl is-active --quiet "$svc" 2>/dev/null; then
pid=$(systemctl show -p MainPID --value "$svc")
printf "%-15s %-10s %8s\n" "$svc" "running" "$pid"
else
printf "%-15s %-10s %8s\n" "$svc" "stopped" "-"
fi
done
Output:
-), in a field
15 characters wide (15), string type (s).
Right-align by omitting the minus. Consistent widths across header and
data rows make the column snap into alignment.
# Zero-padded numbers — useful for filenames, sequences for i in {1..5}; do printf "backup_%04d.tar.gz\n" "$i" done # Floating point precision printf "Pi: %.4f\n" 3.14159265 printf "Price: $%8.2f\n" 1234.5 # Always show sign printf "Temp delta: %+.1f C\n" -2.7 printf "Temp delta: %+.1f C\n" 4.3 # Hex and octal conversion printf "Dec: %d Hex: %#x Oct: %#o\n" 255 255 255 # Scientific notation printf "%.3e\n" 0.000123456Output:
# flag adds the type prefix —
0x for hex, 0 for octal. Useful when the
output needs to be unambiguous about its base.
The -v flag writes formatted output into a variable instead
of stdout — no subshell needed:
# Store formatted string in a variable printf -v timestamp "%s" "$(date '+%Y-%m-%d %H:%M:%S')" printf -v logline "[%s] %s: %s\n" "$timestamp" "INFO" "Service started" echo "$logline" # Build a zero-padded filename printf -v filename "report_%04d.html" "$report_num" echo "Writing to: $filename" # Format a table row for later use printf -v row "%-20s %10.2f %8d\n" "$dept" "$budget" "$headcount" report+="$row" # accumulate into a report string
var=$(printf ...)
spawns a subshell. printf -v var ... does not —
it is faster and does not strip trailing newlines the way
command substitution does.
# Common escape sequences in format string printf "Tab:\there\n" printf "Newline:\nSecond line\n" printf "Bell:\a\n" # terminal bell printf "Carriage return:\r" # overwrite current line printf "Backslash: \\\n" printf "No newline at end: " # useful for prompts # ANSI color codes with printf printf "\033[32m%s\033[0m\n" "This is green" printf "\033[31mERROR:\033[0m %s\n" "something failed" printf "\033[33mWARN:\033[0m %s\n" "disk at 85%%" printf "\033[34mINFO:\033[0m %s\n" "service started" # Unicode characters printf "\U2714 Done\n" # ✔ Done printf "\U274C Failed\n" # ✖ FailedOutput (color codes render in terminal):
% starts a specifier. To print a literal percent sign
use %% — e.g., printf "Disk: %d%%\n" 85
prints Disk: 85%.
#!/bin/bash
# Disk usage report with printf formatting
BORDER=$(printf '%0.s-' {1..55})
printf "\n%s\n" "$BORDER"
printf " %-20s %10s %10s %8s\n" "FILESYSTEM" "SIZE" "USED" "USE%"
printf "%s\n" "$BORDER"
df -h --output=target,size,used,pcent | tail -n +2 | while read -r fs size used pct; do
pct_num=${pct//%/}
if (( pct_num >= 90 )); then
flag="(!)"
elif (( pct_num >= 75 )); then
flag="(*)"
else
flag=""
fi
printf " %-20s %10s %10s %7s %s\n" "$fs" "$size" "$used" "$pct" "$flag"
done
printf "%s\n\n" "$BORDER"
Output:
# echo is fine for simple output echo "Starting backup..." echo "Done." # printf is better when: # 1. Content might start with a dash (echo -n problem) var="-n tricky" echo $var # echo interprets -n as a flag! printf "%s\n" "$var" # safe — always treated as data # 2. You need precise field widths printf "%-12s %s\n" "Hostname:" "$(hostname)" printf "%-12s %s\n" "Uptime:" "$(uptime -p)" printf "%-12s %s\n" "Kernel:" "$(uname -r)" # 3. You need no trailing newline (for progress indicators) for i in {1..10}; do printf "\rProgress: %3d%%" "$((i*10))" sleep 0.2 done printf "\n" # 4. Writing to a variable cleanly printf -v greeting "Hello, %s! You have %d messages." "$USER" 5Output:
# Timestamped log function log() { local level=$1; shift printf "[%s] [%-5s] %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$level" "$*" } log INFO "Starting deployment" log WARN "Config file missing — using defaults" log ERROR "Cannot connect to database" # Progress bar progress_bar() { local current=$1 total=$2 local pct=$(( current * 100 / total )) local filled=$(( pct / 2 )) local bar printf -v bar "%-50s" "$(printf '%0.s#' $(seq 1 $filled))" printf "\r[%s] %3d%%" "${bar// /-}" "$pct" } # Hex dump a string string="Hello" printf '%s' "$string" | od -A x -t x1z # Generate separator lines of exact width printf '%*s\n' 60 '' | tr ' ' '=' # 60 equal signs printf '%0.s-' {1..40}; echo # 40 dashesOutput:
| Pattern | What it does |
|---|---|
| printf "%s\n" "$var" | Safe string print — always treats content as data |
| printf "%-20s %10s\n" | Left-align 20, right-align 10 — column layout |
| printf "%05d\n" "$n" | Zero-pad integer to 5 digits |
| printf "%.2f\n" "$n" | Float to 2 decimal places |
| printf "%x\n" "$n" | Decimal to hexadecimal |
| printf "%o\n" "$n" | Decimal to octal |
| printf "%%\n" | Literal percent sign |
| printf -v VAR "fmt" args | Write to variable — no subshell |
| printf "%s\n" a b c | Format repeats — prints each on own line |
| printf "\033[32m%s\033[0m\n" | ANSI green color |
| printf '\r%s' "$msg" | Overwrite current line (progress) |
| printf '%0.s-' {1..40} | Repeat character N times |
| Situation | Use |
|---|---|
| Simple message, no formatting | echo — simpler |
Content might start with - | printf "%s\n" — safe |
| Aligned columns / fixed width | printf |
| Number formatting | printf |
| Write to variable | printf -v |
| No trailing newline | printf (or echo -n) |
| ANSI color codes | printf — more reliable |
| Portable across sh/bash/dash | printf — consistent behavior |