diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -109,6 +109,7 @@
 * [Codacy](https://www.codacy.com/)
 * [Code Climate](https://codeclimate.com/)
 * [Code Factor](https://www.codefactor.io/)
+* [Github](https://github.com/features/actions)(only Linux)
 
 Services and platforms with third party plugins:
 
@@ -116,7 +117,7 @@
 
 Most other services, including [GitLab](https://about.gitlab.com/), let you install
 ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),
-or by downloading and unpacking a [binary release](#installing-the-shellcheck-binary).
+or by downloading and unpacking a [binary release](#installing-a-pre-compiled-binary).
 
 It's a good idea to manually install a specific ShellCheck version regardless. This avoids
 any surprise build breaks when a new version with new warnings is published.
@@ -209,15 +210,21 @@
 
 or use `koalaman/shellcheck-alpine` if you want a larger Alpine Linux based image to extend. It works exactly like a regular Alpine image, but has shellcheck preinstalled.
 
+Using the [nix package manager](https://nixos.org/nix):
+```sh
+nix-env -iA nixpkgs.shellcheck
+```
+
 Alternatively, you can download pre-compiled binaries for the latest release here:
 
-* [Linux, x86_64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
-* [Linux, armv6hf](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
-* [Linux, aarch64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.armv6hf.tar.xz) aka ARM64 (statically linked)
-* [MacOS, x86_64](https://shellcheck.storage.googleapis.com/shellcheck-stable.darwin.x86_64.tar.xz)
-* [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-stable.zip)
+* [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
+* [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
+* [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked)
+* [MacOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz)
+* [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip)
 
-or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums, older versions and the latest daily builds.
+or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases
+(including the [latest](https://github.com/koalaman/shellcheck/releases/tag/latest) meta-release for daily git builds).
 
 Distro packages already come with a `man` page. If you are building from source, it can be installed with:
 
@@ -244,7 +251,7 @@
 
 ```bash
 scversion="stable" # or "v0.4.7", or "latest"
-wget -qO- "https://storage.googleapis.com/shellcheck/shellcheck-${scversion?}.linux.x86_64.tar.xz" | tar -xJv
+wget -qO- "https://github.com/koalaman/shellcheck/releases/download/${scversion?}/shellcheck-${scversion?}.linux.x86_64.tar.xz" | tar -xJv
 cp "shellcheck-${scversion}/shellcheck" /usr/bin/
 shellcheck --version
 ```
diff --git a/Setup.hs b/Setup.hs
--- a/Setup.hs
+++ b/Setup.hs
@@ -1,36 +1,2 @@
-import Distribution.PackageDescription (
-  HookedBuildInfo,
-  emptyHookedBuildInfo )
-import Distribution.Simple (
-  Args,
-  UserHooks ( preSDist ),
-  defaultMainWithHooks,
-  simpleUserHooks )
-import Distribution.Simple.Setup ( SDistFlags )
-
-import System.Process ( system )
-
-
-main = defaultMainWithHooks myHooks
-  where
-    myHooks = simpleUserHooks { preSDist = myPreSDist }
-
--- | This hook will be executed before e.g. @cabal sdist@. It runs
---   pandoc to create the man page from shellcheck.1.md. If the pandoc
---   command is not found, this will fail with an error message:
---
---     /bin/sh: pandoc: command not found
---
---   Since the man page is listed in the Extra-Source-Files section of
---   our cabal file, a failure here should result in a failure to
---   create the distribution tarball (that's a good thing).
---
-myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo
-myPreSDist _ _ = do
-  putStrLn "Building the man page (shellcheck.1) with pandoc..."
-  putStrLn pandoc_cmd
-  result <- system pandoc_cmd
-  putStrLn $ "pandoc exited with " ++ show result
-  return emptyHookedBuildInfo
-  where
-    pandoc_cmd = "pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1"
+import Distribution.Simple
+main = defaultMain
diff --git a/ShellCheck.cabal b/ShellCheck.cabal
--- a/ShellCheck.cabal
+++ b/ShellCheck.cabal
@@ -1,5 +1,5 @@
 Name:             ShellCheck
-Version:          0.7.0
+Version:          0.7.1
 Synopsis:         Shell script analysis tool
 License:          GPL-3
 License-file:     LICENSE
@@ -7,7 +7,7 @@
 Author:           Vidar Holen
 Maintainer:       vidar@vidarholen.net
 Homepage:         https://www.shellcheck.net/
-Build-Type:       Custom
+Build-Type:       Simple
 Cabal-Version:    >= 1.8
 Bug-reports:      https://github.com/koalaman/shellcheck/issues
 Description:
@@ -26,19 +26,13 @@
     -- documentation
     README.md
     shellcheck.1.md
-    -- built with a cabal sdist hook
-    shellcheck.1
+    -- A script to build the man page using pandoc
+    manpage
     -- convenience script for stripping tests
     striptests
     -- tests
     test/shellcheck.hs
 
-custom-setup
-  setup-depends:
-    base    >= 4    && <5,
-    process >= 1.0  && <1.7,
-    Cabal   >= 1.10 && <2.5
-
 source-repository head
     type: git
     location: git://github.com/koalaman/shellcheck.git
@@ -51,9 +45,7 @@
     build-depends:
       aeson,
       array,
-      -- GHC 7.6.3 (base 4.6.0.1) is buggy (#1131, #1119) in optimized mode.
-      -- Just disable that version entirely to fail fast.
-      base > 4.6.0.1 && < 5,
+      base >= 4.8.0.0 && < 5,
       bytestring,
       containers >= 0.5,
       deepseq >= 1.4.0.0,
diff --git a/manpage b/manpage
new file mode 100644
--- /dev/null
+++ b/manpage
@@ -0,0 +1,4 @@
+#!/bin/sh
+echo >&2 "Generating man page using pandoc"
+pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1 || exit
+echo >&2 "Done. You can read it with:   man ./shellcheck.1"
diff --git a/shellcheck.1 b/shellcheck.1
deleted file mode 100644
--- a/shellcheck.1
+++ /dev/null
@@ -1,449 +0,0 @@
-.\" Automatically generated by Pandoc 2.2.1
-.\"
-.TH "SHELLCHECK" "1" "" "Shell script analysis tool" ""
-.hy
-.SH NAME
-.PP
-shellcheck \- Shell script analysis tool
-.SH SYNOPSIS
-.PP
-\f[B]shellcheck\f[] [\f[I]OPTIONS\f[]...] \f[I]FILES\f[]...
-.SH DESCRIPTION
-.PP
-ShellCheck is a static analysis and linting tool for sh/bash scripts.
-It\[aq]s mainly focused on handling typical beginner and intermediate
-level syntax errors and pitfalls where the shell just gives a cryptic
-error message or strange behavior, but it also reports on a few more
-advanced issues where corner cases can cause delayed failures.
-.PP
-ShellCheck gives shell specific advice.
-Consider this line:
-.IP
-.nf
-\f[C]
-((\ area\ =\ 3.14*r*r\ ))
-\f[]
-.fi
-.IP \[bu] 2
-For scripts starting with \f[C]#!/bin/sh\f[] (or when using
-\f[C]\-s\ sh\f[]), ShellCheck will warn that \f[C]((\ ..\ ))\f[] is not
-POSIX compliant (similar to checkbashisms).
-.IP \[bu] 2
-For scripts starting with \f[C]#!/bin/bash\f[] (or using
-\f[C]\-s\ bash\f[]), ShellCheck will warn that decimals are not
-supported.
-.IP \[bu] 2
-For scripts starting with \f[C]#!/bin/ksh\f[] (or using
-\f[C]\-s\ ksh\f[]), ShellCheck will not warn at all, as \f[C]ksh\f[]
-supports decimals in arithmetic contexts.
-.SH OPTIONS
-.TP
-.B \f[B]\-a\f[],\ \f[B]\-\-check\-sourced\f[]
-Emit warnings in sourced files.
-Normally, \f[C]shellcheck\f[] will only warn about issues in the
-specified files.
-With this option, any issues in sourced files will also be reported.
-.RS
-.RE
-.TP
-.B \f[B]\-C\f[][\f[I]WHEN\f[]],\ \f[B]\-\-color\f[][=\f[I]WHEN\f[]]
-For TTY output, enable colors \f[I]always\f[], \f[I]never\f[] or
-\f[I]auto\f[].
-The default is \f[I]auto\f[].
-\f[B]\-\-color\f[] without an argument is equivalent to
-\f[B]\-\-color=always\f[].
-.RS
-.RE
-.TP
-.B \f[B]\-i\f[]\ \f[I]CODE1\f[][,\f[I]CODE2\f[]...],\ \f[B]\-\-include=\f[]\f[I]CODE1\f[][,\f[I]CODE2\f[]...]
-Explicitly include only the specified codes in the report.
-Subsequent \f[B]\-i\f[] options are cumulative, but all the codes can be
-specified at once, comma\-separated as a single argument.
-Include options override any provided exclude options.
-.RS
-.RE
-.TP
-.B \f[B]\-e\f[]\ \f[I]CODE1\f[][,\f[I]CODE2\f[]...],\ \f[B]\-\-exclude=\f[]\f[I]CODE1\f[][,\f[I]CODE2\f[]...]
-Explicitly exclude the specified codes from the report.
-Subsequent \f[B]\-e\f[] options are cumulative, but all the codes can be
-specified at once, comma\-separated as a single argument.
-.RS
-.RE
-.TP
-.B \f[B]\-f\f[] \f[I]FORMAT\f[], \f[B]\-\-format=\f[]\f[I]FORMAT\f[]
-Specify the output format of shellcheck, which prints its results in the
-standard output.
-Subsequent \f[B]\-f\f[] options are ignored, see \f[B]FORMATS\f[] below
-for more information.
-.RS
-.RE
-.TP
-.B \f[B]\-\-list\-optional\f[]
-Output a list of known optional checks.
-These can be enabled with \f[B]\-o\f[] flags or \f[B]enable\f[]
-directives.
-.RS
-.RE
-.TP
-.B \f[B]\-\-norc\f[]
-Don\[aq]t try to look for .shellcheckrc configuration files.
-.RS
-.RE
-.TP
-.B \f[B]\-o\f[]\ \f[I]NAME1\f[][,\f[I]NAME2\f[]...],\ \f[B]\-\-enable=\f[]\f[I]NAME1\f[][,\f[I]NAME2\f[]...]
-Enable optional checks.
-The special name \f[I]all\f[] enables all of them.
-Subsequent \f[B]\-o\f[] options accumulate.
-This is equivalent to specifying \f[B]enable\f[] directives.
-.RS
-.RE
-.TP
-.B \f[B]\-P\f[]\ \f[I]SOURCEPATH\f[],\ \f[B]\-\-source\-path=\f[]\f[I]SOURCEPATH\f[]
-Specify paths to search for sourced files, separated by \f[C]:\f[] on
-Unix and \f[C];\f[] on Windows.
-This is equivalent to specifying \f[C]search\-path\f[] directives.
-.RS
-.RE
-.TP
-.B \f[B]\-s\f[]\ \f[I]shell\f[],\ \f[B]\-\-shell=\f[]\f[I]shell\f[]
-Specify Bourne shell dialect.
-Valid values are \f[I]sh\f[], \f[I]bash\f[], \f[I]dash\f[] and
-\f[I]ksh\f[].
-The default is to deduce the shell from the file\[aq]s \f[C]shell\f[]
-directive, shebang, or \f[C]\&.bash/.bats/.dash/.ksh\f[] extension, in
-that order.
-\f[I]sh\f[] refers to POSIX \f[C]sh\f[] (not the system\[aq]s), and will
-warn of portability issues.
-.RS
-.RE
-.TP
-.B \f[B]\-S\f[]\ \f[I]SEVERITY\f[],\ \f[B]\-\-severity=\f[]\f[I]severity\f[]
-Specify minimum severity of errors to consider.
-Valid values in order of severity are \f[I]error\f[], \f[I]warning\f[],
-\f[I]info\f[] and \f[I]style\f[].
-The default is \f[I]style\f[].
-.RS
-.RE
-.TP
-.B \f[B]\-V\f[],\ \f[B]\-\-version\f[]
-Print version information and exit.
-.RS
-.RE
-.TP
-.B \f[B]\-W\f[] \f[I]NUM\f[],\ \f[B]\-\-wiki\-link\-count=NUM\f[]
-For TTY output, show \f[I]NUM\f[] wiki links to more information about
-mentioned warnings.
-Set to 0 to disable them entirely.
-.RS
-.RE
-.TP
-.B \f[B]\-x\f[],\ \f[B]\-\-external\-sources\f[]
-Follow \[aq]source\[aq] statements even when the file is not specified
-as input.
-By default, \f[C]shellcheck\f[] will only follow files specified on the
-command line (plus \f[C]/dev/null\f[]).
-This option allows following any file the script may \f[C]source\f[].
-.RS
-.RE
-.TP
-.B \f[B]FILES...\f[]
-One or more script files to check, or "\-" for standard input.
-.RS
-.RE
-.SH FORMATS
-.TP
-.B \f[B]tty\f[]
-Plain text, human readable output.
-This is the default.
-.RS
-.RE
-.TP
-.B \f[B]gcc\f[]
-GCC compatible output.
-Useful for editors that support compiling and showing syntax errors.
-.RS
-.PP
-For example, in Vim,
-\f[C]:set\ makeprg=shellcheck\\\ \-f\\\ gcc\\\ %\f[] will allow using
-\f[C]:make\f[] to check the script, and \f[C]:cnext\f[] to jump to the
-next error.
-.IP
-.nf
-\f[C]
-<file>:<line>:<column>:\ <type>:\ <message>
-\f[]
-.fi
-.RE
-.TP
-.B \f[B]checkstyle\f[]
-Checkstyle compatible XML output.
-Supported directly or through plugins by many IDEs and build monitoring
-systems.
-.RS
-.IP
-.nf
-\f[C]
-<?xml\ version=\[aq]1.0\[aq]\ encoding=\[aq]UTF\-8\[aq]?>
-<checkstyle\ version=\[aq]4.3\[aq]>
-\ \ <file\ name=\[aq]file\[aq]>
-\ \ \ \ <error
-\ \ \ \ \ \ line=\[aq]line\[aq]
-\ \ \ \ \ \ column=\[aq]column\[aq]
-\ \ \ \ \ \ severity=\[aq]severity\[aq]
-\ \ \ \ \ \ message=\[aq]message\[aq]
-\ \ \ \ \ \ source=\[aq]ShellCheck.SC####\[aq]\ />
-\ \ \ \ ...
-\ \ </file>
-\ \ ...
-</checkstyle>
-\f[]
-.fi
-.RE
-.TP
-.B \f[B]diff\f[]
-Auto\-fixes in unified diff format.
-Can be piped to \f[C]git\ apply\f[] or \f[C]patch\ \-p1\f[] to
-automatically apply fixes.
-.RS
-.IP
-.nf
-\f[C]
-\-\-\-\ a/test.sh
-+++\ b/test.sh
-\@\@\ \-2,6\ +2,6\ \@\@
-\ ##\ Example\ of\ a\ broken\ script.
-\ for\ f\ in\ $(ls\ *.m3u)
-\ do
-\-\ \ grep\ \-qi\ hq.*mp3\ $f\ \\
-+\ \ grep\ \-qi\ hq.*mp3\ "$f"\ \\
-\ \ \ \ \ &&\ echo\ \-e\ \[aq]Playlist\ $f\ contains\ a\ HQ\ file\ in\ mp3\ format\[aq]
-\ done
-\f[]
-.fi
-.RE
-.TP
-.B \f[B]json1\f[]
-Json is a popular serialization format that is more suitable for web
-applications.
-ShellCheck\[aq]s json is compact and contains only the bare minimum.
-Tabs are counted as 1 character.
-.RS
-.IP
-.nf
-\f[C]
-{
-\ \ comments:\ [
-\ \ \ \ {
-\ \ \ \ \ \ "file":\ "filename",
-\ \ \ \ \ \ "line":\ lineNumber,
-\ \ \ \ \ \ "column":\ columnNumber,
-\ \ \ \ \ \ "level":\ "severitylevel",
-\ \ \ \ \ \ "code":\ errorCode,
-\ \ \ \ \ \ "message":\ "warning\ message"
-\ \ \ \ },
-\ \ \ \ ...
-\ \ ]
-}
-\f[]
-.fi
-.RE
-.TP
-.B \f[B]json\f[]
-This is a legacy version of the \f[B]json1\f[] format.
-It\[aq]s a raw array of comments, and all offsets have a tab stop of 8.
-.RS
-.RE
-.TP
-.B \f[B]quiet\f[]
-Suppress all normal output.
-Exit with zero if no issues are found, otherwise exit with one.
-Stops processing after the first issue.
-.RS
-.RE
-.SH DIRECTIVES
-.PP
-ShellCheck directives can be specified as comments in the shell script.
-If they appear before the first command, they are considered file\-wide.
-Otherwise, they apply to the immediately following command or block:
-.IP
-.nf
-\f[C]
-#\ shellcheck\ key=value\ key=value
-command\-or\-structure
-\f[]
-.fi
-.PP
-For example, to suppress SC2035 about using \f[C]\&./*.jpg\f[]:
-.IP
-.nf
-\f[C]
-#\ shellcheck\ disable=SC2035
-echo\ "Files:\ "\ *.jpg
-\f[]
-.fi
-.PP
-To tell ShellCheck where to look for an otherwise dynamically determined
-file:
-.IP
-.nf
-\f[C]
-#\ shellcheck\ source=./lib.sh
-source\ "$(find_install_dir)/lib.sh"
-\f[]
-.fi
-.PP
-Here a shell brace group is used to suppress a warning on multiple
-lines:
-.IP
-.nf
-\f[C]
-#\ shellcheck\ disable=SC2016
-{
-\ \ echo\ \[aq]Modifying\ $PATH\[aq]
-\ \ echo\ \[aq]PATH=foo:$PATH\[aq]\ >>\ ~/.bashrc
-}
-\f[]
-.fi
-.PP
-Valid keys are:
-.TP
-.B \f[B]disable\f[]
-Disables a comma separated list of error codes for the following
-command.
-The command can be a simple command like \f[C]echo\ foo\f[], or a
-compound command like a function definition, subshell block or loop.
-.RS
-.RE
-.TP
-.B \f[B]enable\f[]
-Enable an optional check by name, as listed with
-\f[B]\-\-list\-optional\f[].
-Only file\-wide \f[C]enable\f[] directives are considered.
-.RS
-.RE
-.TP
-.B \f[B]source\f[]
-Overrides the filename included by a \f[C]source\f[]/\f[C]\&.\f[]
-statement.
-This can be used to tell shellcheck where to look for a file whose name
-is determined at runtime, or to skip a source by telling it to use
-\f[C]/dev/null\f[].
-.RS
-.RE
-.TP
-.B \f[B]source\-path\f[]
-Add a directory to the search path for \f[C]source\f[]/\f[C]\&.\f[]
-statements (by default, only ShellCheck\[aq]s working directory is
-included).
-Absolute paths will also be rooted in these paths.
-The special path \f[C]SCRIPTDIR\f[] can be used to specify the currently
-checked script\[aq]s directory, as in \f[C]source\-path=SCRIPTDIR\f[] or
-\f[C]source\-path=SCRIPTDIR/../libs\f[].
-Multiple paths accumulate, and \f[C]\-P\f[] takes precedence over them.
-.RS
-.RE
-.TP
-.B \f[B]shell\f[]
-Overrides the shell detected from the shebang.
-This is useful for files meant to be included (and thus lacking a
-shebang), or possibly as a more targeted alternative to
-\[aq]disable=2039\[aq].
-.RS
-.RE
-.SH RC FILES
-.PP
-Unless \f[C]\-\-norc\f[] is used, ShellCheck will look for a file
-\f[C]\&.shellcheckrc\f[] or \f[C]shellcheckrc\f[] in the script\[aq]s
-directory and each parent directory.
-If found, it will read \f[C]key=value\f[] pairs from it and treat them
-as file\-wide directives.
-.PP
-Here is an example \f[C]\&.shellcheckrc\f[]:
-.IP
-.nf
-\f[C]
-#\ Look\ for\ \[aq]source\[aq]d\ files\ relative\ to\ the\ checked\ script,
-#\ and\ also\ look\ for\ absolute\ paths\ in\ /mnt/chroot
-source\-path=SCRIPTDIR
-source\-path=/mnt/chroot
-
-#\ Turn\ on\ warnings\ for\ unquoted\ variables\ with\ safe\ values
-enable=quote\-safe\-variables
-
-#\ Turn\ on\ warnings\ for\ unassigned\ uppercase\ variables
-enable=check\-unassigned\-uppercase
-
-#\ Allow\ using\ `which`\ since\ it\ gives\ full\ paths\ and\ is\ common\ enough
-disable=SC2230
-\f[]
-.fi
-.PP
-If no \f[C]\&.shellcheckrc\f[] is found in any of the parent
-directories, ShellCheck will look in \f[C]~/.shellcheckrc\f[] followed
-by the XDG config directory (usually \f[C]~/.config/shellcheckrc\f[]) on
-Unix, or \f[C]%APPDATA%/shellcheckrc\f[] on Windows.
-Only the first file found will be used.
-.PP
-Note for Snap users: the Snap sandbox disallows access to hidden files.
-Use \f[C]shellcheckrc\f[] without the dot instead.
-.PP
-Note for Docker users: ShellCheck will only be able to look for files
-that are mounted in the container, so \f[C]~/.shellcheckrc\f[] will not
-be read.
-.SH ENVIRONMENT VARIABLES
-.PP
-The environment variable \f[C]SHELLCHECK_OPTS\f[] can be set with
-default flags:
-.IP
-.nf
-\f[C]
-export\ SHELLCHECK_OPTS=\[aq]\-\-shell=bash\ \-\-exclude=SC2016\[aq]
-\f[]
-.fi
-.PP
-Its value will be split on spaces and prepended to the command line on
-each invocation.
-.SH RETURN VALUES
-.PP
-ShellCheck uses the follow exit codes:
-.IP \[bu] 2
-0: All files successfully scanned with no issues.
-.IP \[bu] 2
-1: All files successfully scanned with some issues.
-.IP \[bu] 2
-2: Some files could not be processed (e.g.
-file not found).
-.IP \[bu] 2
-3: ShellCheck was invoked with bad syntax (e.g.
-unknown flag).
-.IP \[bu] 2
-4: ShellCheck was invoked with bad options (e.g.
-unknown formatter).
-.SH LOCALE
-.PP
-This version of ShellCheck is only available in English.
-All files are leniently decoded as UTF\-8, with a fallback of
-ISO\-8859\-1 for invalid sequences.
-\f[C]LC_CTYPE\f[] is respected for output, and defaults to UTF\-8 for
-locales where encoding is unspecified (such as the \f[C]C\f[] locale).
-.PP
-Windows users seeing
-\f[C]commitBuffer:\ invalid\ argument\ (invalid\ character)\f[] should
-set their terminal to use UTF\-8 with \f[C]chcp\ 65001\f[].
-.SH AUTHORS
-.PP
-ShellCheck is developed and maintained by Vidar Holen, with assistance
-from a long list of wonderful contributors.
-.SH REPORTING BUGS
-.PP
-Bugs and issues can be reported on GitHub:
-.PP
-https://github.com/koalaman/shellcheck/issues
-.SH COPYRIGHT
-.PP
-Copyright 2012\-2019, Vidar Holen and contributors.
-Licensed under the GNU General Public License version 3 or later, see
-https://gnu.org/licenses/gpl.html
-.SH SEE ALSO
-.PP
-sh(1) bash(1)
diff --git a/shellcheck.1.md b/shellcheck.1.md
--- a/shellcheck.1.md
+++ b/shellcheck.1.md
@@ -107,7 +107,7 @@
 
 **-x**,\ **--external-sources**
 
-:   Follow 'source' statements even when the file is not specified as input.
+:   Follow `source` statements even when the file is not specified as input.
     By default, `shellcheck` will only follow files specified on the command
     line (plus `/dev/null`). This option allows following any file the script
     may `source`.
@@ -275,8 +275,8 @@
     # Turn on warnings for unassigned uppercase variables
     enable=check-unassigned-uppercase
 
-    # Allow using `which` since it gives full paths and is common enough
-    disable=SC2230
+    # Allow [ ! -z foo ] instead of suggesting -n
+    disable=SC2236
 
 If no `.shellcheckrc` is found in any of the parent directories, ShellCheck
 will look in `~/.shellcheckrc` followed by the XDG config directory
@@ -301,7 +301,7 @@
 
 # RETURN VALUES
 
-ShellCheck uses the follow exit codes:
+ShellCheck uses the following exit codes:
 
 + 0: All files successfully scanned with no issues.
 + 1: All files successfully scanned with some issues.
diff --git a/shellcheck.hs b/shellcheck.hs
--- a/shellcheck.hs
+++ b/shellcheck.hs
@@ -491,6 +491,12 @@
         first <- a arg
         if not first then return False else b arg
 
+    findM p = foldr go (pure Nothing)
+      where
+        go x acc = do
+            b <- p x
+            if b then pure (Just x) else acc
+
     findSourceFile inputs sourcePathFlag currentScript sourcePathAnnotation original =
         if isAbsolute original
         then
@@ -500,11 +506,11 @@
             find original original
       where
         find filename deflt = do
-            sources <- filterM ((allowable inputs) `andM` doesFileExist) $
+            sources <- findM ((allowable inputs) `andM` doesFileExist) $
                         (adjustPath filename):(map (</> filename) $ map adjustPath $ sourcePathFlag ++ sourcePathAnnotation)
             case sources of
-                [] -> return deflt
-                (first:_) -> return first
+                Nothing -> return deflt
+                Just first -> return first
         scriptdir = dropFileName currentScript
         adjustPath str =
             case (splitDirectories str) of
diff --git a/src/ShellCheck/AST.hs b/src/ShellCheck/AST.hs
--- a/src/ShellCheck/AST.hs
+++ b/src/ShellCheck/AST.hs
@@ -17,7 +17,7 @@
     You should have received a copy of the GNU General Public License
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 -}
-{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric, DeriveAnyClass, DeriveTraversable, PatternSynonyms #-}
 module ShellCheck.AST where
 
 import GHC.Generics (Generic)
@@ -37,110 +37,112 @@
 data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq)
 
 newtype Root = Root Token
-data Token =
-    TA_Binary Id String Token Token
-    | TA_Assignment Id String Token Token
-    | TA_Variable Id String [Token]
-    | TA_Expansion Id [Token]
-    | TA_Sequence Id [Token]
-    | TA_Trinary Id Token Token Token
-    | TA_Unary Id String Token
-    | TC_And Id ConditionType String Token Token
-    | TC_Binary Id ConditionType String Token Token
-    | TC_Group Id ConditionType Token
-    | TC_Nullary Id ConditionType Token
-    | TC_Or Id ConditionType String Token Token
-    | TC_Unary Id ConditionType String Token
-    | TC_Empty Id ConditionType
-    | T_AND_IF Id
-    | T_AndIf Id Token Token
-    | T_Arithmetic Id Token
-    | T_Array Id [Token]
-    | T_IndexedElement Id [Token] Token
+data Token = OuterToken Id (InnerToken Token) deriving (Show)
+
+data InnerToken t =
+    Inner_TA_Binary String t t
+    | Inner_TA_Assignment String t t
+    | Inner_TA_Variable String [t]
+    | Inner_TA_Expansion [t]
+    | Inner_TA_Sequence [t]
+    | Inner_TA_Trinary t t t
+    | Inner_TA_Unary String t
+    | Inner_TC_And ConditionType String t t
+    | Inner_TC_Binary ConditionType String t t
+    | Inner_TC_Group ConditionType t
+    | Inner_TC_Nullary ConditionType t
+    | Inner_TC_Or ConditionType String t t
+    | Inner_TC_Unary ConditionType String t
+    | Inner_TC_Empty ConditionType
+    | Inner_T_AND_IF
+    | Inner_T_AndIf t t
+    | Inner_T_Arithmetic t
+    | Inner_T_Array [t]
+    | Inner_T_IndexedElement [t] t
     -- Store the index as string, and parse as arithmetic or string later
-    | T_UnparsedIndex Id SourcePos String
-    | T_Assignment Id AssignmentMode String [Token] Token
-    | T_Backgrounded Id Token
-    | T_Backticked Id [Token]
-    | T_Bang Id
-    | T_Banged Id Token
-    | T_BraceExpansion Id [Token]
-    | T_BraceGroup Id [Token]
-    | T_CLOBBER Id
-    | T_Case Id
-    | T_CaseExpression Id Token [(CaseType, [Token], [Token])]
-    | T_Condition Id ConditionType Token
-    | T_DGREAT Id
-    | T_DLESS Id
-    | T_DLESSDASH Id
-    | T_DSEMI Id
-    | T_Do Id
-    | T_DollarArithmetic Id Token
-    | T_DollarBraced Id Bool Token
-    | T_DollarBracket Id Token
-    | T_DollarDoubleQuoted Id [Token]
-    | T_DollarExpansion Id [Token]
-    | T_DollarSingleQuoted Id String
-    | T_DollarBraceCommandExpansion Id [Token]
-    | T_Done Id
-    | T_DoubleQuoted Id [Token]
-    | T_EOF Id
-    | T_Elif Id
-    | T_Else Id
-    | T_Esac Id
-    | T_Extglob Id String [Token]
-    | T_FdRedirect Id String Token
-    | T_Fi Id
-    | T_For Id
-    | T_ForArithmetic Id Token Token Token [Token]
-    | T_ForIn Id String [Token] [Token]
-    | T_Function Id FunctionKeyword FunctionParentheses String Token
-    | T_GREATAND Id
-    | T_Glob Id String
-    | T_Greater Id
-    | T_HereDoc Id Dashed Quoted String [Token]
-    | T_HereString Id Token
-    | T_If Id
-    | T_IfExpression Id [([Token],[Token])] [Token]
-    | T_In  Id
-    | T_IoFile Id Token Token
-    | T_IoDuplicate Id Token String
-    | T_LESSAND Id
-    | T_LESSGREAT Id
-    | T_Lbrace Id
-    | T_Less Id
-    | T_Literal Id String
-    | T_Lparen Id
-    | T_NEWLINE Id
-    | T_NormalWord Id [Token]
-    | T_OR_IF Id
-    | T_OrIf Id Token Token
-    | T_ParamSubSpecialChar Id String -- e.g. '%' in ${foo%bar}  or '/' in ${foo/bar/baz}
-    | T_Pipeline Id [Token] [Token] -- [Pipe separators] [Commands]
-    | T_ProcSub Id String [Token]
-    | T_Rbrace Id
-    | T_Redirecting Id [Token] Token
-    | T_Rparen Id
-    | T_Script Id Token [Token] -- Shebang T_Literal, followed by script.
-    | T_Select Id
-    | T_SelectIn Id String [Token] [Token]
-    | T_Semi Id
-    | T_SimpleCommand Id [Token] [Token]
-    | T_SingleQuoted Id String
-    | T_Subshell Id [Token]
-    | T_Then Id
-    | T_Until Id
-    | T_UntilExpression Id [Token] [Token]
-    | T_While Id
-    | T_WhileExpression Id [Token] [Token]
-    | T_Annotation Id [Annotation] Token
-    | T_Pipe Id String
-    | T_CoProc Id (Maybe String) Token
-    | T_CoProcBody Id Token
-    | T_Include Id Token
-    | T_SourceCommand Id Token Token
-    | T_BatsTest Id Token Token
-    deriving (Show)
+    | Inner_T_UnparsedIndex SourcePos String
+    | Inner_T_Assignment AssignmentMode String [t] t
+    | Inner_T_Backgrounded t
+    | Inner_T_Backticked [t]
+    | Inner_T_Bang
+    | Inner_T_Banged t
+    | Inner_T_BraceExpansion [t]
+    | Inner_T_BraceGroup [t]
+    | Inner_T_CLOBBER
+    | Inner_T_Case
+    | Inner_T_CaseExpression t [(CaseType, [t], [t])]
+    | Inner_T_Condition ConditionType t
+    | Inner_T_DGREAT
+    | Inner_T_DLESS
+    | Inner_T_DLESSDASH
+    | Inner_T_DSEMI
+    | Inner_T_Do
+    | Inner_T_DollarArithmetic t
+    | Inner_T_DollarBraced Bool t
+    | Inner_T_DollarBracket t
+    | Inner_T_DollarDoubleQuoted [t]
+    | Inner_T_DollarExpansion [t]
+    | Inner_T_DollarSingleQuoted String
+    | Inner_T_DollarBraceCommandExpansion [t]
+    | Inner_T_Done
+    | Inner_T_DoubleQuoted [t]
+    | Inner_T_EOF
+    | Inner_T_Elif
+    | Inner_T_Else
+    | Inner_T_Esac
+    | Inner_T_Extglob String [t]
+    | Inner_T_FdRedirect String t
+    | Inner_T_Fi
+    | Inner_T_For
+    | Inner_T_ForArithmetic t t t [t]
+    | Inner_T_ForIn String [t] [t]
+    | Inner_T_Function FunctionKeyword FunctionParentheses String t
+    | Inner_T_GREATAND
+    | Inner_T_Glob String
+    | Inner_T_Greater
+    | Inner_T_HereDoc Dashed Quoted String [t]
+    | Inner_T_HereString t
+    | Inner_T_If
+    | Inner_T_IfExpression [([t],[t])] [t]
+    | Inner_T_In
+    | Inner_T_IoFile t t
+    | Inner_T_IoDuplicate t String
+    | Inner_T_LESSAND
+    | Inner_T_LESSGREAT
+    | Inner_T_Lbrace
+    | Inner_T_Less
+    | Inner_T_Literal String
+    | Inner_T_Lparen
+    | Inner_T_NEWLINE
+    | Inner_T_NormalWord [t]
+    | Inner_T_OR_IF
+    | Inner_T_OrIf t t
+    | Inner_T_ParamSubSpecialChar String -- e.g. '%' in ${foo%bar}  or '/' in ${foo/bar/baz}
+    | Inner_T_Pipeline [t] [t] -- [Pipe separators] [Commands]
+    | Inner_T_ProcSub String [t]
+    | Inner_T_Rbrace
+    | Inner_T_Redirecting [t] t
+    | Inner_T_Rparen
+    | Inner_T_Script t [t] -- Shebang T_Literal, followed by script.
+    | Inner_T_Select
+    | Inner_T_SelectIn String [t] [t]
+    | Inner_T_Semi
+    | Inner_T_SimpleCommand [t] [t]
+    | Inner_T_SingleQuoted String
+    | Inner_T_Subshell [t]
+    | Inner_T_Then
+    | Inner_T_Until
+    | Inner_T_UntilExpression [t] [t]
+    | Inner_T_While
+    | Inner_T_WhileExpression [t] [t]
+    | Inner_T_Annotation [Annotation] t
+    | Inner_T_Pipe String
+    | Inner_T_CoProc (Maybe String) t
+    | Inner_T_CoProcBody t
+    | Inner_T_Include t
+    | Inner_T_SourceCommand t t
+    | Inner_T_BatsTest t t
+    deriving (Show, Eq, Functor, Foldable, Traversable)
 
 data Annotation =
     DisableComment Integer
@@ -151,240 +153,125 @@
     deriving (Show, Eq)
 data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
 
--- This is an abomination.
-tokenEquals :: Token -> Token -> Bool
-tokenEquals a b = kludge a == kludge b
-    where kludge s = Re.subRegex (Re.mkRegex "\\(Id [0-9]+\\)") (show s) "(Id 0)"
+pattern T_AND_IF id = OuterToken id Inner_T_AND_IF
+pattern T_Bang id = OuterToken id Inner_T_Bang
+pattern T_Case id = OuterToken id Inner_T_Case
+pattern TC_Empty id typ = OuterToken id (Inner_TC_Empty typ)
+pattern T_CLOBBER id = OuterToken id Inner_T_CLOBBER
+pattern T_DGREAT id = OuterToken id Inner_T_DGREAT
+pattern T_DLESS id = OuterToken id Inner_T_DLESS
+pattern T_DLESSDASH id = OuterToken id Inner_T_DLESSDASH
+pattern T_Do id = OuterToken id Inner_T_Do
+pattern T_DollarSingleQuoted id str = OuterToken id (Inner_T_DollarSingleQuoted str)
+pattern T_Done id = OuterToken id Inner_T_Done
+pattern T_DSEMI id = OuterToken id Inner_T_DSEMI
+pattern T_Elif id = OuterToken id Inner_T_Elif
+pattern T_Else id = OuterToken id Inner_T_Else
+pattern T_EOF id = OuterToken id Inner_T_EOF
+pattern T_Esac id = OuterToken id Inner_T_Esac
+pattern T_Fi id = OuterToken id Inner_T_Fi
+pattern T_For id = OuterToken id Inner_T_For
+pattern T_Glob id str = OuterToken id (Inner_T_Glob str)
+pattern T_GREATAND id = OuterToken id Inner_T_GREATAND
+pattern T_Greater id = OuterToken id Inner_T_Greater
+pattern T_If id = OuterToken id Inner_T_If
+pattern T_In id = OuterToken id Inner_T_In
+pattern T_Lbrace id = OuterToken id Inner_T_Lbrace
+pattern T_Less id = OuterToken id Inner_T_Less
+pattern T_LESSAND id = OuterToken id Inner_T_LESSAND
+pattern T_LESSGREAT id = OuterToken id Inner_T_LESSGREAT
+pattern T_Literal id str = OuterToken id (Inner_T_Literal str)
+pattern T_Lparen id = OuterToken id Inner_T_Lparen
+pattern T_NEWLINE id = OuterToken id Inner_T_NEWLINE
+pattern T_OR_IF id = OuterToken id Inner_T_OR_IF
+pattern T_ParamSubSpecialChar id str = OuterToken id (Inner_T_ParamSubSpecialChar str)
+pattern T_Pipe id str = OuterToken id (Inner_T_Pipe str)
+pattern T_Rbrace id = OuterToken id Inner_T_Rbrace
+pattern T_Rparen id = OuterToken id Inner_T_Rparen
+pattern T_Select id = OuterToken id Inner_T_Select
+pattern T_Semi id = OuterToken id Inner_T_Semi
+pattern T_SingleQuoted id str = OuterToken id (Inner_T_SingleQuoted str)
+pattern T_Then id = OuterToken id Inner_T_Then
+pattern T_UnparsedIndex id pos str = OuterToken id (Inner_T_UnparsedIndex pos str)
+pattern T_Until id = OuterToken id Inner_T_Until
+pattern T_While id = OuterToken id Inner_T_While
+pattern TA_Assignment id op t1 t2 = OuterToken id (Inner_TA_Assignment op t1 t2)
+pattern TA_Binary id op t1 t2 = OuterToken id (Inner_TA_Binary op t1 t2)
+pattern TA_Expansion id t = OuterToken id (Inner_TA_Expansion t)
+pattern T_AndIf id t u = OuterToken id (Inner_T_AndIf t u)
+pattern T_Annotation id anns t = OuterToken id (Inner_T_Annotation anns t)
+pattern T_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c)
+pattern T_Array id t = OuterToken id (Inner_T_Array t)
+pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l)
+pattern T_Assignment id mode var indices value = OuterToken id (Inner_T_Assignment mode var indices value)
+pattern TA_Trinary id t1 t2 t3 = OuterToken id (Inner_TA_Trinary t1 t2 t3)
+pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1)
+pattern TA_Variable id str t = OuterToken id (Inner_TA_Variable str t)
+pattern T_Backgrounded id l = OuterToken id (Inner_T_Backgrounded l)
+pattern T_Backticked id list = OuterToken id (Inner_T_Backticked list)
+pattern T_Banged id l = OuterToken id (Inner_T_Banged l)
+pattern T_BatsTest id name t = OuterToken id (Inner_T_BatsTest name t)
+pattern T_BraceExpansion id list = OuterToken id (Inner_T_BraceExpansion list)
+pattern T_BraceGroup id l = OuterToken id (Inner_T_BraceGroup l)
+pattern TC_And id typ str t1 t2 = OuterToken id (Inner_TC_And typ str t1 t2)
+pattern T_CaseExpression id word cases = OuterToken id (Inner_T_CaseExpression word cases)
+pattern TC_Binary id typ op lhs rhs = OuterToken id (Inner_TC_Binary typ op lhs rhs)
+pattern TC_Group id typ token = OuterToken id (Inner_TC_Group typ token)
+pattern TC_Nullary id typ token = OuterToken id (Inner_TC_Nullary typ token)
+pattern T_Condition id typ token = OuterToken id (Inner_T_Condition typ token)
+pattern T_CoProcBody id t = OuterToken id (Inner_T_CoProcBody t)
+pattern T_CoProc id var body = OuterToken id (Inner_T_CoProc var body)
+pattern TC_Or id typ str t1 t2 = OuterToken id (Inner_TC_Or typ str t1 t2)
+pattern TC_Unary id typ op token = OuterToken id (Inner_TC_Unary typ op token)
+pattern T_DollarArithmetic id c = OuterToken id (Inner_T_DollarArithmetic c)
+pattern T_DollarBraceCommandExpansion id list = OuterToken id (Inner_T_DollarBraceCommandExpansion list)
+pattern T_DollarBraced id braced op = OuterToken id (Inner_T_DollarBraced braced op)
+pattern T_DollarBracket id c = OuterToken id (Inner_T_DollarBracket c)
+pattern T_DollarDoubleQuoted id list = OuterToken id (Inner_T_DollarDoubleQuoted list)
+pattern T_DollarExpansion id list = OuterToken id (Inner_T_DollarExpansion list)
+pattern T_DoubleQuoted id list = OuterToken id (Inner_T_DoubleQuoted list)
+pattern T_Extglob id str l = OuterToken id (Inner_T_Extglob str l)
+pattern T_FdRedirect id v t = OuterToken id (Inner_T_FdRedirect v t)
+pattern T_ForArithmetic id a b c group = OuterToken id (Inner_T_ForArithmetic a b c group)
+pattern T_ForIn id v w l = OuterToken id (Inner_T_ForIn v w l)
+pattern T_Function id a b name body = OuterToken id (Inner_T_Function a b name body)
+pattern T_HereDoc id d q str l = OuterToken id (Inner_T_HereDoc d q str l)
+pattern T_HereString id word = OuterToken id (Inner_T_HereString word)
+pattern T_IfExpression id conditions elses = OuterToken id (Inner_T_IfExpression conditions elses)
+pattern T_Include id script = OuterToken id (Inner_T_Include script)
+pattern T_IndexedElement id indices t = OuterToken id (Inner_T_IndexedElement indices t)
+pattern T_IoDuplicate id op num = OuterToken id (Inner_T_IoDuplicate op num)
+pattern T_IoFile id op file = OuterToken id (Inner_T_IoFile op file)
+pattern T_NormalWord id list = OuterToken id (Inner_T_NormalWord list)
+pattern T_OrIf id t u = OuterToken id (Inner_T_OrIf t u)
+pattern T_Pipeline id l1 l2 = OuterToken id (Inner_T_Pipeline l1 l2)
+pattern T_ProcSub id typ l = OuterToken id (Inner_T_ProcSub typ l)
+pattern T_Redirecting id redirs cmd = OuterToken id (Inner_T_Redirecting redirs cmd)
+pattern T_Script id shebang list = OuterToken id (Inner_T_Script shebang list)
+pattern T_SelectIn id v w l = OuterToken id (Inner_T_SelectIn v w l)
+pattern T_SimpleCommand id vars cmds = OuterToken id (Inner_T_SimpleCommand vars cmds)
+pattern T_SourceCommand id includer t_include = OuterToken id (Inner_T_SourceCommand includer t_include)
+pattern T_Subshell id l = OuterToken id (Inner_T_Subshell l)
+pattern T_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l)
+pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression c l)
 
+{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-}
+
 instance Eq Token where
-    (==) = tokenEquals
+    OuterToken _ a == OuterToken _ b = a == b
 
 analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> m Token) -> Token -> m Token
 analyze f g i =
     round
   where
-    round t = do
+    round t@(OuterToken id it) = do
         f t
-        newT <- delve t
+        newIt <- traverse round it
         g t
-        i newT
-    roundAll = mapM round
-
-    dl l v = do
-        x <- roundAll l
-        return $ v x
-    dll l m v = do
-        x <- roundAll l
-        y <- roundAll m
-        return $ v x y
-    d1 t v = do
-        x <- round t
-        return $ v x
-    d2 t1 t2 v = do
-        x <- round t1
-        y <- round t2
-        return $ v x y
-
-    delve (T_NormalWord id list) = dl list $ T_NormalWord id
-    delve (T_DoubleQuoted id list) = dl list $ T_DoubleQuoted id
-    delve (T_DollarDoubleQuoted id list) = dl list $ T_DollarDoubleQuoted id
-    delve (T_DollarExpansion id list) = dl list $ T_DollarExpansion id
-    delve (T_DollarBraceCommandExpansion id list) = dl list $ T_DollarBraceCommandExpansion id
-    delve (T_BraceExpansion id list) = dl list $ T_BraceExpansion id
-    delve (T_Backticked id list) = dl list $ T_Backticked id
-    delve (T_DollarArithmetic id c) = d1 c $ T_DollarArithmetic id
-    delve (T_DollarBracket id c) = d1 c $ T_DollarBracket id
-    delve (T_IoFile id op file) = d2 op file $ T_IoFile id
-    delve (T_IoDuplicate id op num) = d1 op $ \x -> T_IoDuplicate id x num
-    delve (T_HereString id word) = d1 word $ T_HereString id
-    delve (T_FdRedirect id v t) = d1 t $ T_FdRedirect id v
-    delve (T_Assignment id mode var indices value) = do
-        a <- roundAll indices
-        b <- round value
-        return $ T_Assignment id mode var a b
-    delve (T_Array id t) = dl t $ T_Array id
-    delve (T_IndexedElement id indices t) = do
-        a <- roundAll indices
-        b <- round t
-        return $ T_IndexedElement id a b
-    delve (T_Redirecting id redirs cmd) = do
-        a <- roundAll redirs
-        b <- round cmd
-        return $ T_Redirecting id a b
-    delve (T_SimpleCommand id vars cmds) = dll vars cmds $ T_SimpleCommand id
-    delve (T_Pipeline id l1 l2) = dll l1 l2 $ T_Pipeline id
-    delve (T_Banged id l) = d1 l $ T_Banged id
-    delve (T_AndIf id t u) = d2 t u $ T_AndIf id
-    delve (T_OrIf id t u) = d2 t u $ T_OrIf id
-    delve (T_Backgrounded id l) = d1 l $ T_Backgrounded id
-    delve (T_Subshell id l) = dl l $ T_Subshell id
-    delve (T_ProcSub id typ l) = dl l $ T_ProcSub id typ
-    delve (T_Arithmetic id c) = d1 c $ T_Arithmetic id
-    delve (T_IfExpression id conditions elses) = do
-        newConds <- mapM (\(c, t) -> do
-                            x <- mapM round c
-                            y <- mapM round t
-                            return (x,y)
-                    ) conditions
-        newElses <- roundAll elses
-        return $ T_IfExpression id newConds newElses
-    delve (T_BraceGroup id l) = dl l $ T_BraceGroup id
-    delve (T_WhileExpression id c l) = dll c l $ T_WhileExpression id
-    delve (T_UntilExpression id c l) = dll c l $ T_UntilExpression id
-    delve (T_ForIn id v w l) = dll w l $ T_ForIn id v
-    delve (T_SelectIn id v w l) = dll w l $ T_SelectIn id v
-    delve (T_CaseExpression id word cases) = do
-        newWord <- round word
-        newCases <- mapM (\(o, c, t) -> do
-                            x <- mapM round c
-                            y <- mapM round t
-                            return (o, x,y)
-                        ) cases
-        return $ T_CaseExpression id newWord newCases
-
-    delve (T_ForArithmetic id a b c group) = do
-        x <- round a
-        y <- round b
-        z <- round c
-        list <- mapM round group
-        return $ T_ForArithmetic id x y z list
-
-    delve (T_Script id s l) = dl l $ T_Script id s
-    delve (T_Function id a b name body) = d1 body $ T_Function id a b name
-    delve (T_Condition id typ token) = d1 token $ T_Condition id typ
-    delve (T_Extglob id str l) = dl l $ T_Extglob id str
-    delve (T_DollarBraced id braced op) = d1 op $ T_DollarBraced id braced
-    delve (T_HereDoc id d q str l) = dl l $ T_HereDoc id d q str
-
-    delve (TC_And id typ str t1 t2) = d2 t1 t2 $ TC_And id typ str
-    delve (TC_Or id typ str t1 t2) = d2 t1 t2 $ TC_Or id typ str
-    delve (TC_Group id typ token) = d1 token $ TC_Group id typ
-    delve (TC_Binary id typ op lhs rhs) = d2 lhs rhs $ TC_Binary id typ op
-    delve (TC_Unary id typ op token) = d1 token $ TC_Unary id typ op
-    delve (TC_Nullary id typ token) = d1 token $ TC_Nullary id typ
-
-    delve (TA_Binary id op t1 t2) = d2 t1 t2 $ TA_Binary id op
-    delve (TA_Assignment id op t1 t2) = d2 t1 t2 $ TA_Assignment id op
-    delve (TA_Unary id op t1) = d1 t1 $ TA_Unary id op
-    delve (TA_Sequence id l) = dl l $ TA_Sequence id
-    delve (TA_Trinary id t1 t2 t3) = do
-        a <- round t1
-        b <- round t2
-        c <- round t3
-        return $ TA_Trinary id a b c
-    delve (TA_Expansion id t) = dl t $ TA_Expansion id
-    delve (TA_Variable id str t) = dl t $ TA_Variable id str
-    delve (T_Annotation id anns t) = d1 t $ T_Annotation id anns
-    delve (T_CoProc id var body) = d1 body $ T_CoProc id var
-    delve (T_CoProcBody id t) = d1 t $ T_CoProcBody id
-    delve (T_Include id script) = d1 script $ T_Include id
-    delve (T_SourceCommand id includer t_include) = d2 includer t_include $ T_SourceCommand id
-    delve (T_BatsTest id name t) = d2 name t $ T_BatsTest id
-    delve t = return t
+        i (OuterToken id newIt)
 
 getId :: Token -> Id
-getId t = case t of
-        T_AND_IF id  -> id
-        T_OR_IF id  -> id
-        T_DSEMI id  -> id
-        T_Semi id  -> id
-        T_DLESS id  -> id
-        T_DGREAT id  -> id
-        T_LESSAND id  -> id
-        T_GREATAND id  -> id
-        T_LESSGREAT id  -> id
-        T_DLESSDASH id  -> id
-        T_CLOBBER id  -> id
-        T_If id  -> id
-        T_Then id  -> id
-        T_Else id  -> id
-        T_Elif id  -> id
-        T_Fi id  -> id
-        T_Do id  -> id
-        T_Done id  -> id
-        T_Case id  -> id
-        T_Esac id  -> id
-        T_While id  -> id
-        T_Until id  -> id
-        T_For id  -> id
-        T_Select id  -> id
-        T_Lbrace id  -> id
-        T_Rbrace id  -> id
-        T_Lparen id  -> id
-        T_Rparen id  -> id
-        T_Bang id  -> id
-        T_In  id  -> id
-        T_NEWLINE id  -> id
-        T_EOF id  -> id
-        T_Less id  -> id
-        T_Greater id  -> id
-        T_SingleQuoted id _  -> id
-        T_Literal id _  -> id
-        T_NormalWord id _  -> id
-        T_DoubleQuoted id _  -> id
-        T_DollarExpansion id _  -> id
-        T_DollarBraced id _ _ -> id
-        T_DollarArithmetic id _  -> id
-        T_BraceExpansion id _  -> id
-        T_ParamSubSpecialChar id _ -> id
-        T_DollarBraceCommandExpansion id _  -> id
-        T_IoFile id _ _  -> id
-        T_IoDuplicate id _ _  -> id
-        T_HereDoc id _ _ _ _ -> id
-        T_HereString id _  -> id
-        T_FdRedirect id _ _  -> id
-        T_Assignment id _ _ _ _  -> id
-        T_Array id _  -> id
-        T_IndexedElement id _ _  -> id
-        T_Redirecting id _ _  -> id
-        T_SimpleCommand id _ _  -> id
-        T_Pipeline id _ _  -> id
-        T_Banged id _  -> id
-        T_AndIf id _ _ -> id
-        T_OrIf id _ _ -> id
-        T_Backgrounded id _  -> id
-        T_IfExpression id _ _  -> id
-        T_Subshell id _  -> id
-        T_BraceGroup id _  -> id
-        T_WhileExpression id _ _  -> id
-        T_UntilExpression id _ _  -> id
-        T_ForIn id _ _ _  -> id
-        T_SelectIn id _ _ _  -> id
-        T_CaseExpression id _ _ -> id
-        T_Function id _ _ _ _  -> id
-        T_Arithmetic id _  -> id
-        T_Script id _ _  -> id
-        T_Condition id _ _  -> id
-        T_Extglob id _ _ -> id
-        T_Backticked id _ -> id
-        TC_And id _ _ _ _  -> id
-        TC_Or id _ _ _ _  -> id
-        TC_Group id _ _  -> id
-        TC_Binary id _ _ _ _  -> id
-        TC_Unary id _ _ _  -> id
-        TC_Nullary id _ _  -> id
-        TA_Binary id _ _ _  -> id
-        TA_Assignment id _ _ _  -> id
-        TA_Unary id _ _  -> id
-        TA_Sequence id _  -> id
-        TA_Trinary id _ _ _  -> id
-        TA_Expansion id _  -> id
-        T_ProcSub id _ _ -> id
-        T_Glob id _ -> id
-        T_ForArithmetic id _ _ _ _ -> id
-        T_DollarSingleQuoted id _ -> id
-        T_DollarDoubleQuoted id _ -> id
-        T_DollarBracket id _ -> id
-        T_Annotation id _ _ -> id
-        T_Pipe id _ -> id
-        T_CoProc id _ _ -> id
-        T_CoProcBody id _ -> id
-        T_Include id _ -> id
-        T_SourceCommand id _ _ -> id
-        T_UnparsedIndex id _ _ -> id
-        TC_Empty id _ -> id
-        TA_Variable id _ _ -> id
-        T_BatsTest id _ _ -> id
+getId (OuterToken id _) = id
 
 blank :: Monad m => Token -> m ()
 blank = const $ return ()
diff --git a/src/ShellCheck/ASTLib.hs b/src/ShellCheck/ASTLib.hs
--- a/src/ShellCheck/ASTLib.hs
+++ b/src/ShellCheck/ASTLib.hs
@@ -25,6 +25,7 @@
 import Control.Monad
 import Data.Char
 import Data.Functor
+import Data.Functor.Identity
 import Data.List
 import Data.Maybe
 
@@ -46,6 +47,7 @@
     T_BraceExpansion {} -> True
     T_Glob {} -> True
     T_Extglob {} -> True
+    T_DoubleQuoted _ l -> any willBecomeMultipleArgs l
     T_NormalWord _ l -> any willSplit l
     _ -> False
 
@@ -175,9 +177,13 @@
 getLiteralString :: Token -> Maybe String
 getLiteralString = getLiteralStringExt (const Nothing)
 
+-- Definitely get a literal string, with a given default for all non-literals
+getLiteralStringDef :: String -> Token -> String
+getLiteralStringDef x = runIdentity . getLiteralStringExt (const $ return x)
+
 -- Definitely get a literal string, skipping over all non-literals
 onlyLiteralString :: Token -> String
-onlyLiteralString = fromJust . getLiteralStringExt (const $ return "")
+onlyLiteralString = getLiteralStringDef ""
 
 -- Maybe get a literal string, but only if it's an unquoted argument.
 getUnquotedLiteral (T_NormalWord _ list) =
@@ -216,7 +222,7 @@
 
 -- Maybe get the literal value of a token, using a custom function
 -- to map unrecognized Tokens into strings.
-getLiteralStringExt :: (Token -> Maybe String) -> Token -> Maybe String
+getLiteralStringExt :: Monad m => (Token -> m String) -> Token -> m String
 getLiteralStringExt more = g
   where
     allInList = fmap concat . mapM g
@@ -297,6 +303,11 @@
 getCommandName :: Token -> Maybe String
 getCommandName = fst . getCommandNameAndToken
 
+-- Maybe get the name+arguments of a command.
+getCommandArgv t = do
+    (T_SimpleCommand _ _ args@(_:_)) <- getCommand t
+    return args
+
 -- Get the command name token from a command, i.e.
 -- the token representing 'ls' in 'ls -la 2> foo'.
 -- If it can't be determined, return the original token.
@@ -362,19 +373,23 @@
 isBraceExpansion t = case t of T_BraceExpansion {} -> True; _ -> False
 
 -- Get the lists of commands from tokens that contain them, such as
--- the body of while loops or branches of if statements.
+-- the conditions and bodies of while loops or branches of if statements.
 getCommandSequences :: Token -> [[Token]]
 getCommandSequences t =
     case t of
         T_Script _ _ cmds -> [cmds]
         T_BraceGroup _ cmds -> [cmds]
         T_Subshell _ cmds -> [cmds]
-        T_WhileExpression _ _ cmds -> [cmds]
-        T_UntilExpression _ _ cmds -> [cmds]
+        T_WhileExpression _ cond cmds -> [cond, cmds]
+        T_UntilExpression _ cond cmds -> [cond, cmds]
         T_ForIn _ _ _ cmds -> [cmds]
         T_ForArithmetic _ _ _ _ cmds -> [cmds]
-        T_IfExpression _ thens elses -> map snd thens ++ [elses]
+        T_IfExpression _ thens elses -> (concatMap (\(a,b) -> [a,b]) thens) ++ [elses]
         T_Annotation _ _ t -> getCommandSequences t
+
+        T_DollarExpansion _ cmds -> [cmds]
+        T_DollarBraceCommandExpansion _ cmds -> [cmds]
+        T_Backticked _ cmds -> [cmds]
         _ -> []
 
 -- Get a list of names of associative arrays
@@ -382,7 +397,7 @@
     nub . execWriter $ doAnalysis f t
   where
     f :: Token -> Writer [String] ()
-    f t@T_SimpleCommand {} = fromMaybe (return ()) $ do
+    f t@T_SimpleCommand {} = sequence_ $ do
         name <- getCommandName t
         let assocNames = ["declare","local","typeset"]
         guard $ elem name assocNames
diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs
--- a/src/ShellCheck/Analytics.hs
+++ b/src/ShellCheck/Analytics.hs
@@ -33,7 +33,7 @@
 import Control.Monad
 import Control.Monad.Identity
 import Control.Monad.State
-import Control.Monad.Writer
+import Control.Monad.Writer hiding ((<>))
 import Control.Monad.Reader
 import Data.Char
 import Data.Functor
@@ -41,6 +41,7 @@
 import Data.List
 import Data.Maybe
 import Data.Ord
+import Data.Semigroup
 import Debug.Trace
 import qualified Data.Map.Strict as Map
 import Test.QuickCheck.All (forAllProperties)
@@ -86,13 +87,8 @@
 
 getEnableDirectives root =
     case root of
-        T_Annotation _ list _ -> mapMaybe getEnable list
+        T_Annotation _ list _ -> [s | EnableComment s <- list]
         _ -> []
-  where
-    getEnable t =
-        case t of
-            EnableComment s -> return s
-            _ -> Nothing
 
 checkList l t = concatMap (\f -> f t) l
 
@@ -125,6 +121,7 @@
     ,checkArithmeticDeref
     ,checkArithmeticBadOctal
     ,checkComparisonAgainstGlob
+    ,checkCaseAgainstGlob
     ,checkCommarrays
     ,checkOrNeq
     ,checkEchoWc
@@ -191,6 +188,8 @@
     ,checkRedirectionToCommand
     ,checkDollarQuoteParen
     ,checkUselessBang
+    ,checkTranslatedStringVariable
+    ,checkModifiedArithmeticInRedirection
     ]
 
 optionalChecks = map fst optionalTreeChecks
@@ -259,12 +258,12 @@
 verifyNotTree :: (Parameters -> Token -> [TokenComment]) -> String -> Bool
 verifyNotTree f s = producesComments f s == Just False
 
-checkCommand str f t@(T_SimpleCommand id _ (cmd:rest)) =
-    when (t `isCommand` str) $ f cmd rest
+checkCommand str f t@(T_SimpleCommand id _ (cmd:rest))
+    | t `isCommand` str = f cmd rest
 checkCommand _ _ _ = return ()
 
-checkUnqualifiedCommand str f t@(T_SimpleCommand id _ (cmd:rest)) =
-    when (t `isUnqualifiedCommand` str) $ f cmd rest
+checkUnqualifiedCommand str f t@(T_SimpleCommand id _ (cmd:rest))
+    | t `isUnqualifiedCommand` str = f cmd rest
 checkUnqualifiedCommand _ _ _ = return ()
 
 
@@ -406,7 +405,7 @@
 prop_checkArithmeticOpCommand2 = verify checkArithmeticOpCommand "foo=bar * 2"
 prop_checkArithmeticOpCommand3 = verifyNot checkArithmeticOpCommand "foo + opts"
 checkArithmeticOpCommand _ (T_SimpleCommand id [T_Assignment {}] (firstWord:_)) =
-    fromMaybe (return ()) $ check <$> getGlobOrLiteralString firstWord
+    mapM_ check $ getGlobOrLiteralString firstWord
   where
     check op =
         when (op `elem` ["+", "-", "*", "/"]) $
@@ -417,7 +416,7 @@
 prop_checkWrongArit = verify checkWrongArithmeticAssignment "i=i+1"
 prop_checkWrongArit2 = verify checkWrongArithmeticAssignment "n=2; i=n*2"
 checkWrongArithmeticAssignment params (T_SimpleCommand id (T_Assignment _ _ _ _ val:[]) []) =
-  fromMaybe (return ()) $ do
+  sequence_ $ do
     str <- getNormalString val
     match <- matchRegex regex str
     var <- match !!! 0
@@ -433,7 +432,7 @@
     insertRef _ = Prelude.id
 
     getNormalString (T_NormalWord _ words) = do
-        parts <- foldl (liftM2 (\x y -> x ++ [y])) (Just []) $ map getLiterals words
+        parts <- mapM getLiterals words
         return $ concat parts
     getNormalString _ = Nothing
 
@@ -452,7 +451,7 @@
 checkUuoc _ (T_Pipeline _ _ (T_Redirecting _ _ cmd:_:_)) =
     checkCommand "cat" (const f) cmd
   where
-    f [word] = unless (mayBecomeMultipleArgs word || isOption word) $
+    f [word] | not (mayBecomeMultipleArgs word || isOption word) =
         style (getId word) 2002 "Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead."
     f _ = return ()
     isOption word = "-" `isPrefixOf` onlyLiteralString word
@@ -490,8 +489,8 @@
 
     for ["grep", "wc"] $
         \(grep:wc:_) ->
-            let flagsGrep = fromMaybe [] $ map snd . getAllFlags <$> getCommand grep
-                flagsWc = fromMaybe [] $ map snd . getAllFlags <$> getCommand wc
+            let flagsGrep = maybe [] (map snd . getAllFlags) $ getCommand grep
+                flagsWc = maybe [] (map snd . getAllFlags) $ getCommand wc
             in
                 unless (any (`elem` ["o", "only-matching", "r", "R", "recursive"]) flagsGrep || any (`elem` ["m", "chars", "w", "words", "c", "bytes", "L", "max-line-length"]) flagsWc || null flagsWc) $
                     style (getId grep) 2126 "Consider using grep -c instead of grep|wc -l."
@@ -502,11 +501,10 @@
         for' ["ls", "xargs"] $
             \x -> warn x 2011 "Use 'find .. -print0 | xargs -0 ..' or 'find .. -exec .. +' to allow non-alphanumeric filenames."
         ]
-    unless didLs $ do
+    unless didLs $ void $
         for ["ls", "?"] $
             \(ls:_) -> unless (hasShortParameter 'N' (oversimplify ls)) $
                 info (getId ls) 2012 "Use find instead of ls to better handle non-alphanumeric filenames."
-        return ()
   where
     for l f =
         let indices = indexOfSublists l (map (headOrDefault "" . oversimplify) commands)
@@ -560,17 +558,15 @@
     isOverride _ = False
 checkShebang params (T_Script _ (T_Literal id sb) _) = execWriter $ do
     unless (shellTypeSpecified params) $ do
-        when (sb == "") $
-            err id 2148 "Tips depend on target shell and yours is unknown. Add a shebang."
+        when (null sb) $
+            err id 2148 "Tips depend on target shell and yours is unknown. Add a shebang or a 'shell' directive."
         when (executableFromShebang sb == "ash") $
             warn id 2187 "Ash scripts will be checked as Dash. Add '# shellcheck shell=dash' to silence."
     unless (null sb) $ do
         unless ("/" `isPrefixOf` sb) $
             err id 2239 "Ensure the shebang uses an absolute path to the interpreter."
-        case words sb of
-            first:_ ->
-                when ("/" `isSuffixOf` first) $
-                    err id 2246 "This shebang specifies a directory. Ensure the interpreter is a file."
+        when ("/" `isSuffixOf` head (words sb)) $
+            err id 2246 "This shebang specifies a directory. Ensure the interpreter is a file."
 
 
 prop_checkForInQuoted = verify checkForInQuoted "for f in \"$(ls)\"; do echo foo; done"
@@ -582,17 +578,29 @@
 prop_checkForInQuoted4a = verifyNot checkForInQuoted "for f in foo{1,2,3}; do true; done"
 prop_checkForInQuoted5 = verify checkForInQuoted "for f in ls; do true; done"
 prop_checkForInQuoted6 = verifyNot checkForInQuoted "for f in \"${!arr}\"; do true; done"
-checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [word@(T_DoubleQuoted id list)]] _) =
-    when (any (\x -> willSplit x && not (mayBecomeMultipleArgs x)) list
-            || (fmap wouldHaveBeenGlob (getLiteralString word) == Just True)) $
+prop_checkForInQuoted7 = verify checkForInQuoted "for f in ls, grep, mv; do true; done"
+prop_checkForInQuoted8 = verify checkForInQuoted "for f in 'ls', 'grep', 'mv'; do true; done"
+prop_checkForInQuoted9 = verifyNot checkForInQuoted "for f in 'ls,' 'grep,' 'mv'; do true; done"
+checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [word@(T_DoubleQuoted id list)]] _)
+    | any (\x -> willSplit x && not (mayBecomeMultipleArgs x)) list
+            || maybe False wouldHaveBeenGlob (getLiteralString word) =
         err id 2066 "Since you double quoted this, it will not word split, and the loop will only run once."
 checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [T_SingleQuoted id _]] _) =
     warn id 2041 "This is a literal string. To run as a command, use $(..) instead of '..' . "
-checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [T_Literal id s]] _) =
-    if ',' `elem` s
-      then unless ('{' `elem` s) $
-            warn id 2042 "Use spaces, not commas, to separate loop elements."
-      else warn id 2043 "This loop will only ever run once for a constant value. Did you perhaps mean to loop over dir/*, $var or $(cmd)?"
+checkForInQuoted _ (T_ForIn _ _ [single] _)
+    | maybe False (',' `elem`) $ getUnquotedLiteral single =
+        warn (getId single) 2042 "Use spaces, not commas, to separate loop elements."
+    | not (willSplit single || mayBecomeMultipleArgs single) =
+        warn (getId single) 2043 "This loop will only ever run once. Bad quoting or missing glob/expansion?"
+checkForInQuoted params (T_ForIn _ _ multiple _) =
+    forM_ multiple $ \arg -> sequence_ $ do
+        suffix <- getTrailingUnquotedLiteral arg
+        string <- getLiteralString suffix
+        guard $ "," `isSuffixOf` string
+        return $
+            warnWithFix (getId arg) 2258
+                "The trailing comma is part of the value, not a separator. Delete or quote it."
+                (fixWith [replaceEnd (getId suffix) params 1 ""])
 checkForInQuoted _ _ = return ()
 
 prop_checkForInCat1 = verify checkForInCat "for f in $(cat foo); do stuff; done"
@@ -710,13 +718,13 @@
   where
     note x = makeComment InfoC x 2094
                 "Make sure not to read and write the same file in the same pipeline."
-    checkOccurrences t@(T_NormalWord exceptId x) u@(T_NormalWord newId y) =
-        when (exceptId /= newId
+    checkOccurrences t@(T_NormalWord exceptId x) u@(T_NormalWord newId y) |
+        exceptId /= newId
                 && x == y
                 && not (isOutput t && isOutput u)
                 && not (special t)
                 && not (any isHarmlessCommand [t,u])
-                && not (any containsAssignment [u])) $ do
+                && not (any containsAssignment [u]) = do
             addComment $ note newId
             addComment $ note exceptId
     checkOccurrences _ _ = return ()
@@ -774,9 +782,9 @@
 prop_checkDollarStar2 = verifyNot checkDollarStar "a=$*"
 prop_checkDollarStar3 = verifyNot checkDollarStar "[[ $* = 'a b' ]]"
 checkDollarStar p t@(T_NormalWord _ [b@(T_DollarBraced id _ _)])
-      | bracedString b == "*"  =
-    unless (isStrictlyQuoteFree (parentMap p) t) $
-        warn id 2048 "Use \"$@\" (with quotes) to prevent whitespace problems."
+      | bracedString b == "*" &&
+        not (isStrictlyQuoteFree (parentMap p) t) =
+            warn id 2048 "Use \"$@\" (with quotes) to prevent whitespace problems."
 checkDollarStar _ _ = return ()
 
 
@@ -792,7 +800,7 @@
 prop_checkUnquotedDollarAt9 = verifyNot checkUnquotedDollarAt "echo ${args[@]:+\"${args[@]}\"}"
 prop_checkUnquotedDollarAt10 = verifyNot checkUnquotedDollarAt "echo ${@+\"$@\"}"
 checkUnquotedDollarAt p word@(T_NormalWord _ parts) | not $ isStrictlyQuoteFree (parentMap p) word =
-    forM_ (take 1 $ filter isArrayExpansion parts) $ \x ->
+    forM_ (find isArrayExpansion parts) $ \x ->
         unless (isQuotedAlternativeReference x) $
             err (getId x) 2068
                 "Double quote array expansions to avoid re-splitting elements."
@@ -804,12 +812,12 @@
 prop_checkConcatenatedDollarAt4 = verifyNot checkConcatenatedDollarAt "echo $@"
 prop_checkConcatenatedDollarAt5 = verifyNot checkConcatenatedDollarAt "echo \"${arr[@]}\""
 checkConcatenatedDollarAt p word@T_NormalWord {}
-    | not $ isQuoteFree (parentMap p) word =
-        unless (null $ drop 1 parts) $
-            mapM_ for array
+    | not $ isQuoteFree (parentMap p) word
+    || null (drop 1 parts) =
+        mapM_ for array
   where
     parts = getWordParts word
-    array = take 1 $ filter isArrayExpansion parts
+    array = find isArrayExpansion parts
     for t = err (getId t) 2145 "Argument mixes string and array. Use * or separate argument."
 checkConcatenatedDollarAt _ _ = return ()
 
@@ -855,7 +863,7 @@
     readF _ _ _ = return []
 
     writeF _ (T_Assignment id mode name [] _) _ (DataString _) = do
-        isArray <- gets (isJust . Map.lookup name)
+        isArray <- gets (Map.member name)
         return $ if not isArray then [] else
             case mode of
                 Assign -> [makeComment WarningC id 2178 "Variable was used as an array but is now assigned a string."]
@@ -931,7 +939,12 @@
 prop_checkSingleQuotedVariables17= verifyNot checkSingleQuotedVariables "rename 's/(.)a/$1/g' *"
 prop_checkSingleQuotedVariables18= verifyNot checkSingleQuotedVariables "echo '``'"
 prop_checkSingleQuotedVariables19= verifyNot checkSingleQuotedVariables "echo '```'"
+prop_checkSingleQuotedVariables20= verifyNot checkSingleQuotedVariables "mumps -run %XCMD 'W $O(^GLOBAL(5))'"
+prop_checkSingleQuotedVariables21= verifyNot checkSingleQuotedVariables "mumps -run LOOP%XCMD --xec 'W $O(^GLOBAL(6))'"
 
+
+
+
 checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
     when (s `matches` re) $
         if "sed" == commandName
@@ -944,7 +957,7 @@
     commandName = fromMaybe "" $ do
         cmd <- getClosestCommand parents t
         name <- getCommandBasename cmd
-        return $ if name == "find" then getFindCommand cmd else if name == "git" then getGitCommand cmd else name
+        return $ if name == "find" then getFindCommand cmd else if name == "git" then getGitCommand cmd else if name == "mumps" then getMumpsCommand cmd else name
 
     isProbablyOk =
             any isOkAssignment (take 3 $ getPath parents t)
@@ -965,6 +978,8 @@
                 ,"rename"
                 ,"unset"
                 ,"git filter-branch"
+                ,"mumps -run %XCMD"
+                ,"mumps -run LOOP%XCMD"
                 ]
             || "awk" `isSuffixOf` commandName
             || "perl" `isPrefixOf` commandName
@@ -994,6 +1009,13 @@
             _ -> "git"
     getGitCommand (T_Redirecting _ _ cmd) = getGitCommand cmd
     getGitCommand _ = "git"
+    getMumpsCommand (T_SimpleCommand _ _ words) =
+        case map getLiteralString words of
+            Just "mumps":Just "-run":Just "%XCMD":_ -> "mumps -run %XCMD"
+            Just "mumps":Just "-run":Just "LOOP%XCMD":_ -> "mumps -run LOOP%XCMD"
+            _ -> "mumps"
+    getMumpsCommand (T_Redirecting _ _ cmd) = getMumpsCommand cmd
+    getMumpsCommand _ = "mumps"
 checkSingleQuotedVariables _ _ = return ()
 
 
@@ -1002,8 +1024,9 @@
 prop_checkUnquotedN3 = verifyNot checkUnquotedN "[[ -n $foo ]] && echo cow"
 prop_checkUnquotedN4 = verify checkUnquotedN "[ -n $cow -o -t 1 ]"
 prop_checkUnquotedN5 = verifyNot checkUnquotedN "[ -n \"$@\" ]"
-checkUnquotedN _ (TC_Unary _ SingleBracket "-n" (T_NormalWord id [t])) | willSplit t =
-       err id 2070 "-n doesn't work with unquoted arguments. Quote or use [[ ]]."
+checkUnquotedN _ (TC_Unary _ SingleBracket "-n" t) | willSplit t =
+    unless (any isArrayExpansion $ getWordParts t) $ -- There's SC2198 for these
+       err (getId t) 2070 "-n doesn't work with unquoted arguments. Quote or use [[ ]]."
 checkUnquotedN _ _ = return ()
 
 prop_checkNumberComparisons1 = verify checkNumberComparisons "[[ $foo < 3 ]]"
@@ -1020,6 +1043,7 @@
 prop_checkNumberComparisons13 = verify checkNumberComparisons "[ $foo > $bar ]"
 prop_checkNumberComparisons14 = verifyNot checkNumberComparisons "[[ foo < bar ]]"
 prop_checkNumberComparisons15 = verifyNot checkNumberComparisons "[ $foo '>' $bar ]"
+prop_checkNumberComparisons16 = verify checkNumberComparisons "[ foo -eq 'y' ]"
 checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do
     if isNum lhs || isNum rhs
       then do
@@ -1043,7 +1067,7 @@
                 Dash -> err id 2073 $ "Escape \\" ++ op ++ " to prevent it redirecting."
                 _ -> err id 2073 $ "Escape \\" ++ op ++ " to prevent it redirecting (or switch to [[ .. ]])."
 
-    when (op `elem` ["-lt", "-gt", "-le", "-ge", "-eq"]) $ do
+    when (op `elem` arithmeticBinaryTestOps) $ do
         mapM_ checkDecimals [lhs, rhs]
         when (typ == SingleBracket) $
             checkStrings [lhs, rhs]
@@ -1060,11 +1084,9 @@
         "Either use integers only, or use bc or awk to compare."
 
       checkStrings =
-        mapM_ stringError . take 1 . filter isNonNum
+        mapM_ stringError . find isNonNum
 
-      isNonNum t = fromMaybe False $ do
-        s <- getLiteralStringExt (const $ return "") t
-        return . not . all numChar $ s
+      isNonNum t = not . all numChar $ onlyLiteralString t
       numChar x = isDigit x || x `elem` "+-. "
 
       stringError t = err (getId t) 2170 $
@@ -1103,8 +1125,8 @@
 checkNumberComparisons _ _ = return ()
 
 prop_checkSingleBracketOperators1 = verify checkSingleBracketOperators "[ test =~ foo ]"
-checkSingleBracketOperators params (TC_Binary id SingleBracket "=~" lhs rhs) =
-    when (shellType params `elem` [Bash, Ksh]) $
+checkSingleBracketOperators params (TC_Binary id SingleBracket "=~" lhs rhs)
+    | shellType params `elem` [Bash, Ksh] =
         err id 2074 $ "Can't use =~ in [ ]. Use [[..]] instead."
 checkSingleBracketOperators _ _ = return ()
 
@@ -1169,10 +1191,10 @@
 prop_checkGlobbedRegex6 = verifyNot checkGlobbedRegex "[[ $foo =~ (o*) ]]"
 prop_checkGlobbedRegex7 = verifyNot checkGlobbedRegex "[[ $foo =~ \\*foo ]]"
 prop_checkGlobbedRegex8 = verifyNot checkGlobbedRegex "[[ $foo =~ x\\* ]]"
-checkGlobbedRegex _ (TC_Binary _ DoubleBracket "=~" _ rhs) =
-    let s = concat $ oversimplify rhs in
-        when (isConfusedGlobRegex s) $
-            warn (getId rhs) 2049 "=~ is for regex, but this looks like a glob. Use = instead."
+checkGlobbedRegex _ (TC_Binary _ DoubleBracket "=~" _ rhs)
+    | isConfusedGlobRegex s =
+        warn (getId rhs) 2049 "=~ is for regex, but this looks like a glob. Use = instead."
+    where s = concat $ oversimplify rhs
 checkGlobbedRegex _ _ = return ()
 
 
@@ -1192,7 +1214,7 @@
         else checkUnmatchable id op lhs rhs
   where
     isDynamic =
-        op `elem` [ "-lt", "-gt", "-le", "-ge", "-eq", "-ne" ]
+        op `elem` arithmeticBinaryTestOps
             && typ == DoubleBracket
         || op `elem` [ "-nt", "-ot", "-ef"]
 
@@ -1210,7 +1232,7 @@
 prop_checkLiteralBreakingTest7 = verifyNot checkLiteralBreakingTest "[ -z $(true) ]"
 prop_checkLiteralBreakingTest8 = verifyNot checkLiteralBreakingTest "[ $(true)$(true) ]"
 prop_checkLiteralBreakingTest10 = verify checkLiteralBreakingTest "[ -z foo ]"
-checkLiteralBreakingTest _ t = potentially $
+checkLiteralBreakingTest _ t = sequence_ $
         case t of
             (TC_Nullary _ _ w@(T_NormalWord _ l)) -> do
                 guard . not $ isConstant w -- Covered by SC2078
@@ -1224,16 +1246,13 @@
   where
     hasEquals = matchToken ('=' `elem`)
     isNonEmpty = matchToken (not . null)
-    matchToken m t = isJust $ do
-        str <- getLiteralString t
-        guard $ m str
-        return ()
+    matchToken m t = maybe False m (getLiteralString t)
 
     comparisonWarning list = do
-        token <- listToMaybe $ filter hasEquals list
+        token <- find hasEquals list
         return $ err (getId token) 2077 "You need spaces around the comparison operator."
     tautologyWarning t s = do
-        token <- listToMaybe $ filter isNonEmpty $ getWordParts t
+        token <- find isNonEmpty $ getWordParts t
         return $ err (getId token) 2157 s
 
 prop_checkConstantNullary = verify checkConstantNullary "[[ '$(foo)' ]]"
@@ -1258,7 +1277,7 @@
 prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))"
 prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar"
 prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
-checkForDecimals params t@(TA_Expansion id _) = potentially $ do
+checkForDecimals params t@(TA_Expansion id _) = sequence_ $ do
     guard $ not (hasFloatingPoint params)
     str <- getLiteralString t
     first <- str !!! 0
@@ -1294,7 +1313,7 @@
     unless (isException $ bracedString b) getWarning
   where
     isException [] = True
-    isException s = any (`elem` "/.:#%?*@$-!+=^,") s || isDigit (head s)
+    isException s@(h:_) = any (`elem` "/.:#%?*@$-!+=^,") s || isDigit h
     getWarning = fromMaybe noWarning . msum . map warningFor $ parents params t
     warningFor t =
         case t of
@@ -1311,7 +1330,7 @@
 prop_checkArithmeticBadOctal1 = verify checkArithmeticBadOctal "(( 0192 ))"
 prop_checkArithmeticBadOctal2 = verifyNot checkArithmeticBadOctal "(( 0x192 ))"
 prop_checkArithmeticBadOctal3 = verifyNot checkArithmeticBadOctal "(( 1 ^ 0777 ))"
-checkArithmeticBadOctal _ t@(TA_Expansion id _) = potentially $ do
+checkArithmeticBadOctal _ t@(TA_Expansion id _) = sequence_ $ do
     str <- getLiteralString t
     guard $ str `matches` octalRE
     return $ err id 2080 "Numbers with leading 0 are considered octal."
@@ -1338,6 +1357,21 @@
 
 checkComparisonAgainstGlob _ _ = return ()
 
+prop_checkCaseAgainstGlob1 = verify checkCaseAgainstGlob "case foo in lol$n) foo;; esac"
+prop_checkCaseAgainstGlob2 = verify checkCaseAgainstGlob "case foo in $(foo)) foo;; esac"
+prop_checkCaseAgainstGlob3 = verifyNot checkCaseAgainstGlob "case foo in *$bar*) foo;; esac"
+checkCaseAgainstGlob _ t =
+    case t of
+        (T_CaseExpression _ _ cases) -> mapM_ check cases
+        _ -> return ()
+  where
+    check (_, list, _) = mapM_ check' list
+    check' expr@(T_NormalWord _ list)
+        -- If it's already a glob, assume that's what the user wanted
+        | not (isGlob expr) && any isQuoteableExpansion list =
+            warn (getId expr) 2254 "Quote expansions in case patterns to match literally rather than as a glob."
+    check' _ = return ()
+
 prop_checkCommarrays1 = verify checkCommarrays "a=(1, 2)"
 prop_checkCommarrays2 = verify checkCommarrays "a+=(1,2,3)"
 prop_checkCommarrays3 = verifyNot checkCommarrays "cow=(1 \"foo,bar\" 3)"
@@ -1365,6 +1399,7 @@
 prop_checkOrNeq6 = verify checkOrNeq "[ $a != a ] || [ $a != b ]"
 prop_checkOrNeq7 = verify checkOrNeq "[ $a != a ] || [ $a != b ] || true"
 prop_checkOrNeq8 = verifyNot checkOrNeq "[[ $a != x || $a != x ]]"
+prop_checkOrNeq9 = verifyNot checkOrNeq "[ 0 -ne $FOO ] || [ 0 -ne $BAR ]"
 -- This only catches the most idiomatic cases. Fixme?
 
 -- For test-level "or": [ x != y -o x != z ]
@@ -1378,7 +1413,7 @@
         warn id 2056 "You probably wanted && here, otherwise it's always true."
 
 -- For command level "or": [ x != y ] || [ x != z ]
-checkOrNeq _ (T_OrIf id lhs rhs) = potentially $ do
+checkOrNeq _ (T_OrIf id lhs rhs) = sequence_ $ do
     (lhs1, op1, rhs1) <- getExpr lhs
     (lhs2, op2, rhs2) <- getExpr rhs
     guard $ op1 == op2 && op1 `elem` ["-ne", "!="]
@@ -1392,9 +1427,17 @@
             T_Pipeline _ _ [x] -> getExpr x
             T_Redirecting _ _ c -> getExpr c
             T_Condition _ _ c -> getExpr c
-            TC_Binary _ _ op lhs rhs -> return (lhs, op, rhs)
-            _ -> fail ""
+            TC_Binary _ _ op lhs rhs -> orient (lhs, op, rhs)
+            _ -> Nothing
 
+    -- Swap items so that the constant side is rhs (or Nothing if both/neither is constant)
+    orient (lhs, op, rhs) =
+        case (isConstant lhs, isConstant rhs) of
+            (True, False) -> return (rhs, op, lhs)
+            (False, True) -> return (lhs, op, rhs)
+            _ -> Nothing
+
+
 checkOrNeq _ _ = return ()
 
 
@@ -1504,8 +1547,8 @@
 prop_checkIndirectExpansion3 = verify checkIndirectExpansion "${$#}"
 prop_checkIndirectExpansion4 = verify checkIndirectExpansion "${var${n}_$((i%2))}"
 prop_checkIndirectExpansion5 = verifyNot checkIndirectExpansion "${bar}"
-checkIndirectExpansion _ (T_DollarBraced i _ (T_NormalWord _ contents)) =
-    when (isIndirection contents) $
+checkIndirectExpansion _ (T_DollarBraced i _ (T_NormalWord _ contents))
+    | isIndirection contents =
         err i 2082 "To expand via indirection, use arrays, ${!name} or (for sh only) eval."
   where
     isIndirection vars =
@@ -1542,8 +1585,8 @@
         case trapped of
             T_DollarExpansion id _ -> warnAboutExpansion id
             T_DollarBraced id _ _ -> warnAboutExpansion id
-            T_Literal id s ->
-                unless (quotesSingleThing a && quotesSingleThing b || isRegex (getPath (parentMap params) trapped)) $
+            T_Literal id s
+                | not (quotesSingleThing a && quotesSingleThing b || isRegex (getPath (parentMap params) trapped)) ->
                     warnAboutLiteral id
             _ -> return ()
 
@@ -1625,9 +1668,9 @@
     doList = doList' . stripCleanup
     -- The second parameter is True if we are in a loop
     -- In that case we should emit the warning also if `exec' is the last statement
-    doList' t@(current:following:_) False = do
+    doList' (current:t@(following:_)) False = do
         commentIfExec current
-        doList (tail t) False
+        doList t False
     doList' (current:tail) True = do
         commentIfExec current
         doList tail True
@@ -1636,8 +1679,8 @@
     commentIfExec (T_Pipeline id _ list) =
       mapM_ commentIfExec $ take 1 list
     commentIfExec (T_Redirecting _ _ f@(
-      T_SimpleCommand id _ (cmd:arg:_))) =
-        when (f `isUnqualifiedCommand` "exec") $
+      T_SimpleCommand id _ (cmd:arg:_)))
+        | f `isUnqualifiedCommand` "exec" =
           warn id 2093
             "Remove \"exec \" if script should continue after this command."
     commentIfExec _ = return ()
@@ -1790,14 +1833,28 @@
 prop_checkSpacefulness36= verifyNotTree checkSpacefulness "arg=$#; echo $arg"
 prop_checkSpacefulness37= verifyNotTree checkSpacefulness "@test 'status' {\n [ $status -eq 0 ]\n}"
 prop_checkSpacefulness37v = verifyTree checkVerboseSpacefulness "@test 'status' {\n [ $status -eq 0 ]\n}"
+prop_checkSpacefulness38= verifyTree checkSpacefulness "a=; echo $a"
+prop_checkSpacefulness39= verifyNotTree checkSpacefulness "a=''\"\"''; b=x$a; echo $b"
+prop_checkSpacefulness40= verifyNotTree checkSpacefulness "a=$((x+1)); echo $a"
 
+data SpaceStatus = SpaceSome | SpaceNone | SpaceEmpty deriving (Eq)
+instance Semigroup SpaceStatus where
+    SpaceNone <> SpaceNone = SpaceNone
+    SpaceSome <> _ = SpaceSome
+    _ <> SpaceSome = SpaceSome
+    SpaceEmpty <> x = x
+    x <> SpaceEmpty = x
+instance Monoid SpaceStatus where
+    mempty = SpaceEmpty
+    mappend = (<>)
+
 -- This is slightly awkward because we want to support structured
 -- optional checks based on nearly the same logic
 checkSpacefulness params = checkSpacefulness' onFind params
   where
     emit x = tell [x]
     onFind spaces token _ =
-        when spaces $
+        when (spaces /= SpaceNone) $
             if isDefaultAssignment (parentMap params) token
             then
                 emit $ makeComment InfoC (getId token) 2223
@@ -1812,7 +1869,6 @@
             any (`isPrefixOf` modifier) ["=", ":="]
             && isParamTo parents ":" token
 
-
 prop_checkSpacefulness4v= verifyTree checkVerboseSpacefulness "foo=3; foo=$(echo $foo)"
 prop_checkSpacefulness8v= verifyTree checkVerboseSpacefulness "a=foo\\ bar; a=foo; rm $a"
 prop_checkSpacefulness28v = verifyTree checkVerboseSpacefulness "exec {n}>&1; echo $n"
@@ -1820,24 +1876,24 @@
 checkVerboseSpacefulness params = checkSpacefulness' onFind params
   where
     onFind spaces token name =
-        when (not spaces && name `notElem` specialVariablesWithoutSpaces) $
+        when (spaces == SpaceNone && name `notElem` specialVariablesWithoutSpaces) $
             tell [makeCommentWithFix StyleC (getId token) 2248
                     "Prefer double quoting even when variables don't contain special characters."
                     (addDoubleQuotesAround params token)]
 
 addDoubleQuotesAround params token = (surroundWidth (getId token) params "\"")
 checkSpacefulness'
-    :: (Bool -> Token -> String -> Writer [TokenComment] ()) ->
+    :: (SpaceStatus -> Token -> String -> Writer [TokenComment] ()) ->
             Parameters -> Token -> [TokenComment]
 checkSpacefulness' onFind params t =
     doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)
   where
-    defaults = zip variablesWithoutSpaces (repeat False)
+    defaults = zip variablesWithoutSpaces (repeat SpaceNone)
 
-    hasSpaces name = gets (Map.findWithDefault True name)
+    hasSpaces name = gets (Map.findWithDefault SpaceSome name)
 
-    setSpaces name bool =
-        modify $ Map.insert name bool
+    setSpaces name status =
+        modify $ Map.insert name status
 
     readF _ token name = do
         spaces <- hasSpaces name
@@ -1854,13 +1910,13 @@
       where
         emit x = tell [x]
 
-    writeF _ _ name (DataString SourceExternal) = setSpaces name True >> return []
-    writeF _ _ name (DataString SourceInteger) = setSpaces name False >> return []
+    writeF _ _ name (DataString SourceExternal) = setSpaces name SpaceSome >> return []
+    writeF _ _ name (DataString SourceInteger) = setSpaces name SpaceNone >> return []
 
     writeF _ _ name (DataString (SourceFrom vals)) = do
         map <- get
         setSpaces name
-            (isSpacefulWord (\x -> Map.findWithDefault True x map) vals)
+            (isSpacefulWord (\x -> Map.findWithDefault SpaceSome x map) vals)
         return []
 
     writeF _ _ _ _ = return []
@@ -1872,24 +1928,28 @@
             (T_DollarBraced _ _ _ ) -> True
             _ -> False
 
-    isSpacefulWord :: (String -> Bool) -> [Token] -> Bool
-    isSpacefulWord f = any (isSpaceful f)
-    isSpaceful :: (String -> Bool) -> Token -> Bool
+    isSpacefulWord :: (String -> SpaceStatus) -> [Token] -> SpaceStatus
+    isSpacefulWord f = mconcat . map (isSpaceful f)
+    isSpaceful :: (String -> SpaceStatus) -> Token -> SpaceStatus
     isSpaceful spacefulF x =
         case x of
-          T_DollarExpansion _ _ -> True
-          T_Backticked _ _ -> True
-          T_Glob _ _         -> True
-          T_Extglob {}       -> True
-          T_Literal _ s      -> s `containsAny` globspace
-          T_SingleQuoted _ s -> s `containsAny` globspace
+          T_DollarExpansion _ _ -> SpaceSome
+          T_Backticked _ _ -> SpaceSome
+          T_Glob _ _         -> SpaceSome
+          T_Extglob {}       -> SpaceSome
+          T_DollarArithmetic _ _ -> SpaceNone
+          T_Literal _ s      -> fromLiteral s
+          T_SingleQuoted _ s -> fromLiteral s
           T_DollarBraced _ _ _ -> spacefulF $ getBracedReference $ bracedString x
           T_NormalWord _ w   -> isSpacefulWord spacefulF w
           T_DoubleQuoted _ w -> isSpacefulWord spacefulF w
-          _ -> False
+          _ -> SpaceEmpty
       where
         globspace = "*?[] \t\n"
         containsAny s = any (`elem` s)
+        fromLiteral "" = SpaceEmpty
+        fromLiteral s | s `containsAny` globspace = SpaceSome
+        fromLiteral _ = SpaceNone
 
 prop_CheckVariableBraces1 = verify checkVariableBraces "a='123'; echo $a"
 prop_CheckVariableBraces2 = verifyNot checkVariableBraces "a='123'; echo ${a}"
@@ -1897,8 +1957,8 @@
 prop_CheckVariableBraces4 = verifyNot checkVariableBraces "echo $* $1"
 checkVariableBraces params t =
     case t of
-        T_DollarBraced id False _ ->
-            unless (name `elem` unbracedVariables) $
+        T_DollarBraced id False _
+            | name `notElem` unbracedVariables ->
                 styleWithFix id 2250
                     "Prefer putting braces around variable references even when not strictly required."
                     (fixFor t)
@@ -1923,7 +1983,7 @@
 checkQuotesInLiterals params t =
     doVariableFlowAnalysis readF writeF Map.empty (variableFlow params)
   where
-    getQuotes name = fmap (Map.lookup name) get
+    getQuotes name = gets (Map.lookup name)
     setQuotes name ref = modify $ Map.insert name ref
     deleteQuotes = modify . Map.delete
     parents = parentMap params
@@ -1958,18 +2018,18 @@
 
     readF _ expr name = do
         assignment <- getQuotes name
-        return
-          (if isJust assignment
-              && not (isParamTo parents "eval" expr)
+        return $ case assignment of
+          Just j
+              | not (isParamTo parents "eval" expr)
               && not (isQuoteFree parents expr)
               && not (squashesQuotes expr)
-              then [
-                  makeComment WarningC (fromJust assignment) 2089 $
+              -> [
+                  makeComment WarningC j 2089 $
                       "Quotes/backslashes will be treated literally. " ++ suggestion,
                   makeComment WarningC (getId expr) 2090
                       "Quotes/backslashes in this variable will not be respected."
                 ]
-              else [])
+          _ -> []
     suggestion =
         if supportsArrays (shellType params)
         then "Use an array."
@@ -2035,7 +2095,7 @@
         in when ('=' `elem` string) $
             modify ((takeWhile (/= '=') string, getId arg):)
 
-    checkArg cmd (_, arg) = potentially $ do
+    checkArg cmd (_, arg) = sequence_ $ do
         literalArg <- getUnquotedLiteral arg  -- only consider unquoted literals
         definitionId <- Map.lookup literalArg functions
         return $ do
@@ -2091,6 +2151,7 @@
 prop_checkUnused44= verifyNotTree checkUnusedAssignments "DEFINE_string \"foo$ibar\" x y"
 prop_checkUnused45= verifyTree checkUnusedAssignments "readonly foo=bar"
 prop_checkUnused46= verifyTree checkUnusedAssignments "readonly foo=(bar)"
+prop_checkUnused47= verifyNotTree checkUnusedAssignments "a=1; alias hello='echo $a'"
 checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)
   where
     flow = variableFlow params
@@ -2151,6 +2212,8 @@
 prop_checkUnassignedReferences36= verifyNotTree checkUnassignedReferences "read -a foo -r <<<\"foo bar\"; echo \"$foo\""
 prop_checkUnassignedReferences37= verifyNotTree checkUnassignedReferences "var=howdy; printf -v 'array[0]' %s \"$var\"; printf %s \"${array[0]}\";"
 prop_checkUnassignedReferences38= verifyTree (checkUnassignedReferences' True) "echo $VAR"
+prop_checkUnassignedReferences39= verifyNotTree checkUnassignedReferences "builtin export var=4; echo $var"
+prop_checkUnassignedReferences40= verifyNotTree checkUnassignedReferences ": ${foo=bar}"
 
 checkUnassignedReferences = checkUnassignedReferences' False
 checkUnassignedReferences' includeGlobals params t = warnings
@@ -2197,14 +2260,14 @@
                     match <- getBestMatch var
                     return $ " (did you mean '" ++ match ++ "'?)"
 
-    warningFor var place = do
+    warningFor (var, place) = do
         guard $ isVariableName var
         guard . not $ isInArray var place || isGuarded place
         (if includeGlobals || isLocal var
          then warningForLocals
          else warningForGlobals) var place
 
-    warnings = execWriter . sequence $ mapMaybe (uncurry warningFor) unassigned
+    warnings = execWriter . sequence $ mapMaybe warningFor unassigned
 
     -- Due to parsing, foo=( [bar]=baz ) parses 'bar' as a reference even for assoc arrays.
     -- Similarly, ${foo[bar baz]} may not be referencing bar/baz. Just skip these.
@@ -2258,43 +2321,79 @@
 prop_checkWhileReadPitfalls6 = verifyNot checkWhileReadPitfalls "while read foo <&3; do ssh $foo; done 3< foo"
 prop_checkWhileReadPitfalls7 = verify checkWhileReadPitfalls "while read foo; do if true; then ssh $foo uptime; fi; done < file"
 prop_checkWhileReadPitfalls8 = verifyNot checkWhileReadPitfalls "while read foo; do ssh -n $foo uptime; done < file"
+prop_checkWhileReadPitfalls9 = verify checkWhileReadPitfalls "while read foo; do ffmpeg -i foo.mkv bar.mkv -an; done"
+prop_checkWhileReadPitfalls10 = verify checkWhileReadPitfalls "while read foo; do mplayer foo.ogv > file; done"
+prop_checkWhileReadPitfalls11 = verifyNot checkWhileReadPitfalls "while read foo; do mplayer foo.ogv <<< q; done"
+prop_checkWhileReadPitfalls12 = verifyNot checkWhileReadPitfalls "while read foo\ndo\nmplayer foo.ogv << EOF\nq\nEOF\ndone"
+prop_checkWhileReadPitfalls13 = verify checkWhileReadPitfalls "while read foo; do x=$(ssh host cmd); done"
+prop_checkWhileReadPitfalls14 = verify checkWhileReadPitfalls "while read foo; do echo $(ssh host cmd) < /dev/null; done"
 
-checkWhileReadPitfalls _ (T_WhileExpression id [command] contents)
+checkWhileReadPitfalls params (T_WhileExpression id [command] contents)
         | isStdinReadCommand command =
     mapM_ checkMuncher contents
   where
-    munchers = [ "ssh", "ffmpeg", "mplayer", "HandBrakeCLI" ]
-    preventionFlags = ["n", "noconsolecontrols" ]
+    -- Map of munching commands to a function that checks if the flags should exclude it
+    munchers = Map.fromList [
+        ("ssh", (hasFlag, addFlag, "-n")),
+        ("ffmpeg", (hasArgument, addFlag, "-nostdin")),
+        ("mplayer", (hasArgument, addFlag, "-noconsolecontrols")),
+        ("HandBrakeCLI", (\_ _ -> False, addRedirect, "< /dev/null"))
+        ]
+    -- Use flag parsing, e.g. "-an" -> "a", "n"
+    hasFlag ('-':flag) = elem flag . map snd . getAllFlags
+    -- Simple string match, e.g. "-an" -> "-an"
+    hasArgument arg = elem arg . mapMaybe getLiteralString . fromJust . getCommandArgv
+    addFlag string cmd = fixWith [replaceEnd (getId $ getCommandTokenOrThis cmd) params 0 (' ':string)]
+    addRedirect string cmd = fixWith [replaceEnd (getId cmd) params 0 (' ':string)]
 
     isStdinReadCommand (T_Pipeline _ _ [T_Redirecting id redirs cmd]) =
         let plaintext = oversimplify cmd
-        in head (plaintext ++ [""]) == "read"
+        in headOrDefault "" plaintext == "read"
             && ("-u" `notElem` plaintext)
-            && all (not . stdinRedirect) redirs
+            && not (any stdinRedirect redirs)
     isStdinReadCommand _ = False
 
-    checkMuncher (T_Pipeline _ _ (T_Redirecting _ redirs cmd:_)) | not $ any stdinRedirect redirs =
+    checkMuncher :: Token -> Writer [TokenComment] ()
+    checkMuncher (T_Pipeline _ _ (T_Redirecting _ redirs cmd:_)) = do
+        -- Check command substitutions regardless of the command
         case cmd of
-            (T_IfExpression _ thens elses) ->
-              mapM_ checkMuncher . concat $ map fst thens ++ map snd thens ++ [elses]
+            T_SimpleCommand _ vars args ->
+                mapM_ checkMuncher $ concat $ concatMap getCommandSequences $ concatMap getWords $ vars ++ args
+            _ -> return ()
 
-            _ -> potentially $ do
+        unless (any stdinRedirect redirs) $ do
+            -- Recurse into ifs/loops/groups/etc if this doesn't redirect
+            mapM_ checkMuncher $ concat $ getCommandSequences cmd
+
+            -- Check the actual command
+            sequence_ $ do
                 name <- getCommandBasename cmd
-                guard $ name `elem` munchers
+                (check, fix, flag) <- Map.lookup name munchers
+                guard $ not (check flag cmd)
 
-                -- Sloppily check if the command has a flag to prevent eating stdin.
-                let flags = getAllFlags cmd
-                guard . not $ any (`elem` preventionFlags) $ map snd flags
                 return $ do
                     info id 2095 $
                         name ++ " may swallow stdin, preventing this loop from working properly."
-                    warn (getId cmd) 2095 $
-                        "Add < /dev/null to prevent " ++ name ++ " from swallowing stdin."
+                    warnWithFix (getId cmd) 2095
+                        ("Use " ++ name ++ " " ++ flag ++ " to prevent " ++ name ++ " from swallowing stdin.")
+                        (fix flag cmd)
     checkMuncher _ = return ()
 
-    stdinRedirect (T_FdRedirect _ fd _)
-        | fd == "" || fd == "0" = True
+    stdinRedirect (T_FdRedirect _ fd op)
+        | fd == "0" = True
+        | fd == "" =
+            case op of
+                T_IoFile _ (T_Less _) _ -> True
+                T_IoDuplicate _ (T_LESSAND _) _ -> True
+                T_HereString _ _ -> True
+                T_HereDoc {} -> True
+                _ -> False
     stdinRedirect _ = False
+
+    getWords t =
+        case t of
+            T_Assignment _ _ _ _ x -> getWordParts x
+            _ -> getWordParts t
 checkWhileReadPitfalls _ _ = return ()
 
 
@@ -2325,6 +2424,7 @@
 prop_checkCharRangeGlob3 = verify checkCharRangeGlob "ls [10-15]"
 prop_checkCharRangeGlob4 = verifyNot checkCharRangeGlob "ls [a-zA-Z]"
 prop_checkCharRangeGlob5 = verifyNot checkCharRangeGlob "tr -d [a-zA-Z]" -- tr has 2060
+prop_checkCharRangeGlob6 = verifyNot checkCharRangeGlob "[[ $x == [!!]* ]]"
 checkCharRangeGlob p t@(T_Glob id str) |
   isCharClass str && not (isParamTo (parentMap p) "tr" t) =
     if ":" `isPrefixOf` contents
@@ -2336,8 +2436,13 @@
             info id 2102 "Ranges can only match single chars (mentioned due to duplicates)."
   where
     isCharClass str = "[" `isPrefixOf` str && "]" `isSuffixOf` str
-    contents = drop 1 . take (length str - 1) $ str
+    contents = dropNegation . drop 1 . take (length str - 1) $ str
     hasDupes = any (>1) . map length . group . sort . filter (/= '-') $ contents
+    dropNegation s =
+        case s of
+            '!':rest -> rest
+            '^':rest -> rest
+            x -> x
 checkCharRangeGlob _ _ = return ()
 
 
@@ -2370,7 +2475,7 @@
                 else findCdPair (b:rest)
             _ -> Nothing
 
-    doList list = potentially $ do
+    doList list = sequence_ $ do
         cd <- findCdPair $ mapMaybe getCandidate list
         return $ info cd 2103 "Use a ( subshell ) to avoid having to cd back."
 
@@ -2457,12 +2562,10 @@
         map (\t@(T_Function _ _ _ name _) -> (name,t)) functions
     functions = execWriter $ doAnalysis (tell . maybeToList . findFunction) root
 
-    findFunction t@(T_Function id _ _ name body) =
-        let flow = getVariableFlow params body
-        in
-          if any (isPositionalReference t) flow && not (any isPositionalAssignment flow)
-            then return t
-            else Nothing
+    findFunction t@(T_Function id _ _ name body)
+        | any (isPositionalReference t) flow && not (any isPositionalAssignment flow)
+        = return t
+        where flow = getVariableFlow params body
     findFunction _ = Nothing
 
     isPositionalAssignment x =
@@ -2484,7 +2587,7 @@
 
     referenceList :: [(String, Bool, Token)]
     referenceList = execWriter $
-        doAnalysis (fromMaybe (return ()) . checkCommand) root
+        doAnalysis (sequence_ . checkCommand) root
     checkCommand :: Token -> Maybe (Writer [(String, Bool, Token)] ())
     checkCommand t@(T_SimpleCommand _ _ (cmd:args)) = do
         str <- getLiteralString cmd
@@ -2500,13 +2603,12 @@
     updateWith x@(name, _, _) = Map.insertWith (++) name [x]
 
     warnForGroup group =
-        when (all isArgumentless group) $
-            -- Allow ignoring SC2120 on the function to ignore all calls
-            let (name, func) = getFunction group
-                ignoring = shouldIgnoreCode params 2120 func
-            in unless ignoring $ do
-                mapM_ suggestParams group
-                warnForDeclaration func name
+        -- Allow ignoring SC2120 on the function to ignore all calls
+        when (all isArgumentless group && not ignoring) $ do
+            mapM_ suggestParams group
+            warnForDeclaration func name
+        where (name, func) = getFunction group
+              ignoring = shouldIgnoreCode params 2120 func
 
     suggestParams (name, _, thing) =
         info (getId thing) 2119 $
@@ -2516,7 +2618,7 @@
             name ++ " references arguments, but none are ever passed."
 
     getFunction ((name, _, _):_) =
-        (name, fromJust $ Map.lookup name functionMap)
+        (name, functionMap Map.! name)
 
 
 prop_checkOverridingPath1 = verify checkOverridingPath "PATH=\"$var/$foo\""
@@ -2530,11 +2632,11 @@
 checkOverridingPath _ (T_SimpleCommand _ vars []) =
     mapM_ checkVar vars
   where
-    checkVar (T_Assignment id Assign "PATH" [] word) =
-        let string = concat $ oversimplify word
-        in unless (any (`isInfixOf` string) ["/bin", "/sbin" ]) $ do
+    checkVar (T_Assignment id Assign "PATH" [] word)
+        | not $ any (`isInfixOf` string) ["/bin", "/sbin" ] = do
             when ('/' `elem` string && ':' `notElem` string) $ notify id
             when (isLiteral word && ':' `notElem` string && '/' `notElem` string) $ notify id
+        where string = concat $ oversimplify word
     checkVar _ = return ()
     notify id = warn id 2123 "PATH is the shell search path. Use another name."
 checkOverridingPath _ _ = return ()
@@ -2545,12 +2647,12 @@
 checkTildeInPath _ (T_SimpleCommand _ vars _) =
     mapM_ checkVar vars
   where
-    checkVar (T_Assignment id Assign "PATH" [] (T_NormalWord _ parts)) =
-        when (any (\x -> isQuoted x && hasTilde x) parts) $
+    checkVar (T_Assignment id Assign "PATH" [] (T_NormalWord _ parts))
+        | any (\x -> isQuoted x && hasTilde x) parts =
             warn id 2147 "Literal tilde in PATH works poorly across programs."
     checkVar _ = return ()
 
-    hasTilde t = fromMaybe False (liftM2 elem (return '~') (getLiteralStringExt (const $ return "") t))
+    hasTilde t = '~' `elem` onlyLiteralString t
     isQuoted T_DoubleQuoted {} = True
     isQuoted T_SingleQuoted {} = True
     isQuoted _ = False
@@ -2560,13 +2662,13 @@
 prop_checkUnsupported4 = verify checkUnsupported "#!/bin/ksh\ncase foo in bar) baz ;;& esac"
 prop_checkUnsupported5 = verify checkUnsupported "#!/bin/bash\necho \"${ ls; }\""
 checkUnsupported params t =
-    when (not (null support) && (shellType params `notElem` support)) $
+    unless (null support || (shellType params `elem` support)) $
         report name
  where
     (name, support) = shellSupport t
     report s = err (getId t) 2127 $
         "To use " ++ s ++ ", specify #!/usr/bin/env " ++
-            (map toLower . intercalate " or " . map show $ support)
+            (intercalate " or " . map (map toLower . show) $ support)
 
 -- TODO: Move more of these checks here
 shellSupport t =
@@ -2590,8 +2692,8 @@
   where
     checkList list =
         mapM_ checkGroup (groupWith (fmap fst) $ map getTarget list)
-    checkGroup (f:_:_:_) | isJust f =
-        style (snd $ fromJust f) 2129
+    checkGroup (Just (_,id):_:_:_) =
+        style id 2129
             "Consider using { cmd1; cmd2; } >> file instead of individual redirects."
     checkGroup _ = return ()
     getTarget (T_Annotation _ _ t) = getTarget t
@@ -2606,21 +2708,23 @@
 
 prop_checkSuspiciousIFS1 = verify checkSuspiciousIFS "IFS=\"\\n\""
 prop_checkSuspiciousIFS2 = verifyNot checkSuspiciousIFS "IFS=$'\\t'"
-checkSuspiciousIFS params (T_Assignment id Assign "IFS" [] value) =
-    potentially $ do
-        str <- getLiteralString value
-        return $ check str
+prop_checkSuspiciousIFS3 = verify checkSuspiciousIFS "IFS=' \\t\\n'"
+checkSuspiciousIFS params (T_Assignment _ _ "IFS" [] value) =
+    mapM_ check $ getLiteralString value
   where
-    n = if shellType params == Sh then "'<literal linefeed here>'" else "$'\\n'"
-    t = if shellType params == Sh then "\"$(printf '\\t')\"" else "$'\\t'"
+    hasDollarSingle = shellType params == Bash || shellType params == Ksh
+    n = if hasDollarSingle then  "$'\\n'" else "'<literal linefeed here>'"
+    t = if hasDollarSingle then  "$'\\t'" else "\"$(printf '\\t')\""
     check value =
         case value of
             "\\n" -> suggest n
-            "/n" -> suggest n
             "\\t" -> suggest t
-            "/t" -> suggest t
+            x | '\\' `elem` x -> suggest2 "a literal backslash"
+            x | 'n' `elem` x -> suggest2 "the literal letter 'n'"
+            x | 't' `elem` x -> suggest2 "the literal letter 't'"
             _ -> return ()
-    suggest r = warn id 2141 $ "Did you mean IFS=" ++ r ++ " ?"
+    suggest r = warn (getId value) 2141 $ "This backslash is literal. Did you mean IFS=" ++ r ++ " ?"
+    suggest2 desc = warn (getId value) 2141 $ "This IFS value contains " ++ desc ++ ". For tabs/linefeeds/escapes, use $'..', literal, or printf."
 checkSuspiciousIFS _ _ = return ()
 
 
@@ -2631,7 +2735,7 @@
 prop_checkGrepQ5= verifyNot checkShouldUseGrepQ "rm $(ls | grep file)"
 prop_checkGrepQ6= verifyNot checkShouldUseGrepQ "[[ -n $(pgrep foo) ]]"
 checkShouldUseGrepQ params t =
-    potentially $ case t of
+    sequence_ $ case t of
         TC_Nullary id _ token -> check id True token
         TC_Unary id _ "-n" token -> check id True token
         TC_Unary id _ "-z" token -> check id False token
@@ -2678,6 +2782,9 @@
 prop_checkTestArgumentSplitting16 = verifyNot checkTestArgumentSplitting "[[ -v foo[123] ]]"
 prop_checkTestArgumentSplitting17 = verifyNot checkTestArgumentSplitting "#!/bin/ksh\n[ -e foo* ]"
 prop_checkTestArgumentSplitting18 = verify checkTestArgumentSplitting "#!/bin/ksh\n[ -d foo* ]"
+prop_checkTestArgumentSplitting19 = verifyNot checkTestArgumentSplitting "[[ var[x] -eq 2*3 ]]"
+prop_checkTestArgumentSplitting20 = verify checkTestArgumentSplitting "[ var[x] -eq 2 ]"
+prop_checkTestArgumentSplitting21 = verify checkTestArgumentSplitting "[ 6 -eq 2*3 ]"
 checkTestArgumentSplitting :: Parameters -> Token -> Writer [TokenComment] ()
 checkTestArgumentSplitting params t =
     case t of
@@ -2707,6 +2814,18 @@
 
         (TC_Unary _ typ op token) -> checkAll typ token
 
+        (TC_Binary _ typ op lhs rhs) | op `elem` arithmeticBinaryTestOps ->
+            if typ == DoubleBracket
+            then
+                mapM_ (\c -> do
+                        checkArrays typ c
+                        checkBraces typ c) [lhs, rhs]
+            else
+                mapM_ (\c -> do
+                        checkNumericalGlob typ c
+                        checkArrays typ c
+                        checkBraces typ c) [lhs, rhs]
+
         (TC_Binary _ typ op lhs rhs) ->
             if op `elem` ["=", "==", "!=", "=~"]
             then do
@@ -2739,13 +2858,18 @@
             then warn (getId token) 2202 "Globs don't work as operands in [ ]. Use a loop."
             else err (getId token) 2203 "Globs are ignored in [[ ]] except right of =/!=. Use a loop."
 
+    checkNumericalGlob SingleBracket token =
+        -- var[x] and x*2 look like globs
+        when (shellType params /= Ksh && isGlob token) $
+            err (getId token) 2255 "[ ] does not apply arithmetic evaluation. Evaluate with $((..)) for numbers, or use string comparator for strings."
 
+
 prop_checkMaskedReturns1 = verify checkMaskedReturns "f() { local a=$(false); }"
 prop_checkMaskedReturns2 = verify checkMaskedReturns "declare a=$(false)"
 prop_checkMaskedReturns3 = verify checkMaskedReturns "declare a=\"`false`\""
 prop_checkMaskedReturns4 = verifyNot checkMaskedReturns "declare a; a=$(false)"
 prop_checkMaskedReturns5 = verifyNot checkMaskedReturns "f() { local -r a=$(false); }"
-checkMaskedReturns _ t@(T_SimpleCommand id _ (cmd:rest)) = potentially $ do
+checkMaskedReturns _ t@(T_SimpleCommand id _ (cmd:rest)) = sequence_ $ do
     name <- getCommandName t
     guard $ name `elem` ["declare", "export"]
         || name == "local" && "r" `notElem` map snd (getAllFlags t)
@@ -2764,9 +2888,20 @@
 
 prop_checkReadWithoutR1 = verify checkReadWithoutR "read -a foo"
 prop_checkReadWithoutR2 = verifyNot checkReadWithoutR "read -ar foo"
-checkReadWithoutR _ t@T_SimpleCommand {} | t `isUnqualifiedCommand` "read" =
-    unless ("r" `elem` map snd (getAllFlags t)) $
+prop_checkReadWithoutR3 = verifyNot checkReadWithoutR "read -t 0"
+prop_checkReadWithoutR4 = verifyNot checkReadWithoutR "read -t 0 && read --d '' -r bar"
+prop_checkReadWithoutR5 = verifyNot checkReadWithoutR "read -t 0 foo < file.txt"
+prop_checkReadWithoutR6 = verifyNot checkReadWithoutR "read -u 3 -t 0"
+checkReadWithoutR _ t@T_SimpleCommand {} | t `isUnqualifiedCommand` "read"
+    && "r" `notElem` map snd flags && not has_t0 =
         info (getId $ getCommandTokenOrThis t) 2162 "read without -r will mangle backslashes."
+  where
+    flags = getAllFlags t
+    has_t0 = Just "0" == do
+        parsed <- getOpts flagsForRead flags
+        t <- lookup "t" parsed
+        getLiteralString t
+
 checkReadWithoutR _ _ = return ()
 
 prop_checkUncheckedCd1 = verifyTree checkUncheckedCdPushdPopd "cd ~/src; rm -r foo"
@@ -2806,15 +2941,15 @@
         []
     else execWriter $ doAnalysis checkElement root
   where
-    checkElement t@T_SimpleCommand {} = do
-        let name = getName t
-        when(name `elem` ["cd", "pushd", "popd"]
+    checkElement t@T_SimpleCommand {}
+        | name `elem` ["cd", "pushd", "popd"]
             && not (isSafeDir t)
             && not (name `elem` ["pushd", "popd"] && ("n" `elem` map snd (getAllFlags t)))
-            && not (isCondition $ getPath (parentMap params) t)) $
+            && not (isCondition $ getPath (parentMap params) t) =
                 warnWithFix (getId t) 2164
                     ("Use '" ++ name ++ " ... || exit' or '" ++ name ++ " ... || return' in case " ++ name ++ " fails.")
                     (fixWith [replaceEnd (getId t) params 0 " || exit"])
+        where name = getName t
     checkElement _ = return ()
     getName t = fromMaybe "" $ getCommandName t
     isSafeDir t = case oversimplify t of
@@ -2826,14 +2961,14 @@
 prop_checkLoopVariableReassignment2 = verify checkLoopVariableReassignment "for i in *; do for((i=0; i<3; i++)); do true; done; done"
 prop_checkLoopVariableReassignment3 = verifyNot checkLoopVariableReassignment "for i in *; do for j in *.bar; do true; done; done"
 checkLoopVariableReassignment params token =
-    potentially $ case token of
+    sequence_ $ case token of
         T_ForIn {} -> check
         T_ForArithmetic {} -> check
         _ -> Nothing
   where
     check = do
         str <- loopVariable token
-        next <- listToMaybe $ filter (\x -> loopVariable x == Just str) path
+        next <- find (\x -> loopVariable x == Just str) path
         return $ do
             warn (getId token) 2165 "This nested loop overrides the index variable of its parent."
             warn (getId next)  2167 "This parent loop has its index variable overridden."
@@ -2859,16 +2994,15 @@
         T_SimpleCommand _ _ tokens@(_:_) -> check (last tokens) token
         _ -> return ()
   where
-    check t command =
-        case t of
-            T_NormalWord id [T_Literal _ str] -> potentially $ do
-                guard $ str `elem` [ "]]", "]" ]
-                let opposite = invert str
-                    parameters = oversimplify command
-                guard $ opposite `notElem` parameters
-                return $ warn id 2171 $
-                    "Found trailing " ++ str ++ " outside test. Add missing " ++ opposite ++ " or quote if intentional."
-            _ -> return ()
+    check (T_NormalWord id [T_Literal _ str]) command
+        | str `elem` [ "]]", "]" ]
+        && opposite `notElem` parameters
+        = warn id 2171 $
+            "Found trailing " ++ str ++ " outside test. Add missing " ++ opposite ++ " or quote if intentional."
+        where
+            opposite = invert str
+            parameters = oversimplify command
+    check _ _ = return ()
     invert s =
         case s of
             "]]" -> "[["
@@ -2888,10 +3022,10 @@
     case token of
         TC_Binary id _ _ lhs rhs -> check lhs rhs
         TA_Binary id _ lhs rhs -> check lhs rhs
-        TA_Unary id _ exp ->
-            when (isExitCode exp) $ message (getId exp)
-        TA_Sequence _ [exp] ->
-            when (isExitCode exp) $ message (getId exp)
+        TA_Unary id _ exp
+            | isExitCode exp -> message (getId exp)
+        TA_Sequence _ [exp]
+            | isExitCode exp -> message (getId exp)
         _ -> return ()
   where
     check lhs rhs =
@@ -2915,12 +3049,12 @@
 prop_checkRedirectedNowhere8 = verifyNot checkRedirectedNowhere "var=`< file`"
 checkRedirectedNowhere params token =
     case token of
-        T_Pipeline _ _ [single] -> potentially $ do
+        T_Pipeline _ _ [single] -> sequence_ $ do
             redir <- getDanglingRedirect single
             guard . not $ isInExpansion token
             return $ warn (getId redir) 2188 "This redirection doesn't have a command. Move to its command (or use 'true' as no-op)."
 
-        T_Pipeline _ _ list -> forM_ list $ \x -> potentially $ do
+        T_Pipeline _ _ list -> forM_ list $ \x -> sequence_ $ do
             redir <- getDanglingRedirect x
             return $ err (getId redir) 2189 "You can't have | between this redirection and the command it should apply to."
 
@@ -2944,9 +3078,13 @@
 prop_checkArrayAssignmentIndices4 = verifyTree checkArrayAssignmentIndices "typeset -A foo; foo+=(bar)"
 prop_checkArrayAssignmentIndices5 = verifyTree checkArrayAssignmentIndices "arr=( [foo]= bar )"
 prop_checkArrayAssignmentIndices6 = verifyTree checkArrayAssignmentIndices "arr=( [foo] = bar )"
-prop_checkArrayAssignmentIndices7 = verifyTree checkArrayAssignmentIndices "arr=( var=value )"
+prop_checkArrayAssignmentIndices7 = verifyNotTree checkArrayAssignmentIndices "arr=( var=value )"
 prop_checkArrayAssignmentIndices8 = verifyNotTree checkArrayAssignmentIndices "arr=( [foo]=bar )"
 prop_checkArrayAssignmentIndices9 = verifyNotTree checkArrayAssignmentIndices "arr=( [foo]=\"\" )"
+prop_checkArrayAssignmentIndices10 = verifyTree checkArrayAssignmentIndices "declare -A arr; arr=( var=value )"
+prop_checkArrayAssignmentIndices11 = verifyTree checkArrayAssignmentIndices "arr=( 1=value )"
+prop_checkArrayAssignmentIndices12 = verifyTree checkArrayAssignmentIndices "arr=( $a=value )"
+prop_checkArrayAssignmentIndices13 = verifyTree checkArrayAssignmentIndices "arr=( $((1+1))=value )"
 checkArrayAssignmentIndices params root =
     runNodeAnalysis check params root
   where
@@ -2967,11 +3105,9 @@
 
             T_NormalWord _ parts ->
                 let literalEquals = do
-                    part <- parts
-                    (id, str) <- case part of
-                        T_Literal id str -> [(id,str)]
-                        _ -> []
-                    guard $ '=' `elem` str
+                    T_Literal id str <- parts
+                    let (before, after) = break ('=' ==) str
+                    guard $ all isDigit before && not (null after)
                     return $ warnWithFix id 2191 "The = here is literal. To assign by index, use ( [index]=value ) with no spaces. To keep as literal, quote it." (surroundWidth id params "\"")
                 in
                     if null literalEquals && isAssociative
@@ -2980,6 +3116,7 @@
 
             _ -> return ()
 
+
 prop_checkUnmatchableCases1 = verify checkUnmatchableCases "case foo in bar) true; esac"
 prop_checkUnmatchableCases2 = verify checkUnmatchableCases "case foo-$bar in ??|*) true; esac"
 prop_checkUnmatchableCases3 = verify checkUnmatchableCases "case foo in foo) true; esac"
@@ -3000,7 +3137,7 @@
             if isConstant word
                 then warn (getId word) 2194
                         "This word is constant. Did you forget the $ on a variable?"
-                else  potentially $ do
+                else  sequence_ $ do
                     pg <- wordToPseudoGlob word
                     return $ mapM_ (check pg) allpatterns
 
@@ -3015,19 +3152,18 @@
     fst3 (x,_,_) = x
     snd3 (_,x,_) = x
     tp = tokenPositions params
-    check target candidate = potentially $ do
+    check target candidate = sequence_ $ do
         candidateGlob <- wordToPseudoGlob candidate
         guard . not $ pseudoGlobsCanOverlap target candidateGlob
         return $ warn (getId candidate) 2195
                     "This pattern will never match the case statement's word. Double check them."
 
-    tupMap f l = zip l (map f l)
+    tupMap f l = map (\x -> (x, f x)) l
     checkDoms ((glob, Just x), rest) =
-        case filter (\(_, p) -> x `pseudoGlobIsSuperSetof` p) valids of
-            ((first,_):_) -> do
+        forM_ (find (\(_, p) -> x `pseudoGlobIsSuperSetof` p) valids) $
+            \(first,_) -> do
                 warn (getId glob) 2221 $ "This pattern always overrides a later one" <> patternContext (getId first)
                 warn (getId first) 2222 $ "This pattern never matches because of a previous pattern" <> patternContext (getId glob)
-            _ -> return ()
       where
         patternContext :: Id -> String
         patternContext id =
@@ -3035,9 +3171,7 @@
               Just l -> " on line " <> show l <> "."
               _      -> "."
 
-        valids = concatMap f rest
-        f (x, Just y) = [(x,y)]
-        f _ = []
+        valids = [(x,y) | (x, Just y) <- rest]
     checkDoms _ = return ()
 
 
@@ -3063,9 +3197,9 @@
 
 
     checkParams id first second = do
-        when (fromMaybe False $ (`elem` unaryTestOps) <$> getLiteralString first) $
+        when (maybe False (`elem` unaryTestOps) $ getLiteralString first) $
             err id 2204 "(..) is a subshell. Did you mean [ .. ], a test expression?"
-        when (fromMaybe False $ (`elem` binaryTestOps) <$> getLiteralString second) $
+        when (maybe False (`elem` binaryTestOps) $ getLiteralString second) $
             warn id 2205 "(..) is a subshell. Did you mean [ .. ], a test expression?"
 
 
@@ -3092,7 +3226,7 @@
         T_DollarBraced id _ str |
             not (isCountingReference part)
             && not (isQuotedAlternativeReference part)
-            && not (getBracedReference (bracedString part) `elem` variablesWithoutSpaces)
+            && getBracedReference (bracedString part) `notElem` variablesWithoutSpaces
             -> warn id 2206 $
                 if shellType params == Ksh
                 then "Quote to prevent word splitting/globbing, or split robustly with read -A or while read."
@@ -3111,7 +3245,7 @@
 prop_checkRedirectionToNumber3 = verifyNot checkRedirectionToNumber "echo foo > '2'"
 prop_checkRedirectionToNumber4 = verifyNot checkRedirectionToNumber "foo 1>&2"
 checkRedirectionToNumber _ t = case t of
-    T_IoFile id _ word -> potentially $ do
+    T_IoFile id _ word -> sequence_ $ do
         file <- getUnquotedLiteral word
         guard $ all isDigit file
         return $ warn id 2210 "This is a file redirection. Was it supposed to be a comparison or fd operation?"
@@ -3121,8 +3255,8 @@
 prop_checkGlobAsCommand2 = verify checkGlobAsCommand "$(var[i])"
 prop_checkGlobAsCommand3 = verifyNot checkGlobAsCommand "echo foo*"
 checkGlobAsCommand _ t = case t of
-    T_SimpleCommand _ _ (first:_) ->
-        when (isGlob first) $
+    T_SimpleCommand _ _ (first:_)
+        | isGlob first ->
             warn (getId first) 2211 "This is a glob used as a command name. Was it supposed to be in ${..}, array, or is it missing quoting?"
     _ -> return ()
 
@@ -3132,8 +3266,8 @@
 prop_checkFlagAsCommand3 = verifyNot checkFlagAsCommand "'--myexec--' args"
 prop_checkFlagAsCommand4 = verifyNot checkFlagAsCommand "var=cmd --arg"  -- Handled by SC2037
 checkFlagAsCommand _ t = case t of
-    T_SimpleCommand _ [] (first:_) ->
-        when (isUnquotedFlag first) $
+    T_SimpleCommand _ [] (first:_)
+        | isUnquotedFlag first ->
             warn (getId first) 2215 "This flag is used as a command name. Bad line break or missing [ .. ]?"
     _ -> return ()
 
@@ -3157,10 +3291,10 @@
 checkPipeToNowhere _ t =
     case t of
         T_Pipeline _ _ (first:rest) -> mapM_ checkPipe rest
-        T_Redirecting _ redirects cmd -> when (any redirectsStdin redirects) $ checkRedir cmd
+        T_Redirecting _ redirects cmd | any redirectsStdin redirects -> checkRedir cmd
         _ -> return ()
   where
-    checkPipe redir = potentially $ do
+    checkPipe redir = sequence_ $ do
         cmd <- getCommand redir
         name <- getCommandBasename cmd
         guard $ name `elem` nonReadingCommands
@@ -3173,7 +3307,7 @@
         return $ warn (getId cmd) 2216 $
             "Piping to '" ++ name ++ "', a command that doesn't read stdin. " ++ suggestion
 
-    checkRedir cmd = potentially $ do
+    checkRedir cmd = sequence_ $ do
         name <- getCommandBasename cmd
         guard $ name `elem` nonReadingCommands
         guard . not $ hasAdditionalConsumers cmd
@@ -3186,9 +3320,8 @@
             "Redirecting to '" ++ name ++ "', a command that doesn't read stdin. " ++ suggestion
 
     -- Could any words in a SimpleCommand consume stdin (e.g. echo "$(cat)")?
-    hasAdditionalConsumers t = fromMaybe True $ do
+    hasAdditionalConsumers t = isNothing $
         doAnalysis (guard . not . mayConsume) t
-        return False
 
     mayConsume t =
         case t of
@@ -3221,7 +3354,7 @@
                 mapM_ (checkUsage m) $ concatMap recursiveSequences cmds
         _ -> return ()
 
-    checkUsage map cmd = potentially $ do
+    checkUsage map cmd = sequence_ $ do
         name <- getCommandName cmd
         def <- Map.lookup name map
         return $
@@ -3338,8 +3471,8 @@
 prop_checkRedirectionToCommand3 = verifyNot checkRedirectionToCommand "ls > myfile"
 checkRedirectionToCommand _ t =
     case t of
-        T_IoFile _ _ (T_NormalWord id [T_Literal _ str]) | str `elem` commonCommands ->
-            unless (str == "file") $ -- This would be confusing
+        T_IoFile _ _ (T_NormalWord id [T_Literal _ str]) | str `elem` commonCommands
+            && str /= "file" -> -- This would be confusing
                 warn id 2238 "Redirecting to/from command name instead of file. Did you want pipes/xargs (or quote to ignore)?"
         _ -> return ()
 
@@ -3378,6 +3511,23 @@
   where
     fix id = fixWith [replaceStart id params 2 "\"$"]
 
+prop_checkTranslatedStringVariable1 = verify checkTranslatedStringVariable "foo_bar2=val; $\"foo_bar2\""
+prop_checkTranslatedStringVariable2 = verifyNot checkTranslatedStringVariable "$\"foo_bar2\""
+prop_checkTranslatedStringVariable3 = verifyNot checkTranslatedStringVariable "$\"..\""
+prop_checkTranslatedStringVariable4 = verifyNot checkTranslatedStringVariable "var=val; $\"$var\""
+prop_checkTranslatedStringVariable5 = verifyNot checkTranslatedStringVariable "foo=var; bar=val2; $\"foo bar\""
+checkTranslatedStringVariable params (T_DollarDoubleQuoted id [T_Literal _ s])
+  | all isVariableChar s
+  && Map.member s assignments
+  = warnWithFix id 2256 "This translated string is the name of a variable. Flip leading $ and \" if this should be a quoted substitution." (fix id)
+  where
+    assignments = foldl (flip ($)) Map.empty (map insertAssignment $ variableFlow params)
+    insertAssignment (Assignment (_, token, name, _)) | isVariableName name =
+        Map.insert name token
+    insertAssignment _ = Prelude.id
+    fix id = fixWith [replaceStart id params 2 "\"$"]
+checkTranslatedStringVariable _ _ = return ()
+
 prop_checkDefaultCase1 = verify checkDefaultCase "case $1 in a) true ;; esac"
 prop_checkDefaultCase2 = verify checkDefaultCase "case $1 in ?*?) true ;; *? ) true ;; esac"
 prop_checkDefaultCase3 = verifyNot checkDefaultCase "case $1 in x|*) true ;; esac"
@@ -3440,6 +3590,41 @@
             [_] -> []
             x:rest -> x : dropLast rest
             _ -> []
+
+prop_checkModifiedArithmeticInRedirection1 = verify checkModifiedArithmeticInRedirection "ls > $((i++))"
+prop_checkModifiedArithmeticInRedirection2 = verify checkModifiedArithmeticInRedirection "cat < \"foo$((i++)).txt\""
+prop_checkModifiedArithmeticInRedirection3 = verifyNot checkModifiedArithmeticInRedirection "while true; do true; done > $((i++))"
+prop_checkModifiedArithmeticInRedirection4 = verify checkModifiedArithmeticInRedirection "cat <<< $((i++))"
+prop_checkModifiedArithmeticInRedirection5 = verify checkModifiedArithmeticInRedirection "cat << foo\n$((i++))\nfoo\n"
+checkModifiedArithmeticInRedirection _ t =
+    case t of
+        T_Redirecting _ redirs (T_SimpleCommand _ _ (_:_)) -> mapM_ checkRedirs redirs
+        _ -> return ()
+  where
+    checkRedirs t =
+        case t of
+            T_FdRedirect _ _ (T_IoFile _ _ word) ->
+                mapM_ checkArithmetic $ getWordParts word
+            T_FdRedirect _ _ (T_HereString _ word) ->
+                mapM_ checkArithmetic $ getWordParts word
+            T_FdRedirect _ _ (T_HereDoc _ _ _ _ list) ->
+                mapM_ checkArithmetic list
+            _ -> return ()
+    checkArithmetic t =
+        case t of
+            T_DollarArithmetic _ x -> checkModifying x
+            _ -> return ()
+    checkModifying t =
+        case t of
+            TA_Sequence _ list -> mapM_ checkModifying list
+            TA_Unary id s _ | s `elem` ["|++", "++|", "|--", "--|"] -> warnFor id
+            TA_Assignment id _ _ _ -> warnFor id
+            TA_Binary _ _ x y -> mapM_ checkModifying [x ,y]
+            TA_Trinary _ x y z -> mapM_ checkModifying [x, y, z]
+            _ -> return ()
+    warnFor id =
+        warn id 2257 "Arithmetic modifications in command redirections may be discarded. Do them separately."
+
 
 return []
 runTests =  $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
diff --git a/src/ShellCheck/Analyzer.hs b/src/ShellCheck/Analyzer.hs
--- a/src/ShellCheck/Analyzer.hs
+++ b/src/ShellCheck/Analyzer.hs
@@ -35,17 +35,18 @@
     arComments =
         filterByAnnotation spec params . nub $
             runAnalytics spec
-            ++ runChecker params (checkers params)
+            ++ runChecker params (checkers spec params)
 }
   where
     params = makeParameters spec
 
-checkers params = mconcat $ map ($ params) [
-    ShellCheck.Checks.Commands.checker,
+checkers spec params = mconcat $ map ($ params) [
+    ShellCheck.Checks.Commands.checker spec,
     ShellCheck.Checks.Custom.checker,
     ShellCheck.Checks.ShellSupport.checker
     ]
 
 optionalChecks = mconcat $ [
-    ShellCheck.Analytics.optionalChecks
+    ShellCheck.Analytics.optionalChecks,
+    ShellCheck.Checks.Commands.optionalChecks
     ]
diff --git a/src/ShellCheck/AnalyzerLib.hs b/src/ShellCheck/AnalyzerLib.hs
--- a/src/ShellCheck/AnalyzerLib.hs
+++ b/src/ShellCheck/AnalyzerLib.hs
@@ -239,19 +239,14 @@
 
 determineShellTest = determineShellTest' Nothing
 determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
-determineShell fallbackShell t = fromMaybe Bash $ do
-    shellString <- foldl mplus Nothing $ getCandidates t
+determineShell fallbackShell t = fromMaybe Bash $
     shellForExecutable shellString `mplus` fallbackShell
   where
-    forAnnotation t =
-        case t of
-            (ShellOverride s) -> return s
-            _                 -> fail ""
-    getCandidates :: Token -> [Maybe String]
-    getCandidates t@T_Script {} = [Just $ fromShebang t]
-    getCandidates (T_Annotation _ annotations s) =
-        map forAnnotation annotations ++
-           [Just $ fromShebang s]
+    shellString = getCandidate t
+    getCandidate :: Token -> String
+    getCandidate t@T_Script {} = fromShebang t
+    getCandidate (T_Annotation _ annotations s) =
+        headOrDefault (fromShebang s) [s | ShellOverride s <- annotations]
     fromShebang (T_Script _ (T_Literal _ s) _) = executableFromShebang s
 
 -- Given a string like "/bin/bash" or "/usr/bin/env dash",
@@ -259,7 +254,7 @@
 executableFromShebang :: String -> String
 executableFromShebang = shellFor
   where
-    shellFor s | "/env " `isInfixOf` s = head (drop 1 (words s)++[""])
+    shellFor s | "/env " `isInfixOf` s = headOrDefault "" (drop 1 $ words s)
     shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
     shellFor s = reverse . takeWhile (/= '/') . reverse $ s
 
@@ -299,7 +294,7 @@
 
 isQuoteFreeNode strict tree t =
     (isQuoteFreeElement t == Just True) ||
-        head (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t) ++ [False])
+        headOrDefault False (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t))
   where
     -- Is this node self-quoting in itself?
     isQuoteFreeElement t =
@@ -454,7 +449,7 @@
         T_BatsTest {} -> SubshellScope "@bats test"
         T_CoProcBody _ _  -> SubshellScope "coproc"
         T_Redirecting {}  ->
-            if fromMaybe False causesSubshell
+            if causesSubshell == Just True
             then SubshellScope "pipeline"
             else NoneScope
         _ -> NoneScope
@@ -513,7 +508,7 @@
         T_DollarBraced _ _ l -> maybeToList $ do
             let string = bracedString t
             let modifier = getBracedModifier string
-            guard $ ":=" `isPrefixOf` modifier
+            guard $ any (`isPrefixOf` modifier) ["=", ":="]
             return (t, t, getBracedReference string, DataString $ SourceFrom [l])
 
         t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo
@@ -548,8 +543,9 @@
             else []
         "trap" ->
             case rest of
-                head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head
+                head:_ -> map (\x -> (base, head, x)) $ getVariablesFromLiteralToken head
                 _ -> []
+        "alias" -> [(base, token, name) | token <- rest, name <- getVariablesFromLiteralToken token]
         _ -> []
   where
     getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]
@@ -567,9 +563,11 @@
 --   VariableName :: String,   -- The variable name, i.e. foo
 --   VariableValue :: DataType -- A description of the value being assigned, i.e. "Literal string with value foo"
 -- )
-getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
+getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T_Literal _ x:_):rest)) =
    filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
     case x of
+        "builtin" ->
+            getModifiedVariableCommand $ T_SimpleCommand id cmdPrefix rest
         "read" ->
             let params = map getLiteral rest
                 readArrayVars = getReadArrayVariables rest
@@ -610,8 +608,7 @@
         _ -> []
   where
     flags = map snd $ getAllFlags base
-    stripEquals s = let rest = dropWhile (/= '=') s in
-        if rest == "" then "" else tail rest
+    stripEquals s = drop 1 $ dropWhile (/= '=') s
     stripEqualsFrom (T_NormalWord id1 (T_Literal id2 s:rs)) =
         T_NormalWord id1 (T_Literal id2 (stripEquals s):rs)
     stripEqualsFrom (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 s]]) =
@@ -642,7 +639,7 @@
     getModifierParam _ _ = []
 
     letParamToLiteral token =
-          if var == ""
+          if null var
             then []
             else [(base, token, var, DataString $ SourceFrom [stripEqualsFrom token])]
         where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ oversimplify token
@@ -761,9 +758,8 @@
         _          -> Nothing
 
     getIfReference context token = maybeToList $ do
-            str <- getLiteralStringExt literalizer token
-            guard . not $ null str
-            when (isDigit $ head str) $ fail "is a number"
+            str@(h:_) <- getLiteralStringExt literalizer token
+            when (isDigit h) $ fail "is a number"
             return (context, token, getBracedReference str)
 
     isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
@@ -783,8 +779,8 @@
 -- Compare a command to a literal. Like above, but checks full path.
 isUnqualifiedCommand token str = isCommandMatch token (== str)
 
-isCommandMatch token matcher = fromMaybe False $
-    fmap matcher (getCommandName token)
+isCommandMatch token matcher = maybe False
+    matcher (getCommandName token)
 
 -- Does this regex look like it was intended as a glob?
 -- True:  *foo*
@@ -805,7 +801,7 @@
 isVariableName _     = False
 
 getVariablesFromLiteralToken token =
-    getVariablesFromLiteral (fromJust $ getLiteralStringExt (const $ return " ") token)
+    getVariablesFromLiteral (getLiteralStringDef " " token)
 
 -- Try to get referenced variables from a literal string like "$foo"
 -- Ignores tons of cases like arithmetic evaluation and array indices.
@@ -855,7 +851,7 @@
 prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
 prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
 prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
-getBracedModifier s = fromMaybe "" . listToMaybe $ do
+getBracedModifier s = headOrDefault "" $ do
     let var = getBracedReference s
     a <- dropModifier s
     dropPrefix var a
@@ -869,15 +865,6 @@
 
 -- Useful generic functions.
 
--- Run an action in a Maybe (or do nothing).
--- Example:
--- potentially $ do
---   s <- getLiteralString cmd
---   guard $ s `elem` ["--recursive", "-r"]
---   return $ warn .. "Something something recursive"
-potentially :: Monad m => Maybe (m ()) -> m ()
-potentially = fromMaybe (return ())
-
 -- Get element 0 or a default. Like `head` but safe.
 headOrDefault _ (a:_) = a
 headOrDefault def _   = def
@@ -932,12 +919,11 @@
 --     Just [("r", -re), ("e", -re), ("d", :), ("u", 3), ("", bar)]
 -- where flags with arguments map to arguments, while others map to themselves.
 -- Any unrecognized flag will result in Nothing.
-getGnuOpts = getOpts getAllFlags
-getBsdOpts = getOpts getLeadingFlags
-getOpts :: (Token -> [(Token, String)]) -> String -> Token -> Maybe [(String, Token)]
-getOpts flagTokenizer string cmd = process flags
+getGnuOpts str t = getOpts str $ getAllFlags t
+getBsdOpts str t = getOpts str $ getLeadingFlags t
+getOpts :: String -> [(Token, String)] -> Maybe [(String, Token)]
+getOpts string flags = process flags
   where
-    flags = flagTokenizer cmd
     flagList (c:':':rest) = ([c], True) : flagList rest
     flagList (c:rest)     = ([c], False) : flagList rest
     flagList []           = []
@@ -952,7 +938,7 @@
         takesArg <- Map.lookup flag1 flagMap
         if takesArg
             then do
-                guard $ flag2 == ""
+                guard $ null flag2
                 more <- process rest
                 return $ (flag1, token2) : more
             else do
diff --git a/src/ShellCheck/Checker.hs b/src/ShellCheck/Checker.hs
--- a/src/ShellCheck/Checker.hs
+++ b/src/ShellCheck/Checker.hs
@@ -48,7 +48,7 @@
   where
     fail = error "Internal shellcheck error: id doesn't exist. Please report!"
 
-shellFromFilename filename = foldl mplus Nothing candidates
+shellFromFilename filename = listToMaybe candidates
   where
     shellExtensions = [(".ksh", Ksh)
                       ,(".bash", Bash)
@@ -57,7 +57,7 @@
                       -- The `.sh` is too generic to determine the shell:
                       -- We fallback to Bash in this case and emit SC2148 if there is no shebang
     candidates =
-        map (\(ext,sh) -> if ext `isSuffixOf` filename then Just sh else Nothing) shellExtensions
+        [sh | (ext,sh) <- shellExtensions, ext `isSuffixOf` filename]
 
 checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
 checkScript sys spec = do
@@ -88,9 +88,9 @@
                     asOptionalChecks = csOptionalChecks spec
                 } where as = newAnalysisSpec root
         let analysisMessages =
-                fromMaybe [] $
+                maybe []
                     (arComments . analyzeScript . analysisSpec)
-                        <$> prRoot result
+                        $ prRoot result
         let translator = tokenToPosition tokenPositions
         return . nub . sortMessages . filter shouldInclude $
             (parseMessages ++ map translator analysisMessages)
@@ -104,7 +104,7 @@
             code     = cCode (pcComment pc)
             severity = cSeverity (pcComment pc)
 
-    sortMessages = sortBy (comparing order)
+    sortMessages = sortOn order
     order pc =
         let pos = pcStartPos pc
             comment = pcComment pc in
@@ -198,11 +198,11 @@
                 }
 
 prop_annotationDisablesBadShebang =
-    [] == check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"
+    null $ check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"
 
 
 prop_canParseDevNull =
-    [] == check "source /dev/null"
+    null $ check "source /dev/null"
 
 prop_failsWhenNotSourcing =
     [1091, 2154] == check "source lol; echo \"$bar\""
@@ -218,7 +218,7 @@
 
 -- FIXME: This should really be giving [1093], "recursively sourced"
 prop_noInfiniteSourcing =
-    [] == checkWithIncludes  [("lib", "source lib")] "source lib"
+    null $ checkWithIncludes  [("lib", "source lib")] "source lib"
 
 prop_canSourceBadSyntax =
     [1094, 2086] == checkWithIncludes [("lib", "for f; do")] "source lib; echo $1"
@@ -239,10 +239,10 @@
     [1037] == checkRecursive [("lib", "echo \"$10\"")] "source lib"
 
 prop_nonRecursiveAnalysis =
-    [] == checkWithIncludes [("lib", "echo $1")] "source lib"
+    null $ checkWithIncludes [("lib", "echo $1")] "source lib"
 
 prop_nonRecursiveParsing =
-    [] == checkWithIncludes [("lib", "echo \"$10\"")] "source lib"
+    null $ checkWithIncludes [("lib", "echo \"$10\"")] "source lib"
 
 prop_sourceDirectiveDoesntFollowFile =
     null $ checkWithIncludes
@@ -288,6 +288,13 @@
         csScript = "(( 3.14 ))"
     }
 
+prop_canDisableShebangWarning = null $ result
+  where
+    result = checkWithSpec [] emptyCheckSpec {
+        csFilename = "file.sh",
+        csScript = "#shellcheck disable=SC2148\nfoo"
+    }
+
 prop_shExtensionDoesntMatter = result == [2148]
   where
     result = checkWithSpec [] emptyCheckSpec {
@@ -328,7 +335,7 @@
     [2154] == checkOptionIncludes (Just [2154]) "#!/bin/sh\n var='a b'\n echo $var\n echo $bar"
 
 
-prop_readsRcFile = result == []
+prop_readsRcFile = null result
   where
     result = checkWithRc "disable=2086" emptyCheckSpec {
         csScript = "#!/bin/sh\necho $1",
diff --git a/src/ShellCheck/Checks/Commands.hs b/src/ShellCheck/Checks/Commands.hs
--- a/src/ShellCheck/Checks/Commands.hs
+++ b/src/ShellCheck/Checks/Commands.hs
@@ -21,7 +21,7 @@
 {-# LANGUAGE FlexibleContexts #-}
 
 -- This module contains checks that examine specific commands by name.
-module ShellCheck.Checks.Commands (checker , ShellCheck.Checks.Commands.runTests) where
+module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where
 
 import ShellCheck.AST
 import ShellCheck.ASTLib
@@ -34,6 +34,7 @@
 import Control.Monad
 import Control.Monad.RWS
 import Data.Char
+import Data.Functor.Identity
 import Data.List
 import Data.Maybe
 import qualified Data.Map.Strict as Map
@@ -90,13 +91,30 @@
     ,checkMvArguments, checkCpArguments, checkLnArguments
     ,checkFindRedirections
     ,checkReadExpansions
-    ,checkWhich
     ,checkSudoRedirect
     ,checkSudoArgs
     ,checkSourceArgs
     ,checkChmodDashr
     ]
 
+optionalChecks = map fst optionalCommandChecks
+optionalCommandChecks :: [(CheckDescription, CommandCheck)]
+optionalCommandChecks = [
+    (newCheckDescription {
+        cdName = "deprecate-which",
+        cdDescription = "Suggest 'command -v' instead of 'which'",
+        cdPositive = "which javac",
+        cdNegative = "command -v javac"
+    }, checkWhich)
+    ]
+optionalCheckMap = Map.fromList $ map (\(desc, check) -> (cdName desc, check)) optionalCommandChecks
+
+prop_verifyOptionalExamples = all check optionalCommandChecks
+  where
+    check (desc, check) =
+      verify check (cdPositive desc)
+      && verifyNot check (cdNegative desc)
+
 buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
 buildCommandMap = foldl' addCheck Map.empty
   where
@@ -105,12 +123,16 @@
 
 
 checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis
-checkCommand map t@(T_SimpleCommand id _ (cmd:rest)) = fromMaybe (return ()) $ do
+checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do
     name <- getLiteralString cmd
     return $
         if '/' `elem` name
         then
             Map.findWithDefault nullCheck (Basename $ basename name) map t
+        else if name == "builtin" && not (null rest) then
+            let t' = T_SimpleCommand id cmdPrefix rest
+                selectedBuiltin = fromMaybe "" $ getLiteralString . head $ rest
+            in Map.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
         else do
             Map.findWithDefault nullCheck (Exactly name) map t
             Map.findWithDefault nullCheck (Basename name) map t
@@ -128,8 +150,14 @@
     map = buildCommandMap list
 
 
-checker :: Parameters -> Checker
-checker params = getChecker commandChecks
+checker :: AnalysisSpec -> Parameters -> Checker
+checker spec params = getChecker $ commandChecks ++ optionals
+  where
+    keys = asOptionalChecks spec
+    optionals =
+        if "all" `elem` keys
+        then map snd optionalCommandChecks
+        else mapMaybe (\x -> Map.lookup x optionalCheckMap) keys
 
 prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
 prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
@@ -169,15 +197,14 @@
 prop_checkFindNameGlob2 = verify checkFindNameGlob "find / -type f -ipath *(foo)"
 prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'"
 checkFindNameGlob = CommandCheck (Basename "find") (f . arguments)  where
-    acceptsGlob (Just s) = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
-    acceptsGlob _ = False
+    acceptsGlob s = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
     f [] = return ()
-    f [x] = return ()
-    f (a:b:r) = do
-        when (acceptsGlob (getLiteralString a) && isGlob b) $ do
-            let (Just s) = getLiteralString a
+    f (x:xs) = g x xs
+    g _ [] = return ()
+    g a (b:r) = do
+        forM_ (getLiteralString a) $ \s -> when (acceptsGlob s && isGlob b) $
             warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it."
-        f (b:r)
+        g b r
 
 
 prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)"
@@ -221,13 +248,12 @@
 checkGrepRe = CommandCheck (Basename "grep") check where
     check cmd = f cmd (arguments cmd)
     -- --regex=*(extglob) doesn't work. Fixme?
-    skippable (Just s) = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s
-    skippable _ = False
+    skippable s = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s
     f _ [] = return ()
     f cmd (x:r) =
-        let str = getLiteralStringExt (const $ return "_") x
+        let str = getLiteralStringDef "_" x
         in
-            if str `elem` [Just "--", Just "-e", Just "--regex"]
+            if str `elem` ["--", "-e", "--regex"]
             then checkRE cmd r -- Regex is *after* this
             else
                 if skippable str
@@ -243,7 +269,7 @@
             let string = concat $ oversimplify re
             if isConfusedGlobRegex string then
                 warn (getId re) 2063 "Grep uses regex, but this looks like a glob."
-              else potentially $ do
+              else sequence_ $ do
                 char <- getSuspiciousRegexWildcard string
                 return $ info (getId re) 2022 $
                     "Note that unlike globs, " ++ [char] ++ "* here matches '" ++ [char, char, char] ++ "' but not '" ++ wordStartingWith char ++ "'."
@@ -252,10 +278,10 @@
         grepGlobFlags = ["fixed-strings", "F", "include", "exclude", "exclude-dir", "o", "only-matching"]
 
     wordStartingWith c =
-        head . filter ([c] `isPrefixOf`) $ candidates
+        headOrDefault (c:"test") . filter ([c] `isPrefixOf`) $ candidates
       where
         candidates =
-            sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords ++ [c:"test"]
+            sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords
 
     getSuspiciousRegexWildcard str =
         if not $ str `matches` contra
@@ -318,10 +344,10 @@
             invalid (getId value)
     f _ = return ()
 
-    isInvalid s = s == "" || any (not . isDigit) s || length s > 5
+    isInvalid s = null s || any (not . isDigit) s || length s > 5
         || let value = (read s :: Integer) in value > 255
 
-    literal token = fromJust $ getLiteralStringExt lit token
+    literal token = runIdentity $ getLiteralStringExt lit token
     lit (T_DollarBraced {}) = return "0"
     lit (T_DollarArithmetic {}) = return "0"
     lit (T_DollarExpansion {}) = return "0"
@@ -338,7 +364,7 @@
     check (exec:arg:term:_) = do
         execS <- getLiteralString exec
         termS <- getLiteralString term
-        cmdS <- getLiteralStringExt (const $ return " ") arg
+        let cmdS = getLiteralStringDef " " arg
 
         guard $ execS `elem` ["-exec", "-execdir"] && termS `elem` [";", "+"]
         guard $ cmdS `matches` commandRegex
@@ -434,7 +460,7 @@
 prop_checkMkdirDashPM21 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../../bin"
 checkMkdirDashPM = CommandCheck (Basename "mkdir") check
   where
-    check t = potentially $ do
+    check t = sequence_ $ do
         let flags = getAllFlags t
         dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags
         dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags
@@ -460,7 +486,7 @@
         first:rest -> unless (isFlag first) $ mapM_ check rest
         _ -> return ()
 
-    check param = potentially $ do
+    check param = sequence_ $ do
         str <- getLiteralString param
         let id = getId param
         return $ sequence_ $ mapMaybe (\f -> f id str) [
@@ -548,7 +574,7 @@
     f _ = return ()
 
     check format more = do
-        fromMaybe (return ()) $ do
+        sequence_ $ do
             string <- getLiteralString format
             let formats = getPrintfFormats string
             let formatCount = length formats
@@ -573,7 +599,7 @@
 
         unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
           info (getId format) 2059
-              "Don't use variables in the printf format string. Use printf \"..%s..\" \"$foo\"."
+              "Don't use variables in the printf format string. Use printf '..%s..' \"$foo\"."
       where
         onlyTrailingTs format argCount =
             all (== 'T') $ drop argCount format
@@ -660,7 +686,7 @@
 prop_checkExportedExpansions4 = verifyNot checkExportedExpansions "export ${foo?}"
 checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments)
   where
-    check t = potentially $ do
+    check t = sequence_ $ do
         var <- getSingleUnmodifiedVariable t
         let name = bracedString var
         return . warn (getId t) 2163 $
@@ -676,13 +702,13 @@
 prop_checkReadExpansions8 = verifyNot checkReadExpansions "read ${var?}"
 checkReadExpansions = CommandCheck (Exactly "read") check
   where
-    options = getGnuOpts "sreu:n:N:i:p:a:"
+    options = getGnuOpts flagsForRead
     getVars cmd = fromMaybe [] $ do
         opts <- options cmd
-        return . map snd $ filter (\(x,_) -> x == "" || x == "a") opts
+        return [y | (x,y) <- opts, null x || x == "a"]
 
     check cmd = mapM_ warning $ getVars cmd
-    warning t = potentially $ do
+    warning t = sequence_ $ do
         var <- getSingleUnmodifiedVariable t
         let name = bracedString var
         guard $ isVariableName name   -- e.g. not $1
@@ -708,7 +734,7 @@
     re = mkRegex "\\$\\{?[0-9*@]"
     f = mapM_ checkArg
     checkArg arg =
-        let string = fromJust $ getLiteralStringExt (const $ return "_") arg in
+        let string = getLiteralStringDef "_" arg in
             when ('=' `elem` string && string `matches` re) $
                 err (getId arg) 2142
                     "Aliases can't use positional parameters. Use a function."
@@ -721,7 +747,7 @@
   where
     f = mapM_ checkArg
     checkArg arg | '=' `elem` concat (oversimplify arg) =
-        forM_ (take 1 $ filter (not . isLiteral) $ getWordParts arg) $
+        forM_ (find (not . isLiteral) $ getWordParts arg) $
             \x -> warn (getId x) 2139 "This expands when defined, not when used. Consider escaping."
     checkArg _ = return ()
 
@@ -754,7 +780,7 @@
     -- path. We assume that all the pre-path flags are single characters from a
     -- list of GNU and macOS flags.
     hasPath (first:rest) =
-        let flag = fromJust $ getLiteralStringExt (const $ return "___") first in
+        let flag = getLiteralStringDef "___" first in
             not ("-" `isPrefixOf` flag) || isLeadingFlag flag && hasPath rest
     hasPath [] = False
     isLeadingFlag flag = length flag <= 2 || all (`elem` leadingFlagChars) flag
@@ -832,7 +858,7 @@
     f :: Token -> Analysis
     f t@(T_SimpleCommand _ _ (cmd:arg1:_))  = do
         path <- getPathM t
-        potentially $ do
+        sequence_ $ do
             options <- getLiteralString arg1
             (T_WhileExpression _ _ body) <- findFirst whileLoop path
             caseCmd <- mapMaybe findCase body !!! 0
@@ -859,7 +885,7 @@
     warnUnhandled optId caseId str =
         warn caseId 2213 $ "getopts specified -" ++ str ++ ", but it's not handled by this 'case'."
 
-    warnRedundant (key, expr) = potentially $ do
+    warnRedundant (key, expr) = sequence_ $ do
         str <- key
         guard $ str `notElem` ["*", ":", "?"]
         return $ warn (getId expr) 2214 "This case is not specified by getopts."
@@ -918,7 +944,7 @@
             Nothing ->
                 checkWord' token
 
-    checkWord' token = fromMaybe (return ()) $ do
+    checkWord' token = sequence_ $ do
         filename <- getPotentialPath token
         let path = fixPath filename
         return . when (path `elem` importantPaths) $
@@ -968,10 +994,9 @@
         _ -> return ()
   where
     args = getAllFlags token
-    params = map fst $ filter (\(_,x) -> x == "") args
+    params = [x | (x,"") <- args]
     hasTarget =
-        any (\x -> x /= "" && x `isPrefixOf` "target-directory") $
-            map snd args
+        any (\(_,x) -> x /= "" && x `isPrefixOf` "target-directory") args
 
 prop_checkMvArguments1 = verify    checkMvArguments "mv 'foo bar'"
 prop_checkMvArguments2 = verifyNot checkMvArguments "mv foo bar"
@@ -1031,7 +1056,7 @@
             Just (T_Redirecting _ redirs _) ->
                 mapM_ warnAbout redirs
     warnAbout (T_FdRedirect _ s (T_IoFile id op file))
-        | (s == "" || s == "&") && not (special file) =
+        | (null s || s == "&") && not (special file) =
         case op of
             T_Less _ ->
               info (getId op) 2024
@@ -1055,9 +1080,9 @@
 prop_checkSudoArgs7 = verifyNot checkSudoArgs "sudo docker export foo"
 checkSudoArgs = CommandCheck (Basename "sudo") f
   where
-    f t = potentially $ do
+    f t = sequence_ $ do
         opts <- parseOpts t
-        let nonFlags = map snd $ filter (\(flag, _) -> flag == "") opts
+        let nonFlags = [x | ("",x) <- opts]
         commandArg <- nonFlags !!! 0
         command <- getLiteralString commandArg
         guard $ command `elem` builtins
@@ -1083,7 +1108,7 @@
 checkChmodDashr = CommandCheck (Basename "chmod") f
   where
     f t = mapM_ check $ arguments t
-    check t = potentially $ do
+    check t = sequence_ $ do
         flag <- getLiteralString t
         guard $ flag == "-r"
         return $ warn (getId t) 2253 "Use -R to recurse, or explicitly a-r to remove read permissions."
diff --git a/src/ShellCheck/Checks/ShellSupport.hs b/src/ShellCheck/Checks/ShellSupport.hs
--- a/src/ShellCheck/Checks/ShellSupport.hs
+++ b/src/ShellCheck/Checks/ShellSupport.hs
@@ -30,6 +30,7 @@
 import Control.Monad
 import Control.Monad.RWS
 import Data.Char
+import Data.Functor.Identity
 import Data.List
 import Data.Maybe
 import qualified Data.Map as Map
@@ -73,7 +74,7 @@
 prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
 checkForDecimals = ForShell [Sh, Dash, Bash] f
   where
-    f t@(TA_Expansion id _) = potentially $ do
+    f t@(TA_Expansion id _) = sequence_ $ do
         str <- getLiteralString t
         first <- str !!! 0
         guard $ isDigit first && '.' `elem` str
@@ -205,7 +206,7 @@
         | op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
             unless isDash $ warnMsg id $ "lexicographical " ++ op ++ " is"
     bashism (TC_Binary id SingleBracket op _ _)
-        | op `elem` [ "-nt", "-ef" ] =
+        | op `elem` [ "-ot", "-nt", "-ef" ] =
             unless isDash $ warnMsg id $ op ++ " is"
     bashism (TC_Binary id SingleBracket "==" _ _) =
             warnMsg id "== in place of = is"
@@ -337,17 +338,17 @@
         in do
             when (name `elem` unsupportedCommands) $
                 warnMsg id $ "'" ++ name ++ "' is"
-            potentially $ do
+            sequence_ $ do
                 allowed' <- Map.lookup name allowedFlags
                 allowed <- allowed'
-                (word, flag) <- listToMaybe $
-                    filter (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
+                (word, flag) <- find
+                    (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
                 return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is"
 
             when (name == "source") $ warnMsg id "'source' in place of '.' is"
             when (name == "trap") $
                 let
-                    check token = potentially $ do
+                    check token = sequence_ $ do
                         str <- getLiteralString token
                         let upper = map toUpper str
                         return $ do
@@ -362,7 +363,7 @@
                 in
                     mapM_ check (drop 1 rest)
 
-            when (name == "printf") $ potentially $ do
+            when (name == "printf") $ sequence_ $ do
                 format <- rest !!! 0  -- flags are covered by allowedFlags
                 let literal = onlyLiteralString format
                 guard $ "%q" `isInfixOf` literal
@@ -456,11 +457,10 @@
 
     -- This should have used backreferences, but TDFA doesn't support them
     sedRe = mkRegex "^s(.)([^\n]*)g?$"
-    isSimpleSed s = fromMaybe False $ do
-        [first,rest] <- matchRegex sedRe s
-        let delimiters = filter (== head first) rest
+    isSimpleSed s = isJust $ do
+        [h:_,rest] <- matchRegex sedRe s
+        let delimiters = filter (== h) rest
         guard $ length delimiters == 2
-        return True
     checkIn id s =
         when (isSimpleSed s) $
             style id 2001 "See if you can use ${variable//search/replace} instead."
@@ -487,11 +487,11 @@
             T_DollarBraced {} -> return "$"
             T_DollarExpansion {} -> return "$"
             T_DollarArithmetic {} -> return "$"
-            otherwise -> return "-"
-    toString t = fromJust $ getLiteralStringExt literalExt t
+            _ -> return "-"
+    toString t = runIdentity $ getLiteralStringExt literalExt t
     isEvaled t = do
         cmd <- getClosestCommandM t
-        return $ isJust cmd && fromJust cmd `isUnqualifiedCommand` "eval"
+        return $ maybe False (`isUnqualifiedCommand` "eval") cmd
 
 
 prop_checkMultiDimensionalArrays1 = verify checkMultiDimensionalArrays "foo[a][b]=3"
diff --git a/src/ShellCheck/Data.hs b/src/ShellCheck/Data.hs
--- a/src/ShellCheck/Data.hs
+++ b/src/ShellCheck/Data.hs
@@ -114,6 +114,10 @@
     "-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>="
   ]
 
+arithmeticBinaryTestOps = [
+    "-eq", "-ne", "-lt", "-le", "-gt", "-ge"
+  ]
+
 unaryTestOps = [
     "!", "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p",
     "-r", "-s", "-S", "-t", "-u", "-w", "-x", "-O", "-G", "-N", "-z", "-n",
@@ -131,4 +135,6 @@
         "ksh"   -> return Ksh
         "ksh88" -> return Ksh
         "ksh93" -> return Ksh
-        otherwise -> Nothing
+        _ -> Nothing
+
+flagsForRead = "sreu:n:N:i:p:a:t:"
diff --git a/src/ShellCheck/Fixer.hs b/src/ShellCheck/Fixer.hs
--- a/src/ShellCheck/Fixer.hs
+++ b/src/ShellCheck/Fixer.hs
@@ -200,7 +200,7 @@
     let si = fromIntegral (start-1)
         ei = fromIntegral (end-1)
         (x, xs) = splitAt si o
-        (y, z) = splitAt (ei - si) xs
+        z = drop (ei - si) xs
     in
     x ++ r ++ z
 
@@ -295,7 +295,7 @@
     -- Trivial O(n * m) implementation
     dumbPrefixSums :: [(Int, Int)] -> [Int] -> [Int]
     dumbPrefixSums kvs targets =
-        let prefixSum target = sum . map snd . filter (\(k,v) -> k <= target) $ kvs
+        let prefixSum target = sum [v | (k,v) <- kvs, k <= target]
         in map prefixSum targets
     -- PSTree O(n * log m) implementation
     smartPrefixSums :: [(Int, Int)] -> [Int] -> [Int]
diff --git a/src/ShellCheck/Formatter/Diff.hs b/src/ShellCheck/Formatter/Diff.hs
--- a/src/ShellCheck/Formatter/Diff.hs
+++ b/src/ShellCheck/Formatter/Diff.hs
@@ -43,14 +43,15 @@
 
 format :: FormatterOptions -> IO Formatter
 format options = do
-    didOutput <- newIORef False
+    foundIssues <- newIORef False
+    reportedIssues <- newIORef False
     shouldColor <- shouldOutputColor (foColorOption options)
     let color = if shouldColor then colorize else nocolor
     return Formatter {
         header = return (),
-        footer = checkFooter didOutput color,
+        footer = checkFooter foundIssues reportedIssues color,
         onFailure = reportFailure color,
-        onResult  = reportResult didOutput color
+        onResult  = reportResult foundIssues reportedIssues color
     }
 
 
@@ -69,9 +70,10 @@
 printErr color = hPutStrLn stderr . color bold . color red
 reportFailure color file msg = printErr color $ file ++ ": " ++ msg
 
-checkFooter didOutput color = do
-    output <- readIORef didOutput
-    unless output $
+checkFooter foundIssues reportedIssues color = do
+    found <- readIORef foundIssues
+    output <- readIORef reportedIssues
+    when (found && not output) $
             printErr color "Issues were detected, but none were auto-fixable. Use another format to see them."
 
 type ColorFunc = (Int -> String -> String)
@@ -79,9 +81,10 @@
 data DiffDoc a = DiffDoc String LFStatus [DiffRegion a]
 data DiffRegion a = DiffRegion (Int, Int) (Int, Int) [Diff a]
 
-reportResult :: (IORef Bool) -> ColorFunc -> CheckResult -> SystemInterface IO -> IO ()
-reportResult didOutput color result sys = do
+reportResult :: (IORef Bool) -> (IORef Bool) -> ColorFunc -> CheckResult -> SystemInterface IO -> IO ()
+reportResult foundIssues reportedIssues color result sys = do
     let comments = crComments result
+    unless (null comments) $ writeIORef foundIssues True
     let suggestedFixes = mapMaybe pcFix comments
     let fixmap = buildFixMap suggestedFixes
     mapM_ output $ M.toList fixmap
@@ -91,7 +94,7 @@
         case file of
             Right contents -> do
                 putStrLn $ formatDoc color $ makeDiff name contents fix
-                writeIORef didOutput True
+                writeIORef reportedIssues True
             Left msg -> reportFailure color name msg
 
 hasTrailingLinefeed str =
diff --git a/src/ShellCheck/Interface.hs b/src/ShellCheck/Interface.hs
--- a/src/ShellCheck/Interface.hs
+++ b/src/ShellCheck/Interface.hs
@@ -256,9 +256,6 @@
 data InsertionPoint = InsertBefore | InsertAfter
     deriving (Show, Eq, Generic, NFData)
 
-instance Ord Replacement where
-    compare r1 r2 = (repStartPos r1) `compare` (repStartPos r2)
-
 newReplacement = Replacement {
     repStartPos = newPosition,
     repEndPos = newPosition,
@@ -316,10 +313,10 @@
     siGetConfig = const $ return Nothing
 }
   where
-    rf file =
-        case filter ((== file) . fst) files of
-            [] -> return $ Left "File not included in mock."
-            [(_, contents)] -> return $ Right contents
+    rf file = return $
+        case find ((== file) . fst) files of
+            Nothing -> Left "File not included in mock."
+            Just (_, contents) -> Right contents
     fs _ _ file = return file
 
 mockRcFile rcfile mock = mock {
diff --git a/src/ShellCheck/Parser.hs b/src/ShellCheck/Parser.hs
--- a/src/ShellCheck/Parser.hs
+++ b/src/ShellCheck/Parser.hs
@@ -34,7 +34,7 @@
 import Control.Monad.Trans
 import Data.Char
 import Data.Functor
-import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub)
+import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub, find)
 import Data.Maybe
 import Data.Monoid
 import Debug.Trace
@@ -186,12 +186,12 @@
 
 -- Get an ID starting from the first token of the list, and ending after the last
 getNextIdSpanningTokenList list =
-    if null list
-    then do
+    case list of
+    [] -> do
         pos <- getPosition
         getNextIdBetween pos pos
-    else
-        getNextIdSpanningTokens (head list) (last list)
+    (h:_) ->
+        getNextIdSpanningTokens h (last list)
 
 -- Get the span covered by an id
 getSpanForId :: Monad m => Id -> SCParser m (SourcePos, SourcePos)
@@ -246,6 +246,10 @@
             parseNotes = n : parseNotes state
         }
 
+ignoreProblemsOf p = do
+    systemState <- lift . lift $ Ms.get
+    p <* (lift . lift . Ms.put $ systemState)
+
 shouldIgnoreCode code = do
     context <- getCurrentContexts
     checkSourced <- Mr.asks checkSourced
@@ -321,16 +325,15 @@
     parseProblemAt pos level code msg
 
 setCurrentContexts c = Ms.modify (\state -> state { contextStack = c })
-getCurrentContexts = contextStack <$> Ms.get
+getCurrentContexts = Ms.gets contextStack
 
 popContext = do
     v <- getCurrentContexts
-    if not $ null v
-        then do
-            let (a:r) = v
+    case v of
+        (a:r) -> do
             setCurrentContexts r
             return $ Just a
-        else
+        [] ->
             return Nothing
 
 pushContext c = do
@@ -437,6 +440,7 @@
     readCondContents `attempting` lookAhead (do
                                 pos <- getPosition
                                 s <- readVariableName
+                                spacing1
                                 when (s `elem` commonCommands) $
                                     parseProblemAt pos WarningC 1014 "Use 'if cmd; then ..' to check exit code, or 'if [[ $(cmd) == .. ]]' to check output.")
 
@@ -582,9 +586,9 @@
             return $ TC_Nullary id typ x
           )
 
-    checkTrailingOp x = fromMaybe (return ()) $ do
+    checkTrailingOp x = sequence_ $ do
         (T_Literal id str) <- getTrailingUnquotedLiteral x
-        trailingOp <- listToMaybe (filter (`isSuffixOf` str) binaryTestOps)
+        trailingOp <- find (`isSuffixOf` str) binaryTestOps
         return $ parseProblemAtId id ErrorC 1108 $
             "You need a space before and after the " ++ trailingOp ++ " ."
 
@@ -910,6 +914,8 @@
 prop_readCondition21 = isOk readCondition "[[ $1 =~ ^(a\\ b)$ ]]"
 prop_readCondition22 = isOk readCondition "[[ $1 =~ \\.a\\.(\\.b\\.)\\.c\\. ]]"
 prop_readCondition23 = isOk readCondition "[[ -v arr[$var] ]]"
+prop_readCondition24 = isWarning readCondition "[[ 1 == 2 ]]]"
+prop_readCondition25 = isOk readCondition "[[ lex.yy.c -ot program.l ]]"
 readCondition = called "test expression" $ do
     opos <- getPosition
     start <- startSpan
@@ -938,6 +944,11 @@
     id <- endSpan start
     when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC 1033 "Did you mean ]] ?"
     when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Did you mean [[ ?"
+    optional $ lookAhead $ do
+        pos <- getPosition
+        notFollowedBy2 readCmdWord <|>
+            parseProblemAt pos ErrorC 1136
+                ("Unexpected characters after terminating " ++ close ++ ". Missing semicolon/linefeed?")
     spacing
     many readCmdWord -- Read and throw away remainders to get then/do warnings. Fixme?
     return $ T_Condition id typ condition
@@ -1032,14 +1043,16 @@
 prop_readNormalWord10 = isWarning readNormalWord "\x201Chello\x201D"
 prop_readNormalWord11 = isWarning readNormalWord "\x2018hello\x2019"
 prop_readNormalWord12 = isWarning readNormalWord "hello\x2018"
-readNormalWord = readNormalishWord ""
+readNormalWord = readNormalishWord "" ["do", "done", "then", "fi", "esac"]
 
-readNormalishWord end = do
+readPatternWord = readNormalishWord "" ["esac"]
+
+readNormalishWord end terms = do
     start <- startSpan
     pos <- getPosition
     x <- many1 (readNormalWordPart end)
     id <- endSpan start
-    checkPossibleTermination pos x
+    checkPossibleTermination pos x terms
     return $ T_NormalWord id x
 
 readIndexSpan = do
@@ -1059,10 +1072,10 @@
         id <- endSpan start
         return $ T_Literal id str
 
-checkPossibleTermination pos [T_Literal _ x] =
-    when (x `elem` ["do", "done", "then", "fi", "esac"]) $
+checkPossibleTermination pos [T_Literal _ x] terminators =
+    when (x `elem` terminators) $
         parseProblemAt pos WarningC 1010 $ "Use semicolon or linefeed before '" ++ x ++ "' (or quote to make it literal)."
-checkPossibleTermination _ _ = return ()
+checkPossibleTermination _ _ _ = return ()
 
 readNormalWordPart end = do
     notFollowedBy2 $ oneOf end
@@ -1541,7 +1554,7 @@
 readDollarExp = arithmetic <|> readDollarExpansion <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarVariable
   where
     arithmetic = readAmbiguous "$((" readDollarArithmetic readDollarExpansion (\pos ->
-        parseNoteAt pos WarningC 1102 "Shells disambiguate $(( differently or not at all. For $(command substition), add space after $( . For $((arithmetics)), fix parsing errors.")
+        parseNoteAt pos ErrorC 1102 "Shells disambiguate $(( differently or not at all. For $(command substition), add space after $( . For $((arithmetics)), fix parsing errors.")
 
 prop_readDollarSingleQuote = isOk readDollarSingleQuote "$'foo\\\'lol'"
 readDollarSingleQuote = called "$'..' expression" $ do
@@ -1733,6 +1746,7 @@
 prop_readHereDoc15= isWarning readScript "cat <<foo\nbar\nfoo bar\nfoo"
 prop_readHereDoc16= isOk readScript "cat <<- ' foo'\nbar\n foo\n"
 prop_readHereDoc17= isWarning readScript "cat <<- ' foo'\nbar\n  foo\n foo\n"
+prop_readHereDoc18= isOk readScript "cat <<'\"foo'\nbar\n\"foo\n"
 prop_readHereDoc20= isWarning readScript "cat << foo\n  foo\n()\nfoo\n"
 prop_readHereDoc21= isOk readScript "# shellcheck disable=SC1039\ncat << foo\n  foo\n()\nfoo\n"
 readHereDoc = called "here document" $ do
@@ -1753,14 +1767,17 @@
     addPendingHereDoc doc
     return doc
   where
-    quotes = "\"'\\"
+    unquote :: String -> (Quoted, String)
+    unquote "" = (Unquoted, "")
+    unquote [c] = (Unquoted, [c])
+    unquote s@(cl:tl) =
+      case reverse tl of
+        (cr:tr) | cr == cl && cl `elem` "\"'" -> (Quoted, reverse tr)
+        _ -> (if '\\' `elem` s then (Quoted, filter ((/=) '\\') s) else (Unquoted, s))
     -- Fun fact: bash considers << foo"" quoted, but not << <("foo").
-    -- Instead of replicating this, just read a token and strip quotes.
     readToken = do
         str <- readStringForParser readNormalWord
-        return (if any (`elem` quotes) str then Quoted else Unquoted,
-                filter (not . (`elem` quotes)) str)
-
+        return $ unquote str
 
 readPendingHereDocs = do
     docs <- popPendingHereDocs
@@ -1809,7 +1826,7 @@
             let thereIsNoTrailer = null trailingSpace && null trailer
             let leaderIsOk = null leadingSpace
                     || dashed == Dashed && leadingSpacesAreTabs
-            let trailerStart = if null trailer then '\0' else head trailer
+            let trailerStart = case trailer of [] -> '\0'; (h:_) -> h
             let hasTrailingSpace = not $ null trailingSpace
             let hasTrailer = not $ null trailer
             let ppt = parseProblemAt trailerPos ErrorC
@@ -2035,7 +2052,11 @@
 
       Just cmd -> do
             validateCommand cmd
-            suffix <- option [] $ getParser readCmdSuffix cmd [
+            -- We have to ignore possible parsing problems from the lookAhead parser
+            firstArgument <- ignoreProblemsOf . optionMaybe . try . lookAhead $ readCmdWord
+            suffix <- option [] $ getParser readCmdSuffix
+                    -- If `export` or other modifier commands are called with `builtin` we have to look at the first argument
+                    (if isCommand ["builtin"] cmd then fromMaybe cmd firstArgument else cmd) [
                         (["declare", "export", "local", "readonly", "typeset"], readModifierSuffix),
                         (["time"], readTimeSuffix),
                         (["let"], readLetSuffix),
@@ -2283,6 +2304,7 @@
 prop_readIfClause3 = isWarning readIfClause "if false; then true; else; echo lol; fi"
 prop_readIfClause4 = isWarning readIfClause "if false; then true; else if true; then echo lol; fi; fi"
 prop_readIfClause5 = isOk readIfClause "if false; then true; else\nif true; then echo lol; fi; fi"
+prop_readIfClause6 = isWarning readIfClause "if true\nthen\nDo the thing\nfi"
 readIfClause = called "if expression" $ do
     start <- startSpan
     pos <- getPosition
@@ -2442,6 +2464,7 @@
 
 
 prop_readForClause = isOk readForClause "for f in *; do rm \"$f\"; done"
+prop_readForClause1 = isOk readForClause "for f in *; { rm \"$f\"; }"
 prop_readForClause3 = isOk readForClause "for f; do foo; done"
 prop_readForClause4 = isOk readForClause "for((i=0; i<10; i++)); do echo $i; done"
 prop_readForClause5 = isOk readForClause "for ((i=0;i<10 && n>x;i++,--n))\ndo \necho $i\ndone"
@@ -2481,7 +2504,7 @@
             "Don't use $ on the iterator name in for loops."
         name <- readVariableName `thenSkip` allspacing
         values <- readInClause <|> (optional readSequentialSep >> return [])
-        group <- readDoGroup id
+        group <- readBraced <|> readDoGroup id
         return $ T_ForIn id name values group
 
 prop_readSelectClause1 = isOk readSelectClause "select foo in *; do echo $foo; done"
@@ -2519,6 +2542,7 @@
 prop_readCaseClause3 = isOk readCaseClause "case foo\n in * ) echo bar & ;; esac"
 prop_readCaseClause4 = isOk readCaseClause "case foo\n in *) echo bar ;& bar) foo; esac"
 prop_readCaseClause5 = isOk readCaseClause "case foo\n in *) echo bar;;& foo) baz;; esac"
+prop_readCaseClause6 = isOk readCaseClause "case foo\n in if) :;; done) :;; esac"
 readCaseClause = called "case expression" $ do
     start <- startSpan
     g_Case
@@ -2642,14 +2666,14 @@
         return $ T_CoProcBody id body
 
 
-readPattern = (readNormalWord `thenSkip` spacing) `sepBy1` (char '|' `thenSkip` spacing)
+readPattern = (readPatternWord `thenSkip` spacing) `sepBy1` (char '|' `thenSkip` spacing)
 
 prop_readCompoundCommand = isOk readCompoundCommand "{ echo foo; }>/dev/null"
 readCompoundCommand = do
     cmd <- choice [
         readBraceGroup,
         readAmbiguous "((" readArithmeticExpression readSubshell (\pos ->
-            parseNoteAt pos WarningC 1105 "Shells disambiguate (( differently or not at all. For subshell, add spaces around ( . For ((, fix parsing errors."),
+            parseNoteAt pos ErrorC 1105 "Shells disambiguate (( differently or not at all. For subshell, add spaces around ( . For ((, fix parsing errors."),
         readSubshell,
         readCondition,
         readWhileClause,
@@ -2867,6 +2891,7 @@
 
 tryWordToken s t = tryParseWordToken s t `thenSkip` spacing
 tryParseWordToken keyword t = try $ do
+    pos <- getPosition
     start <- startSpan
     str <- anycaseString keyword
     id <- endSpan start
@@ -2882,9 +2907,10 @@
             _ -> return ()
 
     lookAhead keywordSeparator
-    when (str /= keyword) $
-        parseProblem ErrorC 1081 $
-            "Scripts are case sensitive. Use '" ++ keyword ++ "', not '" ++ str ++ "'."
+    when (str /= keyword) $ do
+        parseProblemAt pos ErrorC 1081 $
+            "Scripts are case sensitive. Use '" ++ keyword ++ "', not '" ++ str ++ "' (or quote if literal)."
+        fail ""
     return $ t id
 
 anycaseString =
@@ -3145,7 +3171,7 @@
             Nothing -> parseProblemAt pos ErrorC 1008 "This shebang was unrecognized. ShellCheck only supports sh/bash/dash/ksh. Add a 'shell' directive to specify."
 
     isValidShell s =
-        let good = s == "" || any (`isPrefixOf` s) goodShells
+        let good = null s || any (`isPrefixOf` s) goodShells
             bad = any (`isPrefixOf` s) badShells
         in
             if good
@@ -3425,4 +3451,3 @@
 
 return []
 runTests = $quickCheckAll
-
