Merge branch 'doctest-new-build' of https://github.com/phadej/shellcheck into phadej-doctest-new-build
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index 4fc3f9f..b7f88e8 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -1,7 +1,7 @@
 #### For bugs
 - Rule Id (if any, e.g. SC1000): 
 - My shellcheck version (`shellcheck --version` or "online"): 
-- [ ] I read the issue's wiki page, e.g. https://github.com/koalaman/shellcheck/wiki/SC2086
+- [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086)
 - [ ] I tried on shellcheck.net and verified that this is still a problem on the latest commit
 
 #### For new checks and feature suggestions
diff --git a/.snapsquid.conf b/.snapsquid.conf
new file mode 100644
index 0000000..205c1a6
--- /dev/null
+++ b/.snapsquid.conf
@@ -0,0 +1,14 @@
+# In 2015, cabal-install had a http bug triggered when proxies didn't keep
+# the connection open. This version made it into Ubuntu Xenial as used by
+# Snapcraft. In June 2018, Snapcraft's proxy started triggering this bug.
+#
+#     https://bugs.launchpad.net/launchpad-buildd/+bug/1797809
+#
+# Workaround: add more proxy
+
+visible_hostname localhost
+http_port 8888
+cache_peer 10.10.10.1 parent 8222 0 no-query default
+cache_peer_domain localhost !.internal
+http_access allow all
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 33f9338..6c239e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,13 +1,30 @@
-## ???
+## Since previous release
+### Added
+- Preliminary support for fix suggestions
+
+## v0.6.0 - 2018-12-02
 ### Added
 - Command line option --severity/-S for filtering by minimum severity
+- Command line option --wiki-link-count/-W for showing wiki links
+- SC2152/SC2151: Warn about bad `exit` values like `1234` and `"foo"`
 - SC2236/SC2237: Suggest -n/-z instead of ! -z/-n
+- SC2238: Warn when redirecting to a known command name, e.g. ls > rm
+- SC2239: Warn if the shebang is not an absolute path, e.g. #!bin/sh
+- SC2240: Warn when passing additional arguments to dot (.) in sh/dash
+- SC1133: Better diagnostics when starting a line with |/||/&&
+
 ### Changed
 - Most warnings now have useful end positions
 - SC1117 about unknown double-quoted escape sequences has been retired
+
 ### Fixed
-- SC2021 no longer triggers for equivalence classes like '[=e=]'
+- SC2021 no longer triggers for equivalence classes like `[=e=]`
 - SC2221/SC2222 no longer mistriggers on fall-through case branches
+- SC2081 about glob matches in `[ .. ]` now also triggers for `!=`
+- SC2086 no longer warns about spaces in `$#`
+- SC2164 no longer suggests subshells for `cd ..; cmd; cd ..`
+- `read -a` is now correctly considered an array assignment
+- SC2039 no longer warns about LINENO now that it's POSIX
 
 ## v0.5.0 - 2018-05-31
 ### Added
diff --git a/LICENSE b/LICENSE
index e600086..0507f1f 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,3 +1,13 @@
+Employer mandated disclaimer:
+
+  I am providing code in the repository to you under an open source license.
+  Because this is my personal repository, the license you receive to my code is
+  from me and other individual contributors, and not my employer (Facebook).
+
+  - Vidar "koala_man" Holen
+
+----
+
                     GNU GENERAL PUBLIC LICENSE
                        Version 3, 29 June 2007
 
diff --git a/README.md b/README.md
index 84d353a..597be37 100644
--- a/README.md
+++ b/README.md
@@ -70,7 +70,7 @@
 
 ![Screenshot of Vim showing inlined shellcheck feedback](doc/vim-syntastic.png).
 
