While writing a shell script, you may run into this error:
./script.sh: line 10: ${VAR//foo/bar}: bad substitutionIt looks like a simple typo, but it is almost always a shell compatibility problem: the shell running your script does not understand a Bash-specific parameter expansion. This article covers the cause, a reproduction, the difference between Bash and sh/zsh, and two ways to fix it.
Cause
The bad substitution error appears when the shell cannot parse a parameter expansion. Syntax like ${VAR//pattern/replacement} (global replace) is not POSIX; it is a Bash extension. So it fails when the shebang is /bin/sh, or when zsh runs in a compatibility mode.
This bites hardest in CI/CD pipelines and Docker containers, where /bin/sh is often dash. A script that works locally in Bash suddenly breaks in production.
Reproduction
Run this with /bin/sh and it fails:
#!/bin/sh
VAR="foo-bar-foo"
echo ${VAR//foo/baz}./sample.sh: 3: Bad substitutionThe same script works with /bin/bash:
#!/bin/bash
VAR="foo-bar-foo"
echo ${VAR//foo/baz}
# => baz-bar-bazFix 1: Change the shebang to Bash
The simplest fix is to make the script run under Bash:
#!/usr/bin/env bashThis lets you use non-POSIX expansions safely. Note that on minimal images (such as Alpine Linux) Bash may not be installed, so you may need to add it (apk add bash).
Fix 2: Rewrite with POSIX-compatible syntax
If you cannot rely on Bash, use sed or another POSIX form to do the replacement:
#!/bin/sh
VAR="foo-bar-foo"
VAR=$(echo "$VAR" | sed 's/foo/baz/g')
echo "$VAR"This runs under sh and zsh as well. It is slightly slower, but it is the safe choice when portability matters.
Avoiding it in practice
- Declare the shell explicitly in every script:
#!/usr/bin/env bashor#!/bin/sh. - Document which shell your CI environment uses.
- Install Bash in your Dockerfile (
RUN apk add bash) if your scripts need it. - Run
shellcheckto detect non-portable syntax before it reaches CI.
Summary
bad substitution is not a typo; it means the shell running the script cannot understand the expansion. Use a Bash shebang, or rewrite the expansion in POSIX-compatible form. In production, keep the shell consistent between local and CI so behavior matches.