40

This is my bash script - I just want to left-pad a set of numbers with zeroes:

printf "%04d" "09"
printf "%04d" "08"
printf "%04d" "07"
printf "%04d" "06"

Output:

./rename.sh: line 3: printf: 09: invalid number 
0000
./rename.sh: line 4: printf: 08: invalid number 
0000 
0007
0006

What...?

Only 09 and 08 are causing the problem: every other number in my sequence seems to be OK.

pfnuesel
  • 14,093
  • 14
  • 58
  • 71
Richard
  • 31,629
  • 29
  • 108
  • 145

9 Answers9

43

If you have your "09" in a variable, you can do

a="09"
echo "$a"
echo "${a#0}"
printf "%04d" "${a#0}"

Why does this help? Well, a number literal starting with 0 but having no x at the 2nd place is interpreted as octal value.

Octal value only have the digits 0..7, 8 and 9 are unknown.

"${a#0}" strips one leading 0. The resulting value can be fed to printf then, which prints it appropriately, with 0 prefixed, in 4 digits.

If you have to expect that you get values such as "009", things get more complicated as you'll have to use a loop which eliminates all excess 0s at the start, or an extglob expression as mentioned in the comments.

glglgl
  • 89,107
  • 13
  • 149
  • 217
  • 3
    Why on earth "-1" without any further notice what was wrong?! – glglgl Nov 10 '11 at 11:13
  • you want `${a##0}` in case you get something like "009" – glenn jackman Nov 10 '11 at 11:47
  • 1
    That does not help at all - `a="009"; echo ${a##0}` yields 09. I would have to use a loop here - but the OP only wroute about `08`, `09` and not `008`, `009`. Alas, this `#`/`##` stuff does not work with regexes - in regex, one could write `s/^0*//g`. – glglgl Nov 10 '11 at 13:37
  • 4
    Ah yes, right. This can be written: `shopt -s extglob; echo ${a##+(0)}` – glenn jackman Nov 10 '11 at 13:59
  • 3
    +1 from me, because your solution did help a lot, but I do have slight doubts that unexplained bash variable substitutions have any educational value for readers who dig this up in the future. To clarify, the OP's error comes from the fact that printf interprets numbers starting with 0 as octal, which does work fine from 00-07. Hence the suggestion to strip leading zeroes before handing the number to printf. – unixtippse Dec 02 '13 at 19:39
  • 2
    @unixtippse You are right, thus I added some explanation now. – glglgl Dec 02 '13 at 19:44
  • Rather than trying to use the shell to parse the string, it might be cleaner to do something like `$(expr $a)` – William Pursell Jul 21 '20 at 23:08
30

Bash's numeric arithmetic evaluation syntax (( ... )) can convert to base 10 (therefor ensuring correct interpretation) with the following syntax: (( 10#$var )). Or, in the case of a raw number: (( 10#08 )). Very simple & clean and can be used anywhere you're sure the base should be 10, but can't guarantee a leading zero won't be included.

So, in your example it would be as follows:

printf "%04d\n" $(( 10#09 ))
printf "%04d\n" $(( 10#08 ))
printf "%04d\n" $(( 10#07 ))
printf "%04d\n" $(( 10#06 ))

Producing the following output:

0009
0008
0007
0006

With this syntax, since you're then working with the value of the variable instead of variable itself, incrementors (( var++ )) & decrementors (( var-- )) won't work, but can still be relatively cleanly implemented as var=$(( 10#var + 1 )) and var=$(( 10#var - 1 )), respectively.

I first encountered this solution here, but this answer to a similar Stack Overflow question also demonstrates it.

Community
  • 1
  • 1
morgant
  • 2,135
  • 2
  • 19
  • 28
  • 3
    Note that this works in bash but not in dash (default ubuntu shell). In dash, the following error appears: "arithmetic expression: expecting EOF" – leszek.hanusz Apr 20 '17 at 08:36
20

Numbers beginning with "0" are treated as octal (i.e. base-8). Therefore, "8" and "9" aren't valid digits.

See http://www.gnu.org/software/bash/manual/bashref.html#Shell-Arithmetic.

This behaviour is inherited from languages like C.

Oliver Charlesworth
  • 267,707
  • 33
  • 569
  • 680
  • Thanks. Any idea how I can left-pad these numbers with zeroes? Do I need to strip the leading zero first, then left-pad - if so, how? – Richard Nov 10 '11 at 10:39
  • Just remove the "0"s from the argument. printf will do the left-padding for you, based on that format string. – Oliver Charlesworth Nov 10 '11 at 10:54
  • 2
    Thanks, but how do I remove the leading zeroes? I can't use `printf "%d" "08"` for the same reason! (the example above is a massive simplification of what I need to do - in fact the argument is a parameter, so I can't edit it by hand each time, I need a programmatic way to strip leading zeroes before adding them...) – Richard Nov 10 '11 at 10:59
  • Oli means you need to remove the leading 0 from the number, not the format. i.e. - printf %04d 9, not 09. – Airsource Ltd Nov 10 '11 at 11:09
  • 1
    @Richard: Oh, I see. Do you mean that the "08" comes from somewhere out of your control, and you want to treat it as if it were "8"? – Oliver Charlesworth Nov 10 '11 at 11:11
  • 1
    See [my answer below](http://stackoverflow.com/questions/8078167/bizarre-issue-with-printf-in-bash-script09-and-08-are-invalid-numbers-07/11804275#answer-11804275) for the specifics, but numbers can be converted to base 10 with the following syntax: `(( 10#$var ))`. Pretty clean & easy. – morgant Aug 03 '12 at 22:50
9

Floating point is handled differently:

printf "%04.f" "009"

This gives the correct output, without dealing with any fancy bashisms (per @Oli Charlesworth's answer, 0*** is treated as octal, but I believe that Bash ignores octal/hex identifiers for floating-point numbers)

Ross Aiken
  • 912
  • 1
  • 6
  • 16
  • Thanks! This works great with output of `date` as in `printf "%.0f\n" $(date +%m)` returned `9` instead of that pesky `09` --and no octal mess. – MarkHu Sep 12 '19 at 01:51
  • Very helpful, I was using `ACCOUNT_ID=$(printf "%012d" $ACCOUNT_ID)` to zero fill AWS account IDs... but failed when people properly included leading zeros with the error `-bash: printf: 012345678901: invalid octal number` ... but using your solution `printf "%012.f" $ACCOUNT_ID` works great! – TryTryAgain Jul 15 '22 at 16:06
4

Just to add to Oli's answer, in order to pad a number with zeroes it is enough to put a 0 after the %, as you did:

printf "%04d" "9"

Community
  • 1
  • 1
Nathan Fellman
  • 122,701
  • 101
  • 260
  • 319
3

Why did “09” and “08” are invalid numbers, “07” and “06” are fine

Because preceding integer by 0 is a conventional shortcut meaning number is octal.

printf "%d\n" 010 0100
8
64

printf "%o\n" 8 64 0100
10
100
100
printf "%04o\n" 8 64 0100
0010
0100
0100

How to work with this?

Using bash integer: preceding number by 10#

Under bash, you could precise by this way, which base is used for number

echo $(( 10#0100))
100
echo $(( 10#0900))
900
echo $(( 10#0900 ))
900

and

echo $(( 8#100 ))
64
echo $(( 2#100 ))
4

Of course!

There are only 10 types of people in the world: those who understand binary, and those who don't.

Using Parameter Expansion

For this, you have to use a variable

a=09
echo ${a#0}
9

This will work fine until you just have only one 0 to drop.

a=0000090

echo ${a#*0}
000090
echo ${a##0}
000090

For this, you coul use extglob feature:

echo ${a##*(0)}
0000090
shopt -s extglob
echo ${a##*(0)}
90

Using floating point with printf

printf "%.0f\n" 09
9

I like using -v option of printf for setting some variable:

printf -v a %.0f 09
echo $a
9

or even if

a=00090
printf -v a %.0f $a
echo $a
90
F. Hauri - Give Up GitHub
  • 64,122
  • 17
  • 116
  • 137
2

Adding * makes shell parameter expansion matching greedy (see, for example, Shell Tipps: use internal string handling)!

# strip leading 0s
- a="009"; echo ${a##0}
+ a="009"; echo ${a##*0}
carlo
  • 37
  • 1
1

Much simpler to convert from octal to decimal:

nm=$((10#$nm))
tripleee
  • 175,061
  • 34
  • 275
  • 318
1

if a=079 first, remove the trailing zeros by

a=`echo $a | sed 's/^0*//'`

then do the zero-padding

printf "%04d" $a