-* Emacs, through [Flycheck](https://github.com/flycheck/flycheck):
+* Emacs, through [Flycheck](https://github.com/flycheck/flycheck) or [Flymake](https://github.com/federicotdn/flymake-shellcheck):
 
 ![Screenshot of emacs showing inlined shellcheck feedback](doc/emacs-flycheck.png).
 
@@ -133,6 +133,10 @@
 
     brew install shellcheck
 
+On OpenBSD:
+
+    pkg_add shellcheck
+
 On openSUSE
 
     zypper in ShellCheck
@@ -168,6 +172,11 @@
 
 or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums, older versions and the latest daily builds.
 
+Distro packages already come with a `man` page. If you are building from source, it can be installed with:
+
+    pandoc -s -t man shellcheck.1.md -o shellcheck.1
+    sudo mv shellcheck.1 /usr/share/man/man1
+
 ## Travis CI
 
 Travis CI has now integrated ShellCheck by default, so you don't need to manually install it.
@@ -445,3 +454,8 @@
 Copyright 2012-2018, Vidar 'koala_man' Holen and contributors.
 
 Happy ShellChecking!
+
+
+## Other Resources                                                                          
+* The wiki has [long form descriptions](https://github.com/koalaman/shellcheck/wiki/Checks) for each warning, e.g. [SC2221](https://github.com/koalaman/shellcheck/wiki/SC2221).
+* ShellCheck does not attempt to enforce any kind of formatting or indenting style, so also check out [shfmt](https://github.com/mvdan/sh)!
diff --git a/ShellCheck.cabal b/ShellCheck.cabal
index c28a11e..00dea36 100644
--- a/ShellCheck.cabal
+++ b/ShellCheck.cabal
@@ -1,5 +1,5 @@
 Name:             ShellCheck
-Version:          0.5.0
+Version:          0.6.0
 Synopsis:         Shell script analysis tool
 License:          GPL-3
 License-file:     LICENSE
@@ -53,6 +53,7 @@
       base > 4.6.0.1 && < 5,
       bytestring,
       containers >= 0.5,
+      deepseq >= 1.4.0.0,
       directory,
       mtl >= 2.2.1,
       parsec,
@@ -88,6 +89,7 @@
       aeson,
       base >= 4 && < 5,
       bytestring,
+      deepseq >= 1.4.0.0,
       ShellCheck,
       containers,
       directory,
diff --git a/quickrun b/quickrun
index 84846e1..f53f1b5 100755
--- a/quickrun
+++ b/quickrun
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
 # quickrun runs ShellCheck in an interpreted mode.
 # This allows testing changes without recompiling.
 
diff --git a/shellcheck.1.md b/shellcheck.1.md
index 8671d1e..6600613 100644
--- a/shellcheck.1.md
+++ b/shellcheck.1.md
@@ -71,6 +71,11 @@
 
 :   Print version information and exit.
 
+**-W** *NUM*,\ **--wiki-link-count=NUM**
+
+:   For TTY output, show *NUM* wiki links to more information about mentioned
+    warnings. Set to 0 to disable them entirely.
+
 **-x**,\ **--external-sources**
 
 :   Follow 'source' statements even when the file is not specified as input.
diff --git a/shellcheck.hs b/shellcheck.hs
index 399e44d..6b9047c 100644
--- a/shellcheck.hs
+++ b/shellcheck.hs
@@ -100,6 +100,9 @@
         "Minimum severity of errors to consider (error, warning, info, style)",
     Option "V" ["version"]
         (NoArg $ Flag "version" "true") "Print version information",
+    Option "W" ["wiki-link-count"]
+        (ReqArg (Flag "wiki-link-count") "NUM")
+        "The number of wiki links to show, when applicable.",
     Option "x" ["external-sources"]
         (NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES"
     ]
@@ -296,6 +299,14 @@
                 }
             }
 
+        Flag "wiki-link-count" countString -> do
+            count <- parseNum countString
+            return options {
+                formatterOptions = (formatterOptions options) {
+                    foWikiLinkCount = count
+                }
+            }
+
         _ -> return options
   where
     die s = do
@@ -304,7 +315,7 @@
     parseNum ('S':'C':str) = parseNum str
     parseNum num = do
         unless (all isDigit num) $ do
-            printErr $ "Bad exclusion: " ++ num
+            printErr $ "Invalid number: " ++ num
             throwError SyntaxFailure
         return (Prelude.read num :: Integer)
 
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index b7f0e96..9c50293 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -37,9 +37,16 @@
     source: ./
     build-packages:
       - cabal-install
+      - squid3
     build: |
+      # See comments in .snapsquid.conf
+      [ "$http_proxy" ] && {
+        squid3 -f .snapsquid.conf
+        export http_proxy="http://localhost:8888"
+        sleep 3
+      }
       cabal sandbox init
-      cabal update
+      cabal update || cat /var/log/squid/*
       cabal install -j
     install: |
       install -d $SNAPCRAFT_PART_INSTALL/usr/bin
diff --git a/src/ShellCheck/AST.hs b/src/ShellCheck/AST.hs
index cd96165..8a6d7b2 100644
--- a/src/ShellCheck/AST.hs
+++ b/src/ShellCheck/AST.hs
@@ -17,14 +17,17 @@
     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 #-}
 module ShellCheck.AST where
 
+import GHC.Generics (Generic)
 import Control.Monad.Identity
+import Control.DeepSeq
 import Text.Parsec
 import qualified ShellCheck.Regex as Re
 import Prelude hiding (id)
 
-newtype Id = Id Int deriving (Show, Eq, Ord)
+newtype Id = Id Int deriving (Show, Eq, Ord, Generic, NFData)
 
 data Quoted = Quoted | Unquoted deriving (Show, Eq)
 data Dashed = Dashed | Undashed deriving (Show, Eq)
diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs
index f6c1cef..a2d6152 100644
--- a/src/ShellCheck/Analytics.hs
+++ b/src/ShellCheck/Analytics.hs
@@ -166,6 +166,7 @@
     ,checkForLoopGlobVariables
     ,checkSubshelledTests
     ,checkInvertedStringTest
+    ,checkRedirectionToCommand
     ]
 
 
@@ -195,8 +196,9 @@
 checkNode f = producesComments (runNodeAnalysis f)
 producesComments :: (Parameters -> Token -> [TokenComment]) -> String -> Maybe Bool
 producesComments f s = do
-        root <- pScript s
-        return . not . null $ runList (defaultSpec root) [f]
+        let pr = pScript s
+        prRoot pr
+        return . not . null $ runList (defaultSpec pr) [f]
 
 -- Copied from https://wiki.haskell.org/Edit_distance
 dist :: Eq a => [a] -> [a] -> Int
@@ -237,6 +239,39 @@
             T_UntilExpression id c l -> take 1 . reverse $ c
             _ -> []
 
+-- helpers to build replacements
+replaceStart id params n r =
+    let tp = tokenPositions params
+        (start, _) = tp Map.! id
+        new_end = start {
+            posColumn = posColumn start + n
+        }
+    in
+    newReplacement {
+        repStartPos = start,
+        repEndPos = new_end,
+        repString = r
+    }
+replaceEnd id params n r =
+    -- because of the way we count columns 1-based
+    -- we have to offset end columns by 1
+    let tp = tokenPositions params
+        (_, end) = tp Map.! id
+        new_start = end {
+            posColumn = posColumn end - n + 1
+        }
+        new_end = end {
+            posColumn = posColumn end + 1
+        }
+    in
+    newReplacement {
+        repStartPos = new_start,
+        repEndPos = new_end,
+        repString = r
+    }
+surroundWidth id params s = fixWith [replaceStart id params 0 s, replaceEnd id params 0 s]
+fixWith fixes = newFix { fixReplacements = fixes }
+
 -- >>> prop $ verify checkEchoWc "n=$(echo $foo | wc -c)"
 checkEchoWc _ (T_Pipeline id _ [a, b]) =
     when (acmd == ["echo", "${VAR}"]) $
@@ -436,17 +471,22 @@
 -- >>> prop $ verifyTree checkShebang "#!/usr/bin/env ash"
 -- >>> prop $ verifyNotTree checkShebang "#!/usr/bin/env ash\n# shellcheck shell=dash\n"
 -- >>> prop $ verifyNotTree checkShebang "#!/usr/bin/env ash\n# shellcheck shell=sh\n"
+-- >>> prop $ verifyTree checkShebang "#!bin/sh\ntrue"
+-- >>> prop $ verifyNotTree checkShebang "# shellcheck shell=sh\ntrue"
+-- >>> prop $ verifyNotTree checkShebang "#!foo\n# shellcheck shell=sh ignore=SC2239\ntrue"
 checkShebang params (T_Annotation _ list t) =
     if any isOverride list then [] else checkShebang params t
   where
     isOverride (ShellOverride _) = True
     isOverride _ = False
-checkShebang params (T_Script id sb _) = execWriter $
+checkShebang params (T_Script 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 (executableFromShebang sb == "ash") $
             warn id 2187 "Ash scripts will be checked as Dash. Add '# shellcheck shell=dash' to silence."
+    unless (null sb || "/" `isPrefixOf` sb) $
+        err id 2239 "Ensure the shebang uses an absolute path to the interpreter."
 
 
 -- |
@@ -727,6 +767,7 @@
 -- >>> prop $ verifyTree checkArrayWithoutIndex "echo $PIPESTATUS"
 -- >>> prop $ verifyTree checkArrayWithoutIndex "a=(a b); a+=c"
 -- >>> prop $ verifyTree checkArrayWithoutIndex "declare -a foo; foo=bar;"
+-- >>> prop $ verifyTree checkArrayWithoutIndex "read -r -a arr <<< 'foo bar'; echo \"$arr\""
 checkArrayWithoutIndex params _ =
     doVariableFlowAnalysis readF writeF defaultMap (variableFlow params)
   where
@@ -1045,7 +1086,7 @@
     error t =
         unless (isConstantNonRe t) $
             err (getId t) 2076
-                "Don't quote rhs of =~, it'll match literally rather than as a regex."
+                "Don't quote right-hand side of =~, it'll match literally rather than as a regex."
     re = mkRegex "[][*.+()|]"
     hasMetachars s = s `matches` re
     isConstantNonRe t = fromMaybe False $ do
@@ -1056,13 +1097,16 @@
 -- |
 -- >>> prop $ verify checkGlobbedRegex "[[ $foo =~ *foo* ]]"
 -- >>> prop $ verify checkGlobbedRegex "[[ $foo =~ f* ]]"
--- >>> prop $ verify checkGlobbedRegex "[[ $foo =~ \\#* ]]"
 -- >>> prop $ verifyNot checkGlobbedRegex "[[ $foo =~ $foo ]]"
 -- >>> prop $ verifyNot checkGlobbedRegex "[[ $foo =~ ^c.* ]]"
+-- >>> prop $ verifyNot checkGlobbedRegex "[[ $foo =~ \\* ]]"
+-- >>> prop $ verifyNot checkGlobbedRegex "[[ $foo =~ (o*) ]]"
+-- >>> prop $ verifyNot checkGlobbedRegex "[[ $foo =~ \\*foo ]]"
+-- >>> prop $ 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. Use == for globs."
+            warn (getId rhs) 2049 "=~ is for regex, but this looks like a glob. Use = instead."
 checkGlobbedRegex _ _ = return ()
 
 
@@ -1221,11 +1265,12 @@
 -- >>> prop $ verify checkComparisonAgainstGlob "[ $cow = *foo* ]"
 -- >>> prop $ verifyNot checkComparisonAgainstGlob "[ $cow = foo ]"
 -- >>> prop $ verify checkComparisonAgainstGlob "[[ $cow != $bar ]]"
+-- >>> prop $ verify checkComparisonAgainstGlob "[ $f != /* ]"
 checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T_DollarBraced _ _]))
     | op `elem` ["=", "==", "!="] =
-        warn id 2053 $ "Quote the rhs of " ++ op ++ " in [[ ]] to prevent glob matching."
+        warn id 2053 $ "Quote the right-hand side of " ++ op ++ " in [[ ]] to prevent glob matching."
 checkComparisonAgainstGlob _ (TC_Binary _ SingleBracket op _ word)
-        | (op == "=" || op == "==") && isGlob word =
+        | op `elem` ["=", "==", "!="] && isGlob word =
     err (getId word) 2081 "[ .. ] can't match globs. Use [[ .. ]] or case statement."
 checkComparisonAgainstGlob _ _ = return ()
 
@@ -1365,8 +1410,10 @@
 -- >>> prop $ verify checkBackticks "echo `foo`"
 -- >>> prop $ verifyNot checkBackticks "echo $(foo)"
 -- >>> prop $ verifyNot checkBackticks "echo `#inlined comment` foo"
-checkBackticks _ (T_Backticked id list) | not (null list) =
-    style id 2006 "Use $(...) notation instead of legacy backticked `...`."
+checkBackticks params (T_Backticked id list) | not (null list) =
+    addComment $
+        makeCommentWithFix StyleC id 2006  "Use $(...) notation instead of legacy backticked `...`."
+            (fixWith [replaceStart id params 1 "$(", replaceEnd id params 1 ")"])
 checkBackticks _ _ = return ()
 
 -- |
@@ -1649,6 +1696,7 @@
 -- >>> prop $ verifyTree checkSpacefulness "for file; do echo $file; done"
 -- >>> prop $ verifyTree checkSpacefulness "declare foo$n=$1"
 -- >>> prop $ verifyNotTree checkSpacefulness "echo ${1+\"$1\"}"
+-- >>> prop $ verifyNotTree checkSpacefulness "arg=$#; echo $arg"
 
 checkSpacefulness params t =
     doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)
@@ -1678,8 +1726,10 @@
                 makeComment InfoC (getId token) 2223
                     "This default assignment may cause DoS due to globbing. Quote it."
             else
-                makeComment InfoC (getId token) 2086
-                    "Double quote to prevent globbing and word splitting."
+                makeCommentWithFix InfoC (getId token) 2086
+                    "Double quote to prevent globbing and word splitting." (surroundWidth (getId token) params "\"")
+                -- makeComment InfoC (getId token) 2086
+                --     "Double quote to prevent globbing and word splitting."
 
     writeF _ _ name (DataString SourceExternal) = setSpaces name True >> return []
     writeF _ _ name (DataString SourceInteger) = setSpaces name False >> return []
@@ -1930,6 +1980,7 @@
 -- >>> prop $ verifyNotTree checkUnassignedReferences "f() { local -A foo; echo \"${foo[@]}\"; }"
 -- >>> prop $ verifyNotTree checkUnassignedReferences "declare -A foo; (( foo[bar] ))"
 -- >>> prop $ verifyNotTree checkUnassignedReferences "echo ${arr[foo-bar]:?fail}"
+-- >>> prop $ verifyNotTree checkUnassignedReferences "read -a foo -r <<<\"foo bar\"; echo \"$foo\""
 checkUnassignedReferences params t = warnings
   where
     (readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty)
@@ -2119,12 +2170,12 @@
 checkCharRangeGlob _ _ = return ()
 
 
-
 -- |
 -- >>> prop $ verify checkCdAndBack "for f in *; do cd $f; git pull; cd ..; done"
 -- >>> prop $ verifyNot checkCdAndBack "for f in *; do cd $f || continue; git pull; cd ..; done"
 -- >>> prop $ verifyNot checkCdAndBack "while [[ $PWD != / ]]; do cd ..; done"
 -- >>> prop $ verify checkCdAndBack "cd $tmp; foo; cd -"
+-- >>> prop $ verifyNot checkCdAndBack "cd ..; foo; cd .."
 checkCdAndBack params = doLists
   where
     shell = shellType params
@@ -2147,10 +2198,20 @@
     getCmd (T_Pipeline id _ [x]) = getCommandName x
     getCmd _ = Nothing
 
+    findCdPair list =
+        case list of
+            (a:b:rest) ->
+                if isCdRevert b && not (isCdRevert a)
+                then return $ getId b
+                else findCdPair (b:rest)
+            _ -> Nothing
+
+
     doList list =
         let cds = filter ((== Just "cd") . getCmd) list in
-            when (length cds >= 2 && isCdRevert (last cds)) $
-               info (getId $ last cds) 2103 message
+            potentially $ do
+                cd <- findCdPair cds
+                return $ info cd 2103 message
 
     message = "Use a ( subshell ) to avoid having to cd back."
 
@@ -2583,7 +2644,8 @@
             && not (isSafeDir t)
             && not (name t `elem` ["pushd", "popd"] && ("n" `elem` map snd (getAllFlags t)))
             && not (isCondition $ getPath (parentMap params) t)) $
-                warn (getId t) 2164 "Use 'cd ... || exit' or 'cd ... || return' in case cd fails."
+                warnWithFix (getId t) 2164 "Use 'cd ... || exit' or 'cd ... || return' in case cd fails."
+                    (fixWith [replaceEnd (getId t) params 0 " || exit"])
     checkElement _ = return ()
     name t = fromMaybe "" $ getCommandName t
     isSafeDir t = case oversimplify t of
@@ -2702,7 +2764,7 @@
         case drop 1 $ getPath (parentMap params) t of
             T_DollarExpansion _ [_] : _ -> True
             T_Backticked _ [_] : _ -> True
-            T_Annotation _ _ u : _ -> isInExpansion u
+            t@T_Annotation {} : _ -> isInExpansion t
             _ -> False
     getDanglingRedirect token =
         case token of
@@ -2745,7 +2807,7 @@
                         T_Literal id str -> [(id,str)]
                         _ -> []
                     guard $ '=' `elem` str
-                    return $ warn id 2191 "The = here is literal. To assign by index, use ( [index]=value ) with no spaces. To keep as literal, quote it."
+                    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
                     then warn (getId t) 2190 "Elements in associative arrays need index, e.g. array=( [index]=value ) ."
@@ -3110,3 +3172,14 @@
                 "-z" -> style (getId t) 2237 "Use [ -n .. ] instead of ! [ -z .. ]."
                 _ -> return ()
         _ -> return ()
+
+-- >>> prop $ verify checkRedirectionToCommand "ls > rm"
+-- >>> prop $ verifyNot checkRedirectionToCommand "ls > 'rm'"
+-- >>> prop $ 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
+                warn id 2238 "Redirecting to/from command name instead of file. Did you want pipes/xargs (or quote to ignore)?"
+        _ -> return ()
+
diff --git a/src/ShellCheck/AnalyzerLib.hs b/src/ShellCheck/AnalyzerLib.hs
index 09f40a3..df5b181 100644
--- a/src/ShellCheck/AnalyzerLib.hs
+++ b/src/ShellCheck/AnalyzerLib.hs
@@ -27,6 +27,7 @@
 import           ShellCheck.Regex
 
 import           Control.Arrow          (first)
+import           Control.DeepSeq
 import           Control.Monad.Identity
 import           Control.Monad.RWS
 import           Control.Monad.State
@@ -81,8 +82,9 @@
     parentMap          :: Map.Map Id Token, -- A map from Id to parent Token
     shellType          :: Shell,            -- The shell type, such as Bash or Ksh
     shellTypeSpecified :: Bool,    -- True if shell type was forced via flags
-    rootNode           :: Token              -- The root node of the AST
-    }
+    rootNode           :: Token,              -- The root node of the AST
+    tokenPositions     :: Map.Map Id (Position, Position) -- map from token id to start and end position
+    } deriving (Show)
 
 -- TODO: Cache results of common AST ops here
 data Cache = Cache {}
@@ -109,11 +111,12 @@
 
 data VariableState = Dead Token String | Alive deriving (Show)
 
-defaultSpec root = spec {
+defaultSpec pr = spec {
     asShellType = Nothing,
     asCheckSourced = False,
-    asExecutionMode = Executed
-} where spec = newAnalysisSpec root
+    asExecutionMode = Executed,
+    asTokenPositions = prTokenPositions pr
+} where spec = newAnalysisSpec (fromJust $ prRoot pr)
 
 pScript s =
   let
@@ -121,13 +124,14 @@
         psFilename = "script",
         psScript = s
     }
-  in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec
+  in runIdentity $ parseScript (mockedSystemInterface []) pSpec
 
 -- For testing. If parsed, returns whether there are any comments
 producesComments :: Checker -> String -> Maybe Bool
 producesComments c s = do
-        root <- pScript s
-        let spec = defaultSpec root
+        let pr = pScript s
+        prRoot pr
+        let spec = defaultSpec pr
         let params = makeParameters spec
         return . not . null $ runChecker params c
 
@@ -142,7 +146,7 @@
         }
     }
 
-addComment note = tell [note]
+addComment note = note `deepseq` tell [note]
 
 warn :: MonadWriter [TokenComment] m => Id -> Code -> String -> m ()
 warn  id code str = addComment $ makeComment WarningC id code str
@@ -150,6 +154,20 @@
 info  id code str = addComment $ makeComment InfoC id code str
 style id code str = addComment $ makeComment StyleC id code str
 
+warnWithFix id code str fix = addComment $
+    let comment = makeComment WarningC id code str in
+    comment {
+        tcFix = Just fix
+    }
+
+makeCommentWithFix :: Severity -> Id -> Code -> String -> Fix -> TokenComment
+makeCommentWithFix severity id code str fix =
+    let comment = makeComment severity id code str
+        withFix = comment {
+            tcFix = Just fix
+        }
+    in withFix `deepseq` withFix
+
 makeParameters spec =
     let params = Parameters {
         rootNode = root,
@@ -164,7 +182,8 @@
 
         shellTypeSpecified = isJust $ asShellType spec,
         parentMap = getParentTree root,
-        variableFlow = getVariableFlow params root
+        variableFlow = getVariableFlow params root,
+        tokenPositions = asTokenPositions spec
     } in params
   where root = asScript spec
 
@@ -197,14 +216,15 @@
 
 
 -- |
--- >>> prop $ determineShell (fromJust $ pScript "#!/bin/sh") == Sh
--- >>> prop $ determineShell (fromJust $ pScript "#!/usr/bin/env ksh") == Ksh
--- >>> prop $ determineShell (fromJust $ pScript "") == Bash
--- >>> prop $ determineShell (fromJust $ pScript "#!/bin/sh -e") == Sh
--- >>> prop $ determineShell (fromJust $ pScript "#!/bin/ksh\n#shellcheck shell=sh\nfoo") == Sh
--- >>> prop $ determineShell (fromJust $ pScript "#shellcheck shell=sh\nfoo") == Sh
--- >>> prop $ determineShell (fromJust $ pScript "#! /bin/sh") == Sh
--- >>> prop $ determineShell (fromJust $ pScript "#! /bin/ash") == Dash
+-- >>> prop $ determineShellTest "#!/bin/sh" == Sh
+-- >>> prop $ determineShellTest "#!/usr/bin/env ksh" == Ksh
+-- >>> prop $ determineShellTest "" == Bash
+-- >>> prop $ determineShellTest "#!/bin/sh -e" == Sh
+-- >>> prop $ determineShellTest "#!/bin/ksh\n#shellcheck shell=sh\nfoo" == Sh
+-- >>> prop $ determineShellTest "#shellcheck shell=sh\nfoo" == Sh
+-- >>> prop $ determineShellTest "#! /bin/sh" == Sh
+-- >>> prop $ determineShellTest "#! /bin/ash" == Dash
+determineShellTest = determineShell . fromJust . prRoot . pScript
 determineShell t = fromMaybe Bash $ do
     shellString <- foldl mplus Nothing $ getCandidates t
     shellForExecutable shellString
@@ -239,9 +259,10 @@
   where
     pre t = modify (first ((:) t))
     post t = do
-        (_:rest, map) <- get
-        case rest of []    -> put (rest, map)
-                     (x:_) -> put (rest, Map.insert (getId t) x map)
+        (x, map) <- get
+        case x of
+          _:rest -> case rest of []    -> put (rest, map)
+                                 (x:_) -> put (rest, Map.insert (getId t) x map)
 
 -- Given a root node, make a map from Id to Token
 getTokenMap :: Token -> Map.Map Id Token
@@ -524,12 +545,22 @@
 
 getReferencedVariableCommand _ = []
 
+-- The function returns a tuple consisting of four items describing an assignment.
+-- Given e.g. declare foo=bar
+-- (
+--   BaseCommand :: Token,     -- The command/structure assigning the variable, i.e. declare foo=bar
+--   AssignmentToken :: Token, -- The specific part that assigns this variable, i.e. foo=bar
+--   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)) =
    filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
     case x of
         "read" ->
-            let params = map getLiteral rest in
-                catMaybes . takeWhile isJust . reverse $ params
+            let params = map getLiteral rest
+                readArrayVars = getReadArrayVariables rest
+            in
+                catMaybes . (++ readArrayVars) . takeWhile isJust . reverse $ params
         "getopts" ->
             case rest of
                 opts:var:_ -> maybeToList $ getLiteral var
@@ -572,10 +603,14 @@
       where
         defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
 
-    getLiteral t = do
+    getLiteralOfDataType t d = do
         s <- getLiteralString t
         when ("-" `isPrefixOf` s) $ fail "argument"
-        return (base, t, s, DataString SourceExternal)
+        return (base, t, s, d)
+
+    getLiteral t = getLiteralOfDataType t (DataString SourceExternal)
+
+    getLiteralArray t = getLiteralOfDataType t (DataArray SourceExternal)
 
     getModifierParamString = getModifierParam DataString
 
@@ -617,6 +652,11 @@
         guard $ isVariableName name
         return (base, lastArg, name, DataArray SourceExternal)
 
+    -- get all the array variables used in read, e.g. read -a arr
+    getReadArrayVariables args = do
+        map (getLiteralArray . snd)
+            (filter (\(x,_) -> getLiteralString x == Just "-a") (zip (args) (tail args)))
+
 getModifiedVariableCommand _ = []
 
 getIndexReferences s = fromMaybe [] $ do
diff --git a/src/ShellCheck/Checker.hs b/src/ShellCheck/Checker.hs
index e78e80f..9a040ee 100644
--- a/src/ShellCheck/Checker.hs
+++ b/src/ShellCheck/Checker.hs
@@ -39,7 +39,8 @@
     return $ newPositionedComment {
         pcStartPos = fst span,
         pcEndPos = snd span,
-        pcComment = tcComment t
+        pcComment = tcComment t,
+        pcFix = tcFix t
     }
   where
     fail = error "Internal shellcheck error: id doesn't exist. Please report!"
@@ -60,11 +61,20 @@
             psShellTypeOverride = csShellTypeOverride spec
         }
         let parseMessages = prComments result
+        let tokenPositions = prTokenPositions result
+        let analysisSpec root =
+                as {
+                    asScript = root,
+                    asShellType = csShellTypeOverride spec,
+                    asCheckSourced = csCheckSourced spec,
+                    asExecutionMode = Executed,
+                    asTokenPositions = tokenPositions
+                } where as = newAnalysisSpec root
         let analysisMessages =
                 fromMaybe [] $
                     (arComments . analyzeScript . analysisSpec)
                         <$> prRoot result
-        let translator = tokenToPosition (prTokenPositions result)
+        let translator = tokenToPosition tokenPositions
         return . nub . sortMessages . filter shouldInclude $
             (parseMessages ++ map translator analysisMessages)
 
@@ -87,13 +97,6 @@
          cMessage comment)
     getPosition = pcStartPos
 
-    analysisSpec root =
-        as {
-            asScript = root,
-            asShellType = csShellTypeOverride spec,
-            asCheckSourced = csCheckSourced spec,
-            asExecutionMode = Executed
-         } where as = newAnalysisSpec root
 
 getErrors sys spec =
     sort . map getCode . crComments $
@@ -244,5 +247,7 @@
 -- >>> 2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
 -- True
 --
+-- >>> check "fun() {\n# shellcheck disable=SC2188\n> /dev/null\n}\n"
+-- []
 doctests :: ()
 doctests = ()
diff --git a/src/ShellCheck/Checks/Commands.hs b/src/ShellCheck/Checks/Commands.hs
index 7d603d3..8faae36 100644
--- a/src/ShellCheck/Checks/Commands.hs
+++ b/src/ShellCheck/Checks/Commands.hs
@@ -56,6 +56,7 @@
     ,checkGrepRe
     ,checkTrapQuotes
     ,checkReturn
+    ,checkExit
     ,checkFindExecWithSingleArgument
     ,checkUnusedEchoEscapes
     ,checkInjectableFindSh
@@ -87,6 +88,7 @@
     ,checkWhich
     ,checkSudoRedirect
     ,checkSudoArgs
+    ,checkSourceArgs
     ]
 
 buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
@@ -281,15 +283,29 @@
 -- >>> prop $ verify checkReturn "return -1"
 -- >>> prop $ verify checkReturn "return 1000"
 -- >>> prop $ verify checkReturn "return 'hello world'"
-checkReturn = CommandCheck (Exactly "return") (f . arguments)
+checkReturn = CommandCheck (Exactly "return") (returnOrExit
+        (\c -> err c 2151 "Only one integer 0-255 can be returned. Use stdout for other data.")
+        (\c -> err c 2152 "Can only return 0-255. Other data should be written to stdout."))
+
+-- |
+-- >>> prop $ verifyNot checkExit "exit"
+-- >>> prop $ verifyNot checkExit "exit 1"
+-- >>> prop $ verifyNot checkExit "exit $var"
+-- >>> prop $ verifyNot checkExit "exit $((a|b))"
+-- >>> prop $ verify checkExit "exit -1"
+-- >>> prop $ verify checkExit "exit 1000"
+-- >>> prop $ verify checkExit "exit 'hello world'"
+checkExit = CommandCheck (Exactly "exit") (returnOrExit
+        (\c -> err c 2241 "The exit status can only be one integer 0-255. Use stdout for other data.")
+        (\c -> err c 2242 "Can only exit with status 0-255. Other data should be written to stdout/stderr."))
+
+returnOrExit multi invalid = (f . arguments)
   where
     f (first:second:_) =
-        err (getId second) 2151
-            "Only one integer 0-255 can be returned. Use stdout for other data."
+        multi (getId first)
     f [value] =
         when (isInvalid $ literal value) $
-            err (getId value) 2152
-                "Can only return 0-255. Other data should be written to stdout."
+            invalid (getId value)
     f _ = return ()
 
     isInvalid s = s == "" || any (not . isDigit) s || length s > 5
@@ -1038,3 +1054,15 @@
     builtins = [ "cd", "eval", "export", "history", "read", "source", "wait" ]
     -- This mess is why ShellCheck prefers not to know.
     parseOpts = getBsdOpts "vAknSbEHPa:g:h:p:u:c:T:r:"
+
+-- |
+-- >>> prop $ verify checkSourceArgs "#!/bin/sh\n. script arg"
+-- >>> prop $ verifyNot checkSourceArgs "#!/bin/sh\n. script"
+-- >>> prop $ verifyNot checkSourceArgs "#!/bin/bash\n. script arg"
+checkSourceArgs = CommandCheck (Exactly ".") f
+  where
+    f t = whenShell [Sh, Dash] $
+        case arguments t of
+            (file:arg1:_) -> warn (getId arg1) 2240 $
+                "The dot command does not support arguments in sh/dash. Set them as variables."
+            _ -> return ()
diff --git a/src/ShellCheck/Data.hs b/src/ShellCheck/Data.hs
index 1fd42b8..8ce0026 100644
--- a/src/ShellCheck/Data.hs
+++ b/src/ShellCheck/Data.hs
@@ -39,7 +39,7 @@
   ]
 
 variablesWithoutSpaces = [
-    "$", "-", "?", "!",
+    "$", "-", "?", "!", "#",
     "BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
     "OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
     "COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
diff --git a/src/ShellCheck/Formatter/JSON.hs b/src/ShellCheck/Formatter/JSON.hs
index aac4d20..9aec751 100644
--- a/src/ShellCheck/Formatter/JSON.hs
+++ b/src/ShellCheck/Formatter/JSON.hs
@@ -39,7 +39,20 @@
         footer = finish ref
     }
 
-instance ToJSON (PositionedComment) where
+instance ToJSON Replacement where
+    toJSON replacement =
+        let start = repStartPos replacement
+            end = repEndPos replacement
+            str = repString replacement in
+        object [
+          "line" .= posLine start,
+          "endLine" .= posLine end,
+          "column" .= posColumn start,
+          "endColumn" .= posColumn end,
+          "replaceWith" .= str
+        ]
+
+instance ToJSON PositionedComment where
   toJSON comment =
     let start = pcStartPos comment
         end = pcEndPos comment
@@ -52,7 +65,8 @@
       "endColumn" .= posColumn end,
       "level" .= severityText comment,
       "code" .= cCode c,
-      "message" .= cMessage c
+      "message" .= cMessage c,
+      "fix" .= pcFix comment
     ]
 
   toEncoding comment =
@@ -68,8 +82,14 @@
       <> "level" .= severityText comment
       <> "code" .= cCode c
       <> "message" .= cMessage c
+      <> "fix" .= pcFix comment
     )
 
+instance ToJSON Fix where
+    toJSON fix = object [
+        "replacements" .= fixReplacements fix
+        ]
+
 outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
 collectResult ref result _ =
     modifyIORef ref (\x -> crComments result ++ x)
@@ -77,4 +97,3 @@
 finish ref = do
     list <- readIORef ref
     BL.putStrLn $ encode list
-
diff --git a/src/ShellCheck/Formatter/TTY.hs b/src/ShellCheck/Formatter/TTY.hs
index f54a0b3..2d6c010 100644
--- a/src/ShellCheck/Formatter/TTY.hs
+++ b/src/ShellCheck/Formatter/TTY.hs
@@ -22,18 +22,28 @@
 import ShellCheck.Interface
 import ShellCheck.Formatter.Format
 
+import Control.Monad
+import Data.IORef
 import Data.List
+import Data.Maybe
 import GHC.Exts
-import System.Info
 import System.IO
+import System.Info
+
+wikiLink = "https://www.shellcheck.net/wiki/"
+
+-- An arbitrary Ord thing to order warnings
+type Ranking = (Char, Severity, Integer)
 
 format :: FormatterOptions -> IO Formatter
-format options = return Formatter {
-    header = return (),
-    footer = return (),
-    onFailure = outputError options,
-    onResult = outputResult options
-}
+format options = do
+    topErrorRef <- newIORef []
+    return Formatter {
+        header = return (),
+        footer = outputWiki topErrorRef,
+        onFailure = outputError options,
+        onResult = outputResult options topErrorRef
+    }
 
 colorForLevel level =
     case level of
@@ -45,13 +55,60 @@
         "source"  -> 0 -- none
         _ -> 0         -- none
 
+rankError :: PositionedComment -> Ranking
+rankError err = (ranking, cSeverity $ pcComment err, cCode $ pcComment err)
+  where
+    ranking =
+        if cCode (pcComment err) `elem` uninteresting
+        then 'Z'
+        else 'A'
+
+    -- A list of the most generic, least directly helpful
+    -- error codes to downrank.
+    uninteresting = [
+        1009, -- Mentioned parser error was..
+        1019, -- Expected this to be an argument
+        1036, -- ( is invalid here
+        1047, -- Expected 'fi'
+        1062, -- Expected 'done'
+        1070, -- Parsing stopped here (generic)
+        1072, -- Missing/unexpected ..
+        1073, -- Couldn't parse this ..
+        1088, -- Parsing stopped here (paren)
+        1089  -- Parsing stopped here (keyword)
+        ]
+
+appendComments errRef comments max = do
+    previous <- readIORef errRef
+    let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments
+    writeIORef errRef . take max . nubBy equal . sort $ previous ++ current
+  where
+    fst3 (x,_,_) = x
+    equal x y = fst3 x == fst3 y
+
+outputWiki :: IORef [(Ranking, Integer, String)] -> IO ()
+outputWiki errRef = do
+    issues <- readIORef errRef
+    unless (null issues) $ do
+        putStrLn "For more information:"
+        mapM_ showErr issues
+  where
+    showErr (_, code, msg) =
+        putStrLn $ "  " ++ wikiLink ++ "SC" ++ show code ++ " -- " ++ shorten msg
+    limit = 36
+    shorten msg =
+        if length msg < limit
+        then msg
+        else (take (limit-3) msg) ++ "..."
+
 outputError options file error = do
     color <- getColorFunc $ foColorOption options
     hPutStrLn stderr $ color "error" $ file ++ ": " ++ error
 
-outputResult options result sys = do
+outputResult options ref result sys = do
     color <- getColorFunc $ foColorOption options
     let comments = crComments result
+    appendComments ref comments (fromIntegral $ foWikiLinkCount options)
     let fileGroups = groupWith sourceFile comments
     mapM_ (outputForFile color sys) fileGroups
 
@@ -62,8 +119,8 @@
     let fileLines = lines contents
     let lineCount = fromIntegral $ length fileLines
     let groups = groupWith lineNo comments
-    mapM_ (\x -> do
-        let lineNum = lineNo (head x)
+    mapM_ (\commentsForLine -> do
+        let lineNum = lineNo (head commentsForLine)
         let line = if lineNum < 1 || lineNum > lineCount
                         then ""
                         else fileLines !! fromIntegral (lineNum - 1)
@@ -71,10 +128,61 @@
         putStrLn $ color "message" $
            "In " ++ fileName ++" line " ++ show lineNum ++ ":"
         putStrLn (color "source" line)
-        mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) x
+        mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) commentsForLine
         putStrLn ""
+        showFixedString color comments lineNum line
       ) groups
 
+hasApplicableFix lineNum comment = fromMaybe False $ do
+    replacements <- fixReplacements <$> pcFix comment
+    guard $ all (\c -> onSameLine (repStartPos c) && onSameLine (repEndPos c)) replacements
+    return True
+  where
+    onSameLine pos = posLine pos == lineNum
+
+-- FIXME: Work correctly with multiple replacements
+showFixedString color comments lineNum line =
+    case filter (hasApplicableFix lineNum) comments of
+        (first:_) -> do
+            -- in the spirit of error prone
+            putStrLn $ color "message" "Did you mean: "
+            putStrLn $ fixedString first line
+            putStrLn ""
+        _ -> return ()
+
+-- need to do something smart about sorting by end index
+fixedString :: PositionedComment -> String -> String
+fixedString comment line =
+    case (pcFix comment) of
+    Nothing -> ""
+    Just rs ->
+        applyReplacement (fixReplacements rs) line 0
+        where
+            applyReplacement [] s _ = s
+            applyReplacement (rep:xs) s offset =
+                let replacementString = repString rep
+                    start = (posColumn . repStartPos) rep
+                    end = (posColumn . repEndPos) rep
+                    z = doReplace start end s replacementString
+                    len_r = (fromIntegral . length) replacementString in
+                applyReplacement xs z (offset + (end - start) + len_r)
+
+-- FIXME: Work correctly with tabs
+-- start and end comes from pos, which is 1 based
+-- doReplace 0 0 "1234" "A" -> "A1234" -- technically not valid
+-- doReplace 1 1 "1234" "A" -> "A1234"
+-- doReplace 1 2 "1234" "A" -> "A234"
+-- doReplace 3 3 "1234" "A" -> "12A34"
+-- doReplace 4 4 "1234" "A" -> "123A4"
+-- doReplace 5 5 "1234" "A" -> "1234A"
+doReplace start end o r =
+    let si = fromIntegral (start-1)
+        ei = fromIntegral (end-1)
+        (x, xs) = splitAt si o
+        (y, z) = splitAt (ei - si) xs
+    in
+    x ++ r ++ z
+
 cuteIndent :: PositionedComment -> String
 cuteIndent comment =
     replicate (fromIntegral $ colNo comment - 1) ' ' ++
@@ -87,7 +195,7 @@
         in
             if sameLine && delta > 2 && delta < 32 then arrow delta else "^--"
 
-code code = "SC" ++ show code
+code num = "SC" ++ show num
 
 getColorFunc colorOption = do
     term <- hIsTerminalDevice stdout
diff --git a/src/ShellCheck/Interface.hs b/src/ShellCheck/Interface.hs
index 8432f0e..092b9e8 100644
--- a/src/ShellCheck/Interface.hs
+++ b/src/ShellCheck/Interface.hs
@@ -17,6 +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 #-}
 module ShellCheck.Interface
     (
     SystemInterface(..)
@@ -24,9 +25,9 @@
     , CheckResult(crFilename, crComments)
     , ParseSpec(psFilename, psScript, psCheckSourced, psShellTypeOverride)
     , ParseResult(prComments, prTokenPositions, prRoot)
-    , AnalysisSpec(asScript, asShellType, asExecutionMode, asCheckSourced)
+    , AnalysisSpec(asScript, asShellType, asExecutionMode, asCheckSourced, asTokenPositions)
     , AnalysisResult(arComments)
-    , FormatterOptions(foColorOption)
+    , FormatterOptions(foColorOption, foWikiLinkCount)
     , Shell(Ksh, Sh, Bash, Dash)
     , ExecutionMode(Executed, Sourced)
     , ErrorMessage
@@ -34,9 +35,9 @@
     , Severity(ErrorC, WarningC, InfoC, StyleC)
     , Position(posFile, posLine, posColumn)
     , Comment(cSeverity, cCode, cMessage)
-    , PositionedComment(pcStartPos , pcEndPos , pcComment)
+    , PositionedComment(pcStartPos , pcEndPos , pcComment, pcFix)
     , ColorOption(ColorAuto, ColorAlways, ColorNever)
-    , TokenComment(tcId, tcComment)
+    , TokenComment(tcId, tcComment, tcFix)
     , emptyCheckResult
     , newParseResult
     , newAnalysisSpec
@@ -49,10 +50,18 @@
     , emptyCheckSpec
     , newPositionedComment
     , newComment
+    , Fix(fixReplacements)
+    , newFix
+    , Replacement(repStartPos, repEndPos, repString)
+    , newReplacement
     ) where
 
 import ShellCheck.AST
+
+import Control.DeepSeq
 import Control.Monad.Identity
+import Data.Monoid
+import GHC.Generics (Generic)
 import qualified Data.Map as Map
 
 
@@ -126,14 +135,16 @@
     asScript :: Token,
     asShellType :: Maybe Shell,
     asExecutionMode :: ExecutionMode,
-    asCheckSourced :: Bool
+    asCheckSourced :: Bool,
+    asTokenPositions :: Map.Map Id (Position, Position)
 }
 
 newAnalysisSpec token = AnalysisSpec {
     asScript = token,
     asShellType = Nothing,
     asExecutionMode = Executed,
-    asCheckSourced = False
+    asCheckSourced = False,
+    asTokenPositions = Map.empty
 }
 
 newtype AnalysisResult = AnalysisResult {
@@ -145,12 +156,14 @@
 }
 
 -- Formatter options
-newtype FormatterOptions = FormatterOptions {
-    foColorOption :: ColorOption
+data FormatterOptions = FormatterOptions {
+    foColorOption :: ColorOption,
+    foWikiLinkCount :: Integer
 }
 
 newFormatterOptions = FormatterOptions {
-    foColorOption = ColorAuto
+    foColorOption = ColorAuto,
+    foWikiLinkCount = 3
 }
 
 
@@ -161,12 +174,13 @@
 type ErrorMessage = String
 type Code = Integer
 
-data Severity = ErrorC | WarningC | InfoC | StyleC deriving (Show, Eq, Ord)
+data Severity = ErrorC | WarningC | InfoC | StyleC
+    deriving (Show, Eq, Ord, Generic, NFData)
 data Position = Position {
     posFile :: String,    -- Filename
     posLine :: Integer,   -- 1 based source line
     posColumn :: Integer  -- 1 based source column, where tabs are 8
-} deriving (Show, Eq)
+} deriving (Show, Eq, Generic, NFData)
 
 newPosition :: Position
 newPosition = Position {
@@ -179,7 +193,7 @@
     cSeverity :: Severity,
     cCode     :: Code,
     cMessage  :: String
-} deriving (Show, Eq)
+} deriving (Show, Eq, Generic, NFData)
 
 newComment :: Comment
 newComment = Comment {
@@ -188,27 +202,52 @@
     cMessage  = ""
 }
 
+-- only support single line for now
+data Replacement = Replacement {
+    repStartPos :: Position,
+    repEndPos :: Position,
+    repString :: String
+} deriving (Show, Eq, Generic, NFData)
+
+newReplacement = Replacement {
+    repStartPos = newPosition,
+    repEndPos = newPosition,
+    repString = ""
+}
+
+data Fix = Fix {
+    fixReplacements :: [Replacement]
+} deriving (Show, Eq, Generic, NFData)
+
+newFix = Fix {
+    fixReplacements = []
+}
+
 data PositionedComment = PositionedComment {
     pcStartPos :: Position,
     pcEndPos   :: Position,
-    pcComment  :: Comment
-} deriving (Show, Eq)
+    pcComment  :: Comment,
+    pcFix      :: Maybe Fix
+} deriving (Show, Eq, Generic, NFData)
 
 newPositionedComment :: PositionedComment
 newPositionedComment = PositionedComment {
     pcStartPos = newPosition,
     pcEndPos   = newPosition,
-    pcComment  = newComment
+    pcComment  = newComment,
+    pcFix      = Nothing
 }
 
 data TokenComment = TokenComment {
     tcId :: Id,
-    tcComment :: Comment
-} deriving (Show, Eq)
+    tcComment :: Comment,
+    tcFix :: Maybe Fix
+} deriving (Show, Eq, Generic, NFData)
 
 newTokenComment = TokenComment {
     tcId = Id 0,
-    tcComment = newComment
+    tcComment = newComment,
+    tcFix = Nothing
 }
 
 data ColorOption =
diff --git a/src/ShellCheck/Parser.hs b/src/ShellCheck/Parser.hs
index 69c8788..98bf17e 100644
--- a/src/ShellCheck/Parser.hs
+++ b/src/ShellCheck/Parser.hs
@@ -628,8 +628,8 @@
             readSingleQuoted,
             readDoubleQuoted,
             readDollarExpression,
-            readNormalLiteral "( ",
-            readPipeLiteral,
+            readLiteralForParser $ readNormalLiteral "( ",
+            readLiteralString "|",
             readGlobLiteral
             ]
         readGlobLiteral = do
@@ -639,19 +639,19 @@
             return $ T_Literal id [s]
         readGroup = called "regex grouping" $ do
             start <- startSpan
-            char '('
+            p1 <- readLiteralString "("
             parts <- many (readPart <|> readRegexLiteral)
-            char ')'
+            p2 <- readLiteralString ")"
             id <- endSpan start
-            return $ T_NormalWord id parts
+            return $ T_NormalWord id (p1:(parts ++ [p2]))
         readRegexLiteral = do
             start <- startSpan
             str <- readGenericLiteral1 (singleQuote <|> doubleQuotable <|> oneOf "()")
             id <- endSpan start
             return $ T_Literal id str
-        readPipeLiteral = do
+        readLiteralString s = do
             start <- startSpan
-            str <- string "|"
+            str <- string s
             id <- endSpan start
             return $ T_Literal id str
 
@@ -1937,7 +1937,14 @@
     word <- readNormalWord
     return $ T_HereString id word
 
-readNewlineList = many1 ((linefeed <|> carriageReturn) `thenSkip` spacing)
+readNewlineList =
+    many1 ((linefeed <|> carriageReturn) `thenSkip` spacing) <* checkBadBreak
+  where
+    checkBadBreak = optional $ do
+                pos <- getPosition
+                try $ lookAhead (oneOf "|&") --  See if the next thing could be |, || or &&
+                parseProblemAt pos ErrorC 1133
+                    "Unexpected start of line. If breaking lines, |/||/&& should be at the end of the previous one."
 readLineBreak = optional readNewlineList
 
 -- |
@@ -2333,7 +2340,7 @@
     allspacing
     list <- readCompoundList
     allspacing
-    char ')' <|> fail ") closing the subshell"
+    char ')' <|> fail "Expected ) closing the subshell"
     id <- endSpan start
     return $ T_Subshell id list
 
@@ -2696,6 +2703,13 @@
   where
     readUntil endPos = anyChar `reluctantlyTill` (getPosition >>= guard . (== endPos))
 
+-- Like readStringForParser, returning the span as a T_Literal
+readLiteralForParser parser = do
+    start <- startSpan
+    str <- readStringForParser parser
+    id <- endSpan start
+    return $ T_Literal id str
+
 -- |
 -- >>> prop $ isOk readAssignmentWord "a=42"
 -- >>> prop $ isOk readAssignmentWord "b=(1 2 3)"
@@ -2715,6 +2729,7 @@
 -- >>> prop $ isOk readAssignmentWord "var=( (1 2) (3 4) )"
 -- >>> prop $ isOk readAssignmentWord "var=( 1 [2]=(3 4) )"
 -- >>> prop $ isOk readAssignmentWord "var=(1 [2]=(3 4))"
+
 readAssignmentWord = readAssignmentWordExt True
 readWellFormedAssignment = readAssignmentWordExt False
 readAssignmentWordExt lenient = try $ do
@@ -2744,9 +2759,10 @@
         when (hasLeftSpace || hasRightSpace) $
             parseNoteAt pos ErrorC 1068 $
                 "Don't put spaces around the "
-                ++ if op == Append
-                    then "+= when appending."
-                    else "= in assignments."
+                ++ (if op == Append
+                    then "+= when appending"
+                    else "= in assignments")
+                ++ " (or quote to make it literal)."
         value <- readArray <|> readNormalWord
         spacing
         return $ T_Assignment id op variable indices value
diff --git a/striptests b/striptests
index cef3f49..fc3db44 100755
--- a/striptests
+++ b/striptests
@@ -1,20 +1,2 @@
 #!/usr/bin/env bash
-# This file *stripped* all unit tests from ShellCheck, removing
-# the dependency on QuickCheck and Template Haskell and
-# reduces the binary size considerably.
-#
-# Currently it only checks that run from right directory and
-# there aren't local git changes
-set -o pipefail
-
-if [[ ! -e ShellCheck.cabal ]]
-then
-  echo "Run me from the ShellCheck directory." >&2
-  exit 1
-fi
-
-if [[ -d '.git' ]] && ! git diff --exit-code > /dev/null 2>&1
-then
-  echo "You have local changes! These may be overwritten." >&2
-  exit 2
-fi
+# This file was deprecated by the doctest build.
diff --git a/test/buildtest b/test/buildtest
new file mode 100755
index 0000000..e3aa1eb
--- /dev/null
+++ b/test/buildtest
@@ -0,0 +1,35 @@
+#!/bin/bash
+# This script configures, builds and runs tests.
+# It's meant for automatic cross-distro testing.
+
+die() { echo "$*" >&2; exit 1; }
+
+[ -e "ShellCheck.cabal" ] ||
+  die "ShellCheck.cabal not in current dir"
+command -v cabal ||
+  die "cabal is missing"
+
+cabal update ||
+  die "can't update"
+cabal install --dependencies-only --enable-tests ||
+  die "can't install dependencies"
+cabal configure --enable-tests ||
+  die "configure failed"
+cabal build ||
+  die "build failed"
+cabal test ||
+  die "test failed"
+
+dist/build/shellcheck/shellcheck - << 'EOF' || die "execution failed"
+#!/bin/sh
+echo "Hello World"
+EOF
+
+dist/build/shellcheck/shellcheck - << 'EOF' && die "negative execution failed"
+#!/bin/sh
+echo $1
+EOF
+
+
+echo "Success"
+exit 0
diff --git a/test/distrotest b/test/distrotest
new file mode 100755
index 0000000..5024054
--- /dev/null
+++ b/test/distrotest
@@ -0,0 +1,80 @@
+#!/bin/bash
+# This script runs 'buildtest' on each of several distros
+# via Docker.
+set -o pipefail
+
+exec 3>&1 4>&2
+die() { echo "$*" >&4; exit 1; }
+
+[ -e "ShellCheck.cabal" ] || die "ShellCheck.cabal not in this dir"
+
+[ "$1" = "--run" ] || {
+cat << EOF
+This script pulls multiple distros via Docker and compiles
+ShellCheck and dependencies for each one. It takes hours,
+and is still highly experimental.
+
+Make sure you're plugged in and have screen/tmux in place,
+then re-run with $0 --run to continue.
+
+Also note that 'dist' will be deleted.
+EOF
+exit 0
+}
+
+echo "Deleting 'dist'..."
+rm -rf dist
+
+log=$(mktemp) || die "Can't create temp file"
+date >> "$log" || die "Can't write to log"
+
+echo "Logging to $log" >&3
+exec >> "$log" 2>&1
+
+final=0
+while read -r distro setup
+do
+  [[ "$distro" = "#"* || -z "$distro" ]] && continue
+  printf '%s ' "$distro" >&3
+  docker pull "$distro" || die "Can't pull $distro"
+  printf 'pulled. ' >&3
+
+  tmp=$(mktemp -d) || die "Can't make temp dir"
+  cp -r . "$tmp/" || die "Can't populate test dir"
+  printf 'Result: ' >&3
+  < /dev/null docker run -v "$tmp:/mnt" "$distro" sh -c "
+    $setup
+    cd /mnt || exit 1
+    test/buildtest
+    "
+  ret=$?
+  if [ "$ret" = 0 ]
+  then
+    echo "OK" >&3
+  else
+    echo "FAIL with $ret. See $log" >&3
+    final=1
+  fi
+  rm -rf "$tmp"
+done << EOF
+# Docker tag          Setup command
+debian:stable         apt-get update && apt-get install -y cabal-install
+debian:testing        apt-get update && apt-get install -y cabal-install
+ubuntu:latest         apt-get update && apt-get install -y cabal-install
+opensuse:latest       zypper install -y cabal-install ghc
+
+# Older Ubuntu versions we want to support
+ubuntu:18.04          apt-get update && apt-get install -y cabal-install
+ubuntu:17.10          apt-get update && apt-get install -y cabal-install
+
+# Misc Haskell including current and latest Stack build
+ubuntu:18.10          set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
+haskell:latest        true
+
+# Known to currently fail
+centos:latest         yum install -y epel-release && yum install -y cabal-install
+fedora:latest         dnf install -y cabal-install
+base/archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
+EOF
+
+exit "$final"
diff --git a/test/stacktest b/test/stacktest
new file mode 100755
index 0000000..dc0113f
--- /dev/null
+++ b/test/stacktest
@@ -0,0 +1,27 @@
+#!/bin/bash
+# This script builds ShellCheck through `stack` using
+# various resolvers. It's run via distrotest.
+
+resolvers=(
+  nightly-"$(date -d "3 days ago" +"%Y-%m-%d")"
+)
+
+die() { echo "$*" >&2; exit 1; }
+
+[ -e "ShellCheck.cabal" ] ||
+  die "ShellCheck.cabal not in current dir"
+[ -e "stack.yaml" ] ||
+  die "stack.yaml not in current dir"
+command -v stack ||
+  die "stack is missing"
+
+stack setup        || die "Failed to setup with default resolver"
+stack build --test || die "Failed to build/test with default resolver"
+
+for resolver in "${resolvers[@]}"
+do
+  stack --resolver="$resolver" setup        || die "Failed to setup $resolver"
+  stack --resolver="$resolver" build --test || die "Failed build/test with $resolver!"
+done
+
+echo "Success"