skip to content

A bash script does `source "$LIB_DIR/common.sh"` and ShellCheck reports SC1090, "can't follow non-constant source"; a second script that sources `lib/common.sh` by a literal path gets SC1091 instead, followed by a pile of "referenced but not assigned" warnings. What is the difference between those two codes, and how do you get ShellCheck to actually read the library?

level: seniorimportance: should knowfreq 34%

answer

  1. variable in the path versus literal path
  2. it never read the library
  3. every helper looks undefined
  4. name the file, then allow reading it
  5. resolve relative to the script's directory

basics

~20 s

SC1090 means the sourced path is built from a variable, so ShellCheck cannot know which file to read; SC1091 means the path is literal but the file was not made available. Fix both with a # shellcheck source= directive and run with -x.

solid answer

~50 s

The two codes mark different failures of the same step. `SC1090` fires when the path is non-constant — `"$LIB_DIR/common.sh"` is only known at runtime, so static analysis cannot resolve it. `SC1091` fires when the path is literal but ShellCheck still did not read the file, typically because external sources are not enabled or the relative path does not resolve from the working directory. Either way ShellCheck has not seen the library, so every function and variable the library defines looks undefined — which is where the `SC2154` "referenced but not assigned" noise comes from. The fix is to tell it where to look: a `# shellcheck source=lib/common.sh` directive on the line above the `source` command, plus `-x` (`--external-sources`) so it is allowed to read files outside the ones you passed in. In a repo, set `source-path=SCRIPTDIR` and `external-sources=true` in `.shellcheckrc` once instead of annotating every call site.

code

bash · 10 lines
bash
#!/usr/bin/env bash
set -euo pipefail

LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib"

# Names the file for analysis only; the runtime path is unchanged.
# shellcheck source=lib/common.sh
source "$LIB_DIR/common.sh"

log_info "starting"

go deeper

for a junior

Know that ShellCheck does not automatically read the files your script sources, and that the resulting undefined-variable warnings are a symptom of that rather than real bugs.

for a middle

Distinguish SC1090 (path built from a variable) from SC1091 (literal path not read), and be able to write the # shellcheck source= directive and add -x.

for a senior

Fix it as configuration: .shellcheckrc with external-sources and source-path=SCRIPTDIR, directives only where the path is truly dynamic, and refuse the blanket disable that would kill SC2154's real value.

for a principal

Own the tradeoff between a shared shell library and the analysis cost it imposes, and set the repo-level policy so the lint result stays trustworthy rather than routinely noisy.

## Why ShellCheck stops at a source line ShellCheck analyses a script as text. When it reaches `source lib/common.sh` (or the POSIX `.` form) it would like to read that file too, because everything the library defines — functions, variables, `set` options — changes what the rest of the script means. Two things can stop it, and each has its own code. **SC1090 — non-constant source.** The path contains an expansion: ```bash source "$LIB_DIR/common.sh" source "$(dirname "$0")/lib/common.sh" ``` The value of `LIB_DIR` or `$0` exists only when the script runs. No amount of configuration lets a static tool resolve it, so ShellCheck reports that it cannot follow and moves on. **SC1091 — not following a file it could name.** The path is literal, so ShellCheck knows *which* file, but it still did not read it: external sources were not enabled, the file is not present relative to the directory ShellCheck was invoked from, or it is genuinely missing. ## The knock-on damage The codes themselves are informational. The real cost is what follows. Because ShellCheck never read the library, it believes: - variables the library assigns are never assigned — `SC2154`, "var is referenced but not assigned", on every use; - variables the library *reads* but your script assigns are unused — `SC2034`, "appears unused"; - functions the library defines are unknown commands. A script that is entirely correct can therefore produce dozens of findings, and the usual reaction — suppressing `SC2154` file-wide — throws away a genuinely useful check. Fixing the source resolution makes the noise disappear at its cause. ## Making it follow the file Two pieces are needed: telling ShellCheck *which* file, and allowing it to read files you did not pass on the command line. **The directive.** On the line above the `source` command: ```bash # shellcheck source=lib/common.sh source "$LIB_DIR/common.sh" ``` The directive names the file for analysis purposes; the runtime path is untouched. Use the path as it exists in the repository. There is also `# shellcheck source=/dev/null` for the case where you deliberately want ShellCheck to treat the source as empty — for example a host-specific config that does not exist in the repo at all. That is an honest suppression: it says "nothing analysable here", and it stops SC1090 without pretending a file exists. **The flag.** `-x`, long form `--external-sources`, permits ShellCheck to open files other than the ones given as arguments. Without it, a directive pointing at a real file still will not be followed. ```bash shellcheck -x scripts/deploy.sh ``` **Resolution root.** Relative paths resolve from ShellCheck's working directory by default, which breaks the moment CI invokes it from the repo root while the script lives three directories down. The `source-path` setting fixes that; the special value `SCRIPTDIR` means "resolve relative to the directory of the script being checked", which is what most repositories actually want: ``` # .shellcheckrc external-sources=true source-path=SCRIPTDIR ``` `source-path` can also be given as a directive (`# shellcheck source-path=SCRIPTDIR`) or repeated for several search directories. With this in `.shellcheckrc`, most `source` lines with literal paths resolve with no per-file annotation at all. One more flag worth knowing: `-a` (`--check-sourced`) additionally reports findings *inside* the sourced files, rather than only using them for context. That is useful for a one-shot audit and usually redundant in CI, where the library files are checked in their own right anyway. ## The library file itself A sourced library normally has no shebang, because it is not executed. That triggers `SC2148`. Give it a first line of `# shellcheck shell=bash` so ShellCheck knows the dialect. Some teams add a shebang anyway as documentation; either is fine as long as it names the shell that will actually run the code. ## Judgment The senior-level point is that these codes are a *configuration* problem masquerading as a code problem. The wrong response is a repo-wide `disable=SC1090,SC2154`, which silences a check that catches real typos in variable names. The right response is one `.shellcheckrc`, `-x` in the lint command, and a directive at the handful of call sites whose path is genuinely dynamic — after which the remaining `SC2154` findings are all real.

  • Why is disabling SC2154 file-wide a bad response to this?
    Because SC2154 is the check that catches a misspelled variable name — `$HOSTANME` instead of `$HOSTNAME` — which in bash expands to the empty string with no error. Silencing it repo-wide to quiet the fallout from an unfollowed source trades a real defect class for a configuration shortcut. Fix the source resolution and the false ones disappear on their own.
  • What do you do when the sourced file genuinely does not exist in the repository, such as a host-specific config?
    Use `# shellcheck source=/dev/null` above the source line. That tells ShellCheck to analyse it as an empty file, which clears SC1090 honestly rather than pretending some other file is the one being loaded. Variables the missing config sets will still look unassigned, so those call sites need their own handling — often a `${VAR:?}` default that documents the requirement.
  • Does the `source=` directive change what the script does at runtime?
    No. It is an ordinary comment; bash ignores it entirely. Only ShellCheck reads it, and only to decide which file to analyse. The runtime path stays whatever the `source` command computes, so the directive and the real path can drift apart — which is a small maintenance cost worth knowing about when a library is moved.

saying these in an interview costs you the question

  • SC1090 means the file is missing
  • Just disable SC1090 and SC2154 for the repo
  • The source= directive changes which file bash loads
  • ShellCheck follows sourced files automatically
  • A sourced library needs no shell directive

context