If a function runs local value=$(command) and then observes status zero, separate the declaration from the assignment before investigating the command itself. The declaration is a command with its own status. ShellCheck documents this boundary in SC2155, including related cases with export and readonly.
This is a Bash function example, not a portable /bin/sh recipe. It addresses a failure that is already hidden when the declaration finishes. A later logger is not required to trigger it.
Compare the two forms in an isolated shell
Save the following as declaration-status.bash and run bash declaration-status.bash. It uses a synthetic producer, writes no files, and contacts no service. The producer emits a recognizable value and deliberately returns seven. A nonempty value is therefore not evidence of success.
set +e
produce() {
printf '%s' 'partial-result'
return 7
}
combined() {
local value="$(produce)"
local observed=$?
printf 'combined: value=%s status=%s\n' "$value" "$observed"
}
separated() {
local value observed
value="$(produce)"
observed=$?
printf 'separated: value=%s status=%s\n' "$value" "$observed"
}
combined
separatedThe expected observation is partial-result in both cases, with status zero for the combined declaration and seven for the separate assignment. The fixture disables errexit only inside the disposable process so both observations can be printed. Do not paste that setting into an application as a general repair.
Return the producer failure deliberately
For an application helper, decide what callers should receive when output is incomplete. This example discards partial output on failure and returns the producer status. The failure branch saves the status before printing its diagnostic.
read_value() {
local value status
if value="$(produce)"; then
printf '%s\n' "$value"
else
status=$?
printf 'producer failed: %s\n' "$status" >&2
return "$status"
fi
}
if read_value; then
printf 'unexpected success\n' >&2
exit 1
else
observed=$?
test "$observed" -eq 7 || exit 1
fiRun this immediately after the first fixture so produce remains defined. It should print a diagnostic to standard error and pass the final status check. If a real producer needs cleanup, keep the producer result separately and choose an explicit cleanup-failure policy. Do not replace its status accidentally with a successful diagnostic.
Review the declaration boundary, then the caller
Search the affected function for declarations containing command substitutions. SC2155 covers the basic pattern; its documentation also explains an optional additional check for masked returns that are suppressed by default. A clean default lint run alone is insufficient evidence that every declaration preserves an underlying result.
After changing the function, exercise both a successful producer and a failing producer in a safe fixture. Confirm the captured value, standard error and caller-visible status independently. If the function returns the right status but the outer script still reports success, continue with the existing guide on preserving status before logging and aggregating batch failures. That is the next boundary, rather than proof that this declaration fix failed.