Adding leading 0 in sed

Viewed 1830

I would like to add leading zeros to a string of digits so as to have exactly three digits. Consider for example

echo "hello_1"   | sed -E 's/([0-9]+)/00\1/'  # hello_001
echo "hello_12"  | sed -E 's/([0-9]+)/0\1/'   # hello_012
echo "hello_123" | sed -E 's/([0-9]+)/\1/'    # hello_123

Now, the issue is that I am using three different expressions here while I get piped to sed several lines at one. Something like

printf "hello_1\nhello_12\nhello_123\n" | sed -E 's/([0-9]+)/0\1/'
hello_01
hello_012
hello_0123

The result is not the one I am looking for. The expected output is

hello_001
hello_012
hello_123

I tried

printf "hello_1\nhello_12\nhello_123\n" |  sed -r ":r;s/\b[0-9]{1,$((3-1))}\b/0&/g;tr"
hello_1
hello_12
hello_123

that I got from this post but it does not work and I don't understand how it works.


I am using GNU sed version 4.2.1 on Linux

5 Answers

If you can consider awk then it is much easier using printf:

printf "hello_1\nhello_12\nhello_123\n" | awk -F_ '{printf "%s%s%03d\n", $1, FS, $2}'

hello_001
hello_012
hello_123

If you want to pad on the left with zeros to exactly 3 digits, then you could use two regex replacements:

  • Replace _[0-9]$ with two zeros padded
  • Replace _[0-9][0-9]$ with one zero padded

Like this:

sed -E -e 's/_([0-9])$/_00\1/' -e 's/_([0-9][0-9])$/_0\1/'

Why it's not working

The sed program you tried doesn't work for you because it anchors digits on a word boundary (\b) to the left and right, and _ is considered a word character (word characters are letters, digits and the underscore). If you replace _ with - (or whitespace), you'll see that it works:

$ printf "hello-1\nhello-12\nhello-123\n" | sed -E ':a; s/\b[0-9]{1,2}\b/0&/g; ta'
hello-001
hello-012
hello-123

General sed solution

To fix it for a general case (and also for your example input), you can use a non-digit character [^0-9] instead of \b on both left and right (combined with ^ and $ to handle the beginning and ending of line):

$ printf "hello_1\nhello_12\nhello_123\n" | sed -E ':a; s/(^|[^0-9])([0-9]{1,2})([^0-9]|$)/\10\2\3/g; ta'
hello_001
hello_012
hello_123

This works similarly to the original expression: it recursively prepends digit zero to any isolated sequence of 1-2 digits. Branching command t loops to the beginning (label :a) as long as substitutes are made -- i.e. as long as there exists an isolated group of digits, shorter than 3 characters.

Capturing groups \1 and \3 are simply passing-thru the context around the digits captured in \2. We need them only for matching the group of isolated digits.

You could also use perl:

printf "hello_1\nhello_12\nhello_123\n" | perl -ne 's/(\d{1,3})$/sprintf("%3.3d", $1)/e; print;' -

This looks for up to 3 digits at the end of the input line and changes that to 3 digits.

This example uses only sed and it's flexible if you wanted to support more than 3 zero-padded digits.

printf "hello_1\nhello_12\nhello_123\n" \
  | sed -e 's:_\([0-9]\):_00\1:;s:_0*\(...\):_\1:'

Here's what it looks like if you wanted 8 zero-padded digits using only sed.

printf "hello_1\nhello_12\nhello_123\n" \
  | sed -e 's:_\([0-9]\):_0000000000\1:;s:_0*\(........\):_\1:'

If it's not obvious, what's happening is - the original numeric portion is retained but several zeros are inserted before it. After that happens, a second pattern keeps only the last N digits, swallowing the extra leading zeros.

Related