Skip to main content
DevOps Insightsguides & answers

Linux shell · troubleshooting

Bad substitution

A bash-only expansion such as ${VAR^^}, ${VAR:0:3} or an array ran under /bin/sh, which on Debian and Ubuntu is dash. Run it with bash, or use a POSIX form.

Reproduced on Ubuntu 24.04, . Where the text says “reproduced”, the behaviour was run on that version, not taken from documentation.

First published

New to this? Read what is actually happening first, then come back to the steps.

On this page · 8 sections

Before you start

  • The script or Dockerfile line, and which shell actually runs it.

Run this first

step 1 of 1
readlink -f /bin/sh

Look for /usr/bin/dash on Debian and Ubuntu

Shows which shell /bin/sh really is on this machine or image.

Work out which cause you have

A few questions to narrow the list. Every answer ends in a command that confirms or rules the cause out — this cannot see your cluster, so nothing here is a certainty until you have checked.

Narrow it down

0 answered · nothing is sent anywhere

Where does the error appear?

Cause space

3 of 3 still possible

  • Bash syntax running under /bin/shCommon
  • A Dockerfile RUN line uses bash syntaxCommon
  • A malformed expansion in bash itselfOccasional

Nothing ruled out yet. Answer the question above and the branches your answer eliminates will strike through here.

Or diagnose it manually

In this order. The first command usually contains the whole answer.

  1. Step 1

    readlink -f /bin/sh

    Shows which shell /bin/sh really is on this machine or image.

    Look for /usr/bin/dash on Debian and Ubuntu

Every cause, and how to fix it

Ordered by how often each one turns out to be the answer.

Bash syntax running under /bin/sh

Common

${VAR^^}, ${VAR,,}, ${VAR:offset:length}, ${VAR//a/b} and arrays are bash features that dash rejects.

Confirm

head -1 script.sh
ls -l /bin/sh

#!/bin/sh, or a sh script.sh call, on a system where /bin/sh points to dash

Fix

  • Start the script with #!/usr/bin/env bash and run it directly or with bash script.sh, not sh script.sh.
  • Or rewrite in POSIX form: printf '%s\n' "$V" | tr '[:lower:]' '[:upper:]' for upper case, printf '%.3s' "$NAME" for the first three characters, ${NAME%%.*} to cut at the first dot.
#!/usr/bin/env bash

Reproduced on Ubuntu 24.04: the same file run with bash printed HELLO.

A Dockerfile RUN line uses bash syntax

Common

Shell-form RUN uses /bin/sh -c by default, so on Debian- and Ubuntu-based images it is dash.

Confirm

grep -n '^SHELL\|^RUN' Dockerfile

No SHELL line, and a RUN using bash-only syntax

Fix

  • Add SHELL ["/bin/bash", "-c"] before the RUN lines that need bash, or call bash explicitly: RUN bash -c '…'.
  • Or keep RUN POSIX, which also works on Alpine images where bash may not be installed.

A malformed expansion in bash itself

Occasional

A space inside the braces, a missing closing brace, or an invalid operator.

Confirm

bash -n script.sh

The line number to inspect

Fix

  • Correct the expansion: ${NAME}, not ${ NAME}.

Understanding it properly

Skip this if you are mid-incident — the working part of the page is above. Worth reading afterwards, because understanding the mechanism is what stops the next one.

In plain terms

Many expansions people use every day are bash features, not part of the basic shell. On Ubuntu and Debian, /bin/sh is a smaller shell called dash that does not know them. Scripts starting with #!/bin/sh, commands run with sh script.sh, and Dockerfile RUN lines all use it.

Words used on this page

dash
The /bin/sh on Debian and Ubuntu: fast, POSIX-only, no bash extensions.
Shebang
The first line, such as #!/usr/bin/env bash, naming the interpreter when the file is run directly.
What is actually happening

Reproduced on 23 October 2026 on Ubuntu 24.04, where /bin/sh is dash. A #!/bin/sh script with ${V^^} stopped with ./b.sh: 3: Bad substitution, exit 2. The same file run with bash printed HELLO. ${NAME:0:3} failed the same way under sh.

sh script.sh ignores the shebang and uses dash whatever the first line says, and /bin/sh -c "…" printed /bin/sh: 1: Bad substitution. Docker documents ["/bin/sh", "-c"] as the default shell for shell-form RUN on Linux, which is why this appears in image builds based on Debian or Ubuntu.

Bash prints the lowercase bad substitution for malformed expansions too: ${ NAME} with a space gave ${ NAME}: bad substitution.

How to tell this is your problem
WhereWhat you see
A #!/bin/sh script, or sh script.shscript.sh: 3: Bad substitution
Dockerfile RUN, Makefile recipes, sh -c/bin/sh: 1: Bad substitution
bash, malformed expansion${ NAME}: bad substitution

How to know it is actually fixed

  • The script or build step runs with the intended shell and prints the expected value.

Stopping it happening again

  • Put #!/usr/bin/env bash on scripts that use bash features, and call them directly.
  • Run shellcheck in CI: with a #!/bin/sh shebang it reports these as SC3059 (case modification) and SC3057 (string indexing), among others.

Sources

Behaviour described here is drawn from official documentation. Where a figure could not be confirmed on an official page it is attributed in the text rather than stated as canonical.

Did this get you to an answer?

No text box on purpose — please do not paste production logs anywhere

Something wrong or out of date? Report it