A bash backup script sets `month=$(date +%m)` and then computes `$(( month + 1 ))`. It works most of the year but in August fails with `value too great for base (error token is "08")`. What happened, and how do you fix it?
answer
- a leading zero is not decoration
- C literal rules apply here
- valid octal has no 8 or 9
- base#digits forces the base
- the $ is required for this one
basics
~20 sA number with a leading zero is octal in bash arithmetic, so 08 is invalid — octal has no digit 8. Force base 10 with the base#number syntax, writing $(( 10#$month + 1 )), or strip the leading zero before doing the math.
solid answer
~50 sInside `$(( ))` bash follows C's literal rules: a leading `0` means octal, `0x` means hexadecimal, and the general form `base#digits` selects any base from 2 to 64. `date +%m` zero-pads, so August gives the string `08`, which bash reads as octal — and 8 is not a valid octal digit, hence "value too great for base". The same applies to `09`, and to zero-padded days 08 and 09 from `date +%d`. The fix is the explicit base prefix: `$(( 10#$month + 1 ))` forces decimal, and it is safe for unpadded values too. Note this is one of the rare places where you must expand the variable with `$` rather than use the bare name, because the prefix has to be textually glued to the digits. If you need the padding back on output, use `printf -v next '%02d' "$(( 10#$month % 12 + 1 ))"`.
code
bash · 8 lines#!/usr/bin/env bash
month=08
# echo $(( month + 1 )) # error: value too great for base
echo $(( 10#$month + 1 )) # 9
echo $(( 010 )) # 8 -- silently octal
echo $(( 2#1011 )) $(( 16#ff )) # 11 255
printf -v next '%02d' "$(( 10#$month % 12 + 1 ))"
echo "$next" # 09go deeper
Recognise the error message and know the one-line fix: write $(( 10#$month + 1 )). Remember that a leading zero means octal in bash arithmetic, so 08 is not a valid number.
Explain the full literal grammar — leading 0 for octal, 0x for hex, base#digits for anything else — and say why 01 through 07 hide the bug while 08 exposes it. Mention that 10# requires the $.
Generalise the rule to every externally sourced value entering arithmetic, and call out the silent variant where a padded number like 017 evaluates to 15 without any error at all.
Treat it as a class of latent, calendar-triggered defects: decide how your team normalises external numeric input at the boundary, and how tests or linting catch bugs that only surface in two months of the year.
## Numeric literals in arithmetic context Bash's arithmetic evaluator recognises constants exactly the way C does, plus one extension: - plain digits — decimal: `42` - leading `0` — **octal**: `010` is 8 - leading `0x` or `0X` — hexadecimal: `0x1f` is 31 - `base#digits` — any base from 2 to 64: `2#1011` is 11, `16#ff` is 255, `10#0075` is 75 The octal rule is inherited from C and predates every shell script that ever parsed a timestamp. It is harmless for hand-written constants — nobody writes `010` meaning ten — but it is a landmine for values that arrive zero-padded from elsewhere. ```bash echo $(( 010 )) # 8, not 10 echo $(( 0x10 )) # 16 echo $(( 07 + 1 )) # 8 -- valid octal, silently wrong if you meant seven echo $(( 08 + 1 )) # error: value too great for base (error token is "08") ``` ## Why this bug is seasonal `date +%m` and `date +%d` zero-pad to two digits, so eight months and days of the year produce a string bash reads as octal. Months `01` through `07` and days `01` through `07` are *valid* octal and evaluate to the same number as decimal — which is why the script appears to work. Only `08` and `09` are invalid, so the failure lands on exactly two months and two days per month. A script written in March and deployed in March passes every test until August. Worse than the loud error is the quiet version: not every zero-padded value contains an 8 or 9. A three-digit padded counter like `017` evaluates to 15 with no error at all, which is a wrong answer rather than a crash. ## The fix The canonical repair is the base prefix: ```bash month=$(date +%m) next=$(( 10#$month + 1 )) ``` `10#` says "interpret the following digits in base 10", which makes the leading zero meaningless. It is safe when the value is not padded — `10#7` is just 7 — so you can apply it unconditionally to anything coming from `date`, a filename, an API response, or a config file. One subtlety worth stating in an interview: this is a rare arithmetic context where you **must** write `$month` rather than the bare name `month`, because the base prefix has to be textually attached to the digits. `$(( 10#month ))` is a syntax error, since `month` is not a digit string. A corollary is that if the variable is empty, `$(( 10#$month ))` becomes `$(( 10# ))` and errors — so guard or default the value if it might be unset. Other approaches and why they are weaker: ```bash next=$(( ${month#0} + 1 )) # strips ONE leading zero; "00" becomes empty and errors next=$(( ${month##+(0)} + 1 )) # needs shopt -s extglob, and "00" still empties out printf -v m '%d' "$month" # printf %d also parses 08 as octal on some builds next=$(( $(( 10#$month )) + 1 )) # works but is needless nesting ``` The `${month#0}` trick is common in the wild and is a trap of its own: a value of `00` becomes the empty string, and `$(( + 1 ))` errors. `10#` handles `00` correctly, yielding 0. ## Getting the padding back Arithmetic strips the padding, so if the output must stay two digits, format it explicitly: ```bash printf -v next_month '%02d' "$(( 10#$month % 12 + 1 ))" echo "$next_month" # 09 in August, 01 in December ``` `printf -v` assigns directly into the variable without a subshell, which is both faster and cleaner than `next=$(printf '%02d' ...)`. ## Where else it shows up Anything zero-padded and arithmetic-bound: build numbers (`build=007`), zero-padded partition or disk indices, `%H`/`%M`/`%S` time components in a duration calculation, ID fields from a fixed-width export, and version segments parsed out of a tag. The habit worth forming is that **any string that came from outside the script and is about to enter `$(( ))` gets `10#`** unless you specifically want the other bases.
- Why does the script work fine for months 01 through 07 but break in August?Digits 0 through 7 are valid octal, and for those values octal and decimal happen to agree, so `$(( 07 + 1 ))` gives 8 and nobody notices. August produces `08`, which contains a digit octal does not have, so the evaluator rejects it outright. The bug was present all year; only two months make it visible.
- Why can't you write `$(( 10#month ))` with the bare variable name?The base prefix must be attached to a literal digit string, and `month` is not one — bash reports a syntax error. This is one of the few arithmetic contexts where you have to expand the variable explicitly as `10#$month`. It also means an empty variable yields a bare `10#`, which errors, so guard or default the value.
- What is wrong with stripping the zero using `${month#0}` instead?It removes only one leading zero and turns a value of `00` into the empty string, so `$(( ${month#0} + 1 ))` errors on that input. It also does nothing for three-digit padding like `007`. `10#$month` handles every case including `00`, which correctly evaluates to 0.
- Is there a silent version of this bug rather than a loud error?Yes, and it is the more dangerous one. A padded value with no 8 or 9 — `017`, for instance — is valid octal and evaluates to 15 with no diagnostic at all. The script produces a confidently wrong number, which is why applying `10#` to externally sourced values should be a habit rather than a reaction to an error.
saying these in an interview costs you the question
- Blaming date for returning a broken value
- Thinking bash rejected 08 because it is a string
- Assuming ${var#0} handles every padded case
- Believing only the error months are affected
- Expecting arithmetic to preserve the zero padding