Long numbers are much easier to scan when thousands are grouped. On Linux you
usually reach for printf with the grouping flag, numfmt, or a short loop in pure
Bash. The sections below are what I actually ran on my workstation, with notes on
locales because that is where most surprises show up.
Tested all the commands and code from this article on Ubuntu 25.04, kernel 6.14.0-37-generic, Bash 5.2.37.
Bash format number with commas using printf
The C-library-backed printf built into Bash supports a thousands grouping modifier:
an apostrophe ' placed after % and before the conversion letter. The character
used between digit groups comes from the active locale (LC_NUMERIC), not from the
format string itself, so forcing LC_ALL or LC_NUMERIC is the usual way to get
commas consistently on servers that default to C or POSIX.
Integers
LC_ALL=en_US.UTF-8 printf "%'d\n" 123456789What printed here:
123,456,789Fractional values
Pick a precision that matches how many digits you want after the decimal point. The integer part is still grouped according to locale rules:
LC_ALL=en_US.UTF-8 printf "%'.3f\n" 1234567890.456Output:
1,234,567,890.456Why LC_ALL=en_US.UTF-8 is a common prefix
In the C or POSIX locale, grouping is typically disabled even when you use %',
so you may see no commas at all:
LC_ALL=C printf "%'d\n" 1234567With LC_ALL=C I get no grouping:
1234567Prefixing a single command with LC_ALL=en_US.UTF-8 (or another UTF-8 locale you have
installed) keeps the rest of your script on the default locale while still printing
human-friendly numbers. Run locale -a to list installed locale names on your system.
Indian numbering (lakhs and crores)
Some locales group the integer part differently from three-digit Western groups. For
example, with en_IN you can see lakh-style grouping on the integer portion:
LC_ALL=en_IN.UTF-8 printf "%'d\n" 7654321Example line (under en_IN):
76,54,321That behavior is correct for that locale; it is not a bug in your script.
GNU numfmt for thousands grouping
When you prefer a dedicated tool, numfmt from GNU coreutils reads a number and
prints it with grouping. It respects locale the same way other libc formatters do, so
set LC_ALL when you need predictable commas:
echo 1234567890.456 | LC_ALL=en_US.UTF-8 numfmt --groupingOutput:
1,234,567,890.456Useful pattern with a variable:
x=1234567
LC_ALL=en_US.UTF-8 numfmt --grouping <<<"$x"Output:
1,234,567numfmt expects a plain numeric string. If the value already contains grouping
separators, the command fails, which helps catch bad input early:
printf '%s\n' "12,345" | LC_ALL=en_US.UTF-8 numfmt --groupingExample stderr from that run:
numfmt: invalid suffix in input: ‘12,345’Example Bash script: comma as thousands separator
Below is a self-contained script that formats a non-negative number passed as the first argument. It inserts commas every three digits on the integer side of the decimal point and leaves the fractional part unchanged. The logic is easy to follow in code reviews when teammates are not yet comfortable with locale rules.
#!/bin/bash
# nicenumber--Format a number with comma thousands separators (ASCII digits, non-negative).
nicenumber() {
local integer decimal thousands remainder result=""
if [[ "$1" == *.* ]]; then
integer="${1%%.*}"
decimal="${1#*.}"
result="${DD:-.}${decimal}"
else
integer="$1"
fi
thousands=$integer
while (( thousands > 999 )); do
remainder=$((thousands % 1000))
while (( ${#remainder} < 3 )); do
remainder="0${remainder}"
done
result="${TD:-,}${remainder}${result}"
thousands=$((thousands / 1000))
done
nicenum="${thousands}${result}"
if [[ -n "${2:-}" ]]; then
printf '%s\n' "$nicenum"
fi
}
DD="."
TD=","
if (($# == 0)); then
echo "Please provide a number" >&2
exit 1
fi
nicenumber "$1" 1Save as nicenumber.sh, run chmod +x nicenumber.sh, then:
./nicenumber.sh 123456789
./nicenumber.sh 1234567890
./nicenumber.sh 1234567890.456Output from those three invocations:
123,456,789
1,234,567,890
1,234,567,890.456How the loop works
The while (( thousands > 999 )) loop peels off the last three decimal digits of the
integer portion, left-pads the remainder to three characters when needed, and
prepends each block to result with the thousands delimiter. When the remaining
thousands value is three digits or fewer, it becomes the leftmost block. The main
script only validates that an argument exists, then calls nicenumber with a second
argument so the function prints the final string. This script does not use getopts;
that sentence in older revisions referred to a different template and does not apply
here.
For signed values, scientific notation, or locale-specific decimal commas, prefer
printf or numfmt after normalizing the input, or extend this function with explicit
sign handling.
Choosing an approach
Use printf with the %' flag when you control Bash and glibc-backed formatting and only need
output for display. Use numfmt --grouping in pipelines or when a standalone filter
reads clearer than a format string. Use a small loop like nicenumber when you must
avoid locale entirely and only support plain ASCII digits and a dot as the decimal
separator. For BusyBox-only systems, test whether %' works; if not, a tiny awk or
sed pipeline is a common fallback, as discussed in community guides for embedded
shells.
Summary
For comma grouping in Bash, printf with %' plus something like
LC_ALL=en_US.UTF-8 is the quick path; numfmt --grouping is handy in pipelines and
when the value already lives in a variable. Grouping always follows LC_NUMERIC, so
do not be shocked by lakh-style output under en_IN, or plain digits under C.
numfmt will complain if the string already has separators. The little nicenumber
loop at the end is there for when you want ASCII digits and a dot as the decimal
mark without leaning on libc formatting.
Further reading: Dave Taylor, Wicked Cool Shell Scripts (O’Reilly) — where I first saw the walk-through style for this kind of script.

