ShellCheck rules explained
Free plain-English fix for every common ShellCheck warning — SC2086, SC2046, SC2068, SC2148 and more — each with a working before/after example.
Browse in-depth explanations of common ShellCheck warnings. Every published guide includes unsafe and corrected examples, practical context, exceptions and related rules.
- SC2046: Quote `$(...)` to prevent word splitting
Command substitution results are word-split and globbed when unquoted. SC2046 explained with safe alternatives.
- SC2148: Add a shebang to your script
Without a shebang the script runs under whatever shell happens to invoke it. Always start with `#!/usr/bin/env bash` (or another explicit interpreter).
- SC2154: Variable is referenced but not assigned
SC2154 explained: variable is referenced but not assigned. Why the warning fires, how typos silently delete the wrong files, and how to fix it with `set -u`.
- SC2155: SC2155 — Split declare and assign: fix in 30 seconds
Fix ShellCheck SC2155: local foo=$(cmd) always returns 0, so set -e never sees the failure. Split the declaration from the assignment instead.
- SC2164: Use `cd ... || exit` to handle a failed `cd`
If `cd` fails the script keeps running in the wrong directory. Always pair it with `|| exit` or `|| return`.
- SC2181: Check exit code directly, not via `$?`
Fix ShellCheck SC2181: checking $? separately is brittle and easy to break when a line is inserted. Test the command directly in the if instead.
- SC2034: Variable appears unused
SC2034: variable appears unused. Why ShellCheck flags it, when it is a real bug, and how to silence false positives with `# shellcheck disable=SC2034`.
- SC1090: Can't follow non-constant source
SC1090 explained: ShellCheck can't follow non-constant source. How to fix it with a `# shellcheck source=` directive, with copy-paste examples.
- SC2002: Useless use of `cat`
`cat file | cmd` spawns an extra process. Use `cmd < file` or `cmd file` instead.
- SC2236: Use `-n` / `-z` instead of `! -z` / `! -n`
Test idioms have direct positive forms — prefer `[ -n "$x" ]` over `[ ! -z "$x" ]`.
- SC2206: Quoting to split into an array
Assigning `arr=( $var )` re-splits on IFS and globs. Use `read -ra` or `mapfile` for safe splitting.
- SC2207: Prefer `mapfile` or `read -a` over splitting output
`arr=( $(cmd) )` is unsafe. Use `mapfile -t arr < <(cmd)` for line-based output.
- SC2103: Use a subshell to avoid having to `cd` back
Wrap a temporary `cd` in `( ... )` so the parent shell keeps its directory.
- SC1091: SC1091 — Not following source file: fix in 30 seconds
Fix ShellCheck SC1091: ShellCheck cannot resolve your source path. How to silence it safely with a directive, without hiding real bugs.
- SC2004: $/${} is unnecessary on arithmetic variables
Inside `$(( ... ))`, Bash already evaluates bare variable names. The leading `$` is noise and can mask intent.
- SC2006: Use $(...) notation instead of legacy `...` backticks
Backticks for command substitution are deprecated. `$(...)` nests cleanly and handles quoting correctly.
- SC2009: Use pgrep instead of grepping ps output
Parsing `ps | grep` is racy and matches the grep itself. `pgrep` exists for exactly this.
- SC2012: Use find instead of ls to iterate files
Parsing `ls` output breaks on filenames with spaces, newlines, or quotes. Use `find` with NUL delimiters.
- SC2015: A && B || C is not if-then-else
The `cmd1 && cmd2 || cmd3` idiom runs cmd3 whenever cmd2 fails — not what most people expect. Use an explicit if.
- SC2016: Expressions do not expand in single quotes
Single-quoted strings are literal. If you want `$var` to expand, switch to double quotes.
- SC2024: sudo doesn't affect redirections
In `sudo cmd > /root/file`, the shell — not sudo — opens the file, so the redirection still runs as your user.
- SC2035: Use ./*glob* or -- so files are not read as flags
A glob that matches a file like `-rf` becomes a deletion flag. Prefix with `./` or `--` to disarm it.
- SC2059: Don't use variables in the printf format string
A printf format string built from user input is a format-string injection. Always use a literal format and pass data as arguments.
- SC2094: Do not read and write one file in a pipeline
`grep foo file > file` truncates the file before grep reads it — you lose everything.
- SC2116: Useless echo — assign the value directly
`x=$(echo "$y")` is the same as `x=$y`. Drop the subshell and the echo.
- SC2126: Consider using grep -c instead of grep|wc -l
Counting matches with `grep pattern | wc -l` spawns two processes. `grep -c` does it in one.
- SC2129: Group commands instead of repeating redirects
Appending the same file from many lines opens/closes it each time. Group with `{ … } >> file` to open once.
- SC2143: Use grep -q instead of comparing output
`if [ -n "$(grep x file)" ]` does extra work. `grep -q` returns success/failure directly.
- SC2145: Argument mixes string and array — use one or the other
"prefix$@" mashes the first array element onto prefix and leaves the rest separate. Almost never what you want.
- SC2153: Possible misspelling: VAR may not be assigned
ShellCheck noticed a variable that looks like a near-miss of one you defined. Often a typo.
- SC2162: read without -r mangles backslashes
Without `-r`, `read` interprets backslashes as line continuations and escape sequences. Always pass `-r`.
- SC2166: Prefer [ p ] && [ q ] over [ p -a q ]
The `-a` and `-o` operators inside `[ ]` are deprecated by POSIX. Chain with `&&` and `||` instead.
- SC2174: mkdir -p -m only sets permissions on the final directory
`mkdir -p -m 700 a/b/c` only chmods `c` — intermediate `a` and `b` use the umask.
- SC2199: Arrays concatenate in [[ ]] — use a loop
Comparing an array against a string in `[[ ]]` joins it with the first IFS char. Almost always a bug.
- SC2230: which is non-standard; use builtin command -v instead
`which` is an external command with quirks across distros. `command -v` is the POSIX, builtin way to find a command.
- SC2038: Use find -exec or -print0 instead of piping to xargs
SC2038: `find ... | xargs` breaks on filenames with spaces, newlines, or quotes. Use `-print0 | xargs -0` or `-exec`.
- SC2044: SC2044 — Stop looping over find output: safe fix
Free fix for ShellCheck SC2044 with a working example. `for f in $(find ...)` breaks on any filename with a space. Paste your script for an instant analysis.
- SC2064: Single-quote trap so variables expand later
SC2064: `trap "cleanup $tmpdir" EXIT` expands `$tmpdir` when the trap is set, not when it fires. Use single quotes.
- SC2076: Do not quote the right side of =~
SC2076: quoting the pattern in `[[ $var =~ "pat" ]]` turns it into a literal string match. Use an unquoted pattern or a variable.
- SC2088: ~ does not expand inside quotes
SC2088: `"~/file"` is the literal 6 characters, not your home directory. Use `$HOME/file` or unquoted `~`.
- SC2013: Read file lines with `while read`, not `for`
`for line in $(cat file)` splits on whitespace, not lines. SC2013 recommends a `while IFS= read -r` loop instead.
- SC2005: Useless `echo $(cmd)` — drop the `echo $(...)` wrapper
`echo $(cmd)` re-splits and re-globs the output for no reason. SC2005 says to run the command directly or quote the substitution.
- SC2010: Don't parse `ls` output — use globs or `find` instead
Piping `ls` into `grep` breaks on filenames with spaces or newlines. SC2010 recommends globs, `find`, or shell parameter matching.
- SC2043: This loop only ever runs once — check the list expression
`for x in "$onething"` iterates a single fixed value. SC2043 flags for-loops whose list clearly has just one item.
- SC2053: Quote the right side of `==` for a literal match
Quoting the RHS of `[[ $x == $pattern ]]` disables glob matching. SC2053 flags cases where the pattern was probably meant to stay unquoted.
- SC2091: Drop `$(...)` if you meant to run the command
Wrapping a command in `$(...)` when used as a standalone statement runs it and discards the output as a no-op. SC2091 flags this pattern.
- SC2119: Function uses `$1` but was called with no args
SC2119 warns when a function referencing `$1` etc. is invoked with no arguments, likely inheriting the caller's unrelated positional params.
- SC2120: Function references arguments but no caller passes any
SC2120 flags a function that reads `$1`/positional params when every call site invokes it with zero arguments.
- SC2140: Confusing string concatenation from stray quotes
A pattern like `"a"$b"c"` looks like broken syntax and often does not mean what it appears to. SC2140 flags this quoting layout.
- SC2178: Array variable is being reassigned as a string
Assigning a plain value to a variable previously declared/used as an array overwrites it with a scalar. SC2178 flags this loss of array-ness.
- SC2184: `unset` requires the variable name, not its expansion
`unset "$var"` unsets the variable named by the value of `$var`, not `$var` itself. SC2184 flags this common mix-up.
- SC2219: Use `(( ... ))` instead of `let ...` for arithmetic
`let expr` is an older, quoting-sensitive arithmetic form. SC2219 recommends the clearer `(( expr ))` compound command.
- SC2242: `exit`/`return` takes only one argument
Extra arguments after `exit`/`return` are ignored, not concatenated or evaluated. SC2242 flags calls that pass more than one value.
- SC2211: This is a glob used as a command name
SC2211: a glob pattern like `*.sh` appears where a command name is expected. Usually a missing loop or misplaced quote.
- SC2239: The shebang specifies an invalid path
SC2239: the interpreter path in the shebang is not an absolute path. Use /usr/bin/env or an absolute path like /bin/bash.