Skip to content

No warning when a command substitution clobbers $? in the same command #3522

Description

@jalcim

For new checks and feature suggestions

The closest existing issues are #1510 (command substitution in an if condition masking errexit) and #3242, which cover different situations. This one is about $? being silently replaced by a command substitution's own exit status within the same command.

Here's a snippet or screenshot that shows the problem:

#!/bin/bash
f=/etc/hosts

grep -q zzzznonexistentzzz "$f"
echo "direct            : $?"

grep -q zzzznonexistentzzz "$f"
echo "with substitution : $(basename "$f") $?"

grep -q zzzznonexistentzzz "$f"
r=$?
echo "captured first    : $(basename "$f") $r"

Output:

direct            : 1
with substitution : hosts 0
captured first    : hosts 1

Bash expands arguments left to right, so $(basename "$f") runs before $? is read and replaces $? with its own exit status. The reported status is basename's, never grep's.

Here's what shellcheck currently says:

Nothing. No output, exit status 0, on shellcheck 0.11.0.

Here's what I wanted or expected to see:

A warning on $? used in a command whose earlier expansions include a command substitution, suggesting the status be captured into a variable first.

Why this matters in practice

This bites hardest in test and verification helpers, where the label being built is exactly what triggers it:

check() { [ "$2" = 0 ] && echo "[ok] $1" || echo "[FAIL] $1"; }

run_the_thing
check "$(basename "$file") : produced valid output" $?

Every such call reports success unconditionally. The check cannot fail, so a red result is unreachable and the suite is green by construction.

I hit this in a hardware verification script: twelve checks written this way had been passing for hours while measuring nothing. They looked identical to the ones that worked.

SC2319 is adjacent but does not cover it. It fires on $? read after a condition ([ ... ]; r=$?), which is a different pattern, and it does not fire on any of the three lines above. Running shellcheck on the corrected form — where the status is captured first, which is the recommended fix — is what produces SC2319 warnings, while the broken form stays silent.

I tested four variants on 0.11.0:

snippet shellcheck output
echo "$(basename "$f")" $? nothing
printf '%s %s' "$(basename "$f")" $? nothing
r=$?; echo "$(basename "$f")" "$r" nothing
if [ $? -eq 0 ] SC2181

A script can therefore be clean by shellcheck's standards and still contain checks that are structurally incapable of failing.

Note

The false-negative is the report. The SC2319 observation is context, not a claim that the rule is wrong — it targets a different pattern and does so correctly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions