---
title: 'Static Analysis'
source: 'https://academia.sh/en/courses/shell-programming/static-analysis'
course: 'Shell Programming'
language: en
updated: '2026-08-17T18:10:00+00:00'
license: 'CC BY-SA 4.0'
---

# Static Analysis

Finding errors without running the script, common shell error classes, portability checking based on the interpreter declaration, and the correct use of warning suppression.

The previous lesson's tools find an error after it has occurred. Most errors
in shell scripts, though, are determined before they occur: an unquoted
expansion, an unchecked exit code, building a file list from a command's
output, using a feature the interpreter declaration does not permit.

These errors can be seen by looking at the code itself, without running it.
**Static analysis** is the method of parsing source text and searching for
known faulty patterns. For the shell, this job is delegated to a tool that
parses the script and applies hundreds of pattern rules.

The tool's value is that it remembers, all at once, every trap this course
has explained one by one.

## An Inspection

The script below carries most of the errors covered in previous lessons at
once:

```sh
#!/bin/sh
log=$1
for f in $(ls logs); do
  echo "processing: $f"
done
if [ $log = "access.log" ]; then
  count=`wc -l < $log`
  echo "lines: $count"
fi
cd /tmp
rm -rf $log_backup
```

The analyzer's long-form output marks the finding beneath the source line
and suggests a corrected form. The same findings read more compactly in
single-line form:

```sh
shellcheck -f gcc flawed.sh
```

```
flawed.sh:3:10: error: Iterating over ls output is fragile. Use globs. [SC2045]
flawed.sh:6:6: note: Double quote to prevent globbing and word splitting. [SC2086]
flawed.sh:7:9: note: Use $(...) notation instead of legacy backticks `...`. [SC2006]
flawed.sh:7:18: note: Double quote to prevent globbing and word splitting. [SC2086]
flawed.sh:10:1: warning: Use 'cd ... || exit' or 'cd ... || return' in case cd fails. [SC2164]
flawed.sh:11:8: warning: log_backup is referenced but not assigned. [SC2154]
```

Five separate error classes, in an eleven-line script. The last one's
consequence is `rm -rf` running with no argument at all in a script without
`set -u`.

The default format is more detailed: it writes the corrected line under
every finding with the heading `Did you mean:`. This format suits working
with a single finding; for a batch inspection, the single-line format above
is appropriate.

## Error Classes Caught

Warnings fall into four groups, and each was the subject of a separate
lesson in this course.

**Expansion and quoting.** Unquoted variable expansion, using `$*` instead
of `"$@"`, the wrong form of array expansion. The entire Variables and
Quoting Rules lesson belongs to this class. This is the most frequently
seen warning.

**Exit code.** An unchecked `cd`, unchecked command substitution, assignment
combined with `local`, a pipeline code getting lost. The traps from the
Error Handling lesson are caught here.

**Shell idioms.** Building a file list from a command's output, a missing
`-r` in a `read` call, the old backtick notation, the wrong operator inside
`[ ]`. Warnings from the Loops and Conditionals lessons.

**Undefined name.** Reading a variable that was never assigned, variables
that turn into a different name through a typo. This class can go unnoticed
even at run time in an interpreted language.

Every warning has an identifier (`SC` and four digits). The identifier
serves two purposes: reaching the warning's detailed explanation, and — when
needed — suppressing that warning in a targeted way.

## Severity Levels

Warnings fall into four levels: `error`, `warning`, `info`, `style`. The
`-S` option hides everything below a given level:

```sh
shellcheck -S warning -f gcc flawed.sh
```

```
flawed.sh:3:10: error: Iterating over ls output is fragile. Use globs. [SC2045]
flawed.sh:10:1: warning: Use 'cd ... || exit' or 'cd ... || return' in case cd fails. [SC2164]
flawed.sh:11:8: warning: log_backup is referenced but not assigned. [SC2154]
```

Three findings remain; the quoting and backtick warnings at the `info` and
`style` levels were hidden.

This is useful when adding analysis to an existing codebase: the `error`
level is cleaned up first, then the threshold is lowered in stages. Keeping
the threshold permanently high, on the other hand, means giving up most of
the tool's value — `info`-level quoting warnings are the ones that most
often turn into real errors.

## Portability Checking

The analyzer reads the interpreter declaration and applies its rules
accordingly. In a script declaring `#!/bin/sh`, bash extensions count as
errors:

```sh
#!/bin/sh
codes=(200 404)
if [[ ${#codes[@]} -gt 1 ]]; then
  echo "multiple codes"
fi
```

```
In portable.sh line 2:
codes=(200 404)
      ^-------^ SC3030 (warning): In POSIX sh, arrays are undefined.


In portable.sh line 3:
if [[ ${#codes[@]} -gt 1 ]]; then
   ^----------------------^ SC3010 (warning): In POSIX sh, [[ ]] is undefined.
```

This is the rule from the Script Anatomy lesson enforced by a tool:
declaring POSIX while using a bash feature causes the script to break
silently on another system. When the same script declares bash, these two
warnings are not produced.

The limit of this check can be seen from here too: the tool decides based on
the **declared** shell. If the declaration is wrong, the check is done
wrong. This is why the interpreter declaration must be written to match the
features the script actually uses.

## Warning Suppression

When a warning needs to be deliberately ignored, a directive is written
into the source code:

```sh
#!/usr/bin/env bash
# shellcheck disable=SC2086
log="a b.log"
echo $log
```

The directive applies to the command **after** it; placed at the top of the
file, before the first command, it covers the whole file.

Suppression is bound by three rules.

**An identifier is given.** The `disable` directive can be written without
an identifier, but then it turns off every warning and also hides real
errors that will occur in the future.

**The scope is narrowed.** File-level suppression takes every line in that
file out of the check. Writing it directly above the line where the warning
arises is preferred.

**A justification is written.** Why the suppressed warning is a false
alarm is stated with a comment. A suppression with no justification tells
the next person reading the code, "I do not know what is going on here."

In cases where word splitting is genuinely wanted, there is a better
solution: putting the value into an array. The notation `"${array[@]}"`
produces no warning and also makes the intent visible in the code.
Suppression is reserved for cases where rewriting is not possible.

## Putting the Script Through Inspection

`report.sh` has been written throughout this course avoiding the patterns
the analyzer would warn about. The small script below follows the same
rules:

```sh
#!/usr/bin/env bash
set -Eeuo pipefail
log="${1:-access.log}"
[ -r "$log" ] || { echo "cannot read: $log" >&2; exit 3; }
count=$(grep -c '' "$log")
printf 'lines: %d\n' "$count"
```

```sh
shellcheck clean.sh; echo "code=$?"
```

```
code=0
```

The exit code is `0` and there is no output. The tool returns a nonzero code
if there is a finding; this makes it possible to turn the check into a
validation step:

```sh
shellcheck report.sh && bash -n report.sh && echo "check passed"
```

Running this command before a submission to version control, or as a
continuous integration step, is the cheapest way to keep the quality of
shell scripts intact.

## The Limit of Static Analysis

The tool sees the code's **form**, not its **meaning**. What it cannot
catch:

- A wrong field number (writing `$8` instead of `$7`).
- A wrong regular expression (the pattern is valid but does not describe
  the intended set).
- Wrong business logic (a percentage divided by the wrong total).
- Errors that depend on the runtime environment (a nonexistent command,
  insufficient permission).

These classes require **testing**: running the script with a known input
and comparing its output against the expected output. This course's
`access.log` file being fixed and reproducible exists precisely to make
this kind of testing possible.

Static analysis does not substitute for testing; it reduces the number of
errors testing has to find.

## Summary

- Static analysis parses a script without running it and searches for
  known faulty patterns.
- The main classes caught are quoting, exit codes, shell idioms, and
  undefined names; all were the subject of separate lessons in this
  course.
- The check is done according to the interpreter declaration; bash
  extensions produce warnings in a script that declares POSIX.
- Suppression is done with an identifier, a narrow scope, and a
  justification; if word splitting is wanted, the correct solution is
  using an array.
- The tool returns a nonzero code if there is a finding; this turns the
  check into a validation step.
- Static analysis sees form, not meaning; a wrong field number and wrong
  business logic can only be found through testing.

## Next Step

The script is correct, robust, and checked — but it is still run by hand.
A report's value lies in its being produced regularly. The final lesson
takes up scheduled tasks: reading a cron entry, at which points the
scheduler's environment departs from an interactive shell, where output
should go, and preventing overlapping runs.
