In a bash script with s=deployment, what do ${s:4:3}, ${s:-3} and ${s: -3} each produce, and why are the last two different?
answer
- offsets start at zero, not one
- one of the three is not a substring at all
- whitespace changes the parse
- negative counts from the far end
- parentheses are the other escape hatch
basics
~20 sThey produce oym, deployment and ent. ${s:4:3} takes three characters from zero-based offset 4. ${s:-3} is not a substring at all — it is the default-value operator, which returns s because s is set. Only ${s: -3}, with a space, means the last three characters.
solid answer
~40 s`${s:offset:length}` extracts a substring: offsets are zero-based, so `${s:4:3}` takes characters 4, 5 and 6 of `deployment` and yields `oym`. Omitting the length runs to the end of the value. A negative offset counts back from the end — but `${s:-3}` is parsed as the `:-` default-value operator, so it returns `deployment` (the value is set and non-empty) rather than the tail. To get the last three characters you must separate the colon from the minus, either as `${s: -3}` with a space or `${s:(-3)}` with parentheses, both of which give `ent`. Bash 4.2 and later also accept a negative *length*, where `${s:1:-1}` means "from offset 1 up to one character before the end".
code
bash · 8 liness=deployment
echo "${s:4:3}" # oym zero-based offset, then a length
echo "${s:4}" # oyment omitted length runs to the end
echo "${s:-3}" # deployment <- default-value operator, NOT a substring
echo "${s: -3}" # ent space separates the colon from the minus
echo "${s:(-3)}" # ent parenthesised alternative
echo "${#s}" # 10go deeper
Know that ${s:offset:length} slices a string with zero-based offsets, and that omitting the length runs to the end of the value.
Explain why ${s:-3} is the default-value operator rather than a substring, and give both fixes — a space before the minus, or parentheses around the offset.
Bring the portability and locale angles: negative lengths need bash 4.2 while macOS ships 3.2, lengths are in characters not bytes, and delimiter-based trimming is the more robust choice for real data.
Be ready to set a standard: position-based slicing encodes assumptions about field widths that outlive the format, so treat it as acceptable only for genuinely fixed-width values and require a parser everywhere else.
## Substring expansion ```bash s=deployment # d e p l o y m e n t # 0 1 2 3 4 5 6 7 8 9 echo "${s:4:3}" # oym echo "${s:4}" # oyment echo "${s:0:6}" # deploy ``` The form is `${parameter:offset:length}`. Offsets are **zero-based** — the most common off-by-one in shell code, because `cut -c` is one-based and people move between them. Omitting `:length` extracts everything from the offset to the end of the value. A length longer than what remains is not an error; you simply get what is there. ## The colon-minus collision This is the real content of the question. Bash decides which operator you meant by looking at the character immediately after the colon: ```bash s=deployment echo "${s:-3}" # deployment — the ${var:-word} default operator, word is "3" echo "${s: -3}" # ent — substring, offset -3 echo "${s:(-3)}" # ent — same thing, parenthesised ``` `${s:-3}` is unambiguously the default-value operator with the word `3`: since `s` is set and non-empty, it expands to `deployment` and the `3` is never used. If `s` had been unset you would get `3` — which looks like a plausible substring result and is why this bug survives review. The fix is to break the `:-` token apart with a space or wrap the offset in parentheses; both are standard idioms, and a computed offset such as `${s:$((n-3))}` sidesteps the problem naturally. ## Negative offsets and negative lengths A negative offset counts from the end of the value: `-1` is the last character, `-3` starts three from the end. Since bash 4.2 the *length* may also be negative, in which case it is an offset from the end rather than a count: ```bash s=deployment echo "${s: -3}" # ent echo "${s:1:-1}" # eploymen — from offset 1 to one before the end ``` Before bash 4.2 a negative length is an error (`substring expression < 0`), so if a script must run on an old bash — macOS still ships bash 3.2 as `/bin/bash` — compute the length arithmetically instead. Naming the bash version an answer depends on is a habit worth having here. ## Length, and how these compose `${#s}` gives the length of the value in characters — 10 for `deployment`. It pairs naturally with substrings for hand-rolled trimming and for validation: ```bash [[ ${#token} -ge 32 ]] || die "token looks truncated" middle=${s:1:${#s}-2} ``` Note that `${#s}` counts characters, not bytes, when the locale is a multibyte one such as UTF-8, and substring offsets are likewise character-based. That means `${s:0:1}` on a value starting with a multibyte character gives you the whole character, not half of it — good behaviour, but it does mean lengths differ from byte counts, which matters if you are sizing a field for a protocol. ## When to use it Substring expansion is the right tool when you know the position: fixed-width identifiers, a short hash prefix (`${sha:0:7}`), splitting a value at a known column. When the boundary is defined by a *delimiter* rather than a position, the trimming operators `#`, `##`, `%` and `%%` express the intent far better and do not break when the field width changes. Reaching for `${s:0:5}` to strip a prefix such as `refs/` works until someone renames the prefix; `${s#refs/}` states what you actually meant. All of these are non-destructive: the variable keeps its original value, and you assign the result if you want to keep it. And substring expansion is a bashism — POSIX `sh` has no `${var:offset:length}` — so a script with a `#!/bin/sh` shebang cannot rely on it.
- What does ${s:1:-1} mean, and when will it fail?Since bash 4.2 a negative length is treated as an offset from the end, so `${s:1:-1}` yields everything from the second character up to one before the last. On bash 4.1 and earlier — including the 3.2 that macOS ships as /bin/bash — it is a fatal `substring expression < 0` error, so compute the length with arithmetic if the script must run there.
- How does ${#s} relate to substring expansion, and does it count bytes or characters?`${#s}` gives the length of the value and is what you use to compute offsets, as in `${s:1:${#s}-2}`. It counts characters, not bytes, in a multibyte locale such as UTF-8, and substring offsets are character-based to match. If you need byte counts for a protocol field, the shell's length is not the number you want.
- When would you prefer ${s#prefix} over ${s:7}?Whenever the boundary is defined by a delimiter or a known prefix rather than a fixed column. `${s#refs/heads/}` states the intent and keeps working if the prefix changes length, whereas `${s:12}` silently produces garbage. Positional substrings belong to genuinely fixed-width data such as a short commit hash.
saying these in an interview costs you the question
- Says ${s:-3} returns the last three characters
- Assumes offsets are one-based like cut -c
- Thinks a negative offset works without the space or parentheses
- Believes negative lengths work on every bash
- Uses fixed offsets where a delimiter defines the boundary