Bash format number with commas on Linux: printf, numfmt, and script examples

Tech reviewed: Deepak Prasad
Bash format number with commas on Linux: printf, numfmt, and script examples

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

bash
LC_ALL=en_US.UTF-8 printf "%'d\n" 123456789

What printed here:

text
123,456,789

Fractional 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:

bash
LC_ALL=en_US.UTF-8 printf "%'.3f\n" 1234567890.456

Output:

text
1,234,567,890.456

Why 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:

bash
LC_ALL=C printf "%'d\n" 1234567

With LC_ALL=C I get no grouping:

text
1234567

Prefixing 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:

bash
LC_ALL=en_IN.UTF-8 printf "%'d\n" 7654321

Example line (under en_IN):

text
76,54,321

That 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:

bash
echo 1234567890.456 | LC_ALL=en_US.UTF-8 numfmt --grouping

Output:

text
1,234,567,890.456

Useful pattern with a variable:

bash
x=1234567
LC_ALL=en_US.UTF-8 numfmt --grouping <<<"$x"

Output:

text
1,234,567

numfmt expects a plain numeric string. If the value already contains grouping separators, the command fails, which helps catch bad input early:

bash
printf '%s\n' "12,345" | LC_ALL=en_US.UTF-8 numfmt --grouping

Example stderr from that run:

text
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.

bash
#!/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" 1

Save as nicenumber.sh, run chmod +x nicenumber.sh, then:

bash
./nicenumber.sh 123456789
./nicenumber.sh 1234567890
./nicenumber.sh 1234567890.456

Output from those three invocations:

text
123,456,789
1,234,567,890
1,234,567,890.456

How 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.

Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with over a decade of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, …

  • Red Hat Certified System Administrator in Red Hat OpenStack
  • Certified Kubernetes Application Developer (CKAD)
  • Red Hat Certified Specialist in Ansible Automation
  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security