diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -1,6 +1,6 @@
-{-# LANGUAGE TemplateHaskell #-}
 {-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE TemplateHaskell #-}
 
 module Main where
 
@@ -8,22 +8,44 @@
 import qualified Data.List.NonEmpty as NonEmpty
 import Data.Semigroup ((<>))
 import qualified Data.Set as Set
-import Data.String
+import Data.String (IsString (fromString))
 import qualified Data.Version
 import qualified Development.GitRev
-import Options.Applicative hiding (ParseError)
-import qualified Paths_hadolint -- version from hadolint.cabal file
-import System.Exit (exitFailure, exitSuccess)
-
 import qualified Hadolint
+import Options.Applicative
+  ( Parser,
+    action,
+    argument,
+    completeWith,
+    execParser,
+    fullDesc,
+    header,
+    help,
+    helper,
+    info,
+    long,
+    maybeReader,
+    metavar,
+    option,
+    progDesc,
+    short,
+    showDefaultWith,
+    str,
+    strOption,
+    switch,
+    value,
+  )
+-- version from hadolint.cabal file
+import qualified Paths_hadolint as Meta
+import System.Exit (exitFailure, exitSuccess)
 
 data CommandOptions = CommandOptions
-    { showVersion :: Bool
-    , configFile :: Maybe FilePath
-    , format :: Hadolint.OutputFormat
-    , dockerfiles :: [String]
-    , lintingOptions :: Hadolint.LintOptions
-    }
+  { showVersion :: Bool,
+    configFile :: Maybe FilePath,
+    format :: Hadolint.OutputFormat,
+    dockerfiles :: [String],
+    lintingOptions :: Hadolint.LintOptions
+  }
 
 toOutputFormat :: String -> Maybe Hadolint.OutputFormat
 toOutputFormat "json" = Just Hadolint.Json
@@ -42,81 +64,85 @@
 
 parseOptions :: Parser CommandOptions
 parseOptions =
-    CommandOptions <$> -- CLI options parser definition
-    version <*>
-    configFile <*>
-    outputFormat <*>
-    files <*>
-    lintOptions
+  CommandOptions
+    <$> version -- CLI options parser definition
+    <*> configFile
+    <*> outputFormat
+    <*> files
+    <*> lintOptions
   where
     version = switch (long "version" <> short 'v' <> help "Show version")
-    --
-    -- | Parse the config filename to use
+
     configFile =
-        optional
-            (strOption
-                 (long "config" <> short 'c' <> metavar "FILENAME" <>
-                  help "Path to the configuration file"))
-    --
-    -- | Parse the output format option
+      optional
+        ( strOption
+            ( long "config" <> short 'c' <> metavar "FILENAME"
+                <> help "Path to the configuration file"
+            )
+        )
+
     outputFormat =
-        option
-            (maybeReader toOutputFormat)
-            (long "format" <> -- options for the output format
-             short 'f' <>
-             help
-                 "The output format for the results [tty | json | checkstyle | codeclimate | codacy]" <>
-             value Hadolint.TTY <> -- The default value
-             showDefaultWith showFormat <>
-             completeWith ["tty", "json", "checkstyle", "codeclimate", "codacy"])
-    --
-    -- | Parse a list of ignored rules
+      option
+        (maybeReader toOutputFormat)
+        ( long "format"
+            <> short 'f' -- options for the output format
+            <> help
+              "The output format for the results [tty | json | checkstyle | codeclimate | codacy]"
+            <> value Hadolint.TTY
+            <> showDefaultWith showFormat -- The default value
+            <> completeWith ["tty", "json", "checkstyle", "codeclimate", "codacy"]
+        )
+
     ignoreList =
-        many
-            (strOption
-                 (long "ignore" <>
-                  help "A rule to ignore. If present, the ignore list in the config file is ignored" <>
-                  metavar "RULECODE"))
-    --
-    -- | Parse a list of dockerfile names
+      many
+        ( strOption
+            ( long "ignore"
+                <> help "A rule to ignore. If present, the ignore list in the config file is ignored"
+                <> metavar "RULECODE"
+            )
+        )
+
     files = many (argument str (metavar "DOCKERFILE..." <> action "file"))
-    --
-    -- | Parse the rule ignore list and the rules configuration into a LintOptions
+
     lintOptions = Hadolint.LintOptions <$> ignoreList <*> parseRulesConfig
-    --
-    -- | Parse all the optional rules configuration
+
     parseRulesConfig =
-        Hadolint.RulesConfig . Set.fromList . fmap fromString <$>
-        many
-            (strOption
-                 (long "trusted-registry" <>
-                  help "A docker registry to allow to appear in FROM instructions" <>
-                  metavar "REGISTRY (e.g. docker.io)"))
+      Hadolint.RulesConfig . Set.fromList . fmap fromString
+        <$> many
+          ( strOption
+              ( long "trusted-registry"
+                  <> help "A docker registry to allow to appear in FROM instructions"
+                  <> metavar "REGISTRY (e.g. docker.io)"
+              )
+          )
 
 main :: IO ()
 main = do
-    cmd <- execParser opts
-    execute cmd
+  cmd <- execParser opts
+  execute cmd
   where
     execute CommandOptions {showVersion = True} = putStrLn getVersion >> exitSuccess
     execute CommandOptions {dockerfiles = []} =
-        putStrLn "Please provide a Dockerfile" >> exitFailure
+      putStrLn "Please provide a Dockerfile" >> exitFailure
     execute cmd = do
-        lintConfig <- Hadolint.applyConfig (configFile cmd) (lintingOptions cmd)
-        let files = NonEmpty.fromList (dockerfiles cmd)
-        case lintConfig of
-            Left err -> error err
-            Right conf -> do
-                res <- Hadolint.lint conf files
-                Hadolint.printResultsAndExit (format cmd) res
+      lintConfig <- Hadolint.applyConfig (configFile cmd) (lintingOptions cmd)
+      let files = NonEmpty.fromList (dockerfiles cmd)
+      case lintConfig of
+        Left err -> error err
+        Right conf -> do
+          res <- Hadolint.lint conf files
+          Hadolint.printResultsAndExit (format cmd) res
     opts =
-        info
-            (helper <*> parseOptions)
-            (fullDesc <> progDesc "Lint Dockerfile for errors and best practices" <>
-             header "hadolint - Dockerfile Linter written in Haskell")
+      info
+        (helper <*> parseOptions)
+        ( fullDesc <> progDesc "Lint Dockerfile for errors and best practices"
+            <> header "hadolint - Dockerfile Linter written in Haskell"
+        )
 
 getVersion :: String
 getVersion
-    | $(Development.GitRev.gitDescribe) == "UNKNOWN" =
-        "Haskell Dockerfile Linter " ++ Data.Version.showVersion Paths_hadolint.version ++ "-no-git"
-    | otherwise = "Haskell Dockerfile Linter " ++ $(Development.GitRev.gitDescribe)
+  | version == "UNKNOWN" =
+    "Haskell Dockerfile Linter " ++ Data.Version.showVersion Meta.version ++ "-no-git"
+  | otherwise = "Haskell Dockerfile Linter " ++ version
+  where
+    version = $(Development.GitRev.gitDescribe)
diff --git a/hadolint.cabal b/hadolint.cabal
--- a/hadolint.cabal
+++ b/hadolint.cabal
@@ -1,13 +1,13 @@
-cabal-version: 1.12
+cabal-version: 2.0
 
 -- This file has been generated from package.yaml by hpack version 0.33.0.
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: f2454ac92e402e2007f598a079c8998996473f8fc6501b81e3eb1f9cd98d5f56
+-- hash: 418683858197c94a7d90196b639708548b3c5966bc20506c43b2383a836a281b
 
 name:           hadolint
-version:        1.18.1
+version:        1.18.2
 synopsis:       Dockerfile Linter JavaScript API
 description:    A smarter Dockerfile linter that helps you build best practice Docker images.
 category:       Development
@@ -45,6 +45,8 @@
       Hadolint.Shell
   other-modules:
       Paths_hadolint
+  autogen-modules:
+      Paths_hadolint
   hs-source-dirs:
       src
   ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path
@@ -52,6 +54,7 @@
       HsYAML
     , ShellCheck >=0.7.1
     , aeson
+    , async
     , base >=4.8 && <5
     , bytestring
     , containers
@@ -60,6 +63,7 @@
     , language-docker >=9.1.2 && <10
     , megaparsec >=7.0
     , mtl
+    , parallel
     , split >=0.2
     , text
     , void
@@ -71,7 +75,7 @@
       Paths_hadolint
   hs-source-dirs:
       app
-  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path
+  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path -O2 -threaded -rtsopts "-with-rtsopts=-N5 -A4m"
   build-depends:
       base >=4.8 && <5
     , containers
diff --git a/src/Hadolint.hs b/src/Hadolint.hs
--- a/src/Hadolint.hs
+++ b/src/Hadolint.hs
@@ -1,8 +1,9 @@
 module Hadolint
-    ( module Hadolint.Lint
-    , module Hadolint.Rules
-    , module Hadolint.Config
-    ) where
+  ( module Hadolint.Lint,
+    module Hadolint.Rules,
+    module Hadolint.Config,
+  )
+where
 
 import Hadolint.Config
 import Hadolint.Lint
diff --git a/src/Hadolint/Config.hs b/src/Hadolint/Config.hs
--- a/src/Hadolint/Config.hs
+++ b/src/Hadolint/Config.hs
@@ -1,35 +1,39 @@
 {-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE OverloadedStrings #-}
 
-module Hadolint.Config (applyConfig, ConfigFile(..)) where
+module Hadolint.Config (applyConfig, ConfigFile (..)) where
 
 import Control.Monad (filterM)
+import qualified Data.ByteString as Bytes
 import Data.Coerce (coerce)
 import Data.Maybe (fromMaybe, listToMaybe)
-import qualified Data.ByteString as Bytes
 import qualified Data.Set as Set
-import qualified Data.YAML as Yaml
 import Data.YAML ((.:?))
-import GHC.Generics
+import qualified Data.YAML as Yaml
+import GHC.Generics (Generic)
+import qualified Hadolint.Lint as Lint
+import qualified Hadolint.Rules as Rules
 import qualified Language.Docker as Docker
 import System.Directory
-       (XdgDirectory(..), doesFileExist, getCurrentDirectory,
-        getXdgDirectory)
+  ( XdgDirectory (..),
+    doesFileExist,
+    getCurrentDirectory,
+    getXdgDirectory,
+  )
 import System.FilePath ((</>))
 
-import qualified Hadolint.Lint as Lint
-import qualified Hadolint.Rules as Rules
-
 data ConfigFile = ConfigFile
-    { ignoredRules :: Maybe [Lint.IgnoreRule]
-    , trustedRegistries :: Maybe [Lint.TrustedRegistry]
-    } deriving (Show, Eq, Generic)
+  { ignoredRules :: Maybe [Lint.IgnoreRule],
+    trustedRegistries :: Maybe [Lint.TrustedRegistry]
+  }
+  deriving (Show, Eq, Generic)
 
 instance Yaml.FromYAML ConfigFile where
-  parseYAML = Yaml.withMap "ConfigFile" $ \m -> ConfigFile
-       <$> m .:? "ignored"
-       <*> m .:? "trustedRegistries"
+  parseYAML = Yaml.withMap "ConfigFile" $ \m ->
+    ConfigFile
+      <$> m .:? "ignored"
+      <*> m .:? "trustedRegistries"
 
 -- | If both the ignoreRules and rulesConfig properties of Lint options are empty
 -- then this function will fill them with the default found in the passed config
@@ -37,50 +41,53 @@
 -- return the error string.
 applyConfig :: Maybe FilePath -> Lint.LintOptions -> IO (Either String Lint.LintOptions)
 applyConfig maybeConfig o
-    | not (null (Lint.ignoreRules o)) && Lint.rulesConfig o /= mempty = return (Right o)
-    | otherwise = do
-        theConfig <-
-            case maybeConfig of
-                Nothing -> findConfig
-                c -> return c
-        case theConfig of
-            Nothing -> return (Right o)
-            Just config -> parseAndApply config
+  | not (null (Lint.ignoreRules o)) && Lint.rulesConfig o /= mempty = return (Right o)
+  | otherwise = do
+    theConfig <-
+      case maybeConfig of
+        Nothing -> findConfig
+        c -> return c
+    case theConfig of
+      Nothing -> return (Right o)
+      Just config -> parseAndApply config
   where
     findConfig = do
-        localConfigFile <- (</> ".hadolint.yaml") <$> getCurrentDirectory
-        configFile <- getXdgDirectory XdgConfig "hadolint.yaml"
-        listToMaybe <$> filterM doesFileExist [localConfigFile, configFile]
+      localConfigFile <- (</> ".hadolint.yaml") <$> getCurrentDirectory
+      configFile <- getXdgDirectory XdgConfig "hadolint.yaml"
+      listToMaybe <$> filterM doesFileExist [localConfigFile, configFile]
+
     parseAndApply :: FilePath -> IO (Either String Lint.LintOptions)
     parseAndApply configFile = do
-        contents <- Bytes.readFile configFile
-        case Yaml.decode1Strict contents of
-            Left (_, err) -> return $ Left (formatError err configFile)
-            Right (ConfigFile ignore trusted) -> return (Right (override ignore trusted))
-    -- | Applies the configuration found in the file to the passed Lint.LintOptions
+      contents <- Bytes.readFile configFile
+      case Yaml.decode1Strict contents of
+        Left (_, err) -> return $ Left (formatError err configFile)
+        Right (ConfigFile ignore trusted) -> return (Right (override ignore trusted))
+
     override ignore trusted = applyTrusted trusted . applyIgnore ignore $ o
     applyIgnore ignore opts =
-        case Lint.ignoreRules opts of
-            [] -> opts {Lint.ignoreRules = fromMaybe [] ignore}
-            _ -> opts
+      case Lint.ignoreRules opts of
+        [] -> opts {Lint.ignoreRules = fromMaybe [] ignore}
+        _ -> opts
+
     applyTrusted trusted opts
-        | null (Rules.allowedRegistries (Lint.rulesConfig opts)) =
-            opts {Lint.rulesConfig = toRules trusted <> Lint.rulesConfig opts}
-        | otherwise = opts
-    -- | Converts a list of TrustedRegistry to a RulesConfig record
+      | null (Rules.allowedRegistries (Lint.rulesConfig opts)) =
+        opts {Lint.rulesConfig = toRules trusted <> Lint.rulesConfig opts}
+      | otherwise = opts
+
     toRules (Just trusted) = Rules.RulesConfig (Set.fromList . coerce $ trusted)
     toRules _ = mempty
+
     formatError err config =
       unlines
-          [ "Error parsing your config file in  '" ++ config ++ "':"
-          , "It should contain one of the keys 'ignored' or 'trustedRegistries'. For example:\n"
-          , "ignored:"
-          , "\t- DL3000"
-          , "\t- SC1099\n\n"
-          , "The key 'trustedRegistries' should contain the names of the allowed docker registries:\n"
-          , "allowedRegistries:"
-          , "\t- docker.io"
-          , "\t- my-company.com"
-          , ""
-          , err
-          ]
+        [ "Error parsing your config file in  '" ++ config ++ "':",
+          "It should contain one of the keys 'ignored' or 'trustedRegistries'. For example:\n",
+          "ignored:",
+          "\t- DL3000",
+          "\t- SC1099\n\n",
+          "The key 'trustedRegistries' should contain the names of the allowed docker registries:\n",
+          "allowedRegistries:",
+          "\t- docker.io",
+          "\t- my-company.com",
+          "",
+          err
+        ]
diff --git a/src/Hadolint/Formatter/Checkstyle.hs b/src/Hadolint/Formatter/Checkstyle.hs
--- a/src/Hadolint/Formatter/Checkstyle.hs
+++ b/src/Hadolint/Formatter/Checkstyle.hs
@@ -1,74 +1,75 @@
-{-# LANGUAGE RecordWildCards #-}
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
 
 module Hadolint.Formatter.Checkstyle
-    ( printResult
-    , formatResult
-    ) where
+  ( printResult,
+    formatResult,
+  )
+where
 
 import qualified Data.ByteString.Builder as Builder
 import qualified Data.ByteString.Lazy.Char8 as B
 import Data.Char
 import Data.Foldable (toList)
 import Data.List (groupBy)
-import Data.Monoid ((<>), mconcat)
+import Data.Monoid (mconcat, (<>))
 import qualified Data.Text as Text
 import Hadolint.Formatter.Format
-import Hadolint.Rules (Metadata(..), RuleCheck(..))
+import Hadolint.Rules (Metadata (..), RuleCheck (..))
 import ShellCheck.Interface
 import Text.Megaparsec (Stream)
 import Text.Megaparsec.Error
 import Text.Megaparsec.Pos (sourceColumn, sourceLine, sourceName, unPos)
 
 data CheckStyle = CheckStyle
-    { file :: String
-    , line :: Int
-    , column :: Int
-    , impact :: String
-    , msg :: String
-    , source :: String
-    }
+  { file :: String,
+    line :: Int,
+    column :: Int,
+    impact :: String,
+    msg :: String,
+    source :: String
+  }
 
 errorToCheckStyle :: (Stream s, ShowErrorComponent e) => ParseErrorBundle s e -> CheckStyle
 errorToCheckStyle err =
-    CheckStyle
-        { file = sourceName pos
-        , line = unPos (sourceLine pos)
-        , column = unPos (sourceColumn pos)
-        , impact = severityText ErrorC
-        , msg = errorBundlePretty err
-        , source = "DL1000"
-        }
+  CheckStyle
+    { file = sourceName pos,
+      line = unPos (sourceLine pos),
+      column = unPos (sourceColumn pos),
+      impact = severityText ErrorC,
+      msg = errorBundlePretty err,
+      source = "DL1000"
+    }
   where
     pos = errorPosition err
 
 ruleToCheckStyle :: RuleCheck -> CheckStyle
 ruleToCheckStyle RuleCheck {..} =
-    CheckStyle
-        { file = Text.unpack filename
-        , line = linenumber
-        , column = 1
-        , impact = severityText (severity metadata)
-        , msg = Text.unpack (message metadata)
-        , source = Text.unpack (code metadata)
-        }
+  CheckStyle
+    { file = Text.unpack filename,
+      line = linenumber,
+      column = 1,
+      impact = severityText (severity metadata),
+      msg = Text.unpack (message metadata),
+      source = Text.unpack (code metadata)
+    }
 
 toXml :: [CheckStyle] -> Builder.Builder
 toXml checks = wrap fileName (foldMap convert checks)
   where
     wrap name innerNode = "<file " <> attr "name" name <> ">" <> innerNode <> "</file>"
     convert CheckStyle {..} =
-        "<error " <> -- Beging the node construction
-        attr "line" (show line) <>
-        attr "column" (show column) <>
-        attr "severity" impact <>
-        attr "message" msg <>
-        attr "source" source <>
-        "/>"
+      "<error "
+        <> attr "line" (show line) -- Beging the node construction
+        <> attr "column" (show column)
+        <> attr "severity" impact
+        <> attr "message" msg
+        <> attr "source" source
+        <> "/>"
     fileName =
-        case checks of
-            [] -> ""
-            h:_ -> file h
+      case checks of
+        [] -> ""
+        h : _ -> file h
 
 attr :: String -> String -> Builder.Builder
 attr name value = Builder.string8 name <> "='" <> Builder.string8 (escape value) <> "' "
@@ -77,14 +78,14 @@
 escape = concatMap doEscape
   where
     doEscape c =
-        if isOk c
-            then [c]
-            else "&#" ++ show (ord c) ++ ";"
+      if isOk c
+        then [c]
+        else "&#" ++ show (ord c) ++ ";"
     isOk x = any (\check -> check x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` [' ', '.', '/'])]
 
 formatResult :: (Stream s, ShowErrorComponent e) => Result s e -> Builder.Builder
 formatResult (Result errors checks) =
-    "<?xml version='1.0' encoding='UTF-8'?><checkstyle version='4.3'>" <> xmlBody <> "</checkstyle>"
+  "<?xml version='1.0' encoding='UTF-8'?><checkstyle version='4.3'>" <> xmlBody <> "</checkstyle>"
   where
     xmlBody = mconcat xmlChunks
     xmlChunks = fmap toXml (groupBy sameFileName flatten)
diff --git a/src/Hadolint/Formatter/Codacy.hs b/src/Hadolint/Formatter/Codacy.hs
--- a/src/Hadolint/Formatter/Codacy.hs
+++ b/src/Hadolint/Formatter/Codacy.hs
@@ -1,53 +1,54 @@
-{-# LANGUAGE RecordWildCards #-}
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
 
 module Hadolint.Formatter.Codacy
-    ( printResult
-    , formatResult
-    ) where
+  ( printResult,
+    formatResult,
+  )
+where
 
 import Data.Aeson hiding (Result)
 import qualified Data.ByteString.Lazy.Char8 as B
 import Data.Monoid ((<>))
 import Data.Sequence (Seq)
 import qualified Data.Text as Text
-import Hadolint.Formatter.Format (Result(..), errorPosition)
-import Hadolint.Rules (Metadata(..), RuleCheck(..))
+import Hadolint.Formatter.Format (Result (..), errorPosition)
+import Hadolint.Rules (Metadata (..), RuleCheck (..))
 import Text.Megaparsec (Stream)
 import Text.Megaparsec.Error
 import Text.Megaparsec.Pos (sourceLine, sourceName, unPos)
 
 data Issue = Issue
-    { filename :: String
-    , msg :: String
-    , patternId :: String
-    , line :: Int
-    }
+  { filename :: String,
+    msg :: String,
+    patternId :: String,
+    line :: Int
+  }
 
 instance ToJSON Issue where
-    toJSON Issue {..} =
-        object ["filename" .= filename, "patternId" .= patternId, "message" .= msg, "line" .= line]
+  toJSON Issue {..} =
+    object ["filename" .= filename, "patternId" .= patternId, "message" .= msg, "line" .= line]
 
 errorToIssue :: (Stream s, ShowErrorComponent e) => ParseErrorBundle s e -> Issue
 errorToIssue err =
-    Issue
-        { filename = sourceName pos
-        , patternId = "DL1000"
-        , msg = errorBundlePretty err
-        , line = linenumber
-        }
+  Issue
+    { filename = sourceName pos,
+      patternId = "DL1000",
+      msg = errorBundlePretty err,
+      line = linenumber
+    }
   where
     pos = errorPosition err
     linenumber = unPos (sourceLine pos)
 
 checkToIssue :: RuleCheck -> Issue
 checkToIssue RuleCheck {..} =
-    Issue
-        { filename = Text.unpack filename
-        , patternId = Text.unpack (code metadata)
-        , msg = Text.unpack (message metadata)
-        , line = linenumber
-        }
+  Issue
+    { filename = Text.unpack filename,
+      patternId = Text.unpack (code metadata),
+      msg = Text.unpack (message metadata),
+      line = linenumber
+    }
 
 formatResult :: (Stream s, ShowErrorComponent e) => Result s e -> Seq Issue
 formatResult (Result errors checks) = allIssues
diff --git a/src/Hadolint/Formatter/Codeclimate.hs b/src/Hadolint/Formatter/Codeclimate.hs
--- a/src/Hadolint/Formatter/Codeclimate.hs
+++ b/src/Hadolint/Formatter/Codeclimate.hs
@@ -1,11 +1,12 @@
-{-# LANGUAGE RecordWildCards #-}
-{-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
 
 module Hadolint.Formatter.Codeclimate
-    ( printResult
-    , formatResult
-    ) where
+  ( printResult,
+    formatResult,
+  )
+where
 
 import Data.Aeson hiding (Result)
 import qualified Data.ByteString.Lazy as B
@@ -13,57 +14,60 @@
 import Data.Sequence (Seq)
 import qualified Data.Text as Text
 import GHC.Generics
-import Hadolint.Formatter.Format (Result(..), errorPosition)
-import Hadolint.Rules (Metadata(..), RuleCheck(..))
+import Hadolint.Formatter.Format (Result (..), errorPosition)
+import Hadolint.Rules (Metadata (..), RuleCheck (..))
 import ShellCheck.Interface
 import Text.Megaparsec (Stream)
 import Text.Megaparsec.Error
 import Text.Megaparsec.Pos (sourceColumn, sourceLine, sourceName, unPos)
 
 data Issue = Issue
-    { checkName :: String
-    , description :: String
-    , location :: Location
-    , impact :: String
-    }
+  { checkName :: String,
+    description :: String,
+    location :: Location,
+    impact :: String
+  }
 
 data Location
-    = LocLine String
-              Int
-    | LocPos String
-             Pos
+  = LocLine
+      String
+      Int
+  | LocPos
+      String
+      Pos
 
 instance ToJSON Location where
-    toJSON (LocLine path l) = object ["path" .= path, "lines" .= object ["begin" .= l, "end" .= l]]
-    toJSON (LocPos path pos) =
-        object ["path" .= path, "positions" .= object ["begin" .= pos, "end" .= pos]]
+  toJSON (LocLine path l) = object ["path" .= path, "lines" .= object ["begin" .= l, "end" .= l]]
+  toJSON (LocPos path pos) =
+    object ["path" .= path, "positions" .= object ["begin" .= pos, "end" .= pos]]
 
 data Pos = Pos
-    { line :: Int
-    , column :: Int
-    } deriving (Generic)
+  { line :: Int,
+    column :: Int
+  }
+  deriving (Generic)
 
 instance ToJSON Pos
 
 instance ToJSON Issue where
-    toJSON Issue {..} =
-        object
-            [ "type" .= ("issue" :: String)
-            , "check_name" .= checkName
-            , "description" .= description
-            , "categories" .= (["Bug Risk"] :: [String])
-            , "location" .= location
-            , "severity" .= impact
-            ]
+  toJSON Issue {..} =
+    object
+      [ "type" .= ("issue" :: String),
+        "check_name" .= checkName,
+        "description" .= description,
+        "categories" .= (["Bug Risk"] :: [String]),
+        "location" .= location,
+        "severity" .= impact
+      ]
 
 errorToIssue :: (Stream s, ShowErrorComponent e) => ParseErrorBundle s e -> Issue
 errorToIssue err =
-    Issue
-        { checkName = "DL1000"
-        , description = errorBundlePretty err
-        , location = LocPos (sourceName pos) Pos {..}
-        , impact = severityText ErrorC
-        }
+  Issue
+    { checkName = "DL1000",
+      description = errorBundlePretty err,
+      location = LocPos (sourceName pos) Pos {..},
+      impact = severityText ErrorC
+    }
   where
     pos = errorPosition err
     line = unPos (sourceLine pos)
@@ -71,20 +75,20 @@
 
 checkToIssue :: RuleCheck -> Issue
 checkToIssue RuleCheck {..} =
-    Issue
-        { checkName = Text.unpack (code metadata)
-        , description = Text.unpack (message metadata)
-        , location = LocLine (Text.unpack filename) linenumber
-        , impact = severityText (severity metadata)
-        }
+  Issue
+    { checkName = Text.unpack (code metadata),
+      description = Text.unpack (message metadata),
+      location = LocLine (Text.unpack filename) linenumber,
+      impact = severityText (severity metadata)
+    }
 
 severityText :: Severity -> String
 severityText severity =
-    case severity of
-        ErrorC -> "blocker"
-        WarningC -> "major"
-        InfoC -> "info"
-        StyleC -> "minor"
+  case severity of
+    ErrorC -> "blocker"
+    WarningC -> "major"
+    InfoC -> "info"
+    StyleC -> "minor"
 
 formatResult :: (Stream s, ShowErrorComponent e) => Result s e -> Seq Issue
 formatResult (Result errors checks) = allIssues
@@ -97,5 +101,5 @@
 printResult result = mapM_ output (formatResult result)
   where
     output value = do
-        B.putStr (encode value)
-        B.putStr (B.singleton 0x00)
+      B.putStr (encode value)
+      B.putStr (B.singleton 0x00)
diff --git a/src/Hadolint/Formatter/Format.hs b/src/Hadolint/Formatter/Format.hs
--- a/src/Hadolint/Formatter/Format.hs
+++ b/src/Hadolint/Formatter/Format.hs
@@ -1,38 +1,39 @@
 module Hadolint.Formatter.Format
-    ( severityText
-    , stripNewlines
-    , errorMessageLine
-    , errorPosition
-    , errorPositionPretty
-    , Text.Megaparsec.Error.errorBundlePretty
-    , Result(..)
-    , isEmpty
-    , toResult
-    ) where
+  ( severityText,
+    stripNewlines,
+    errorMessageLine,
+    errorPosition,
+    errorPositionPretty,
+    Text.Megaparsec.Error.errorBundlePretty,
+    Result (..),
+    isEmpty,
+    toResult,
+  )
+where
 
 import Data.List (sort)
 import qualified Data.List.NonEmpty as NE
 import Data.Monoid (Monoid)
 import Data.Semigroup
-import qualified Data.Sequence as Seq
 import Data.Sequence (Seq)
+import qualified Data.Sequence as Seq
 import Hadolint.Rules
 import ShellCheck.Interface
-import Text.Megaparsec (Stream(..), pstateSourcePos)
+import Text.Megaparsec (Stream (..), pstateSourcePos)
 import Text.Megaparsec.Error
 import Text.Megaparsec.Pos (SourcePos, sourcePosPretty)
 
 data Result s e = Result
-    { errors :: Seq (ParseErrorBundle s e)
-    , checks :: Seq RuleCheck
-    }
+  { errors :: !(Seq (ParseErrorBundle s e)),
+    checks :: !(Seq RuleCheck)
+  }
 
 instance Semigroup (Result s e) where
-    (Result e1 c1) <> (Result e2 c2) = Result (e1 <> e2) (c1 <> c2)
+  (Result e1 c1) <> (Result e2 c2) = Result (e1 <> e2) (c1 <> c2)
 
 instance Monoid (Result s e) where
-    mappend = (<>)
-    mempty = Result mempty mempty
+  mappend = (<>)
+  mempty = Result mempty mempty
 
 isEmpty :: Result s e -> Bool
 isEmpty (Result Seq.Empty Seq.Empty) = True
@@ -40,33 +41,35 @@
 
 toResult :: Either (ParseErrorBundle s e) [RuleCheck] -> Result s e
 toResult res =
-    case res of
-        Left err -> Result (Seq.singleton err) mempty
-        Right c -> Result mempty (Seq.fromList (sort c))
+  case res of
+    Left err -> Result (Seq.singleton err) mempty
+    Right c -> Result mempty (Seq.fromList (sort c))
 
 severityText :: Severity -> String
 severityText s =
-    case s of
-        ErrorC -> "error"
-        WarningC -> "warning"
-        InfoC -> "info"
-        StyleC -> "style"
+  case s of
+    ErrorC -> "error"
+    WarningC -> "warning"
+    InfoC -> "info"
+    StyleC -> "style"
 
 stripNewlines :: String -> String
 stripNewlines =
-    map (\c ->
-             if c == '\n'
-                 then ' '
-                 else c)
+  map
+    ( \c ->
+        if c == '\n'
+          then ' '
+          else c
+    )
 
 errorMessageLine :: (Stream s, ShowErrorComponent e) => ParseErrorBundle s e -> String
 errorMessageLine err@(ParseErrorBundle e _) =
-    errorPositionPretty err ++ " " ++ parseErrorTextPretty (NE.head e)
+  errorPositionPretty err ++ " " ++ parseErrorTextPretty (NE.head e)
 
 errorPositionPretty :: Stream s => ParseErrorBundle s e -> String
 errorPositionPretty err = sourcePosPretty (errorPosition err)
 
 errorPosition :: Stream s => ParseErrorBundle s e -> Text.Megaparsec.Pos.SourcePos
 errorPosition (ParseErrorBundle e s) =
-    let (_, posState) = reachOffset (errorOffset (NE.head e)) s
-     in pstateSourcePos posState
+  let (_, posState) = reachOffset (errorOffset (NE.head e)) s
+   in pstateSourcePos posState
diff --git a/src/Hadolint/Formatter/Json.hs b/src/Hadolint/Formatter/Json.hs
--- a/src/Hadolint/Formatter/Json.hs
+++ b/src/Hadolint/Formatter/Json.hs
@@ -1,46 +1,47 @@
-{-# LANGUAGE RecordWildCards #-}
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
 
 module Hadolint.Formatter.Json
-    ( printResult
-    , formatResult
-    ) where
+  ( printResult,
+    formatResult,
+  )
+where
 
 import Data.Aeson hiding (Result)
 import qualified Data.ByteString.Lazy.Char8 as B
 import Data.Monoid ((<>))
-import Hadolint.Formatter.Format (Result(..), errorPosition, severityText)
-import Hadolint.Rules (Metadata(..), RuleCheck(..))
+import Hadolint.Formatter.Format (Result (..), errorPosition, severityText)
+import Hadolint.Rules (Metadata (..), RuleCheck (..))
 import ShellCheck.Interface
 import Text.Megaparsec (Stream)
 import Text.Megaparsec.Error
 import Text.Megaparsec.Pos (sourceColumn, sourceLine, sourceName, unPos)
 
 data JsonFormat s e
-    = JsonCheck RuleCheck
-    | JsonParseError (ParseErrorBundle s e)
+  = JsonCheck RuleCheck
+  | JsonParseError (ParseErrorBundle s e)
 
 instance (Stream s, ShowErrorComponent e) => ToJSON (JsonFormat s e) where
-    toJSON (JsonCheck RuleCheck {..}) =
-        object
-            [ "file" .= filename
-            , "line" .= linenumber
-            , "column" .= (1 :: Int)
-            , "level" .= severityText (severity metadata)
-            , "code" .= code metadata
-            , "message" .= message metadata
-            ]
-    toJSON (JsonParseError err) =
-        object
-            [ "file" .= sourceName pos
-            , "line" .= unPos (sourceLine pos)
-            , "column" .= unPos (sourceColumn pos)
-            , "level" .= severityText ErrorC
-            , "code" .= ("DL1000" :: String)
-            , "message" .= errorBundlePretty err
-            ]
-      where
-        pos = errorPosition err
+  toJSON (JsonCheck RuleCheck {..}) =
+    object
+      [ "file" .= filename,
+        "line" .= linenumber,
+        "column" .= (1 :: Int),
+        "level" .= severityText (severity metadata),
+        "code" .= code metadata,
+        "message" .= message metadata
+      ]
+  toJSON (JsonParseError err) =
+    object
+      [ "file" .= sourceName pos,
+        "line" .= unPos (sourceLine pos),
+        "column" .= unPos (sourceColumn pos),
+        "level" .= severityText ErrorC,
+        "code" .= ("DL1000" :: String),
+        "message" .= errorBundlePretty err
+      ]
+    where
+      pos = errorPosition err
 
 formatResult :: (Stream s, ShowErrorComponent e) => Result s e -> Value
 formatResult (Result errors checks) = toJSON allMessages
diff --git a/src/Hadolint/Formatter/TTY.hs b/src/Hadolint/Formatter/TTY.hs
--- a/src/Hadolint/Formatter/TTY.hs
+++ b/src/Hadolint/Formatter/TTY.hs
@@ -2,17 +2,18 @@
 {-# LANGUAGE OverloadedStrings #-}
 
 module Hadolint.Formatter.TTY
-    ( printResult
-    , formatError
-    , formatChecks
-    ) where
+  ( printResult,
+    formatError,
+    formatChecks,
+  )
+where
 
 import Data.Semigroup ((<>))
 import qualified Data.Text as Text
 import Hadolint.Formatter.Format
 import Hadolint.Rules
 import Language.Docker.Syntax
-import Text.Megaparsec (Stream(..))
+import Text.Megaparsec (Stream (..))
 import Text.Megaparsec.Error
 
 formatErrors :: (Stream s, ShowErrorComponent e, Functor f) => f (ParseErrorBundle s e) -> f String
@@ -25,7 +26,7 @@
 formatChecks = fmap formatCheck
   where
     formatCheck (RuleCheck meta source line _) =
-        formatPos source line <> code meta <> " " <> message meta
+      formatPos source line <> code meta <> " " <> message meta
 
 formatPos :: Filename -> Linenumber -> Text.Text
 formatPos source line = source <> ":" <> Text.pack (show line) <> " "
diff --git a/src/Hadolint/Lint.hs b/src/Hadolint/Lint.hs
--- a/src/Hadolint/Lint.hs
+++ b/src/Hadolint/Lint.hs
@@ -2,13 +2,11 @@
 
 module Hadolint.Lint where
 
+import qualified Control.Concurrent.Async as Async
+import Control.Parallel.Strategies (parListChunk, rseq, using)
 import qualified Data.List.NonEmpty as NonEmpty
 import Data.Text (Text)
-import qualified Language.Docker as Docker
-import Language.Docker.Parser (DockerfileError, Error)
-import Language.Docker.Syntax (Dockerfile)
-import System.Exit (exitFailure, exitSuccess)
-
+import GHC.Conc (numCapabilities)
 import qualified Hadolint.Formatter.Checkstyle as Checkstyle
 import qualified Hadolint.Formatter.Codacy as Codacy
 import qualified Hadolint.Formatter.Codeclimate as Codeclimate
@@ -16,64 +14,67 @@
 import qualified Hadolint.Formatter.Json as Json
 import qualified Hadolint.Formatter.TTY as TTY
 import qualified Hadolint.Rules as Rules
+import qualified Language.Docker as Docker
+import Language.Docker.Parser (DockerfileError, Error)
+import Language.Docker.Syntax (Dockerfile)
+import System.Exit (exitFailure, exitSuccess)
 
 type IgnoreRule = Text
 
 type TrustedRegistry = Text
 
 data LintOptions = LintOptions
-    { ignoreRules :: [IgnoreRule]
-    , rulesConfig :: Rules.RulesConfig
-    } deriving (Show)
+  { ignoreRules :: [IgnoreRule],
+    rulesConfig :: Rules.RulesConfig
+  }
+  deriving (Show)
 
 data OutputFormat
-    = Json
-    | TTY
-    | CodeclimateJson
-    | Checkstyle
-    | Codacy
-    deriving (Show, Eq)
+  = Json
+  | TTY
+  | CodeclimateJson
+  | Checkstyle
+  | Codacy
+  deriving (Show, Eq)
 
 printResultsAndExit :: OutputFormat -> Format.Result Text DockerfileError -> IO ()
 printResultsAndExit format allResults = do
-    printResult allResults
-    if not . Format.isEmpty $ allResults
-        then exitFailure
-        else exitSuccess
+  printResult allResults
+  if not . Format.isEmpty $ allResults
+    then exitFailure
+    else exitSuccess
   where
     printResult res =
-        case format of
-            TTY -> TTY.printResult res
-            Json -> Json.printResult res
-            Checkstyle -> Checkstyle.printResult res
-            CodeclimateJson -> Codeclimate.printResult res >> exitSuccess
-            Codacy -> Codacy.printResult res >> exitSuccess
+      case format of
+        TTY -> TTY.printResult res
+        Json -> Json.printResult res
+        Checkstyle -> Checkstyle.printResult res
+        CodeclimateJson -> Codeclimate.printResult res >> exitSuccess
+        Codacy -> Codacy.printResult res >> exitSuccess
 
 -- | Performs the process of parsing the dockerfile and analyzing it with all the applicable
 -- rules, depending on the list of ignored rules.
 -- Depending on the preferred printing format, it will output the results to stdout
 lint :: LintOptions -> NonEmpty.NonEmpty String -> IO (Format.Result Text DockerfileError)
 lint LintOptions {ignoreRules = ignoreList, rulesConfig} dFiles = do
-    processedFiles <- mapM (lintDockerfile ignoreList) (NonEmpty.toList dFiles)
-    return (results processedFiles)
+  parsedFiles <- Async.mapConcurrently parseFile (NonEmpty.toList dFiles)
+  let results = lintAll parsedFiles `using` parListChunk (div numCapabilities 2) rseq
+  return $ mconcat results
   where
-    results = foldMap Format.toResult -- Parse and check rules for each dockerfile,
-                                      -- then convert them to a Result and combine with
-                                      -- the result of the previous dockerfile results
-    lintDockerfile ignoreRules dockerFile = do
-        ast <- parseFilename dockerFile
-        return (processedFile ast)
+    parseFile :: String -> IO (Either Error Dockerfile)
+    parseFile "-" = Docker.parseStdin
+    parseFile s = Docker.parseFile s
+
+    lintAll = fmap (lintDockerfile ignoreList)
+
+    lintDockerfile ignoreRules ast = processedFile ast
       where
-        processedFile = fmap processRules
+        processedFile = Format.toResult . fmap processRules
         processRules fileLines = filter ignoredRules (analyzeAll rulesConfig fileLines)
         ignoredRules = ignoreFilter ignoreRules
-        -- | Returns true if the rule should be ignored
+
         ignoreFilter :: [IgnoreRule] -> Rules.RuleCheck -> Bool
         ignoreFilter rules (Rules.RuleCheck (Rules.Metadata code _ _) _ _ _) = code `notElem` rules
-        -- | Support UNIX convention of passing "-" instead of "/dev/stdin"
-        parseFilename :: String -> IO (Either Error Dockerfile)
-        parseFilename "-" = Docker.parseStdin
-        parseFilename s = Docker.parseFile s
 
 -- | Returns the result of applying all the rules to the given dockerfile
 analyzeAll :: Rules.RulesConfig -> Dockerfile -> [Rules.RuleCheck]
diff --git a/src/Hadolint/Rules.hs b/src/Hadolint/Rules.hs
--- a/src/Hadolint/Rules.hs
+++ b/src/Hadolint/Rules.hs
@@ -6,24 +6,24 @@
 import Control.Arrow ((&&&))
 import Data.List (foldl', isInfixOf, isPrefixOf, mapAccumL, nub)
 import Data.List.NonEmpty (toList)
-import qualified Hadolint.Shell as Shell
-import Language.Docker.Syntax
-
 import qualified Data.Map as Map
 import Data.Semigroup (Semigroup, (<>))
 import qualified Data.Set as Set
 import qualified Data.Text as Text
 import Data.Void (Void)
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+import ShellCheck.Interface (Severity (..))
 import qualified ShellCheck.Interface
-import ShellCheck.Interface (Severity(..))
 import qualified Text.Megaparsec as Megaparsec
 import qualified Text.Megaparsec.Char as Megaparsec
 
 data Metadata = Metadata
-    { code :: Text.Text
-    , severity :: Severity
-    , message :: Text.Text
-    } deriving (Eq)
+  { code :: Text.Text,
+    severity :: Severity,
+    message :: Text.Text
+  }
+  deriving (Eq, Show)
 
 -- a check is the application of a rule on a specific part of code
 -- the enforced result and the affected position
@@ -31,26 +31,29 @@
 -- and simple to develop new rules
 -- line numbers in the negative range are meant for the global context
 data RuleCheck = RuleCheck
-    { metadata :: Metadata
-    , filename :: Filename
-    , linenumber :: Linenumber
-    , success :: Bool
-    } deriving (Eq)
+  { metadata :: Metadata,
+    filename :: Filename,
+    linenumber :: Linenumber,
+    success :: Bool
+  }
+  deriving (Eq, Show)
 
 -- | Contains the required parameters for optional rules
 newtype RulesConfig = RulesConfig
-    { allowedRegistries :: Set.Set Registry -- ^ The docker registries that are allowed in FROM
-    } deriving (Show, Eq)
+  { -- | The docker registries that are allowed in FROM
+    allowedRegistries :: Set.Set Registry
+  }
+  deriving (Show, Eq)
 
 instance Ord RuleCheck where
-    a `compare` b = linenumber a `compare` linenumber b
+  a `compare` b = linenumber a `compare` linenumber b
 
 instance Semigroup RulesConfig where
-    RulesConfig a <> RulesConfig b = RulesConfig (a <> b)
+  RulesConfig a <> RulesConfig b = RulesConfig (a <> b)
 
 instance Monoid RulesConfig where
-    mempty = RulesConfig mempty
-    mappend = (<>)
+  mempty = RulesConfig mempty
+  mappend = (<>)
 
 type IgnoreRuleParser = Megaparsec.Parsec Void Text.Text
 
@@ -59,8 +62,8 @@
 -- | A function to check individual dockerfile instructions.
 -- It gets the current state and a line number.
 -- It should return the new state and whether or not the check passes for the given instruction.
-type SimpleCheckerWithState state
-     = state -> Linenumber -> Instruction Shell.ParsedShell -> (state, Bool)
+type SimpleCheckerWithState state =
+  state -> Linenumber -> Instruction Shell.ParsedShell -> (state, Bool)
 
 -- | A function to check individual dockerfile instructions.
 -- It gets the current line number.
@@ -70,14 +73,14 @@
 -- | A function to check individual dockerfile instructions.
 -- It should return the new state and a list of Metadata records.
 -- Each Metadata record signifies a failing check for the given instruction.
-type CheckerWithState state
-     = state -> Linenumber -> Instruction Shell.ParsedShell -> (state, [Metadata])
+type CheckerWithState state =
+  state -> Linenumber -> Instruction Shell.ParsedShell -> (state, [Metadata])
 
 link :: Metadata -> Text.Text
 link (Metadata code _ _)
-    | "SC" `Text.isPrefixOf` code = "https://github.com/koalaman/shellcheck/wiki/" <> code
-    | "DL" `Text.isPrefixOf` code = "https://github.com/hadolint/hadolint/wiki/" <> code
-    | otherwise = "https://github.com/hadolint/hadolint"
+  | "SC" `Text.isPrefixOf` code = "https://github.com/koalaman/shellcheck/wiki/" <> code
+  | "DL" `Text.isPrefixOf` code = "https://github.com/hadolint/hadolint/wiki/" <> code
+  | otherwise = "https://github.com/hadolint/hadolint"
 
 -- a Rule takes a Dockerfile with parsed shell and returns the executed checks
 type Rule = ParsedFile -> [RuleCheck]
@@ -86,58 +89,58 @@
 -- for the according line number
 mapInstructions :: CheckerWithState state -> state -> Rule
 mapInstructions f initialState dockerfile =
-    let (_, results) = mapAccumL applyRule initialState dockerfile
-     in concat results
+  let (_, results) = mapAccumL applyRule initialState dockerfile
+   in concat results
   where
     applyRule state (InstructionPos (OnBuild i) source linenumber) =
-        applyWithState state source linenumber i -- All rules applying to instructions also apply to ONBUILD,
-                                                 -- so we unwrap the OnBuild constructor and check directly the inner
-                                                 -- instruction
+      applyWithState state source linenumber i -- All rules applying to instructions also apply to ONBUILD,
+      -- so we unwrap the OnBuild constructor and check directly the inner
+      -- instruction
     applyRule state (InstructionPos i source linenumber) =
-        applyWithState state source linenumber i -- Otherwise, normal instructions are not unwrapped
+      applyWithState state source linenumber i -- Otherwise, normal instructions are not unwrapped
     applyWithState state source linenumber instruction =
-        let (newState, res) = f state linenumber instruction
-         in (newState, [RuleCheck m source linenumber False | m <- res])
+      let (newState, res) = f state linenumber instruction
+       in (newState, [RuleCheck m source linenumber False | m <- res])
 
 instructionRule ::
-       Text.Text -> Severity -> Text.Text -> (Instruction Shell.ParsedShell -> Bool) -> Rule
+  Text.Text -> Severity -> Text.Text -> (Instruction Shell.ParsedShell -> Bool) -> Rule
 instructionRule code severity message check =
-    instructionRuleLine code severity message (const check)
+  instructionRuleLine code severity message (const check)
 
 instructionRuleLine :: Text.Text -> Severity -> Text.Text -> SimpleCheckerWithLine -> Rule
 instructionRuleLine code severity message check =
-    instructionRuleState code severity message checkAndDropState ()
+  instructionRuleState code severity message checkAndDropState ()
   where
     checkAndDropState state line instr = (state, check line instr)
 
 instructionRuleState ::
-       Text.Text -> Severity -> Text.Text -> SimpleCheckerWithState state -> state -> Rule
+  Text.Text -> Severity -> Text.Text -> SimpleCheckerWithState state -> state -> Rule
 instructionRuleState code severity message f = mapInstructions constMetadataCheck
   where
     meta = Metadata code severity message
     constMetadataCheck st ln instr =
-        let (newSt, success) = f st ln instr
-         in if not success
-                then (newSt, [meta])
-                else (newSt, [])
+      let (newSt, success) = f st ln instr
+       in if not success
+            then (newSt, [meta])
+            else (newSt, [])
 
 withState :: a -> b -> (a, b)
 withState st res = (st, res)
 
 argumentsRule :: (Shell.ParsedShell -> a) -> Arguments Shell.ParsedShell -> a
 argumentsRule applyRule args =
-    case args of
-        ArgumentsText as -> applyRule as
-        ArgumentsList as -> applyRule as
+  case args of
+    ArgumentsText as -> applyRule as
+    ArgumentsList as -> applyRule as
 
 -- Enforce rules on a dockerfile and return failed checks
 analyze :: [Rule] -> Dockerfile -> [RuleCheck]
 analyze list dockerfile =
-    [ result -- Keep the result
-    | rule <- list -- for each rule in the list
-    , result <- rule parsedFile -- after applying the rule to the file
-    , notIgnored result -- and only keep failures that were not ignored
-    ]
+  [ result -- Keep the result
+    | rule <- list, -- for each rule in the list
+      result <- rule parsedFile, -- after applying the rule to the file
+      notIgnored result -- and only keep failures that were not ignored
+  ]
   where
     notIgnored RuleCheck {metadata = Metadata {code}, linenumber} = not (wasIgnored code linenumber)
     wasIgnored c ln = not $ null [line | (line, codes) <- allIgnores, line == ln, c `elem` codes]
@@ -146,22 +149,21 @@
 
 ignored :: Dockerfile -> [(Linenumber, [Text.Text])]
 ignored dockerfile =
-    [(l + 1, ignores) | (l, Just ignores) <- map (lineNumber &&& extractIgnored) dockerfile]
+  [(l + 1, ignores) | (l, Just ignores) <- map (lineNumber &&& extractIgnored) dockerfile]
   where
     extractIgnored = ignoreFromInstruction . instruction
     ignoreFromInstruction (Comment comment) = parseComment comment
     ignoreFromInstruction _ = Nothing
-    -- | Parses the comment text and extracts the ignored rule names
     parseComment :: Text.Text -> Maybe [Text.Text]
     parseComment = Megaparsec.parseMaybe commentParser
     commentParser :: IgnoreRuleParser [Text.Text]
     commentParser =
-        spaces >> -- The parser for the ignored rules
-        string "hadolint" >>
-        spaces1 >>
-        string "ignore=" >>
-        spaces >>
-        Megaparsec.sepBy1 ruleName (spaces >> string "," >> spaces)
+      spaces
+        >> string "hadolint" -- The parser for the ignored rules
+        >> spaces1
+        >> string "ignore="
+        >> spaces
+        >> Megaparsec.sepBy1 ruleName (spaces >> string "," >> spaces)
     string = Megaparsec.string
     spaces = Megaparsec.takeWhileP Nothing space
     spaces1 = Megaparsec.takeWhile1P Nothing space
@@ -170,46 +172,46 @@
 
 rules :: [Rule]
 rules =
-    [ absoluteWorkdir
-    , shellcheck
-    , invalidCmd
-    , copyInsteadAdd
-    , copyEndingSlash
-    , copyFromExists
-    , copyFromAnother
-    , fromAliasUnique
-    , noRootUser
-    , noCd
-    , noSudo
-    , noAptGetUpgrade
-    , noApkUpgrade
-    , noLatestTag
-    , noUntagged
-    , noPlatformFlag
-    , aptGetVersionPinned
-    , aptGetCleanup
-    , apkAddVersionPinned
-    , apkAddNoCache
-    , useAdd
-    , pipVersionPinned
-    , npmVersionPinned
-    , invalidPort
-    , aptGetNoRecommends
-    , aptGetYes
-    , wgetOrCurl
-    , hasNoMaintainer
-    , multipleCmds
-    , multipleEntrypoints
-    , useShell
-    , useJsonArgs
-    , usePipefail
-    , noApt
-    , gemVersionPinned
-    , yumYes
-    , noYumUpdate
-    , yumCleanup
-    , yumVersionPinned
-    ]
+  [ absoluteWorkdir,
+    shellcheck,
+    invalidCmd,
+    copyInsteadAdd,
+    copyEndingSlash,
+    copyFromExists,
+    copyFromAnother,
+    fromAliasUnique,
+    noRootUser,
+    noCd,
+    noSudo,
+    noAptGetUpgrade,
+    noApkUpgrade,
+    noLatestTag,
+    noUntagged,
+    noPlatformFlag,
+    aptGetVersionPinned,
+    aptGetCleanup,
+    apkAddVersionPinned,
+    apkAddNoCache,
+    useAdd,
+    pipVersionPinned,
+    npmVersionPinned,
+    invalidPort,
+    aptGetNoRecommends,
+    aptGetYes,
+    wgetOrCurl,
+    hasNoMaintainer,
+    multipleCmds,
+    multipleEntrypoints,
+    useShell,
+    useJsonArgs,
+    usePipefail,
+    noApt,
+    gemVersionPinned,
+    yumYes,
+    noYumUpdate,
+    yumCleanup,
+    yumVersionPinned
+  ]
 
 optionalRules :: RulesConfig -> [Rule]
 optionalRules RulesConfig {allowedRegistries} = [registryIsAllowed allowedRegistries]
@@ -221,7 +223,7 @@
 
 allAliasedImages :: ParsedFile -> [(Linenumber, ImageAlias)]
 allAliasedImages dockerfile =
-    [(l, alias) | (l, Just alias) <- map extractAlias (allFromImages dockerfile)]
+  [(l, alias) | (l, Just alias) <- map extractAlias (allFromImages dockerfile)]
   where
     extractAlias (l, f) = (l, fromAlias f)
 
@@ -232,16 +234,16 @@
 --  are defined before the given line number.
 previouslyDefinedAliases :: Linenumber -> ParsedFile -> [Text.Text]
 previouslyDefinedAliases line dockerfile =
-    [i | (l, ImageAlias i) <- allAliasedImages dockerfile, l < line]
+  [i | (l, ImageAlias i) <- allAliasedImages dockerfile, l < line]
 
 -- | Returns the result of running the check function on the image alias
 --   name, if the passed instruction is a FROM instruction with a stage alias.
 --   Otherwise, returns True.
 aliasMustBe :: (Text.Text -> Bool) -> Instruction a -> Bool
 aliasMustBe predicate fromInstr =
-    case fromInstr of
-        From BaseImage {alias = Just (ImageAlias as)} -> predicate as
-        _ -> True
+  case fromInstr of
+    From BaseImage {alias = Just (ImageAlias as)} -> predicate as
+    _ -> True
 
 fromName :: BaseImage -> Text.Text
 fromName BaseImage {image = Image {imageName}} = imageName
@@ -269,7 +271,7 @@
 -- | Converts ShellCheck errors into our own errors type
 commentMetadata :: ShellCheck.Interface.PositionedComment -> Metadata
 commentMetadata c =
-    Metadata (Text.pack ("SC" ++ show (code c))) (severity c) (Text.pack (message c))
+  Metadata (Text.pack ("SC" ++ show (code c))) (severity c) (Text.pack (message c))
   where
     severity pc = ShellCheck.Interface.cSeverity $ ShellCheck.Interface.pcComment pc
     code pc = ShellCheck.Interface.cCode $ ShellCheck.Interface.pcComment pc
@@ -282,9 +284,9 @@
     severity = ErrorC
     message = "Use absolute WORKDIR"
     check (Workdir loc)
-        | "$" `Text.isPrefixOf` loc = True
-        | "/" `Text.isPrefixOf` loc = True
-        | otherwise = False
+      | "$" `Text.isPrefixOf` loc = True
+      | "/" `Text.isPrefixOf` loc = True
+      | otherwise = False
     check _ = True
 
 hasNoMaintainer :: Rule
@@ -306,8 +308,8 @@
     code = "DL4003"
     severity = WarningC
     message =
-        "Multiple `CMD` instructions found. If you list more than one `CMD` then only the last \
-        \`CMD` will take effect"
+      "Multiple `CMD` instructions found. If you list more than one `CMD` then only the last \
+      \`CMD` will take effect"
     check _ _ From {} = withState False True -- Reset the state each time we find a FROM
     check st _ Cmd {} = withState True (not st) -- Remember we found a CMD, fail if we found a CMD before
     check st _ _ = withState st True
@@ -318,11 +320,11 @@
     code = "DL4004"
     severity = ErrorC
     message =
-        "Multiple `ENTRYPOINT` instructions found. If you list more than one `ENTRYPOINT` then \
-        \only the last `ENTRYPOINT` will take effect"
+      "Multiple `ENTRYPOINT` instructions found. If you list more than one `ENTRYPOINT` then \
+      \only the last `ENTRYPOINT` will take effect"
     check _ _ From {} = withState False True -- Reset the state each time we find a FROM
     check st _ Entrypoint {} = withState True (not st) -- Remember we found an ENTRYPOINT
-                                                       -- and fail if we found another one before
+    -- and fail if we found another one before
     check st _ _ = withState st True
 
 wgetOrCurl :: Rule
@@ -335,11 +337,11 @@
     check _ _ (From _) = withState Set.empty True -- Reset the state for each stage
     check state _ _ = withState state True
     detectDoubleUsage state args =
-        let newArgs = extractCommands args
-            newState = Set.union state newArgs
-         in withState newState (Set.null newArgs || Set.size newState < 2)
+      let newArgs = extractCommands args
+          newState = Set.union state newArgs
+       in withState newState (Set.null newArgs || Set.size newState < 2)
     extractCommands args =
-        Set.fromList [w | w <- Shell.findCommandNames args, w == "curl" || w == "wget"]
+      Set.fromList [w | w <- Shell.findCommandNames args, w == "curl" || w == "wget"]
 
 invalidCmd :: Rule
 invalidCmd = instructionRule code severity message check
@@ -347,8 +349,8 @@
     code = "DL3001"
     severity = InfoC
     message =
-        "For some bash commands it makes no sense running them in a Docker container like `ssh`, \
-        \`vim`, `shutdown`, `service`, `ps`, `free`, `top`, `kill`, `mount`, `ifconfig`"
+      "For some bash commands it makes no sense running them in a Docker container like `ssh`, \
+      \`vim`, `shutdown`, `service`, `ps`, `free`, `top`, `kill`, `mount`, `ifconfig`"
     check (Run (RunArgs args _)) = argumentsRule detectInvalid args
     check _ = True
     detectInvalid args = null [arg | arg <- Shell.findCommandNames args, arg `elem` invalidCmds]
@@ -362,8 +364,8 @@
     message = "Last USER should not be root"
     check _ _ (From from) = withState (Just from) True -- Remember the last FROM instruction found
     check st@(Just from) line (User user)
-        | isRoot user && lastUserIsRoot from line = withState st False
-        | otherwise = withState st True
+      | isRoot user && lastUserIsRoot from line = withState st False
+      | otherwise = withState st True
     check st _ _ = withState st True
     --
     --
@@ -372,20 +374,20 @@
     --
     rootStages :: Map.Map BaseImage Linenumber
     rootStages =
-        let indexedInstructions = map (instruction &&& lineNumber) dockerfile
-            (_, usersMap) = foldl' buildMap (Nothing, Map.empty) indexedInstructions
-         in usersMap
+      let indexedInstructions = map (instruction &&& lineNumber) dockerfile
+          (_, usersMap) = foldl' buildMap (Nothing, Map.empty) indexedInstructions
+       in usersMap
     --
     --
     buildMap (_, st) (From from, _) = (Just from, st) -- Remember the FROM we are currently inspecting
     buildMap (Just from, st) (User user, line)
-        | isRoot user = (Just from, Map.insert from line st) -- Remember the line with a root user
-        | otherwise = (Just from, Map.delete from st) -- Forget there was a root used for this FROM
+      | isRoot user = (Just from, Map.insert from line st) -- Remember the line with a root user
+      | otherwise = (Just from, Map.delete from st) -- Forget there was a root used for this FROM
     buildMap st _ = st
     --
     --
     isRoot user =
-        Text.isPrefixOf "root:" user || Text.isPrefixOf "0:" user || user == "root" || user == "0"
+      Text.isPrefixOf "root:" user || Text.isPrefixOf "0:" user || user == "root" || user == "0"
 
 noCd :: Rule
 noCd = instructionRule code severity message check
@@ -402,8 +404,8 @@
     code = "DL3004"
     severity = ErrorC
     message =
-        "Do not use sudo as it leads to unpredictable behavior. Use a tool like gosu to enforce \
-        \root"
+      "Do not use sudo as it leads to unpredictable behavior. Use a tool like gosu to enforce \
+      \root"
     check (Run (RunArgs args _)) = argumentsRule (not . usingProgram "sudo") args
     check _ = True
 
@@ -414,7 +416,7 @@
     severity = ErrorC
     message = "Do not use apt-get upgrade or dist-upgrade"
     check (Run (RunArgs args _)) =
-        argumentsRule (Shell.noCommands (Shell.cmdHasArgs "apt-get" ["upgrade"])) args
+      argumentsRule (Shell.noCommands (Shell.cmdHasArgs "apt-get" ["upgrade"])) args
     check _ = True
 
 noUntagged :: Rule
@@ -426,7 +428,7 @@
     check _ (From BaseImage {image = (Image _ "scratch")}) = True
     check _ (From BaseImage {digest = Just _}) = True
     check line (From BaseImage {image = (Image _ i), tag = Nothing}) =
-        i `elem` previouslyDefinedAliases line dockerfile
+      i `elem` previouslyDefinedAliases line dockerfile
     check _ _ = True
 
 noLatestTag :: Rule
@@ -435,8 +437,8 @@
     code = "DL3007"
     severity = WarningC
     message =
-        "Using latest is prone to errors if the image will ever update. Pin the version explicitly \
-        \to a release tag"
+      "Using latest is prone to errors if the image will ever update. Pin the version explicitly \
+      \to a release tag"
     check (From BaseImage {tag = Just t}) = t /= "latest"
     check _ = True
 
@@ -446,20 +448,20 @@
     code = "DL3008"
     severity = WarningC
     message =
-        "Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get \
-        \install <package>=<version>`"
+      "Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get \
+      \install <package>=<version>`"
     check (Run (RunArgs args _)) = argumentsRule (all versionFixed . aptGetPackages) args
     check _ = True
     versionFixed package = "=" `Text.isInfixOf` package || ("/" `Text.isInfixOf` package || ".deb" `Text.isSuffixOf` package)
 
 aptGetPackages :: Shell.ParsedShell -> [Text.Text]
 aptGetPackages args =
-    [ arg
-    | cmd <- Shell.presentCommands args
-    , Shell.cmdHasArgs "apt-get" ["install"] cmd
-    , arg <- Shell.getArgsNoFlags (dropTarget cmd)
-    , arg /= "install"
-    ]
+  [ arg
+    | cmd <- Shell.presentCommands args,
+      Shell.cmdHasArgs "apt-get" ["install"] cmd,
+      arg <- Shell.getArgsNoFlags (dropTarget cmd),
+      arg /= "install"
+  ]
   where
     dropTarget = Shell.dropFlagArg ["t", "target-release"]
 
@@ -469,33 +471,29 @@
     code = "DL3009"
     severity = InfoC
     message = "Delete the apt-get lists after installing something"
-    -- | 'check' returns a tuple (state, check_result)
-    --   The state in this case is the FROM instruction where the current instruction we are
-    --   inspecting is nested in.
-    --   We only care for users to delete the lists folder if the FROM clase we're is is the last one
-    --   or if it is used as the base image for another FROM clause.
+
     check _ line f@(From _) = withState (Just (line, f)) True -- Remember the last FROM instruction found
     check st@(Just (line, From baseimage)) _ (Run (RunArgs args _)) =
-        withState st (argumentsRule (didNotForgetToCleanup line baseimage) args)
+      withState st (argumentsRule (didNotForgetToCleanup line baseimage) args)
     check st _ _ = withState st True
     -- Check all commands in the script for the presence of apt-get update
     -- If the command is there, then we need to verify that the user is also removing the lists folder
     didNotForgetToCleanup line baseimage args
-        | not (hasUpdate args) || not (imageIsUsed line baseimage) = True
-        | otherwise = hasCleanup args
+      | not (hasUpdate args) || not (imageIsUsed line baseimage) = True
+      | otherwise = hasCleanup args
     hasCleanup args =
-        any (Shell.cmdHasArgs "rm" ["-rf", "/var/lib/apt/lists/*"]) (Shell.presentCommands args)
+      any (Shell.cmdHasArgs "rm" ["-rf", "/var/lib/apt/lists/*"]) (Shell.presentCommands args)
     hasUpdate args = any (Shell.cmdHasArgs "apt-get" ["update"]) (Shell.presentCommands args)
     imageIsUsed line baseimage = isLastImage line baseimage || imageIsUsedLater line baseimage
     isLastImage line baseimage =
-        case reverse (allFromImages dockerfile) of
-            lst:_ -> (line, baseimage) == lst
-            _ -> True
+      case reverse (allFromImages dockerfile) of
+        lst : _ -> (line, baseimage) == lst
+        _ -> True
     imageIsUsedLater line baseimage =
-        case fromAlias baseimage of
-            Nothing -> True
-            Just (ImageAlias alias) ->
-                alias `elem` [i | (l, i) <- allImageNames dockerfile, l > line]
+      case fromAlias baseimage of
+        Nothing -> True
+        Just (ImageAlias alias) ->
+          alias `elem` [i | (l, i) <- allImageNames dockerfile, l > line]
 
 noApkUpgrade :: Rule
 noApkUpgrade = instructionRule code severity message check
@@ -512,19 +510,19 @@
     code = "DL3018"
     severity = WarningC
     message =
-        "Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`"
+      "Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`"
     check (Run (RunArgs args _)) = argumentsRule (\as -> and [versionFixed p | p <- apkAddPackages as]) args
     check _ = True
     versionFixed package = "=" `Text.isInfixOf` package
 
 apkAddPackages :: Shell.ParsedShell -> [Text.Text]
 apkAddPackages args =
-    [ arg
-    | cmd <- Shell.presentCommands args
-    , Shell.cmdHasArgs "apk" ["add"] cmd
-    , arg <- Shell.getArgsNoFlags (dropTarget cmd)
-    , arg /= "add"
-    ]
+  [ arg
+    | cmd <- Shell.presentCommands args,
+      Shell.cmdHasArgs "apk" ["add"] cmd,
+      arg <- Shell.getArgsNoFlags (dropTarget cmd),
+      arg /= "add"
+  ]
   where
     dropTarget = Shell.dropFlagArg ["t", "virtual", "repository", "X"]
 
@@ -534,8 +532,8 @@
     code = "DL3019"
     severity = InfoC
     message =
-        "Use the `--no-cache` switch to avoid the need to use `--update` and remove \
-        \`/var/cache/apk/*` when done installing packages"
+      "Use the `--no-cache` switch to avoid the need to use `--update` and remove \
+      \`/var/cache/apk/*` when done installing packages"
     check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotCacheOption) args
     check _ = True
     forgotCacheOption cmd = Shell.cmdHasArgs "apk" ["add"] cmd && not (Shell.hasFlag "no-cache" cmd)
@@ -547,29 +545,29 @@
     severity = InfoC
     message = "Use ADD for extracting archives into an image"
     check (Copy (CopyArgs srcs _ _ _)) =
-        and
-            [ not (format `Text.isSuffixOf` src)
-            | SourcePath src <- toList srcs
-            , format <- archiveFormats
-            ]
+      and
+        [ not (format `Text.isSuffixOf` src)
+          | SourcePath src <- toList srcs,
+            format <- archiveFormats
+        ]
     check _ = True
     archiveFormats =
-        [ ".tar"
-        , ".tar.bz2"
-        , ".tb2"
-        , ".tbz"
-        , ".tbz2"
-        , ".tar.gz"
-        , ".tgz"
-        , ".tpz"
-        , ".tar.lz"
-        , ".tar.lzma"
-        , ".tlz"
-        , ".tar.xz"
-        , ".txz"
-        , ".tar.Z"
-        , ".tZ"
-        ]
+      [ ".tar",
+        ".tar.bz2",
+        ".tb2",
+        ".tbz",
+        ".tbz2",
+        ".tar.gz",
+        ".tgz",
+        ".tpz",
+        ".tar.lz",
+        ".tar.lzma",
+        ".tlz",
+        ".tar.xz",
+        ".txz",
+        ".tar.Z",
+        ".tZ"
+      ]
 
 invalidPort :: Rule
 invalidPort = instructionRule code severity message check
@@ -578,8 +576,8 @@
     severity = ErrorC
     message = "Valid UNIX ports range from 0 to 65535"
     check (Expose (Ports ports)) =
-        and [p <= 65535 | Port p _ <- ports] &&
-        and [l <= 65535 && m <= 65535 | PortRange l m _ <- ports]
+      and [p <= 65535 | Port p _ <- ports]
+        && and [l <= 65535 && m <= 65535 | PortRange l m _ <- ports]
     check _ = True
 
 pipVersionPinned :: Rule
@@ -588,46 +586,55 @@
     code = "DL3013"
     severity = WarningC
     message =
-        "Pin versions in pip. Instead of `pip install <package>` use `pip install \
-        \<package>==<version>`"
+      "Pin versions in pip. Instead of `pip install <package>` use `pip install \
+      \<package>==<version>`"
     check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotToPinVersion) args
     check _ = True
     forgotToPinVersion cmd =
-        isPipInstall cmd && not (hasBuildConstraint cmd) && not (all versionFixed (packages cmd))
+      isPipInstall cmd && not (hasBuildConstraint cmd) && not (all versionFixed (packages cmd))
     -- Check if the command is a pip* install command, and that specific packages are being listed
     isPipInstall cmd = (isStdPipInstall cmd || isPythonPipInstall cmd) && not (requirementInstall cmd)
     isStdPipInstall cmd@(Shell.Command name _ _) = "pip" `Text.isPrefixOf` name && ["install"] `isInfixOf` Shell.getArgs cmd
-    isPythonPipInstall cmd@(Shell.Command name _ _) = "python" `Text.isPrefixOf` name &&
-        ["-m", "pip", "install"] `isInfixOf` Shell.getArgs cmd
+    isPythonPipInstall cmd@(Shell.Command name _ _) =
+      "python" `Text.isPrefixOf` name
+        && ["-m", "pip", "install"] `isInfixOf` Shell.getArgs cmd
     -- If the user is installing requirements from a file or just the local module, then we are not interested
     -- in running this rule
-    requirementInstall cmd = ["--requirement"] `isInfixOf` Shell.getArgs cmd ||
-        ["-r"] `isInfixOf` Shell.getArgs cmd ||
-        ["."] `isInfixOf` Shell.getArgs cmd
+    requirementInstall cmd =
+      ["--requirement"] `isInfixOf` Shell.getArgs cmd
+        || ["-r"] `isInfixOf` Shell.getArgs cmd
+        || ["."] `isInfixOf` Shell.getArgs cmd
     hasBuildConstraint cmd = Shell.hasFlag "constraint" cmd || Shell.hasFlag "c" cmd
     packages cmd =
-        stripInstallPrefix $
-        Shell.getArgsNoFlags $ Shell.dropFlagArg
-            [ "abi"
-            , "b", "build"
-            , "e", "editable"
-            , "extra-index-url"
-            , "f", "find-links"
-            , "i", "index-url"
-            , "implementation"
-            , "no-binary"
-            , "only-binary"
-            , "platform"
-            , "prefix"
-            , "progress-bar"
-            , "proxy"
-            , "python-version"
-            , "root"
-            , "src"
-            , "t", "target"
-            , "trusted-host"
-            , "upgrade-strategy"
-            ] cmd
+      stripInstallPrefix $
+        Shell.getArgsNoFlags $
+          Shell.dropFlagArg
+            [ "abi",
+              "b",
+              "build",
+              "e",
+              "editable",
+              "extra-index-url",
+              "f",
+              "find-links",
+              "i",
+              "index-url",
+              "implementation",
+              "no-binary",
+              "only-binary",
+              "platform",
+              "prefix",
+              "progress-bar",
+              "proxy",
+              "python-version",
+              "root",
+              "src",
+              "t",
+              "target",
+              "trusted-host",
+              "upgrade-strategy"
+            ]
+            cmd
     versionFixed package = hasVersionSymbol package || isVersionedGit package
     isVersionedGit package = "git+http" `Text.isInfixOf` package && "@" `Text.isInfixOf` package
     versionSymbols = ["==", ">=", "<=", ">", "<", "!=", "~=", "==="]
@@ -636,36 +643,35 @@
 stripInstallPrefix :: [Text.Text] -> [Text.Text]
 stripInstallPrefix cmd = dropWhile (== "install") (dropWhile (/= "install") cmd)
 
-{-|
-  Rule for pinning NPM packages to version, tag, or commit
-  supported formats by Hadolint
-    npm install (with no args, in package dir)
-    npm install [<@scope>/]<name>
-    npm install [<@scope>/]<name>@<tag>
-    npm install [<@scope>/]<name>@<version>
-    npm install git[+http|+https]://<git-host>/<git-user>/<repo-name>[#<commit>|#semver:<semver>]
-    npm install git+ssh://<git-host>:<git-user>/<repo-name>[#<commit>|#semver:<semver>]
--}
+-- |
+--  Rule for pinning NPM packages to version, tag, or commit
+--  supported formats by Hadolint
+--    npm install (with no args, in package dir)
+--    npm install [<@scope>/]<name>
+--    npm install [<@scope>/]<name>@<tag>
+--    npm install [<@scope>/]<name>@<version>
+--    npm install git[+http|+https]://<git-host>/<git-user>/<repo-name>[#<commit>|#semver:<semver>]
+--    npm install git+ssh://<git-host>:<git-user>/<repo-name>[#<commit>|#semver:<semver>]
 npmVersionPinned :: Rule
 npmVersionPinned = instructionRule code severity message check
   where
     code = "DL3016"
     severity = WarningC
     message =
-        "Pin versions in npm. Instead of `npm install <package>` use `npm install \
-        \<package>@<version>`"
+      "Pin versions in npm. Instead of `npm install <package>` use `npm install \
+      \<package>@<version>`"
     check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotToPinVersion) args
     check _ = True
     forgotToPinVersion cmd =
-        isNpmInstall cmd && installIsFirst cmd && not (all versionFixed (packages cmd))
+      isNpmInstall cmd && installIsFirst cmd && not (all versionFixed (packages cmd))
     isNpmInstall = Shell.cmdHasArgs "npm" ["install"]
     installIsFirst cmd = ["install"] `isPrefixOf` Shell.getArgsNoFlags cmd
     packages cmd = stripInstallPrefix (Shell.getArgsNoFlags cmd)
     versionFixed package
-        | hasGitPrefix package = isVersionedGit package
-        | hasTarballSuffix package = True
-        | isFolder package = True
-        | otherwise = hasVersionSymbol package
+      | hasGitPrefix package = isVersionedGit package
+      | hasTarballSuffix package = True
+      | isFolder package = True
+      | otherwise = hasVersionSymbol package
     gitPrefixes = ["git://", "git+ssh://", "git+http://", "git+https://"]
     hasGitPrefix package = or [p `Text.isPrefixOf` package | p <- gitPrefixes]
     tarballSuffixes = [".tar", ".tar.gz", ".tgz"]
@@ -676,9 +682,9 @@
     hasVersionSymbol package = "@" `Text.isInfixOf` dropScope package
       where
         dropScope pkg =
-            if "@" `Text.isPrefixOf` pkg
-                then Text.dropWhile ('/' <) pkg
-                else pkg
+          if "@" `Text.isPrefixOf` pkg
+            then Text.dropWhile ('/' <) pkg
+            else pkg
 
 aptGetYes :: Rule
 aptGetYes = instructionRule code severity message check
@@ -703,31 +709,32 @@
     forgotNoInstallRecommends cmd = isAptGetInstall cmd && not (disablesRecommendOption cmd)
     isAptGetInstall = Shell.cmdHasArgs "apt-get" ["install"]
     disablesRecommendOption cmd =
-        Shell.hasFlag "no-install-recommends" cmd ||
-        Shell.hasArg "APT::Install-Recommends=false" cmd
+      Shell.hasFlag "no-install-recommends" cmd
+        || Shell.hasArg "APT::Install-Recommends=false" cmd
 
 isArchive :: Text.Text -> Bool
 isArchive path =
-    or
-    ([ ftype `Text.isSuffixOf` path
-    | ftype <-
-          [ ".tar"
-          , ".gz"
-          , ".bz2"
-          , ".xz"
-          , ".zip"
-          , ".tgz"
-          , ".tb2"
-          , ".tbz"
-          , ".tbz2"
-          , ".lz"
-          , ".lzma"
-          , ".tlz"
-          , ".txz"
-          , ".Z"
-          , ".tZ"
-          ]
-    ])
+  or
+    ( [ ftype `Text.isSuffixOf` path
+        | ftype <-
+            [ ".tar",
+              ".gz",
+              ".bz2",
+              ".xz",
+              ".zip",
+              ".tgz",
+              ".tb2",
+              ".tbz",
+              ".tbz2",
+              ".lz",
+              ".lzma",
+              ".tlz",
+              ".txz",
+              ".Z",
+              ".tZ"
+            ]
+      ]
+    )
 
 isUrl :: Text.Text -> Bool
 isUrl path = or ([proto `Text.isPrefixOf` path | proto <- ["https://", "http://"]])
@@ -739,7 +746,7 @@
     severity = ErrorC
     message = "Use COPY instead of ADD for files and folders"
     check (Add (AddArgs srcs _ _)) =
-        and [isArchive src || isUrl src | SourcePath src <- toList srcs]
+      and [isArchive src || isUrl src | SourcePath src <- toList srcs]
     check _ = True
 
 copyEndingSlash :: Rule
@@ -749,8 +756,8 @@
     severity = ErrorC
     message = "COPY with more than 2 arguments requires the last argument to end with /"
     check (Copy (CopyArgs sources t _ _))
-        | length sources > 1 = endsWithSlash t
-        | otherwise = True
+      | length sources > 1 = endsWithSlash t
+      | otherwise = True
     check _ = True
     endsWithSlash (TargetPath t) = not (Text.null t) && Text.last t == '/'
 
@@ -769,12 +776,10 @@
     code = "DL3023"
     severity = ErrorC
     message = "COPY --from should reference a previously defined FROM alias"
-    -- | 'check' returns a tuple (state, check_result)
-    --   The state in this case is the FROM instruction where the current instruction we are
-    --   inspecting is nested in.
+
     check _ _ f@(From _) = withState (Just f) True -- Remember the last FROM instruction found
     check st@(Just fromInstr) _ (Copy (CopyArgs _ _ _ (CopySource stageName))) =
-        withState st (aliasMustBe (/= stageName) fromInstr) -- Cannot copy from itself!
+      withState st (aliasMustBe (/= stageName) fromInstr) -- Cannot copy from itself!
     check state _ _ = withState state True
 
 fromAliasUnique :: Rule
@@ -811,7 +816,7 @@
     code = "DL3027"
     severity = WarningC
     message =
-        "Do not use apt as it is meant to be a end-user tool, use apt-get or apt-cache instead"
+      "Do not use apt as it is meant to be a end-user tool, use apt-get or apt-cache instead"
     check (Run (RunArgs args _)) = argumentsRule (not . usingProgram "apt") args
     check _ = True
 
@@ -820,28 +825,29 @@
   where
     code = "DL4006"
     severity = WarningC
-    message = "Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using \
-              \/bin/sh in an alpine image or if your shell is symlinked to busybox then consider \
-              \explicitly setting your SHELL to /bin/ash, or disable this check"
+    message =
+      "Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using \
+      \/bin/sh in an alpine image or if your shell is symlinked to busybox then consider \
+      \explicitly setting your SHELL to /bin/ash, or disable this check"
     check _ _ From {} = (False, True) -- Reset the state each time we find a new FROM
     check _ _ (Shell args)
-        | argumentsRule isPowerShell args = (True, True)
-        | otherwise = (argumentsRule hasPipefailOption args, True)
+      | argumentsRule isPowerShell args = (True, True)
+      | otherwise = (argumentsRule hasPipefailOption args, True)
     check False _ (Run (RunArgs args _)) = (False, argumentsRule notHasPipes args)
     check st _ _ = (st, True)
     isPowerShell (Shell.ParsedShell orig _ _) = "pwsh" `Text.isPrefixOf` orig
     notHasPipes script = not (Shell.hasPipes script)
     hasPipefailOption script =
-        not $
+      not $
         null
-            [ True
-            | cmd@(Shell.Command name arguments _) <- Shell.presentCommands script
-            , validShell <- ["/bin/bash", "/bin/zsh", "/bin/ash", "bash", "zsh", "ash"]
-            , name == validShell
-            , Shell.hasFlag "o" cmd
-            , arg <- Shell.arg <$> arguments
-            , arg == "pipefail"
-            ]
+          [ True
+            | cmd@(Shell.Command name arguments _) <- Shell.presentCommands script,
+              validShell <- ["/bin/bash", "/bin/zsh", "/bin/ash", "bash", "zsh", "ash"],
+              name == validShell,
+              Shell.hasFlag "o" cmd,
+              arg <- Shell.arg <$> arguments,
+              arg == "pipefail"
+          ]
 
 registryIsAllowed :: Set.Set Registry -> Rule
 registryIsAllowed allowed = instructionRuleState code severity message check Set.empty
@@ -851,15 +857,14 @@
     message = "Use only an allowed registry in the FROM image"
     check st _ (From BaseImage {image, alias}) = withState (Set.insert alias st) (doCheck st image)
     check st _ _ = (st, True)
-    -- |Transforms an Image into a Maybe ImageAlias by using the Image name
     toImageAlias = Just . ImageAlias . imageName
-    -- | Returns True if the image being used is a previous aliased image
-    -- or if the image registry is in the set of allowed registries
+
     doCheck st img = Set.member (toImageAlias img) st || Set.null allowed || isAllowed img
     isAllowed Image {registryName = Just registry} = Set.member registry allowed
     isAllowed Image {registryName = Nothing, imageName} =
-        imageName == "scratch" ||
-        Set.member "docker.io" allowed || Set.member "hub.docker.com" allowed
+      imageName == "scratch"
+        || Set.member "docker.io" allowed
+        || Set.member "hub.docker.com" allowed
 
 gemVersionPinned :: Rule
 gemVersionPinned = instructionRule code severity message check
@@ -867,8 +872,8 @@
     code = "DL3028"
     severity = WarningC
     message =
-        "Pin versions in gem install. Instead of `gem install <gem>` use `gem \
-        \install <gem>:<version>`"
+      "Pin versions in gem install. Instead of `gem install <gem>` use `gem \
+      \install <gem>:<version>`"
     check (Run (RunArgs args _)) = argumentsRule (all versionFixed . gems) args
     check _ = True
     versionFixed package = ":" `Text.isInfixOf` package
@@ -901,11 +906,18 @@
     severity = ErrorC
     message = "Do not use yum update."
     check (Run (RunArgs args _)) =
-      argumentsRule (Shell.noCommands (
-                       Shell.cmdHasArgs "yum" ["update",
-                                               "update-to",
-                                               "upgrade",
-                                               "upgrade-to"])) args
+      argumentsRule
+        ( Shell.noCommands
+            ( Shell.cmdHasArgs
+                "yum"
+                [ "update",
+                  "update-to",
+                  "upgrade",
+                  "upgrade-to"
+                ]
+            )
+        )
+        args
     check _ = True
 
 yumCleanup :: Rule
@@ -914,9 +926,11 @@
     code = "DL3032"
     severity = WarningC
     message = "`yum clean all` missing after yum command."
-    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands yumInstall) args ||
-                                   (argumentsRule (Shell.anyCommands yumInstall) args &&
-                                    argumentsRule (Shell.anyCommands yumClean) args)
+    check (Run (RunArgs args _)) =
+      argumentsRule (Shell.noCommands yumInstall) args
+        || ( argumentsRule (Shell.anyCommands yumInstall) args
+               && argumentsRule (Shell.anyCommands yumClean) args
+           )
     check _ = True
     yumInstall = Shell.cmdHasArgs "yum" ["install"]
     yumClean = Shell.cmdHasArgs "yum" ["clean", "all"]
@@ -929,25 +943,25 @@
     message = "Specify version with `yum install -y <package>-<version>`."
     check (Run (RunArgs args _)) = argumentsRule (all versionFixed . yumPackages) args
     check _ = True
-    versionFixed package = "-" `Text.isInfixOf` package
-                        || ".rpm" `Text.isSuffixOf` package
+    versionFixed package =
+      "-" `Text.isInfixOf` package
+        || ".rpm" `Text.isSuffixOf` package
 
 yumPackages :: Shell.ParsedShell -> [Text.Text]
-yumPackages args = [arg | cmd <- Shell.presentCommands args,
-                          Shell.cmdHasArgs "yum" ["install"] cmd,
-                          arg <- Shell.getArgsNoFlags cmd,
-                          arg /= "install"]
+yumPackages args =
+  [ arg | cmd <- Shell.presentCommands args, Shell.cmdHasArgs "yum" ["install"] cmd, arg <- Shell.getArgsNoFlags cmd, arg /= "install"
+  ]
 
 gems :: Shell.ParsedShell -> [Text.Text]
 gems shell =
-    [ arg
-    | cmd <- Shell.presentCommands shell
-    , Shell.cmdHasArgs "gem" ["install", "i"] cmd
-    , not (Shell.cmdHasArgs "gem" ["-v"] cmd)
-    , not (Shell.cmdHasArgs "gem" ["--version"] cmd)
-    , not (Shell.cmdHasPrefixArg "gem" "--version=" cmd)
-    , arg <- Shell.getArgsNoFlags cmd
-    , arg /= "install"
-    , arg /= "i"
-    , arg /= "--"
-    ]
+  [ arg
+    | cmd <- Shell.presentCommands shell,
+      Shell.cmdHasArgs "gem" ["install", "i"] cmd,
+      not (Shell.cmdHasArgs "gem" ["-v"] cmd),
+      not (Shell.cmdHasArgs "gem" ["--version"] cmd),
+      not (Shell.cmdHasPrefixArg "gem" "--version=" cmd),
+      arg <- Shell.getArgsNoFlags cmd,
+      arg /= "install",
+      arg /= "i",
+      arg /= "--"
+  ]
diff --git a/src/Hadolint/Shell.hs b/src/Hadolint/Shell.hs
--- a/src/Hadolint/Shell.hs
+++ b/src/Hadolint/Shell.hs
@@ -1,7 +1,7 @@
-{-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE NamedFieldPuns #-}
-{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE PatternSynonyms #-}
+{-# LANGUAGE ScopedTypeVariables #-}
 
 module Hadolint.Shell where
 
@@ -10,51 +10,53 @@
 import Data.Maybe (listToMaybe, mapMaybe)
 import Data.Semigroup ((<>))
 import qualified Data.Set as Set
-import qualified Data.Text as Text
 import Data.Text (Text)
+import qualified Data.Text as Text
+import ShellCheck.AST (Id (..), Token (..), pattern T_Pipe, pattern T_SimpleCommand)
 import qualified ShellCheck.AST
-import ShellCheck.AST (Id(..), Token(..), pattern T_SimpleCommand, pattern T_Pipe)
 import qualified ShellCheck.ASTLib
-import ShellCheck.Checker
+import ShellCheck.Checker (checkScript)
 import ShellCheck.Interface
 import qualified ShellCheck.Parser
 
 data CmdPart = CmdPart
-    { arg :: !Text
-    , partId :: !Int
-    } deriving (Show)
+  { arg :: !Text,
+    partId :: !Int
+  }
+  deriving (Show)
 
 data Command = Command
-    { name :: !Text.Text
-    , arguments :: [CmdPart]
-    , flags :: [CmdPart]
-    } deriving (Show)
+  { name :: !Text.Text,
+    arguments :: [CmdPart],
+    flags :: [CmdPart]
+  }
+  deriving (Show)
 
 data ParsedShell = ParsedShell
-    { original :: !Text.Text
-    , parsed :: !ParseResult
-    , presentCommands :: ![Command]
-    }
+  { original :: !Text.Text,
+    parsed :: !ParseResult,
+    presentCommands :: ![Command]
+  }
 
 data ShellOpts = ShellOpts
-    { shellName :: Text.Text
-    , envVars :: Set.Set Text.Text
-    }
+  { shellName :: Text.Text,
+    envVars :: Set.Set Text.Text
+  }
 
 defaultShellOpts :: ShellOpts
 defaultShellOpts = ShellOpts "/bin/sh -c" defaultVars
   where
     defaultVars =
-        Set.fromList
-            [ "HTTP_PROXY"
-            , "http_proxy"
-            , "HTTPS_PROXY"
-            , "https_proxy"
-            , "FTP_PROXY"
-            , "ftp_proxy"
-            , "NO_PROXY"
-            , "no_proxy"
-            ]
+      Set.fromList
+        [ "HTTP_PROXY",
+          "http_proxy",
+          "HTTPS_PROXY",
+          "https_proxy",
+          "FTP_PROXY",
+          "ftp_proxy",
+          "NO_PROXY",
+          "no_proxy"
+        ]
 
 addVars :: [Text.Text] -> ShellOpts -> ShellOpts
 addVars vars (ShellOpts n v) = ShellOpts n (v <> Set.fromList vars)
@@ -64,50 +66,49 @@
 
 shellcheck :: ShellOpts -> ParsedShell -> [PositionedComment]
 shellcheck (ShellOpts sh env) (ParsedShell txt _ _) =
-    if "pwsh" `Text.isPrefixOf` sh
-        then [] -- Do no run for powershell
-        else runShellCheck
+  if "pwsh" `Text.isPrefixOf` sh
+    then [] -- Do no run for powershell
+    else runShellCheck
   where
     runShellCheck = crComments $ runIdentity $ checkScript si spec
     si = mockedSystemInterface [("", "")]
     spec =
-        emptyCheckSpec
-            { csFilename = "" -- filename can be ommited because we only want the parse results back
-            , csScript = script
-            , csCheckSourced = False
-            , csExcludedWarnings = exclusions
-            , csShellTypeOverride = Nothing
-            , csMinSeverity = StyleC
-            }
+      emptyCheckSpec
+        { csFilename = "", -- filename can be ommited because we only want the parse results back
+          csScript = script,
+          csCheckSourced = False,
+          csExcludedWarnings = exclusions,
+          csShellTypeOverride = Nothing,
+          csMinSeverity = StyleC
+        }
     script = "#!" ++ extractShell sh ++ "\n" ++ printVars ++ Text.unpack txt
     exclusions =
-        [ 2187 -- exclude the warning about the ash shell not being supported
-        , 1090 -- requires a directive (shell comment) that can't be expressed in a Dockerfile
-        ]
-    -- | Shellcheck complains when the shebang has more than one argument, so we only take the first
+      [ 2187, -- exclude the warning about the ash shell not being supported
+        1090 -- requires a directive (shell comment) that can't be expressed in a Dockerfile
+      ]
+
     extractShell s =
-        maybe "" Text.unpack (listToMaybe . Text.words $ s)
-    -- | Inject all the collected env vars as exported variables so they can be used
+      maybe "" Text.unpack (listToMaybe . Text.words $ s)
     printVars = Text.unpack . Text.unlines . Set.toList $ Set.map (\v -> "export " <> v <> "=1") env
 
 parseShell :: Text.Text -> ParsedShell
 parseShell txt = ParsedShell {original = txt, parsed = parsedResult, presentCommands = commands}
   where
     parsedResult =
-        runIdentity $
+      runIdentity $
         ShellCheck.Parser.parseScript
-            (mockedSystemInterface [("", "")])
-            newParseSpec
-                { psFilename = "" -- There is no filename
-                , psScript = "#!/bin/bash\n" ++ Text.unpack txt
-                , psCheckSourced = False
-                }
-    -- | Extract all commands with their name
+          (mockedSystemInterface [("", "")])
+          newParseSpec
+            { psFilename = "", -- There is no filename
+              psScript = "#!/bin/bash\n" ++ Text.unpack txt,
+              psCheckSourced = False
+            }
+
     commands = mapMaybe extractNames (findCommandsInResult parsedResult)
     extractNames token =
-        case ShellCheck.ASTLib.getCommandName token of
-            Nothing -> Nothing
-            Just n -> Just $ Command (Text.pack n) allArgs (getAllFlags allArgs)
+      case ShellCheck.ASTLib.getCommandName token of
+        Nothing -> Nothing
+        Just n -> Just $ Command (Text.pack n) allArgs (getAllFlags allArgs)
       where
         allArgs = extractAllArgs token
 
@@ -118,15 +119,15 @@
 
 extractTokensWith :: forall a. (Token -> Maybe a) -> ParseResult -> [a]
 extractTokensWith extractor ast =
-    case prRoot ast of
-        Nothing -> []
-        Just script -> execWriter $ ShellCheck.AST.doAnalysis extract script
+  case prRoot ast of
+    Nothing -> []
+    Just script -> execWriter $ ShellCheck.AST.doAnalysis extract script
   where
     extract :: Token -> Writer [a] ()
     extract token =
-        case extractor token of
-            Nothing -> return ()
-            Just t -> tell [t]
+      case extractor token of
+        Nothing -> return ()
+        Just t -> tell [t]
 
 findPipes :: ParsedShell -> [Token]
 findPipes (ParsedShell _ ast _) = extractTokensWith pipesExtractor ast
@@ -151,21 +152,21 @@
 
 cmdHasArgs :: Text.Text -> [Text.Text] -> Command -> Bool
 cmdHasArgs expectedName expectedArgs (Command n args _)
-    | expectedName /= n = False
-    | otherwise = not $ null [arg | CmdPart arg _ <- args, arg `elem` expectedArgs]
+  | expectedName /= n = False
+  | otherwise = not $ null [arg | CmdPart arg _ <- args, arg `elem` expectedArgs]
 
 cmdHasPrefixArg :: Text.Text -> Text.Text -> Command -> Bool
 cmdHasPrefixArg expectedName expectedArg (Command n args _)
-    | expectedName /= n = False
-    | otherwise = not $ null [arg | CmdPart arg _ <- args, expectedArg `Text.isPrefixOf` arg]
+  | expectedName /= n = False
+  | otherwise = not $ null [arg | CmdPart arg _ <- args, expectedArg `Text.isPrefixOf` arg]
 
 extractAllArgs :: Token -> [CmdPart]
-extractAllArgs (T_SimpleCommand _ _ (_:allArgs)) = map mkPart allArgs
+extractAllArgs (T_SimpleCommand _ _ (_ : allArgs)) = map mkPart allArgs
   where
     mkPart token =
-        CmdPart
-            (Text.pack . concat $ ShellCheck.ASTLib.oversimplify token)
-            (mkId (ShellCheck.AST.getId token))
+      CmdPart
+        (Text.pack . concat $ ShellCheck.ASTLib.oversimplify token)
+        (mkId (ShellCheck.AST.getId token))
     mkId (Id i) = i
 extractAllArgs _ = []
 
@@ -176,10 +177,10 @@
 getAllFlags = concatMap flag
   where
     flag (CmdPart arg pId)
-        | arg == "--" || arg == "-" = []
-        | "--" `Text.isPrefixOf` arg = [CmdPart (Text.drop 2 . Text.takeWhile (/= '=') $ arg) pId]
-        | "-" `Text.isPrefixOf` arg = map (`CmdPart` pId) (Text.chunksOf 1 (Text.tail arg))
-        | otherwise = []
+      | arg == "--" || arg == "-" = []
+      | "--" `Text.isPrefixOf` arg = [CmdPart (Text.drop 2 . Text.takeWhile (/= '=') $ arg) pId]
+      | "-" `Text.isPrefixOf` arg = map (`CmdPart` pId) (Text.chunksOf 1 (Text.tail arg))
+      | otherwise = []
 
 getArgsNoFlags :: Command -> [Text.Text]
 getArgsNoFlags args = map arg $ filter (notAFlagId . partId) (arguments args)
@@ -200,4 +201,6 @@
   where
     idsToDrop = Set.fromList [getValueId fId arguments | CmdPart f fId <- flags, f `elem` flagsToDrop]
     filterdArgs = [arg | arg@(CmdPart _ aId) <- arguments, not (aId `Set.member` idsToDrop)]
-getValueId fId flags = foldl min (maxBound :: Int) $ filter (>fId) $ map partId flags 
+
+getValueId :: Int -> [CmdPart] -> Int
+getValueId fId flags = foldl min (maxBound :: Int) $ filter (> fId) $ map partId flags
diff --git a/test/ConfigSpec.hs b/test/ConfigSpec.hs
--- a/test/ConfigSpec.hs
+++ b/test/ConfigSpec.hs
@@ -1,54 +1,54 @@
 {-# LANGUAGE OverloadedStrings #-}
+
 module ConfigSpec where
 
-import Test.HUnit
-import Test.Hspec
 import Control.Monad (unless)
 import qualified Data.ByteString.Char8 as Bytes
 import qualified Data.YAML as Yaml
-
 import Hadolint.Config
+import Test.HUnit
+import Test.Hspec
 
 tests :: SpecWith ()
 tests =
-    describe "Config" $ do
-        it "Parses config with only ignores" $
-            let configFile =
-                    [ "ignored:"
-                    , "- DL3000"
-                    , "- SC1010"
-                    ]
-                expected = ConfigFile (Just ["DL3000", "SC1010"]) Nothing
-            in assertConfig expected (Bytes.unlines configFile)
+  describe "Config" $ do
+    it "Parses config with only ignores" $
+      let configFile =
+            [ "ignored:",
+              "- DL3000",
+              "- SC1010"
+            ]
+          expected = ConfigFile (Just ["DL3000", "SC1010"]) Nothing
+       in assertConfig expected (Bytes.unlines configFile)
 
-        it "Parses config with only trustedRegistries" $
-            let configFile =
-                    [ "trustedRegistries:"
-                    , "- hub.docker.com"
-                    , "- my.shady.xyz"
-                    ]
-                expected = ConfigFile Nothing (Just ["hub.docker.com", "my.shady.xyz"])
-            in assertConfig expected (Bytes.unlines configFile)
+    it "Parses config with only trustedRegistries" $
+      let configFile =
+            [ "trustedRegistries:",
+              "- hub.docker.com",
+              "- my.shady.xyz"
+            ]
+          expected = ConfigFile Nothing (Just ["hub.docker.com", "my.shady.xyz"])
+       in assertConfig expected (Bytes.unlines configFile)
 
-        it "Parses full file" $
-            let configFile =
-                    [ "trustedRegistries:"
-                    , "- hub.docker.com"
-                    , ""
-                    , "ignored:"
-                    , "- DL3000"
-                    ]
-                expected = ConfigFile (Just ["DL3000"]) (Just ["hub.docker.com"])
-            in assertConfig expected (Bytes.unlines configFile)
+    it "Parses full file" $
+      let configFile =
+            [ "trustedRegistries:",
+              "- hub.docker.com",
+              "",
+              "ignored:",
+              "- DL3000"
+            ]
+          expected = ConfigFile (Just ["DL3000"]) (Just ["hub.docker.com"])
+       in assertConfig expected (Bytes.unlines configFile)
 
 assertConfig :: HasCallStack => ConfigFile -> Bytes.ByteString -> Assertion
 assertConfig config s =
-    case Yaml.decode1Strict s of
-        Left (_, err) ->
-            assertFailure err
-        Right result ->
-            checkResult result
+  case Yaml.decode1Strict s of
+    Left (_, err) ->
+      assertFailure err
+    Right result ->
+      checkResult result
   where
     checkResult result =
-        unless (result == config) $
-            assertFailure ("Config \n\n" ++ show config  ++ "\n\n is not \n\n" ++ show result)
+      unless (result == config) $
+        assertFailure ("Config \n\n" ++ show config ++ "\n\n is not \n\n" ++ show result)
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -1,1292 +1,1307 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE OverloadedLists #-}
-import Test.HUnit hiding (Label)
-import Test.Hspec
-import Control.Monad (when, unless)
-
-import Hadolint.Formatter.TTY (formatError, formatChecks)
-import Hadolint.Rules
-
-import Language.Docker.Parser
-import Language.Docker.Syntax
-import Data.Semigroup ((<>))
-import qualified Data.Text as Text
-
-import qualified ConfigSpec
-
-main :: IO ()
-main =
-    hspec $ do
-        describe "FROM rules" $ do
-            it "no untagged" $ ruleCatches noUntagged "FROM debian"
-            it "no untagged with name" $ ruleCatches noUntagged "FROM debian AS builder"
-            it "explicit latest" $ ruleCatches noLatestTag "FROM debian:latest"
-            it "explicit latest with name" $ ruleCatches noLatestTag "FROM debian:latest AS builder"
-            it "explicit tagged" $ ruleCatchesNot noLatestTag "FROM debian:jessie"
-            it "explicit platform flag" $ ruleCatches noPlatformFlag "FROM --platform=linux debian:jessie"
-            it "no platform flag" $ ruleCatchesNot noPlatformFlag "FROM debian:jessie"
-            it "explicit SHA" $
-                ruleCatchesNot noLatestTag
-                    "FROM hub.docker.io/debian@sha256:\
-                    \7959ed6f7e35f8b1aaa06d1d8259d4ee25aa85a086d5c125480c333183f9deeb"
-            it "explicit tagged with name" $
-                ruleCatchesNot noLatestTag "FROM debian:jessie AS builder"
-            it "untagged digest is not an error" $
-                ruleCatchesNot noUntagged "FROM ruby@sha256:f1dbca0f5dbc9"
-            it "untagged digest is not an error" $
-                ruleCatchesNot noUntagged "FROM ruby:2"
-            it "local aliases are OK to be untagged" $
-                let dockerFile =
-                        [ "FROM golang:1.9.3-alpine3.7 AS build"
-                        , "RUN foo"
-                        , "FROM build as unit-test"
-                        , "RUN bar"
-                        , "FROM alpine:3.7"
-                        , "RUN baz"
-                        ]
-                in do
-                  ruleCatchesNot noUntagged $ Text.unlines dockerFile
-                  onBuildRuleCatchesNot noUntagged $ Text.unlines dockerFile
-            it "other untagged cases are not ok" $
-                let dockerFile =
-                        [ "FROM golang:1.9.3-alpine3.7 AS build"
-                        , "RUN foo"
-                        , "FROM node as unit-test"
-                        , "RUN bar"
-                        , "FROM alpine:3.7"
-                        , "RUN baz"
-                        ]
-                in do
-                  ruleCatches noUntagged $ Text.unlines dockerFile
-                  onBuildRuleCatches noUntagged $ Text.unlines dockerFile
-        --
-        describe "no root or sudo rules" $ do
-            it "sudo" $ do
-              ruleCatches noSudo "RUN sudo apt-get update"
-              onBuildRuleCatches noSudo "RUN sudo apt-get update"
-
-            it "last user should not be root" $
-                let dockerFile =
-                        [ "FROM scratch"
-                        , "USER root"
-                        ]
-                in ruleCatches noRootUser $ Text.unlines dockerFile
-
-            it "no root" $
-                let dockerFile =
-                        [ "FROM scratch"
-                        , "USER foo"
-                        ]
-                in ruleCatchesNot noRootUser $ Text.unlines dockerFile
-
-            it "no root UID" $
-                let dockerFile =
-                        [ "FROM scratch"
-                        , "USER 0"
-                        ]
-                in ruleCatches noRootUser $ Text.unlines dockerFile
-
-            it "no root:root" $
-                let dockerFile =
-                        [ "FROM scratch"
-                        , "USER root:root"
-                        ]
-                in ruleCatches noRootUser $ Text.unlines dockerFile
-
-            it "no UID:GID" $
-                let dockerFile =
-                        [ "FROM scratch"
-                        , "USER 0:0"
-                        ]
-                in ruleCatches noRootUser $ Text.unlines dockerFile
-
-            it "can switch back to non root" $
-                let dockerFile =
-                        [ "FROM scratch"
-                        , "USER root"
-                        , "RUN something"
-                        , "USER foo"
-                        ]
-                in ruleCatchesNot noRootUser $ Text.unlines dockerFile
-
-            it "warns on transitive root user" $
-                let dockerFile =
-                        [ "FROM debian as base"
-                        , "USER root"
-                        , "RUN something"
-                        , "FROM base"
-                        , "RUN something else"
-                        ]
-                in ruleCatches noRootUser $ Text.unlines dockerFile
-
-            it "warns on multiple stages" $
-                let dockerFile =
-                        [ "FROM debian as base"
-                        , "USER root"
-                        , "RUN something"
-                        , "FROM scratch"
-                        , "USER foo"
-                        , "RUN something else"
-                        ]
-                in ruleCatches noRootUser $ Text.unlines dockerFile
-
-            it "does not warn when switching in multiple stages" $
-                let dockerFile =
-                        [ "FROM debian as base"
-                        , "USER root"
-                        , "RUN something"
-                        , "USER foo"
-                        , "FROM scratch"
-                        , "RUN something else"
-                        ]
-                in ruleCatchesNot noRootUser $ Text.unlines dockerFile
-
-            it "install sudo" $ do
-                ruleCatchesNot noSudo "RUN apt-get install sudo"
-                onBuildRuleCatchesNot noSudo "RUN apt-get install sudo"
-            it "sudo chained programs" $ do
-                ruleCatches noSudo "RUN apt-get update && sudo apt-get install"
-                onBuildRuleCatches noSudo "RUN apt-get update && sudo apt-get install"
-        --
-        describe "invalid CMD rules" $ do
-            it "invalid cmd" $ do
-                ruleCatches invalidCmd "RUN top"
-                onBuildRuleCatches invalidCmd "RUN top"
-            it "install ssh" $ do
-                ruleCatchesNot invalidCmd "RUN apt-get install ssh"
-                onBuildRuleCatchesNot invalidCmd "RUN apt-get install ssh"
-        --
-        describe "gem" $
-          describe "version pinning" $ do
-            describe "i" $ do
-              it "unpinned" $ do
-                ruleCatches gemVersionPinned "RUN gem i bundler"
-                onBuildRuleCatches gemVersionPinned "RUN gem i bundler"
-              it "pinned" $ do
-                ruleCatchesNot gemVersionPinned "RUN gem i bundler:1"
-                onBuildRuleCatchesNot gemVersionPinned "RUN gem i bundler:1"
-              it "multi" $ do
-                ruleCatches gemVersionPinned "RUN gem i bunlder:1 nokogiri"
-                onBuildRuleCatches gemVersionPinned "RUN gem i bunlder:1 nokogiri"
-                ruleCatchesNot gemVersionPinned "RUN gem i bunlder:1 nokogirii:1"
-                onBuildRuleCatchesNot gemVersionPinned "RUN gem i bunlder:1 nokogiri:1"
-            describe "install" $ do
-              it "unpinned" $ do
-                ruleCatches gemVersionPinned "RUN gem install bundler"
-                onBuildRuleCatches gemVersionPinned "RUN gem install bundler"
-              it "pinned" $ do
-                ruleCatchesNot gemVersionPinned "RUN gem install bundler:1"
-                onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler:1"
-              it "does not warn on -v" $ do
-                ruleCatchesNot gemVersionPinned "RUN gem install bundler -v '2.0.1'"
-                onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler -v '2.0.1'"
-              it "does not warn on --version without =" $ do
-                ruleCatchesNot gemVersionPinned "RUN gem install bundler --version '2.0.1'"
-                onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler --version '2.0.1'"
-              it "does not warn on --version with =" $ do
-                ruleCatchesNot gemVersionPinned "RUN gem install bundler --version='2.0.1'"
-                onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler --version='2.0.1'"
-              it "does not warn on extra flags" $ do
-                ruleCatchesNot gemVersionPinned "RUN gem install bundler:2.0.1 -- --use-system-libraries=true"
-                onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler:2.0.1 -- --use-system-libraries=true"
-        --
-        describe "yum rules" $ do
-            it "yum update" $ do
-                ruleCatches noYumUpdate "RUN yum update"
-                onBuildRuleCatches noYumUpdate "RUN yum update"
-            it "yum version pinning" $ do
-                ruleCatches yumVersionPinned "RUN yum install -y tomcat && yum clean all"
-                onBuildRuleCatches yumVersionPinned "RUN yum install -y tomcat && yum clean all"
-            it "yum no clean all" $ do
-                ruleCatches yumCleanup "RUN yum install -y mariadb-10.4"
-                onBuildRuleCatches yumCleanup "RUN yum install -y mariadb-10.4"
-            it "yum non-interactive" $ do
-                ruleCatches yumYes "RUN yum install httpd-2.4.24 && yum clean all"
-                onBuildRuleCatches yumYes "RUN yum install httpd-2.4.24 && yum clean all"
-        --
-        describe "apt-get rules" $ do
-            it "apt" $
-                let dockerFile =
-                        [ "FROM ubuntu"
-                        , "RUN apt install python"
-                        ]
-                in do
-                  ruleCatches noApt $ Text.unlines dockerFile
-                  onBuildRuleCatches noApt $ Text.unlines dockerFile
-            it "apt-get upgrade" $ do
-                ruleCatches noAptGetUpgrade "RUN apt-get update && apt-get upgrade"
-                onBuildRuleCatches noAptGetUpgrade "RUN apt-get update && apt-get upgrade"
-            it "apt-get version pinning" $ do
-                ruleCatches aptGetVersionPinned "RUN apt-get update && apt-get install python"
-                onBuildRuleCatches aptGetVersionPinned "RUN apt-get update && apt-get install python"
-            it "apt-get no cleanup" $
-                let dockerFile =
-                        [ "FROM scratch"
-                        , "RUN apt-get update && apt-get install python"
-                        ]
-                in do
-                  ruleCatches aptGetCleanup $ Text.unlines dockerFile
-                  onBuildRuleCatches aptGetCleanup $ Text.unlines dockerFile
-            it "apt-get cleanup in stage image" $
-                let dockerFile =
-                        [ "FROM ubuntu as foo"
-                        , "RUN apt-get update && apt-get install python"
-                        , "FROM scratch"
-                        , "RUN echo hey!"
-                        ]
-                in do
-                  ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile
-                  onBuildRuleCatchesNot aptGetCleanup $ Text.unlines dockerFile
-            it "apt-get no cleanup in last stage" $
-                let dockerFile =
-                        [ "FROM ubuntu as foo"
-                        , "RUN hey!"
-                        , "FROM scratch"
-                        , "RUN apt-get update && apt-get install python"
-                        ]
-                in do
-                  ruleCatches aptGetCleanup $ Text.unlines dockerFile
-                  onBuildRuleCatches aptGetCleanup $ Text.unlines dockerFile
-            it "apt-get no cleanup in intermediate stage" $
-                let dockerFile =
-                        [ "FROM ubuntu as foo"
-                        , "RUN apt-get update && apt-get install python"
-                        , "FROM foo"
-                        , "RUN hey!"
-                        ]
-                in do
-                  ruleCatches aptGetCleanup $ Text.unlines dockerFile
-                  onBuildRuleCatches aptGetCleanup $ Text.unlines dockerFile
-            it "no warn apt-get cleanup in intermediate stage that cleans lists" $
-                let dockerFile =
-                        [ "FROM ubuntu as foo"
-                        , "RUN apt-get update && apt-get install python && rm -rf /var/lib/apt/lists/*"
-                        , "FROM foo"
-                        , "RUN hey!"
-                        ]
-                in do
-                  ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile
-                  onBuildRuleCatchesNot aptGetCleanup $ Text.unlines dockerFile
-            it "no warn apt-get cleanup in intermediate stage when stage not used later" $
-                let dockerFile =
-                        [ "FROM ubuntu as foo"
-                        , "RUN apt-get update && apt-get install python"
-                        , "FROM scratch"
-                        , "RUN hey!"
-                        ]
-                in do
-                  ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile
-                  onBuildRuleCatchesNot aptGetCleanup $ Text.unlines dockerFile
-            it "apt-get cleanup" $
-                let dockerFile =
-                        [ "FROM scratch"
-                        , "RUN apt-get update && apt-get install python && rm -rf /var/lib/apt/lists/*"
-                        ]
-                in do
-                  ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile
-                  onBuildRuleCatchesNot aptGetCleanup $ Text.unlines dockerFile
-
-            it "apt-get pinned chained" $
-                let dockerFile =
-                        [ "RUN apt-get update \\"
-                        , " && apt-get -yqq --no-install-recommends install nodejs=0.10 \\"
-                        , " && rm -rf /var/lib/apt/lists/*"
-                        ]
-                in do
-                  ruleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile
-                  onBuildRuleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile
-
-            it "apt-get pinned regression" $
-                let dockerFile =
-                        [ "RUN apt-get update && apt-get install --no-install-recommends -y \\"
-                        , "python-demjson=2.2.2* \\"
-                        , "wget=1.16.1* \\"
-                        , "git=1:2.5.0* \\"
-                        , "ruby=1:2.1.*"
-                        ]
-                in do
-                  ruleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile
-                  onBuildRuleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile
-
-            it "has deprecated maintainer" $
-                ruleCatches hasNoMaintainer "FROM busybox\nMAINTAINER hudu@mail.com"
-        --
-        describe "apk add rules" $ do
-            it "apk upgrade" $ do
-                ruleCatches noApkUpgrade "RUN apk update && apk upgrade"
-                onBuildRuleCatches noApkUpgrade "RUN apk update && apk upgrade"
-            it "apk add version pinning single" $ do
-                ruleCatches apkAddVersionPinned "RUN apk add flex"
-                onBuildRuleCatches apkAddVersionPinned "RUN apk add flex"
-            it "apk add no version pinning single" $ do
-                ruleCatchesNot apkAddVersionPinned "RUN apk add flex=2.6.4-r1"
-                onBuildRuleCatchesNot apkAddVersionPinned "RUN apk add flex=2.6.4-r1"
-            it "apk add version pinned chained" $
-                let dockerFile =
-                        [ "RUN apk add --no-cache flex=2.6.4-r1 \\"
-                        , " && pip install -r requirements.txt"
-                        ]
-                in do
-                  ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
-                  onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
-            it "apk add version pinned regression" $
-                let dockerFile =
-                        [ "RUN apk add --no-cache \\"
-                        , "flex=2.6.4-r1 \\"
-                        , "libffi=3.2.1-r3 \\"
-                        , "python2=2.7.13-r1 \\"
-                        , "libbz2=1.0.6-r5"
-                        ]
-                in do
-                  ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
-                  onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
-            it "apk add version pinned regression - one missed" $
-                let dockerFile =
-                        [ "RUN apk add --no-cache \\"
-                        , "flex=2.6.4-r1 \\"
-                        , "libffi \\"
-                        , "python2=2.7.13-r1 \\"
-                        , "libbz2=1.0.6-r5"
-                        ]
-                in do
-                  ruleCatches apkAddVersionPinned $ Text.unlines dockerFile
-                  onBuildRuleCatches apkAddVersionPinned $ Text.unlines dockerFile
-            it "apk add with --no-cache" $ do
-                ruleCatches apkAddNoCache "RUN apk add flex=2.6.4-r1"
-                onBuildRuleCatches apkAddNoCache "RUN apk add flex=2.6.4-r1"
-            it "apk add without --no-cache" $ do
-                ruleCatchesNot apkAddNoCache "RUN apk add --no-cache flex=2.6.4-r1"
-                onBuildRuleCatchesNot apkAddNoCache "RUN apk add --no-cache flex=2.6.4-r1"
-            it "apk add virtual package" $
-                let dockerFile =
-                        [ "RUN apk add \\"
-                        , "--virtual build-dependencies \\"
-                        , "python-dev=1.1.1 build-base=2.2.2 wget=3.3.3 \\"
-                        , "&& pip install -r requirements.txt \\"
-                        , "&& python setup.py install \\"
-                        , "&& apk del build-dependencies"
-                        ]
-                in do
-                  ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
-                  onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
-            it "apk add with repository without equal sign" $
-                let dockerFile =
-                        [ "RUN apk add --no-cache \\"
-                        , "--repository https://nl.alpinelinux.org/alpine/edge/testing \\"
-                        , "flow=0.78.0-r0"
-                        ]
-                in do
-                ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
-                onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
-            it "apk add with repository with equal sign" $
-                let dockerFile =
-                        [ "RUN apk add --no-cache \\"
-                        , "--repository=https://nl.alpinelinux.org/alpine/edge/testing \\"
-                        , "flow=0.78.0-r0"
-                        ]
-                in do
-                ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
-                onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
-            it "apk add with repository (-X) without equal sign" $
-                let dockerFile =
-                        [ "RUN apk add --no-cache \\"
-                        , "-X https://nl.alpinelinux.org/alpine/edge/testing \\"
-                        , "flow=0.78.0-r0"
-                        ]
-                in do
-                ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
-                onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
-        --
-        describe "EXPOSE rules" $ do
-            it "invalid port" $ ruleCatches invalidPort "EXPOSE 80000"
-            it "valid port" $ ruleCatchesNot invalidPort "EXPOSE 60000"
-        --
-        describe "pip pinning" $ do
-            it "pip2 version not pinned" $ do
-                ruleCatches pipVersionPinned "RUN pip2 install MySQL_python"
-                onBuildRuleCatches pipVersionPinned "RUN pip2 install MySQL_python"
-            it "pip3 version not pinned" $ do
-                ruleCatches pipVersionPinned "RUN pip3 install MySQL_python"
-                onBuildRuleCatches pipVersionPinned "RUN pip2 install MySQL_python"
-            it "pip3 version pinned" $ do
-                ruleCatchesNot pipVersionPinned "RUN pip3 install MySQL_python==1.2.2"
-                onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install MySQL_python==1.2.2"
-            it "pip install requirements" $ do
-                ruleCatchesNot pipVersionPinned "RUN pip install -r requirements.txt"
-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install -r requirements.txt"
-            it "pip install requirements with long flag" $ do
-                ruleCatchesNot pipVersionPinned "RUN pip install --requirement requirements.txt"
-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install --requirement requirements.txt"
-            it "pip install use setup.py" $ do
-                ruleCatchesNot pipVersionPinned "RUN pip install ."
-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install ."
-            it "pip version not pinned" $ do
-                ruleCatches pipVersionPinned "RUN pip install MySQL_python"
-                onBuildRuleCatches pipVersionPinned "RUN pip install MySQL_python"
-            it "pip version pinned" $ do
-                ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2"
-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2"
-            it "pip version pinned with ~= operator" $ do
-                ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python~=1.2.2"
-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python~=1.2.2"
-            it "pip version pinned with === operator" $ do
-                ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python===1.2.2"
-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python===1.2.2"
-            it "pip version pinned with flag --ignore-installed" $ do
-                ruleCatchesNot pipVersionPinned "RUN pip install --ignore-installed MySQL_python==1.2.2"
-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install --ignore-installed MySQL_python==1.2.2"
-            it "pip version pinned with flag --build" $ do
-                ruleCatchesNot pipVersionPinned "RUN pip3 install --build /opt/yamllint yamllint==1.20.0"
-                onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --build /opt/yamllint yamllint==1.20.0"
-            it "pip version pinned with flag --prefix" $ do
-                ruleCatchesNot pipVersionPinned "RUN pip3 install --prefix /opt/yamllint yamllint==1.20.0"
-                onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --prefix /opt/yamllint yamllint==1.20.0"
-            it "pip version pinned with flag --root" $ do
-                ruleCatchesNot pipVersionPinned "RUN pip3 install --root /opt/yamllint yamllint==1.20.0"
-                onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --root /opt/yamllint yamllint==1.20.0"
-            it "pip version pinned with flag --target" $ do
-                ruleCatchesNot pipVersionPinned "RUN pip3 install --target /opt/yamllint yamllint==1.20.0"
-                onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --target /opt/yamllint yamllint==1.20.0"
-            it "pip version pinned with flag --trusted-host" $ do
-                ruleCatchesNot pipVersionPinned "RUN pip3 install --trusted-host host example==1.2.2"
-                onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --trusted-host host example==1.2.2"
-            it "pip version pinned with python -m" $ do
-                ruleCatchesNot pipVersionPinned "RUN python -m pip install example==1.2.2"
-                onBuildRuleCatchesNot pipVersionPinned "RUN python -m pip install example==1.2.2"
-            it "pip version not pinned with python -m" $ do
-                ruleCatches pipVersionPinned "RUN python -m pip install example"
-                onBuildRuleCatches pipVersionPinned "RUN python -m pip install --index-url url example"
-            it "pip install git" $ do
-                ruleCatchesNot
-                    pipVersionPinned
-                    "RUN pip install git+https://github.com/rtfd/r-ext.git@0.6-alpha#egg=r-ext"
-                onBuildRuleCatchesNot
-                    pipVersionPinned
-                    "RUN pip install git+https://github.com/rtfd/r-ext.git@0.6-alpha#egg=r-ext"
-            it "pip install unversioned git" $ do
-                ruleCatches
-                    pipVersionPinned
-                    "RUN pip install git+https://github.com/rtfd/read-ext.git#egg=read-ext"
-                onBuildRuleCatches
-                    pipVersionPinned
-                    "RUN pip install git+https://github.com/rtfd/read-ext.git#egg=read-ext"
-            it "pip install upper bound" $ do
-                ruleCatchesNot pipVersionPinned "RUN pip install 'alabaster>=0.7'"
-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install 'alabaster>=0.7'"
-            it "pip install lower bound" $ do
-                ruleCatchesNot pipVersionPinned "RUN pip install 'alabaster<0.7'"
-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install 'alabaster<0.7'"
-            it "pip install excluded version" $ do
-                ruleCatchesNot pipVersionPinned "RUN pip install 'alabaster!=0.7'"
-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install 'alabaster!=0.7'"
-            it "pip install user directory" $ do
-                ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2 --user"
-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2 --user"
-            it "pip install no pip version check" $ do
-                ruleCatchesNot
-                    pipVersionPinned
-                    "RUN pip install MySQL_python==1.2.2 --disable-pip-version-check"
-                onBuildRuleCatchesNot
-                    pipVersionPinned
-                    "RUN pip install MySQL_python==1.2.2 --disable-pip-version-check"
-            it "pip install --index-url" $ do
-                ruleCatchesNot
-                    pipVersionPinned
-                    "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"
-                onBuildRuleCatchesNot
-                    pipVersionPinned
-                    "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"
-            it "pip install index-url with -i flag" $ do
-                ruleCatchesNot
-                    pipVersionPinned
-                    "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"
-                onBuildRuleCatchesNot
-                    pipVersionPinned
-                    "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"
-            it "pip install --index-url with --extra-index-url" $ do
-                ruleCatchesNot
-                    pipVersionPinned
-                    "RUN pip install --index-url https://eg.com/foo --extra-index-url https://ex-eg.io/foo foobar==1.0.0"
-                onBuildRuleCatchesNot
-                    pipVersionPinned
-                    "RUN pip install --index-url https://eg.com/foo --extra-index-url https://ex-eg.io/foo foobar==1.0.0"
-            it "pip install no cache dir" $ do
-                ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2 --no-cache-dir"
-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2 --no-cache-dir"
-            it "pip install constraints file - long version argument" $ do
-                ruleCatchesNot pipVersionPinned "RUN pip install pykafka --constraint http://foo.bar.baz"
-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install pykafka --constraint http://foo.bar.baz"
-            it "pip install constraints file - short version argument" $ do
-                ruleCatchesNot pipVersionPinned "RUN pip install pykafka -c http://foo.bar.baz"
-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install pykafka -c http://foo.bar.baz"
-            it "pip install --index-url with --extra-index-url with basic auth" $ do
-                ruleCatchesNot
-                    pipVersionPinned
-                    "RUN pip install --index-url https://user:pass@eg.com/foo --extra-index-url https://user:pass@ex-eg.io/foo foobar==1.0.0"
-                onBuildRuleCatchesNot
-                    pipVersionPinned
-                    "RUN pip install --index-url https://user:pass@eg.com/foo --extra-index-url https://user:pass@ex-eg.io/foo foobar==1.0.0"
-        --
-        describe "npm pinning" $ do
-            it "version pinned in package.json" $ do
-                ruleCatchesNot npmVersionPinned "RUN npm install"
-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install"
-            it "version pinned in package.json with arguments" $ do
-                ruleCatchesNot npmVersionPinned "RUN npm install --progress=false"
-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install --progress=false"
-            it "version pinned" $ do
-                ruleCatchesNot npmVersionPinned "RUN npm install express@4.1.1"
-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install express@4.1.1"
-            it "version pinned with scope" $ do
-                ruleCatchesNot npmVersionPinned "RUN npm install @myorg/privatepackage@\">=0.1.0\""
-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install @myorg/privatepackage@\">=0.1.0\""
-            it "version pinned multiple packages" $ do
-                ruleCatchesNot npmVersionPinned "RUN npm install express@\"4.1.1\" sax@0.1.1"
-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install express@\"4.1.1\" sax@0.1.1"
-            it "version pinned with --global" $ do
-                ruleCatchesNot npmVersionPinned "RUN npm install --global express@\"4.1.1\""
-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install --global express@\"4.1.1\""
-            it "version pinned with -g" $ do
-                ruleCatchesNot npmVersionPinned "RUN npm install -g express@\"4.1.1\""
-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install -g express@\"4.1.1\""
-            it "version does not have to be pinned for tarball suffix .tar" $ do
-                ruleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tar"
-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tar"
-            it "version does not have to be pinned for tarball suffix .tar.gz" $ do
-                ruleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tar.gz"
-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tar.gz"
-            it "version does not have to be pinned for tarball suffix .tgz" $ do
-                ruleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tgz"
-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tgz"
-            it "version does not have to be pinned for folder - absolute path" $ do
-                ruleCatchesNot npmVersionPinned "RUN npm install /folder"
-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install /folder"
-            it "version does not have to be pinned for folder - relative path from current folder" $ do
-                ruleCatchesNot npmVersionPinned "RUN npm install ./folder"
-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install ./folder"
-            it "version does not have to be pinned for folder - relative path to parent folder" $ do
-                ruleCatchesNot npmVersionPinned "RUN npm install ../folder"
-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install ../folder"
-            it "version does not have to be pinned for folder - relative path from home" $ do
-                ruleCatchesNot npmVersionPinned "RUN npm install ~/folder"
-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install ~/folder"
-            it "commit pinned for git+ssh" $ do
-                ruleCatchesNot
-                    npmVersionPinned
-                    "RUN npm install git+ssh://git@github.com:npm/npm.git#v1.0.27"
-                onBuildRuleCatchesNot
-                    npmVersionPinned
-                    "RUN npm install git+ssh://git@github.com:npm/npm.git#v1.0.27"
-            it "commit pinned for git+http" $ do
-                ruleCatchesNot
-                    npmVersionPinned
-                    "RUN npm install git+http://isaacs@github.com/npm/npm#semver:^5.0"
-                onBuildRuleCatchesNot
-                    npmVersionPinned
-                    "RUN npm install git+http://isaacs@github.com/npm/npm#semver:^5.0"
-            it "commit pinned for git+https" $ do
-                ruleCatchesNot
-                    npmVersionPinned
-                    "RUN npm install git+https://isaacs@github.com/npm/npm.git#v1.0.27"
-                onBuildRuleCatchesNot
-                    npmVersionPinned
-                    "RUN npm install git+https://isaacs@github.com/npm/npm.git#v1.0.27"
-            it "commit pinned for git" $ do
-                ruleCatchesNot
-                    npmVersionPinned
-                    "RUN npm install git://github.com/npm/npm.git#v1.0.27"
-                onBuildRuleCatchesNot
-                    npmVersionPinned
-                    "RUN npm install git://github.com/npm/npm.git#v1.0.27"
-            it "npm run install is fine" $ do
-                ruleCatchesNot
-                    npmVersionPinned
-                    "RUN npm run --crazy install"
-                onBuildRuleCatchesNot
-                    npmVersionPinned
-                    "RUN npm run --crazy install"
-
-      --version range is not supported
-            it "version pinned with scope" $ do
-                ruleCatchesNot npmVersionPinned "RUN npm install @myorg/privatepackage@\">=0.1.0 <0.2.0\""
-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install @myorg/privatepackage@\">=0.1.0 <0.2.0\""
-            it "version not pinned" $ do
-                ruleCatches npmVersionPinned "RUN npm install express"
-                onBuildRuleCatches npmVersionPinned "RUN npm install express"
-            it "version not pinned with scope" $ do
-                ruleCatches npmVersionPinned "RUN npm install @myorg/privatepackage"
-                onBuildRuleCatches npmVersionPinned "RUN npm install @myorg/privatepackage"
-            it "version not pinned multiple packages" $ do
-                ruleCatches npmVersionPinned "RUN npm install express sax@0.1.1"
-                onBuildRuleCatches npmVersionPinned "RUN npm install express sax@0.1.1"
-            it "version not pinned with --global" $ do
-                ruleCatches npmVersionPinned "RUN npm install --global express"
-                onBuildRuleCatches npmVersionPinned "RUN npm install --global express"
-            it "commit not pinned for git+ssh" $ do
-                ruleCatches npmVersionPinned "RUN npm install git+ssh://git@github.com:npm/npm.git"
-                onBuildRuleCatches npmVersionPinned "RUN npm install git+ssh://git@github.com:npm/npm.git"
-            it "commit not pinned for git+http" $ do
-                ruleCatches npmVersionPinned "RUN npm install git+http://isaacs@github.com/npm/npm"
-                onBuildRuleCatches npmVersionPinned "RUN npm install git+http://isaacs@github.com/npm/npm"
-            it "commit not pinned for git+https" $ do
-                ruleCatches
-                    npmVersionPinned
-                    "RUN npm install git+https://isaacs@github.com/npm/npm.git"
-                onBuildRuleCatches
-                    npmVersionPinned
-                    "RUN npm install git+https://isaacs@github.com/npm/npm.git"
-            it "commit not pinned for git" $ do
-                ruleCatches npmVersionPinned "RUN npm install git://github.com/npm/npm.git"
-                onBuildRuleCatches npmVersionPinned "RUN npm install git://github.com/npm/npm.git"
-        --
-        describe "use SHELL" $ do
-            it "RUN ln" $ do
-                ruleCatches useShell "RUN ln -sfv /bin/bash /bin/sh"
-                onBuildRuleCatches useShell "RUN ln -sfv /bin/bash /bin/sh"
-            it "RUN ln with unrelated symlinks" $ do
-                ruleCatchesNot useShell "RUN ln -sf /bin/true /sbin/initctl"
-                onBuildRuleCatchesNot useShell "RUN ln -sf /bin/true /sbin/initctl"
-            it "RUN ln with multiple acceptable commands" $ do
-                ruleCatchesNot useShell "RUN ln -s foo bar && unrelated && something_with /bin/sh"
-                onBuildRuleCatchesNot useShell "RUN ln -s foo bar && unrelated && something_with /bin/sh"
-        --
-        --
-        describe "Shellcheck" $ do
-            it "runs shellchek on RUN instructions" $ do
-                ruleCatches shellcheck "RUN echo $MISSING_QUOTES"
-                onBuildRuleCatches shellcheck "RUN echo $MISSING_QUOTES"
-            it "not warns on valid scripts" $ do
-                ruleCatchesNot shellcheck "RUN echo foo"
-                onBuildRuleCatchesNot shellcheck "RUN echo foo"
-
-            it "Does not complain on default env vars" $
-                let dockerFile = Text.unlines
-                        [ "RUN echo \"$HTTP_PROXY\""
-                        , "RUN echo \"$http_proxy\""
-                        , "RUN echo \"$HTTPS_PROXY\""
-                        , "RUN echo \"$https_proxy\""
-                        , "RUN echo \"$FTP_PROXY\""
-                        , "RUN echo \"$ftp_proxy\""
-                        , "RUN echo \"$NO_PROXY\""
-                        , "RUN echo \"$no_proxy\""
-                        ]
-                in do
-                  ruleCatchesNot shellcheck dockerFile
-                  onBuildRuleCatchesNot shellcheck dockerFile
-
-            it "Complain on missing env vars" $
-                let dockerFile = Text.unlines
-                        [ "RUN echo \"$RTTP_PROXY\""
-                        ]
-                in do
-                  ruleCatches shellcheck dockerFile
-                  onBuildRuleCatches shellcheck dockerFile
-
-            it "Is aware of ARGS and ENV" $
-                let dockerFile = Text.unlines
-                        [ "ARG foo=bar"
-                        , "ARG another_foo"
-                        , "ENV bar=10 baz=20"
-                        , "RUN echo \"$foo\""
-                        , "RUN echo \"$another_foo\""
-                        , "RUN echo \"$bar\""
-                        , "RUN echo \"$baz\""
-                        ]
-                in do
-                  ruleCatchesNot shellcheck dockerFile
-                  onBuildRuleCatchesNot shellcheck dockerFile
-
-            it "Resets env vars after a FROM" $
-                let dockerFile = Text.unlines
-                        [ "ARG foo=bar"
-                        , "ARG another_foo"
-                        , "ENV bar=10 baz=20"
-                        , "FROM debian"
-                        , "RUN echo \"$foo\""
-                        ]
-                in do
-                  ruleCatches shellcheck dockerFile
-                  onBuildRuleCatches shellcheck dockerFile
-
-            it "Defaults the shell to sh" $
-                let dockerFile = Text.unlines
-                        [ "RUN echo $RANDOM"
-                        ]
-                in do
-                  ruleCatches shellcheck dockerFile
-                  onBuildRuleCatches shellcheck dockerFile
-
-            it "Can change the shell check to bash" $
-                let dockerFile = Text.unlines
-                        [ "SHELL [\"/bin/bash\", \"-eo\", \"pipefail\", \"-c\"]"
-                        , "RUN echo $RANDOM"
-                        ]
-                in do
-                  ruleCatchesNot shellcheck dockerFile
-                  onBuildRuleCatchesNot shellcheck dockerFile
-
-            it "Resets the SHELL to sh after a FROM" $
-                let dockerFile = Text.unlines
-                        [ "SHELL [\"/bin/bash\", \"-eo\", \"pipefail\", \"-c\"]"
-                        , "FROM debian"
-                        , "RUN echo $RANDOM"
-                        ]
-                in do
-                  ruleCatches shellcheck dockerFile
-                  onBuildRuleCatches shellcheck dockerFile
-
-            it "Does not complain on ash shell" $
-                let dockerFile = Text.unlines
-                        [ "SHELL [\"/bin/ash\", \"-o\", \"pipefail\", \"-c\"]"
-                        , "RUN echo hello"
-                        ]
-                in do
-                  ruleCatchesNot shellcheck dockerFile
-                  onBuildRuleCatchesNot shellcheck dockerFile
-
-            it "Does not complain on powershell" $
-                let dockerFile = Text.unlines
-                        [ "SHELL [\"pwsh\", \"-c\"]"
-                        , "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"
-                        ]
-                in do
-                  ruleCatchesNot shellcheck dockerFile
-                  onBuildRuleCatchesNot shellcheck dockerFile
-        --
-        --
-        describe "COPY rules" $ do
-            it "use add" $ ruleCatches useAdd "COPY packaged-app.tar /usr/src/app"
-            it "use not add" $ ruleCatchesNot useAdd "COPY package.json /usr/src/app"
-        --
-        describe "other rules" $ do
-            it "apt-get auto yes" $ do
-              ruleCatches aptGetYes "RUN apt-get install python"
-              onBuildRuleCatches aptGetYes "RUN apt-get install python"
-            it "apt-get yes shortflag" $ do
-              ruleCatchesNot aptGetYes "RUN apt-get install -yq python"
-              onBuildRuleCatchesNot aptGetYes "RUN apt-get install -yq python"
-            it "apt-get yes quiet level 2 implies -y" $ do
-                ruleCatchesNot aptGetYes "RUN apt-get install -qq python"
-                onBuildRuleCatchesNot aptGetYes "RUN apt-get install -qq python"
-            it "apt-get yes different pos" $ do
-                ruleCatchesNot aptGetYes "RUN apt-get install -y python"
-                onBuildRuleCatchesNot aptGetYes "RUN apt-get install -y python"
-            it "apt-get with auto yes" $ do
-                ruleCatchesNot aptGetYes "RUN apt-get -y install python"
-                onBuildRuleCatchesNot aptGetYes "RUN apt-get -y install python"
-            it "apt-get with auto expanded yes" $ do
-                ruleCatchesNot aptGetYes "RUN apt-get --yes install python"
-                onBuildRuleCatchesNot aptGetYes "RUN apt-get --yes install python"
-            it "apt-get with assume-yes" $ do
-                ruleCatchesNot aptGetYes "RUN apt-get --assume-yes install python"
-                onBuildRuleCatchesNot aptGetYes "RUN apt-get --assume-yes install python"
-            it "apt-get install recommends" $ do
-                ruleCatchesNot
-                    aptGetNoRecommends
-                    "RUN apt-get install --no-install-recommends python"
-                onBuildRuleCatchesNot
-                    aptGetNoRecommends
-                    "RUN apt-get install --no-install-recommends python"
-            it "apt-get no install recommends" $ do
-                ruleCatches aptGetNoRecommends "RUN apt-get install python"
-                onBuildRuleCatches aptGetNoRecommends "RUN apt-get install python"
-            it "apt-get no install recommends" $ do
-                ruleCatches aptGetNoRecommends "RUN apt-get -y install python"
-                onBuildRuleCatches aptGetNoRecommends "RUN apt-get -y install python"
-            it "apt-get no install recommends via option" $ do
-                ruleCatchesNot aptGetNoRecommends "RUN apt-get -o APT::Install-Recommends=false install python"
-                onBuildRuleCatchesNot aptGetNoRecommends "RUN apt-get -o APT::Install-Recommends=false install python"
-            it "apt-get version" $ do
-                ruleCatchesNot aptGetVersionPinned "RUN apt-get install -y python=1.2.2"
-                onBuildRuleCatchesNot aptGetVersionPinned "RUN apt-get install -y python=1.2.2"
-            it "apt-get version" $ do
-                ruleCatchesNot aptGetVersionPinned "RUN apt-get install ./wkhtmltox_0.12.5-1.bionic_amd64.deb"
-                onBuildRuleCatchesNot aptGetVersionPinned "RUN apt-get install ./wkhtmltox_0.12.5-1.bionic_amd64.deb"
-            it "apt-get pinned" $ do
-                ruleCatchesNot
-                    aptGetVersionPinned
-                    "RUN apt-get -y --no-install-recommends install nodejs=0.10"
-                onBuildRuleCatchesNot
-                    aptGetVersionPinned
-                    "RUN apt-get -y --no-install-recommends install nodejs=0.10"
-            it "apt-get tolerate target-release" $
-                let dockerFile =
-                        [ "RUN set -e &&\\"
-                        , " echo \"deb http://http.debian.net/debian jessie-backports main\" \
-                          \> /etc/apt/sources.list.d/jessie-backports.list &&\\"
-                        , " apt-get update &&\\"
-                        , " apt-get install -y --no-install-recommends -t jessie-backports \
-                          \openjdk-8-jdk=8u131-b11-1~bpo8+1 &&\\"
-                        , " rm -rf /var/lib/apt/lists/*"
-                        ]
-                in do
-                  ruleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile
-                  onBuildRuleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile
-
-            it "has maintainer" $ ruleCatches hasNoMaintainer "FROM debian\nMAINTAINER Lukas"
-            it "has maintainer first" $ ruleCatches hasNoMaintainer "MAINTAINER Lukas\nFROM DEBIAN"
-            it "has no maintainer" $ ruleCatchesNot hasNoMaintainer "FROM debian"
-            it "using add" $ ruleCatches copyInsteadAdd "ADD file /usr/src/app/"
-
-            it "many cmds" $
-                let dockerFile =
-                        [ "FROM debian"
-                        , "CMD bash"
-                        , "RUN foo"
-                        , "CMD another"
-                        ]
-                in ruleCatches multipleCmds $ Text.unlines dockerFile
-
-            it "single cmds, different stages" $
-                let dockerFile =
-                        [ "FROM debian as distro1"
-                        , "CMD bash"
-                        , "RUN foo"
-                        , "FROM debian as distro2"
-                        , "CMD another"
-                        ]
-                in ruleCatchesNot multipleCmds $ Text.unlines dockerFile
-
-            it "many cmds, different stages" $
-                let dockerFile =
-                        [ "FROM debian as distro1"
-                        , "CMD bash"
-                        , "RUN foo"
-                        , "CMD another"
-                        , "FROM debian as distro2"
-                        , "CMD another"
-                        ]
-                in ruleCatches multipleCmds $ Text.unlines dockerFile
-
-            it "single cmd" $ ruleCatchesNot multipleCmds "CMD /bin/true"
-            it "no cmd" $ ruleCatchesNot multipleEntrypoints "FROM busybox"
-
-            it "many entrypoints" $
-                let dockerFile =
-                        [ "FROM debian"
-                        , "ENTRYPOINT bash"
-                        , "RUN foo"
-                        , "ENTRYPOINT another"
-                        ]
-                in ruleCatches multipleEntrypoints $ Text.unlines dockerFile
-
-            it "single entrypoint, different stages" $
-                let dockerFile =
-                        [ "FROM debian as distro1"
-                        , "ENTRYPOINT bash"
-                        , "RUN foo"
-                        , "FROM debian as distro2"
-                        , "ENTRYPOINT another"
-                        ]
-                in ruleCatchesNot multipleEntrypoints $ Text.unlines dockerFile
-
-            it "many entrypoints, different stages" $
-                let dockerFile =
-                        [ "FROM debian as distro1"
-                        , "ENTRYPOINT bash"
-                        , "RUN foo"
-                        , "ENTRYPOINT another"
-                        , "FROM debian as distro2"
-                        , "ENTRYPOINT another"
-                        ]
-                in ruleCatches multipleEntrypoints $ Text.unlines dockerFile
-            it "single entry" $ ruleCatchesNot multipleEntrypoints "ENTRYPOINT /bin/true"
-            it "no entry" $ ruleCatchesNot multipleEntrypoints "FROM busybox"
-            it "workdir variable" $ ruleCatchesNot absoluteWorkdir "WORKDIR ${work}"
-            it "scratch" $ ruleCatchesNot noUntagged "FROM scratch"
-        --
-        describe "add files and archives" $ do
-            it "add for tar" $ ruleCatchesNot copyInsteadAdd "ADD file.tar /usr/src/app/"
-            it "add for zip" $ ruleCatchesNot copyInsteadAdd "ADD file.zip /usr/src/app/"
-            it "add for gzip" $ ruleCatchesNot copyInsteadAdd "ADD file.gz /usr/src/app/"
-            it "add for bz2" $ ruleCatchesNot copyInsteadAdd "ADD file.bz2 /usr/src/app/"
-            it "add for xz" $ ruleCatchesNot copyInsteadAdd "ADD file.xz /usr/src/app/"
-            it "add for tgz" $ ruleCatchesNot copyInsteadAdd "ADD file.tgz /usr/src/app/"
-            it "add for url" $ ruleCatchesNot copyInsteadAdd "ADD http://file.com /usr/src/app/"
-        --
-        describe "copy last argument" $ do
-            it "no warn on 2 args" $ ruleCatchesNot copyEndingSlash "COPY foo bar"
-            it "warn on 3 args" $ ruleCatches copyEndingSlash "COPY foo bar baz"
-            it "no warn on 3 args" $ ruleCatchesNot copyEndingSlash "COPY foo bar baz/"
-        --
-        describe "copy from existing alias" $ do
-            it "warn on missing alias" $ ruleCatches copyFromExists "COPY --from=foo bar ."
-            it "warn on alias defined after" $
-                let dockerFile =
-                        [ "FROM scratch"
-                        , "COPY --from=build foo ."
-                        , "FROM node as build"
-                        , "RUN baz"
-                        ]
-                in ruleCatches copyFromExists $ Text.unlines dockerFile
-            it "don't warn on correctly defined aliases" $
-                let dockerFile =
-                        [ "FROM scratch as build"
-                        , "RUN foo"
-                        , "FROM node"
-                        , "COPY --from=build foo ."
-                        , "RUN baz"
-                        ]
-                in ruleCatchesNot copyFromExists $ Text.unlines dockerFile
-        --
-        describe "copy from own FROM" $ do
-            it "warn on copying from your the same FROM" $
-                let dockerFile =
-                        [ "FROM node as foo"
-                        , "COPY --from=foo bar ."
-                        ]
-                in ruleCatches copyFromAnother $ Text.unlines dockerFile
-            it "don't warn on copying form other sources" $
-                let dockerFile =
-                        [ "FROM scratch as build"
-                        , "RUN foo"
-                        , "FROM node as run"
-                        , "COPY --from=build foo ."
-                        , "RUN baz"
-                        ]
-                in ruleCatchesNot copyFromAnother $ Text.unlines dockerFile
-        --
-        describe "Duplicate aliases" $ do
-            it "warn on duplicate aliases" $
-                let dockerFile =
-                        [ "FROM node as foo"
-                        , "RUN something"
-                        , "FROM scratch as foo"
-                        , "RUN something"
-                        ]
-                in ruleCatches fromAliasUnique $ Text.unlines dockerFile
-            it "don't warn on unique aliases" $
-                let dockerFile =
-                        [ "FROM scratch as build"
-                        , "RUN foo"
-                        , "FROM node as run"
-                        , "RUN baz"
-                        ]
-                in ruleCatchesNot fromAliasUnique $ Text.unlines dockerFile
-        --
-        describe "format error" $
-            it "display error after line pos" $ do
-                let ast = parseText "FOM debian:jessie"
-                    expectedMsg = "<string>:1:1 unexpected 'F' expecting '#', ADD, ARG, CMD, COPY, ENTRYPOINT, " <>
-                                  "ENV, EXPOSE, FROM, HEALTHCHECK, LABEL, MAINTAINER, ONBUILD, RUN, SHELL, STOPSIGNAL, " <>
-                                  "USER, VOLUME, WORKDIR, or end of input "
-                case ast of
-                    Left err -> assertEqual "Unexpected error msg" expectedMsg (formatError err)
-                    Right _ -> assertFailure "AST should fail parsing"
-        --
-        describe "Rules can be ignored with inline comments" $ do
-            it "ignores single rule" $
-                let dockerFile =
-                        [ "FROM ubuntu"
-                        , "# hadolint ignore=DL3002"
-                        , "USER root"
-                        ]
-                in ruleCatchesNot noRootUser $ Text.unlines dockerFile
-            it "ignores only the given rule" $
-                let dockerFile =
-                        [ "FROM scratch"
-                        , "# hadolint ignore=DL3001"
-                        , "USER root"
-                        ]
-                in ruleCatches noRootUser $ Text.unlines dockerFile
-            it "ignores only the given rule, when multiple passed" $
-                let dockerFile =
-                        [ "FROM scratch"
-                        , "# hadolint ignore=DL3001,DL3002"
-                        , "USER root"
-                        ]
-                in ruleCatchesNot noRootUser $ Text.unlines dockerFile
-            it "ignores the rule only if directly above the instruction" $
-                let dockerFile =
-                        [ "# hadolint ignore=DL3001,DL3002"
-                        , "FROM ubuntu"
-                        , "USER root"
-                        ]
-                in ruleCatches noRootUser $ Text.unlines dockerFile
-            it "won't ignore the rule if passed invalid rule names" $
-                let dockerFile =
-                        [ "FROM scratch"
-                        , "# hadolint ignore=crazy,DL3002"
-                        , "USER root"
-                        ]
-                in ruleCatches noRootUser $ Text.unlines dockerFile
-            it "ignores multiple rules correctly, even with some extra whitespace" $
-                let dockerFile =
-                        [ "FROM node as foo"
-                        , "# hadolint ignore=DL3023, DL3021"
-                        , "COPY --from=foo bar baz ."
-                        ]
-                in do
-                  ruleCatchesNot copyFromAnother $ Text.unlines dockerFile
-                  ruleCatchesNot copyEndingSlash $ Text.unlines dockerFile
-        --
-        describe "JSON notation in ENTRYPOINT and CMD" $ do
-            it "warn on ENTRYPOINT" $
-                let dockerFile =
-                        [ "FROM node as foo"
-                        , "ENTRYPOINT something"
-                        ]
-                in ruleCatches useJsonArgs $ Text.unlines dockerFile
-            it "don't warn on ENTRYPOINT json notation" $
-                let dockerFile =
-                        [ "FROM scratch as build"
-                        , "ENTRYPOINT [\"foo\", \"bar\"]"
-                        ]
-                in ruleCatchesNot useJsonArgs $ Text.unlines dockerFile
-            it "warn on CMD" $
-                let dockerFile =
-                        [ "FROM node as foo"
-                        , "CMD something"
-                        ]
-                in ruleCatches useJsonArgs $ Text.unlines dockerFile
-            it "don't warn on CMD json notation" $
-                let dockerFile =
-                        [ "FROM scratch as build"
-                        , "CMD [\"foo\", \"bar\"]"
-                        , "CMD [ \"foo\", \"bar\" ]"
-                        ]
-                in ruleCatchesNot useJsonArgs $ Text.unlines dockerFile
-
-        --
-        describe "Detects missing pipefail option" $ do
-            it "warn on missing pipefail" $
-                let dockerFile =
-                        [ "FROM scratch"
-                        , "RUN wget -O - https://some.site | wc -l > /number"
-                        ]
-                in ruleCatches usePipefail $ Text.unlines dockerFile
-            it "don't warn on commands with no pipes" $
-                let dockerFile =
-                        [ "FROM scratch as build"
-                        , "RUN wget -O - https://some.site && wc -l file > /number"
-                        ]
-                in ruleCatchesNot usePipefail $ Text.unlines dockerFile
-            it "don't warn on commands with pipes and the pipefail option" $
-                let dockerFile =
-                        [ "FROM scratch as build"
-                        , "SHELL [\"/bin/bash\", \"-eo\", \"pipefail\", \"-c\"]"
-                        , "RUN wget -O - https://some.site | wc -l file > /number"
-                        ]
-                in ruleCatchesNot usePipefail $ Text.unlines dockerFile
-            it "don't warn on commands with pipes and the pipefail option 2" $
-                let dockerFile =
-                        [ "FROM scratch as build"
-                        , "SHELL [\"/bin/bash\", \"-e\", \"-o\", \"pipefail\", \"-c\"]"
-                        , "RUN wget -O - https://some.site | wc -l file > /number"
-                        ]
-                in ruleCatchesNot usePipefail $ Text.unlines dockerFile
-            it "don't warn on commands with pipes and the pipefail option 3" $
-                let dockerFile =
-                        [ "FROM scratch as build"
-                        , "SHELL [\"/bin/bash\", \"-o\", \"errexit\", \"-o\", \"pipefail\", \"-c\"]"
-                        , "RUN wget -O - https://some.site | wc -l file > /number"
-                        ]
-                in ruleCatchesNot usePipefail $ Text.unlines dockerFile
-            it "don't warn on commands with pipes and the pipefail zsh" $
-                let dockerFile =
-                        [ "FROM scratch as build"
-                        , "SHELL [\"/bin/zsh\", \"-o\", \"pipefail\", \"-c\"]"
-                        , "RUN wget -O - https://some.site | wc -l file > /number"
-                        ]
-                in ruleCatchesNot usePipefail $ Text.unlines dockerFile
-            it "don't warn on powershell" $
-                let dockerFile =
-                        [ "FROM scratch as build"
-                        , "SHELL [\"pwsh\", \"-c\"]"
-                        , "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"
-                        ]
-                in ruleCatchesNot usePipefail $ Text.unlines dockerFile
-            it "warns when using plain sh" $
-                let dockerFile =
-                        [ "FROM scratch as build"
-                        , "SHELL [\"/bin/sh\", \"-o\", \"pipefail\", \"-c\"]"
-                        , "RUN wget -O - https://some.site | wc -l file > /number"
-                        ]
-                in ruleCatches usePipefail $ Text.unlines dockerFile
-            it "warn on missing pipefail in the next image" $
-                let dockerFile =
-                        [ "FROM scratch as build"
-                        , "SHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]"
-                        , "RUN wget -O - https://some.site | wc -l file > /number"
-                        , "FROM scratch as build2"
-                        , "RUN wget -O - https://some.site | wc -l file > /number"
-                        ]
-                in ruleCatches usePipefail $ Text.unlines dockerFile
-            it "warn on missing pipefail if next SHELL is not using it" $
-                let dockerFile =
-                        [ "FROM scratch as build"
-                        , "SHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]"
-                        , "RUN wget -O - https://some.site | wc -l file > /number"
-                        , "SHELL [\"/bin/sh\", \"-c\"]"
-                        , "RUN wget -O - https://some.site | wc -l file > /number"
-                        ]
-                in ruleCatches usePipefail $ Text.unlines dockerFile
-        --
-        describe "Allowed docker registries" $ do
-            it "warn on non-allowed registry" $
-                let dockerFile =
-                        [ "FROM random.com/debian"
-                        ]
-                in ruleCatches (registryIsAllowed ["docker.io"]) $ Text.unlines dockerFile
-            it "don't warn on empty allowed registries" $
-                let dockerFile =
-                        [ "FROM random.com/debian"
-                        ]
-                in ruleCatchesNot (registryIsAllowed []) $ Text.unlines dockerFile
-            it "don't warn on allowed registries" $
-                let dockerFile =
-                        [ "FROM random.com/debian"
-                        ]
-                in ruleCatchesNot (registryIsAllowed ["x.com", "random.com"]) $ Text.unlines dockerFile
-            it "doesn't warn on scratch image" $
-                let dockerFile =
-                        [ "FROM scratch"
-                        ]
-                in ruleCatchesNot (registryIsAllowed ["x.com", "random.com"]) $ Text.unlines dockerFile
-            it "allows boths all forms of docker.io" $
-                let dockerFile =
-                        [ "FROM ubuntu:18.04 AS builder1"
-                        , "FROM zemanlx/ubuntu:18.04 AS builder2"
-                        , "FROM docker.io/zemanlx/ubuntu:18.04 AS builder3"
-                        ]
-                in ruleCatchesNot (registryIsAllowed ["docker.io"]) $ Text.unlines dockerFile
-
-            it "allows using previous stages" $
-                let dockerFile =
-                        [ "FROM random.com/foo AS builder1"
-                        , "FROM builder1 AS builder2"
-                        ]
-                in ruleCatchesNot (registryIsAllowed ["random.com"]) $ Text.unlines dockerFile
-        --
-        describe "Wget or Curl" $ do
-            it "warns when using both wget and curl" $
-                let dockerFile =
-                        [ "FROM node as foo"
-                        , "RUN wget my.xyz"
-                        , "RUN curl localhost"
-                        ]
-                in ruleCatches wgetOrCurl $ Text.unlines dockerFile
-            it "warns when using both wget and curl in same instruction" $
-                let dockerFile =
-                        [ "FROM node as foo"
-                        , "RUN wget my.xyz && curl localhost"
-                        ]
-                in ruleCatches wgetOrCurl $ Text.unlines dockerFile
-            it "does not warn when using only wget" $
-                let dockerFile =
-                        [ "FROM node as foo"
-                        , "RUN wget my.xyz"
-                        ]
-                in ruleCatchesNot wgetOrCurl $ Text.unlines dockerFile
-            it "does not warn when using both curl and wget in different stages" $
-                let dockerFile =
-                        [ "FROM node as foo"
-                        , "RUN wget my.xyz"
-                        , "FROM scratch"
-                        , "RUN curl localhost"
-                        ]
-                in ruleCatchesNot wgetOrCurl $ Text.unlines dockerFile
-            it "does not warns when using both, on a single stage" $
-                let dockerFile =
-                        [ "FROM node as foo"
-                        , "RUN wget my.xyz"
-                        , "RUN curl localhost"
-                        , "FROM scratch"
-                        , "RUN curl localhost"
-                        ]
-                in ruleCatches wgetOrCurl $ Text.unlines dockerFile
-            it "only warns on the relevant RUN instruction" $
-                let dockerFile =
-                        [ "FROM node as foo"
-                        , "RUN wget my.xyz"
-                        , "RUN curl my.xyz"
-                        , "RUN echo hello"
-                        ]
-                in assertChecks wgetOrCurl
-                                (Text.unlines dockerFile)
-                                (\checks -> assertBool
-                                                    "Expecting warnings only in 1 RUN instruction"
-                                                    (length checks == 1)
-                                )
-            it "only warns on many relevant RUN instructions" $
-                let dockerFile =
-                        [ "FROM node as foo"
-                        , "RUN wget my.xyz"
-                        , "RUN curl my.xyz"
-                        , "RUN echo hello"
-                        , "RUN wget foo.com"
-                        ]
-                in assertChecks wgetOrCurl
-                                (Text.unlines dockerFile)
-                                (\checks -> assertBool
-                                                    "Expecting warnings only in 2 RUN instructions"
-                                                    (length checks == 2)
-                                )
-        --
-        describe "Regression Tests" $
-            it "Comments with backslashes at the end are just comments" $
-                let dockerFile =
-                        [ "FROM alpine:3.6"
-                        , "# The following comment makes hadolint still complain about DL4006"
-                        , "# \\"
-                        , "# should solve DL4006"
-                        , "SHELL [\"/bin/sh\", \"-o\", \"pipefail\", \"-c\"]"
-                        , "# RUN with pipe. causes DL4006, but should be fixed by above SHELL"
-                        , "RUN echo \"kaka\" | sed 's/a/o/g' >> /root/afile"
-                        ]
-                in ruleCatches usePipefail $ Text.unlines dockerFile
-
-        -- Run tests for the Config module
-        ConfigSpec.tests
-
-assertChecks :: HasCallStack => Rule -> Text.Text -> ([RuleCheck] -> IO a) -> IO a
-assertChecks rule s makeAssertions =
-    case parseText (s <> "\n") of
-        Left err -> assertFailure $ show err
-        Right dockerFile -> makeAssertions $ analyze [rule] dockerFile
-
-assertOnBuildChecks :: HasCallStack => Rule -> Text.Text -> ([RuleCheck] -> IO a) -> IO a
-assertOnBuildChecks rule s makeAssertions =
-    case parseText (s <> "\n") of
-        Left err -> assertFailure $ show err
-        Right dockerFile -> checkOnBuild dockerFile
-  where
-    checkOnBuild dockerFile = makeAssertions $ analyze [rule] (fmap wrapInOnBuild dockerFile)
-    wrapInOnBuild (InstructionPos (Run args) so li) = InstructionPos (OnBuild (Run args)) so li
-    wrapInOnBuild i = i
-
-selectChecksWithLines :: [RuleCheck] -> [RuleCheck]
-selectChecksWithLines checks = [c | c <- checks, linenumber c <= 0]
-
--- Assert a failed check exists for rule
-ruleCatches :: HasCallStack => Rule -> Text.Text -> Assertion
-ruleCatches rule s = assertChecks rule s f
-  where
-    f checks = do
-      when (null checks) $
-        assertFailure "I was expecting to catch at least one error"
-      assertBool "Incorrect line number for result" $ null $ selectChecksWithLines checks
-
-onBuildRuleCatches :: HasCallStack => Rule -> Text.Text -> Assertion
-onBuildRuleCatches rule s = assertOnBuildChecks rule s f
-  where
-    f checks = do
-      when (length checks /= 1) $
-        assertFailure (Text.unpack . Text.unlines . formatChecks $ checks)
-      assertBool "Incorrect line number for result" $ null $ selectChecksWithLines checks
-
-ruleCatchesNot :: HasCallStack => Rule -> Text.Text -> Assertion
-ruleCatchesNot rule s = assertChecks rule s f
-  where
-    f checks =
-      unless (null checks) $
-        assertFailure $ "Not expecting the following errors: \n" ++
-                        (Text.unpack . Text.unlines . formatChecks $ checks)
-
-onBuildRuleCatchesNot :: HasCallStack => Rule -> Text.Text -> Assertion
-onBuildRuleCatchesNot rule s = assertOnBuildChecks rule s f
-  where
-    f checks =
-      unless (null checks) $
-        assertFailure $ "Not expecting the following errors: \n" ++
-                        (Text.unpack . Text.unlines . formatChecks $ checks)
+{-# LANGUAGE OverloadedLists #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+import qualified ConfigSpec
+import Control.Monad (unless, when)
+import Data.Semigroup ((<>))
+import qualified Data.Text as Text
+import Hadolint.Formatter.TTY (formatChecks, formatError)
+import Hadolint.Rules
+import Language.Docker.Parser
+import Language.Docker.Syntax
+import Test.HUnit hiding (Label)
+import Test.Hspec
+
+main :: IO ()
+main =
+  hspec $ do
+    describe "FROM rules" $ do
+      it "no untagged" $ ruleCatches noUntagged "FROM debian"
+      it "no untagged with name" $ ruleCatches noUntagged "FROM debian AS builder"
+      it "explicit latest" $ ruleCatches noLatestTag "FROM debian:latest"
+      it "explicit latest with name" $ ruleCatches noLatestTag "FROM debian:latest AS builder"
+      it "explicit tagged" $ ruleCatchesNot noLatestTag "FROM debian:jessie"
+      it "explicit platform flag" $ ruleCatches noPlatformFlag "FROM --platform=linux debian:jessie"
+      it "no platform flag" $ ruleCatchesNot noPlatformFlag "FROM debian:jessie"
+      it "explicit SHA" $
+        ruleCatchesNot
+          noLatestTag
+          "FROM hub.docker.io/debian@sha256:\
+          \7959ed6f7e35f8b1aaa06d1d8259d4ee25aa85a086d5c125480c333183f9deeb"
+      it "explicit tagged with name" $
+        ruleCatchesNot noLatestTag "FROM debian:jessie AS builder"
+      it "untagged digest is not an error" $
+        ruleCatchesNot noUntagged "FROM ruby@sha256:f1dbca0f5dbc9"
+      it "untagged digest is not an error" $
+        ruleCatchesNot noUntagged "FROM ruby:2"
+      it "local aliases are OK to be untagged" $
+        let dockerFile =
+              [ "FROM golang:1.9.3-alpine3.7 AS build",
+                "RUN foo",
+                "FROM build as unit-test",
+                "RUN bar",
+                "FROM alpine:3.7",
+                "RUN baz"
+              ]
+         in do
+              ruleCatchesNot noUntagged $ Text.unlines dockerFile
+              onBuildRuleCatchesNot noUntagged $ Text.unlines dockerFile
+      it "other untagged cases are not ok" $
+        let dockerFile =
+              [ "FROM golang:1.9.3-alpine3.7 AS build",
+                "RUN foo",
+                "FROM node as unit-test",
+                "RUN bar",
+                "FROM alpine:3.7",
+                "RUN baz"
+              ]
+         in do
+              ruleCatches noUntagged $ Text.unlines dockerFile
+              onBuildRuleCatches noUntagged $ Text.unlines dockerFile
+    --
+    describe "no root or sudo rules" $ do
+      it "sudo" $ do
+        ruleCatches noSudo "RUN sudo apt-get update"
+        onBuildRuleCatches noSudo "RUN sudo apt-get update"
+
+      it "last user should not be root" $
+        let dockerFile =
+              [ "FROM scratch",
+                "USER root"
+              ]
+         in ruleCatches noRootUser $ Text.unlines dockerFile
+
+      it "no root" $
+        let dockerFile =
+              [ "FROM scratch",
+                "USER foo"
+              ]
+         in ruleCatchesNot noRootUser $ Text.unlines dockerFile
+
+      it "no root UID" $
+        let dockerFile =
+              [ "FROM scratch",
+                "USER 0"
+              ]
+         in ruleCatches noRootUser $ Text.unlines dockerFile
+
+      it "no root:root" $
+        let dockerFile =
+              [ "FROM scratch",
+                "USER root:root"
+              ]
+         in ruleCatches noRootUser $ Text.unlines dockerFile
+
+      it "no UID:GID" $
+        let dockerFile =
+              [ "FROM scratch",
+                "USER 0:0"
+              ]
+         in ruleCatches noRootUser $ Text.unlines dockerFile
+
+      it "can switch back to non root" $
+        let dockerFile =
+              [ "FROM scratch",
+                "USER root",
+                "RUN something",
+                "USER foo"
+              ]
+         in ruleCatchesNot noRootUser $ Text.unlines dockerFile
+
+      it "warns on transitive root user" $
+        let dockerFile =
+              [ "FROM debian as base",
+                "USER root",
+                "RUN something",
+                "FROM base",
+                "RUN something else"
+              ]
+         in ruleCatches noRootUser $ Text.unlines dockerFile
+
+      it "warns on multiple stages" $
+        let dockerFile =
+              [ "FROM debian as base",
+                "USER root",
+                "RUN something",
+                "FROM scratch",
+                "USER foo",
+                "RUN something else"
+              ]
+         in ruleCatches noRootUser $ Text.unlines dockerFile
+
+      it "does not warn when switching in multiple stages" $
+        let dockerFile =
+              [ "FROM debian as base",
+                "USER root",
+                "RUN something",
+                "USER foo",
+                "FROM scratch",
+                "RUN something else"
+              ]
+         in ruleCatchesNot noRootUser $ Text.unlines dockerFile
+
+      it "install sudo" $ do
+        ruleCatchesNot noSudo "RUN apt-get install sudo"
+        onBuildRuleCatchesNot noSudo "RUN apt-get install sudo"
+      it "sudo chained programs" $ do
+        ruleCatches noSudo "RUN apt-get update && sudo apt-get install"
+        onBuildRuleCatches noSudo "RUN apt-get update && sudo apt-get install"
+    --
+    describe "invalid CMD rules" $ do
+      it "invalid cmd" $ do
+        ruleCatches invalidCmd "RUN top"
+        onBuildRuleCatches invalidCmd "RUN top"
+      it "install ssh" $ do
+        ruleCatchesNot invalidCmd "RUN apt-get install ssh"
+        onBuildRuleCatchesNot invalidCmd "RUN apt-get install ssh"
+    --
+    describe "gem" $
+      describe "version pinning" $ do
+        describe "i" $ do
+          it "unpinned" $ do
+            ruleCatches gemVersionPinned "RUN gem i bundler"
+            onBuildRuleCatches gemVersionPinned "RUN gem i bundler"
+          it "pinned" $ do
+            ruleCatchesNot gemVersionPinned "RUN gem i bundler:1"
+            onBuildRuleCatchesNot gemVersionPinned "RUN gem i bundler:1"
+          it "multi" $ do
+            ruleCatches gemVersionPinned "RUN gem i bunlder:1 nokogiri"
+            onBuildRuleCatches gemVersionPinned "RUN gem i bunlder:1 nokogiri"
+            ruleCatchesNot gemVersionPinned "RUN gem i bunlder:1 nokogirii:1"
+            onBuildRuleCatchesNot gemVersionPinned "RUN gem i bunlder:1 nokogiri:1"
+        describe "install" $ do
+          it "unpinned" $ do
+            ruleCatches gemVersionPinned "RUN gem install bundler"
+            onBuildRuleCatches gemVersionPinned "RUN gem install bundler"
+          it "pinned" $ do
+            ruleCatchesNot gemVersionPinned "RUN gem install bundler:1"
+            onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler:1"
+          it "does not warn on -v" $ do
+            ruleCatchesNot gemVersionPinned "RUN gem install bundler -v '2.0.1'"
+            onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler -v '2.0.1'"
+          it "does not warn on --version without =" $ do
+            ruleCatchesNot gemVersionPinned "RUN gem install bundler --version '2.0.1'"
+            onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler --version '2.0.1'"
+          it "does not warn on --version with =" $ do
+            ruleCatchesNot gemVersionPinned "RUN gem install bundler --version='2.0.1'"
+            onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler --version='2.0.1'"
+          it "does not warn on extra flags" $ do
+            ruleCatchesNot gemVersionPinned "RUN gem install bundler:2.0.1 -- --use-system-libraries=true"
+            onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler:2.0.1 -- --use-system-libraries=true"
+    --
+    describe "yum rules" $ do
+      it "yum update" $ do
+        ruleCatches noYumUpdate "RUN yum update"
+        onBuildRuleCatches noYumUpdate "RUN yum update"
+      it "yum version pinning" $ do
+        ruleCatches yumVersionPinned "RUN yum install -y tomcat && yum clean all"
+        onBuildRuleCatches yumVersionPinned "RUN yum install -y tomcat && yum clean all"
+      it "yum no clean all" $ do
+        ruleCatches yumCleanup "RUN yum install -y mariadb-10.4"
+        onBuildRuleCatches yumCleanup "RUN yum install -y mariadb-10.4"
+      it "yum non-interactive" $ do
+        ruleCatches yumYes "RUN yum install httpd-2.4.24 && yum clean all"
+        onBuildRuleCatches yumYes "RUN yum install httpd-2.4.24 && yum clean all"
+    --
+    describe "apt-get rules" $ do
+      it "apt" $
+        let dockerFile =
+              [ "FROM ubuntu",
+                "RUN apt install python"
+              ]
+         in do
+              ruleCatches noApt $ Text.unlines dockerFile
+              onBuildRuleCatches noApt $ Text.unlines dockerFile
+      it "apt-get upgrade" $ do
+        ruleCatches noAptGetUpgrade "RUN apt-get update && apt-get upgrade"
+        onBuildRuleCatches noAptGetUpgrade "RUN apt-get update && apt-get upgrade"
+      it "apt-get version pinning" $ do
+        ruleCatches aptGetVersionPinned "RUN apt-get update && apt-get install python"
+        onBuildRuleCatches aptGetVersionPinned "RUN apt-get update && apt-get install python"
+      it "apt-get no cleanup" $
+        let dockerFile =
+              [ "FROM scratch",
+                "RUN apt-get update && apt-get install python"
+              ]
+         in do
+              ruleCatches aptGetCleanup $ Text.unlines dockerFile
+              onBuildRuleCatches aptGetCleanup $ Text.unlines dockerFile
+      it "apt-get cleanup in stage image" $
+        let dockerFile =
+              [ "FROM ubuntu as foo",
+                "RUN apt-get update && apt-get install python",
+                "FROM scratch",
+                "RUN echo hey!"
+              ]
+         in do
+              ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile
+              onBuildRuleCatchesNot aptGetCleanup $ Text.unlines dockerFile
+      it "apt-get no cleanup in last stage" $
+        let dockerFile =
+              [ "FROM ubuntu as foo",
+                "RUN hey!",
+                "FROM scratch",
+                "RUN apt-get update && apt-get install python"
+              ]
+         in do
+              ruleCatches aptGetCleanup $ Text.unlines dockerFile
+              onBuildRuleCatches aptGetCleanup $ Text.unlines dockerFile
+      it "apt-get no cleanup in intermediate stage" $
+        let dockerFile =
+              [ "FROM ubuntu as foo",
+                "RUN apt-get update && apt-get install python",
+                "FROM foo",
+                "RUN hey!"
+              ]
+         in do
+              ruleCatches aptGetCleanup $ Text.unlines dockerFile
+              onBuildRuleCatches aptGetCleanup $ Text.unlines dockerFile
+      it "no warn apt-get cleanup in intermediate stage that cleans lists" $
+        let dockerFile =
+              [ "FROM ubuntu as foo",
+                "RUN apt-get update && apt-get install python && rm -rf /var/lib/apt/lists/*",
+                "FROM foo",
+                "RUN hey!"
+              ]
+         in do
+              ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile
+              onBuildRuleCatchesNot aptGetCleanup $ Text.unlines dockerFile
+      it "no warn apt-get cleanup in intermediate stage when stage not used later" $
+        let dockerFile =
+              [ "FROM ubuntu as foo",
+                "RUN apt-get update && apt-get install python",
+                "FROM scratch",
+                "RUN hey!"
+              ]
+         in do
+              ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile
+              onBuildRuleCatchesNot aptGetCleanup $ Text.unlines dockerFile
+      it "apt-get cleanup" $
+        let dockerFile =
+              [ "FROM scratch",
+                "RUN apt-get update && apt-get install python && rm -rf /var/lib/apt/lists/*"
+              ]
+         in do
+              ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile
+              onBuildRuleCatchesNot aptGetCleanup $ Text.unlines dockerFile
+
+      it "apt-get pinned chained" $
+        let dockerFile =
+              [ "RUN apt-get update \\",
+                " && apt-get -yqq --no-install-recommends install nodejs=0.10 \\",
+                " && rm -rf /var/lib/apt/lists/*"
+              ]
+         in do
+              ruleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile
+              onBuildRuleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile
+
+      it "apt-get pinned regression" $
+        let dockerFile =
+              [ "RUN apt-get update && apt-get install --no-install-recommends -y \\",
+                "python-demjson=2.2.2* \\",
+                "wget=1.16.1* \\",
+                "git=1:2.5.0* \\",
+                "ruby=1:2.1.*"
+              ]
+         in do
+              ruleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile
+              onBuildRuleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile
+
+      it "has deprecated maintainer" $
+        ruleCatches hasNoMaintainer "FROM busybox\nMAINTAINER hudu@mail.com"
+    --
+    describe "apk add rules" $ do
+      it "apk upgrade" $ do
+        ruleCatches noApkUpgrade "RUN apk update && apk upgrade"
+        onBuildRuleCatches noApkUpgrade "RUN apk update && apk upgrade"
+      it "apk add version pinning single" $ do
+        ruleCatches apkAddVersionPinned "RUN apk add flex"
+        onBuildRuleCatches apkAddVersionPinned "RUN apk add flex"
+      it "apk add no version pinning single" $ do
+        ruleCatchesNot apkAddVersionPinned "RUN apk add flex=2.6.4-r1"
+        onBuildRuleCatchesNot apkAddVersionPinned "RUN apk add flex=2.6.4-r1"
+      it "apk add version pinned chained" $
+        let dockerFile =
+              [ "RUN apk add --no-cache flex=2.6.4-r1 \\",
+                " && pip install -r requirements.txt"
+              ]
+         in do
+              ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
+              onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
+      it "apk add version pinned regression" $
+        let dockerFile =
+              [ "RUN apk add --no-cache \\",
+                "flex=2.6.4-r1 \\",
+                "libffi=3.2.1-r3 \\",
+                "python2=2.7.13-r1 \\",
+                "libbz2=1.0.6-r5"
+              ]
+         in do
+              ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
+              onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
+      it "apk add version pinned regression - one missed" $
+        let dockerFile =
+              [ "RUN apk add --no-cache \\",
+                "flex=2.6.4-r1 \\",
+                "libffi \\",
+                "python2=2.7.13-r1 \\",
+                "libbz2=1.0.6-r5"
+              ]
+         in do
+              ruleCatches apkAddVersionPinned $ Text.unlines dockerFile
+              onBuildRuleCatches apkAddVersionPinned $ Text.unlines dockerFile
+      it "apk add with --no-cache" $ do
+        ruleCatches apkAddNoCache "RUN apk add flex=2.6.4-r1"
+        onBuildRuleCatches apkAddNoCache "RUN apk add flex=2.6.4-r1"
+      it "apk add without --no-cache" $ do
+        ruleCatchesNot apkAddNoCache "RUN apk add --no-cache flex=2.6.4-r1"
+        onBuildRuleCatchesNot apkAddNoCache "RUN apk add --no-cache flex=2.6.4-r1"
+      it "apk add virtual package" $
+        let dockerFile =
+              [ "RUN apk add \\",
+                "--virtual build-dependencies \\",
+                "python-dev=1.1.1 build-base=2.2.2 wget=3.3.3 \\",
+                "&& pip install -r requirements.txt \\",
+                "&& python setup.py install \\",
+                "&& apk del build-dependencies"
+              ]
+         in do
+              ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
+              onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
+      it "apk add with repository without equal sign" $
+        let dockerFile =
+              [ "RUN apk add --no-cache \\",
+                "--repository https://nl.alpinelinux.org/alpine/edge/testing \\",
+                "flow=0.78.0-r0"
+              ]
+         in do
+              ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
+              onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
+      it "apk add with repository with equal sign" $
+        let dockerFile =
+              [ "RUN apk add --no-cache \\",
+                "--repository=https://nl.alpinelinux.org/alpine/edge/testing \\",
+                "flow=0.78.0-r0"
+              ]
+         in do
+              ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
+              onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
+      it "apk add with repository (-X) without equal sign" $
+        let dockerFile =
+              [ "RUN apk add --no-cache \\",
+                "-X https://nl.alpinelinux.org/alpine/edge/testing \\",
+                "flow=0.78.0-r0"
+              ]
+         in do
+              ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
+              onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
+    --
+    describe "EXPOSE rules" $ do
+      it "invalid port" $ ruleCatches invalidPort "EXPOSE 80000"
+      it "valid port" $ ruleCatchesNot invalidPort "EXPOSE 60000"
+    --
+    describe "pip pinning" $ do
+      it "pip2 version not pinned" $ do
+        ruleCatches pipVersionPinned "RUN pip2 install MySQL_python"
+        onBuildRuleCatches pipVersionPinned "RUN pip2 install MySQL_python"
+      it "pip3 version not pinned" $ do
+        ruleCatches pipVersionPinned "RUN pip3 install MySQL_python"
+        onBuildRuleCatches pipVersionPinned "RUN pip2 install MySQL_python"
+      it "pip3 version pinned" $ do
+        ruleCatchesNot pipVersionPinned "RUN pip3 install MySQL_python==1.2.2"
+        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install MySQL_python==1.2.2"
+      it "pip install requirements" $ do
+        ruleCatchesNot pipVersionPinned "RUN pip install -r requirements.txt"
+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install -r requirements.txt"
+      it "pip install requirements with long flag" $ do
+        ruleCatchesNot pipVersionPinned "RUN pip install --requirement requirements.txt"
+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install --requirement requirements.txt"
+      it "pip install use setup.py" $ do
+        ruleCatchesNot pipVersionPinned "RUN pip install ."
+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install ."
+      it "pip version not pinned" $ do
+        ruleCatches pipVersionPinned "RUN pip install MySQL_python"
+        onBuildRuleCatches pipVersionPinned "RUN pip install MySQL_python"
+      it "pip version pinned" $ do
+        ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2"
+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2"
+      it "pip version pinned with ~= operator" $ do
+        ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python~=1.2.2"
+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python~=1.2.2"
+      it "pip version pinned with === operator" $ do
+        ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python===1.2.2"
+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python===1.2.2"
+      it "pip version pinned with flag --ignore-installed" $ do
+        ruleCatchesNot pipVersionPinned "RUN pip install --ignore-installed MySQL_python==1.2.2"
+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install --ignore-installed MySQL_python==1.2.2"
+      it "pip version pinned with flag --build" $ do
+        ruleCatchesNot pipVersionPinned "RUN pip3 install --build /opt/yamllint yamllint==1.20.0"
+        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --build /opt/yamllint yamllint==1.20.0"
+      it "pip version pinned with flag --prefix" $ do
+        ruleCatchesNot pipVersionPinned "RUN pip3 install --prefix /opt/yamllint yamllint==1.20.0"
+        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --prefix /opt/yamllint yamllint==1.20.0"
+      it "pip version pinned with flag --root" $ do
+        ruleCatchesNot pipVersionPinned "RUN pip3 install --root /opt/yamllint yamllint==1.20.0"
+        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --root /opt/yamllint yamllint==1.20.0"
+      it "pip version pinned with flag --target" $ do
+        ruleCatchesNot pipVersionPinned "RUN pip3 install --target /opt/yamllint yamllint==1.20.0"
+        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --target /opt/yamllint yamllint==1.20.0"
+      it "pip version pinned with flag --trusted-host" $ do
+        ruleCatchesNot pipVersionPinned "RUN pip3 install --trusted-host host example==1.2.2"
+        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --trusted-host host example==1.2.2"
+      it "pip version pinned with python -m" $ do
+        ruleCatchesNot pipVersionPinned "RUN python -m pip install example==1.2.2"
+        onBuildRuleCatchesNot pipVersionPinned "RUN python -m pip install example==1.2.2"
+      it "pip version not pinned with python -m" $ do
+        ruleCatches pipVersionPinned "RUN python -m pip install example"
+        onBuildRuleCatches pipVersionPinned "RUN python -m pip install --index-url url example"
+      it "pip install git" $ do
+        ruleCatchesNot
+          pipVersionPinned
+          "RUN pip install git+https://github.com/rtfd/r-ext.git@0.6-alpha#egg=r-ext"
+        onBuildRuleCatchesNot
+          pipVersionPinned
+          "RUN pip install git+https://github.com/rtfd/r-ext.git@0.6-alpha#egg=r-ext"
+      it "pip install unversioned git" $ do
+        ruleCatches
+          pipVersionPinned
+          "RUN pip install git+https://github.com/rtfd/read-ext.git#egg=read-ext"
+        onBuildRuleCatches
+          pipVersionPinned
+          "RUN pip install git+https://github.com/rtfd/read-ext.git#egg=read-ext"
+      it "pip install upper bound" $ do
+        ruleCatchesNot pipVersionPinned "RUN pip install 'alabaster>=0.7'"
+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install 'alabaster>=0.7'"
+      it "pip install lower bound" $ do
+        ruleCatchesNot pipVersionPinned "RUN pip install 'alabaster<0.7'"
+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install 'alabaster<0.7'"
+      it "pip install excluded version" $ do
+        ruleCatchesNot pipVersionPinned "RUN pip install 'alabaster!=0.7'"
+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install 'alabaster!=0.7'"
+      it "pip install user directory" $ do
+        ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2 --user"
+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2 --user"
+      it "pip install no pip version check" $ do
+        ruleCatchesNot
+          pipVersionPinned
+          "RUN pip install MySQL_python==1.2.2 --disable-pip-version-check"
+        onBuildRuleCatchesNot
+          pipVersionPinned
+          "RUN pip install MySQL_python==1.2.2 --disable-pip-version-check"
+      it "pip install --index-url" $ do
+        ruleCatchesNot
+          pipVersionPinned
+          "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"
+        onBuildRuleCatchesNot
+          pipVersionPinned
+          "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"
+      it "pip install index-url with -i flag" $ do
+        ruleCatchesNot
+          pipVersionPinned
+          "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"
+        onBuildRuleCatchesNot
+          pipVersionPinned
+          "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"
+      it "pip install --index-url with --extra-index-url" $ do
+        ruleCatchesNot
+          pipVersionPinned
+          "RUN pip install --index-url https://eg.com/foo --extra-index-url https://ex-eg.io/foo foobar==1.0.0"
+        onBuildRuleCatchesNot
+          pipVersionPinned
+          "RUN pip install --index-url https://eg.com/foo --extra-index-url https://ex-eg.io/foo foobar==1.0.0"
+      it "pip install no cache dir" $ do
+        ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2 --no-cache-dir"
+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2 --no-cache-dir"
+      it "pip install constraints file - long version argument" $ do
+        ruleCatchesNot pipVersionPinned "RUN pip install pykafka --constraint http://foo.bar.baz"
+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install pykafka --constraint http://foo.bar.baz"
+      it "pip install constraints file - short version argument" $ do
+        ruleCatchesNot pipVersionPinned "RUN pip install pykafka -c http://foo.bar.baz"
+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install pykafka -c http://foo.bar.baz"
+      it "pip install --index-url with --extra-index-url with basic auth" $ do
+        ruleCatchesNot
+          pipVersionPinned
+          "RUN pip install --index-url https://user:pass@eg.com/foo --extra-index-url https://user:pass@ex-eg.io/foo foobar==1.0.0"
+        onBuildRuleCatchesNot
+          pipVersionPinned
+          "RUN pip install --index-url https://user:pass@eg.com/foo --extra-index-url https://user:pass@ex-eg.io/foo foobar==1.0.0"
+    --
+    describe "npm pinning" $ do
+      it "version pinned in package.json" $ do
+        ruleCatchesNot npmVersionPinned "RUN npm install"
+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install"
+      it "version pinned in package.json with arguments" $ do
+        ruleCatchesNot npmVersionPinned "RUN npm install --progress=false"
+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install --progress=false"
+      it "version pinned" $ do
+        ruleCatchesNot npmVersionPinned "RUN npm install express@4.1.1"
+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install express@4.1.1"
+      it "version pinned with scope" $ do
+        ruleCatchesNot npmVersionPinned "RUN npm install @myorg/privatepackage@\">=0.1.0\""
+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install @myorg/privatepackage@\">=0.1.0\""
+      it "version pinned multiple packages" $ do
+        ruleCatchesNot npmVersionPinned "RUN npm install express@\"4.1.1\" sax@0.1.1"
+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install express@\"4.1.1\" sax@0.1.1"
+      it "version pinned with --global" $ do
+        ruleCatchesNot npmVersionPinned "RUN npm install --global express@\"4.1.1\""
+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install --global express@\"4.1.1\""
+      it "version pinned with -g" $ do
+        ruleCatchesNot npmVersionPinned "RUN npm install -g express@\"4.1.1\""
+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install -g express@\"4.1.1\""
+      it "version does not have to be pinned for tarball suffix .tar" $ do
+        ruleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tar"
+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tar"
+      it "version does not have to be pinned for tarball suffix .tar.gz" $ do
+        ruleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tar.gz"
+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tar.gz"
+      it "version does not have to be pinned for tarball suffix .tgz" $ do
+        ruleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tgz"
+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tgz"
+      it "version does not have to be pinned for folder - absolute path" $ do
+        ruleCatchesNot npmVersionPinned "RUN npm install /folder"
+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install /folder"
+      it "version does not have to be pinned for folder - relative path from current folder" $ do
+        ruleCatchesNot npmVersionPinned "RUN npm install ./folder"
+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install ./folder"
+      it "version does not have to be pinned for folder - relative path to parent folder" $ do
+        ruleCatchesNot npmVersionPinned "RUN npm install ../folder"
+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install ../folder"
+      it "version does not have to be pinned for folder - relative path from home" $ do
+        ruleCatchesNot npmVersionPinned "RUN npm install ~/folder"
+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install ~/folder"
+      it "commit pinned for git+ssh" $ do
+        ruleCatchesNot
+          npmVersionPinned
+          "RUN npm install git+ssh://git@github.com:npm/npm.git#v1.0.27"
+        onBuildRuleCatchesNot
+          npmVersionPinned
+          "RUN npm install git+ssh://git@github.com:npm/npm.git#v1.0.27"
+      it "commit pinned for git+http" $ do
+        ruleCatchesNot
+          npmVersionPinned
+          "RUN npm install git+http://isaacs@github.com/npm/npm#semver:^5.0"
+        onBuildRuleCatchesNot
+          npmVersionPinned
+          "RUN npm install git+http://isaacs@github.com/npm/npm#semver:^5.0"
+      it "commit pinned for git+https" $ do
+        ruleCatchesNot
+          npmVersionPinned
+          "RUN npm install git+https://isaacs@github.com/npm/npm.git#v1.0.27"
+        onBuildRuleCatchesNot
+          npmVersionPinned
+          "RUN npm install git+https://isaacs@github.com/npm/npm.git#v1.0.27"
+      it "commit pinned for git" $ do
+        ruleCatchesNot
+          npmVersionPinned
+          "RUN npm install git://github.com/npm/npm.git#v1.0.27"
+        onBuildRuleCatchesNot
+          npmVersionPinned
+          "RUN npm install git://github.com/npm/npm.git#v1.0.27"
+      it "npm run install is fine" $ do
+        ruleCatchesNot
+          npmVersionPinned
+          "RUN npm run --crazy install"
+        onBuildRuleCatchesNot
+          npmVersionPinned
+          "RUN npm run --crazy install"
+
+      --version range is not supported
+      it "version pinned with scope" $ do
+        ruleCatchesNot npmVersionPinned "RUN npm install @myorg/privatepackage@\">=0.1.0 <0.2.0\""
+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install @myorg/privatepackage@\">=0.1.0 <0.2.0\""
+      it "version not pinned" $ do
+        ruleCatches npmVersionPinned "RUN npm install express"
+        onBuildRuleCatches npmVersionPinned "RUN npm install express"
+      it "version not pinned with scope" $ do
+        ruleCatches npmVersionPinned "RUN npm install @myorg/privatepackage"
+        onBuildRuleCatches npmVersionPinned "RUN npm install @myorg/privatepackage"
+      it "version not pinned multiple packages" $ do
+        ruleCatches npmVersionPinned "RUN npm install express sax@0.1.1"
+        onBuildRuleCatches npmVersionPinned "RUN npm install express sax@0.1.1"
+      it "version not pinned with --global" $ do
+        ruleCatches npmVersionPinned "RUN npm install --global express"
+        onBuildRuleCatches npmVersionPinned "RUN npm install --global express"
+      it "commit not pinned for git+ssh" $ do
+        ruleCatches npmVersionPinned "RUN npm install git+ssh://git@github.com:npm/npm.git"
+        onBuildRuleCatches npmVersionPinned "RUN npm install git+ssh://git@github.com:npm/npm.git"
+      it "commit not pinned for git+http" $ do
+        ruleCatches npmVersionPinned "RUN npm install git+http://isaacs@github.com/npm/npm"
+        onBuildRuleCatches npmVersionPinned "RUN npm install git+http://isaacs@github.com/npm/npm"
+      it "commit not pinned for git+https" $ do
+        ruleCatches
+          npmVersionPinned
+          "RUN npm install git+https://isaacs@github.com/npm/npm.git"
+        onBuildRuleCatches
+          npmVersionPinned
+          "RUN npm install git+https://isaacs@github.com/npm/npm.git"
+      it "commit not pinned for git" $ do
+        ruleCatches npmVersionPinned "RUN npm install git://github.com/npm/npm.git"
+        onBuildRuleCatches npmVersionPinned "RUN npm install git://github.com/npm/npm.git"
+    --
+    describe "use SHELL" $ do
+      it "RUN ln" $ do
+        ruleCatches useShell "RUN ln -sfv /bin/bash /bin/sh"
+        onBuildRuleCatches useShell "RUN ln -sfv /bin/bash /bin/sh"
+      it "RUN ln with unrelated symlinks" $ do
+        ruleCatchesNot useShell "RUN ln -sf /bin/true /sbin/initctl"
+        onBuildRuleCatchesNot useShell "RUN ln -sf /bin/true /sbin/initctl"
+      it "RUN ln with multiple acceptable commands" $ do
+        ruleCatchesNot useShell "RUN ln -s foo bar && unrelated && something_with /bin/sh"
+        onBuildRuleCatchesNot useShell "RUN ln -s foo bar && unrelated && something_with /bin/sh"
+    --
+    --
+    describe "Shellcheck" $ do
+      it "runs shellchek on RUN instructions" $ do
+        ruleCatches shellcheck "RUN echo $MISSING_QUOTES"
+        onBuildRuleCatches shellcheck "RUN echo $MISSING_QUOTES"
+      it "not warns on valid scripts" $ do
+        ruleCatchesNot shellcheck "RUN echo foo"
+        onBuildRuleCatchesNot shellcheck "RUN echo foo"
+
+      it "Does not complain on default env vars" $
+        let dockerFile =
+              Text.unlines
+                [ "RUN echo \"$HTTP_PROXY\"",
+                  "RUN echo \"$http_proxy\"",
+                  "RUN echo \"$HTTPS_PROXY\"",
+                  "RUN echo \"$https_proxy\"",
+                  "RUN echo \"$FTP_PROXY\"",
+                  "RUN echo \"$ftp_proxy\"",
+                  "RUN echo \"$NO_PROXY\"",
+                  "RUN echo \"$no_proxy\""
+                ]
+         in do
+              ruleCatchesNot shellcheck dockerFile
+              onBuildRuleCatchesNot shellcheck dockerFile
+
+      it "Complain on missing env vars" $
+        let dockerFile =
+              Text.unlines
+                [ "RUN echo \"$RTTP_PROXY\""
+                ]
+         in do
+              ruleCatches shellcheck dockerFile
+              onBuildRuleCatches shellcheck dockerFile
+
+      it "Is aware of ARGS and ENV" $
+        let dockerFile =
+              Text.unlines
+                [ "ARG foo=bar",
+                  "ARG another_foo",
+                  "ENV bar=10 baz=20",
+                  "RUN echo \"$foo\"",
+                  "RUN echo \"$another_foo\"",
+                  "RUN echo \"$bar\"",
+                  "RUN echo \"$baz\""
+                ]
+         in do
+              ruleCatchesNot shellcheck dockerFile
+              onBuildRuleCatchesNot shellcheck dockerFile
+
+      it "Resets env vars after a FROM" $
+        let dockerFile =
+              Text.unlines
+                [ "ARG foo=bar",
+                  "ARG another_foo",
+                  "ENV bar=10 baz=20",
+                  "FROM debian",
+                  "RUN echo \"$foo\""
+                ]
+         in do
+              ruleCatches shellcheck dockerFile
+              onBuildRuleCatches shellcheck dockerFile
+
+      it "Defaults the shell to sh" $
+        let dockerFile =
+              Text.unlines
+                [ "RUN echo $RANDOM"
+                ]
+         in do
+              ruleCatches shellcheck dockerFile
+              onBuildRuleCatches shellcheck dockerFile
+
+      it "Can change the shell check to bash" $
+        let dockerFile =
+              Text.unlines
+                [ "SHELL [\"/bin/bash\", \"-eo\", \"pipefail\", \"-c\"]",
+                  "RUN echo $RANDOM"
+                ]
+         in do
+              ruleCatchesNot shellcheck dockerFile
+              onBuildRuleCatchesNot shellcheck dockerFile
+
+      it "Resets the SHELL to sh after a FROM" $
+        let dockerFile =
+              Text.unlines
+                [ "SHELL [\"/bin/bash\", \"-eo\", \"pipefail\", \"-c\"]",
+                  "FROM debian",
+                  "RUN echo $RANDOM"
+                ]
+         in do
+              ruleCatches shellcheck dockerFile
+              onBuildRuleCatches shellcheck dockerFile
+
+      it "Does not complain on ash shell" $
+        let dockerFile =
+              Text.unlines
+                [ "SHELL [\"/bin/ash\", \"-o\", \"pipefail\", \"-c\"]",
+                  "RUN echo hello"
+                ]
+         in do
+              ruleCatchesNot shellcheck dockerFile
+              onBuildRuleCatchesNot shellcheck dockerFile
+
+      it "Does not complain on powershell" $
+        let dockerFile =
+              Text.unlines
+                [ "SHELL [\"pwsh\", \"-c\"]",
+                  "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"
+                ]
+         in do
+              ruleCatchesNot shellcheck dockerFile
+              onBuildRuleCatchesNot shellcheck dockerFile
+    --
+    --
+    describe "COPY rules" $ do
+      it "use add" $ ruleCatches useAdd "COPY packaged-app.tar /usr/src/app"
+      it "use not add" $ ruleCatchesNot useAdd "COPY package.json /usr/src/app"
+    --
+    describe "other rules" $ do
+      it "apt-get auto yes" $ do
+        ruleCatches aptGetYes "RUN apt-get install python"
+        onBuildRuleCatches aptGetYes "RUN apt-get install python"
+      it "apt-get yes shortflag" $ do
+        ruleCatchesNot aptGetYes "RUN apt-get install -yq python"
+        onBuildRuleCatchesNot aptGetYes "RUN apt-get install -yq python"
+      it "apt-get yes quiet level 2 implies -y" $ do
+        ruleCatchesNot aptGetYes "RUN apt-get install -qq python"
+        onBuildRuleCatchesNot aptGetYes "RUN apt-get install -qq python"
+      it "apt-get yes different pos" $ do
+        ruleCatchesNot aptGetYes "RUN apt-get install -y python"
+        onBuildRuleCatchesNot aptGetYes "RUN apt-get install -y python"
+      it "apt-get with auto yes" $ do
+        ruleCatchesNot aptGetYes "RUN apt-get -y install python"
+        onBuildRuleCatchesNot aptGetYes "RUN apt-get -y install python"
+      it "apt-get with auto expanded yes" $ do
+        ruleCatchesNot aptGetYes "RUN apt-get --yes install python"
+        onBuildRuleCatchesNot aptGetYes "RUN apt-get --yes install python"
+      it "apt-get with assume-yes" $ do
+        ruleCatchesNot aptGetYes "RUN apt-get --assume-yes install python"
+        onBuildRuleCatchesNot aptGetYes "RUN apt-get --assume-yes install python"
+      it "apt-get install recommends" $ do
+        ruleCatchesNot
+          aptGetNoRecommends
+          "RUN apt-get install --no-install-recommends python"
+        onBuildRuleCatchesNot
+          aptGetNoRecommends
+          "RUN apt-get install --no-install-recommends python"
+      it "apt-get no install recommends" $ do
+        ruleCatches aptGetNoRecommends "RUN apt-get install python"
+        onBuildRuleCatches aptGetNoRecommends "RUN apt-get install python"
+      it "apt-get no install recommends" $ do
+        ruleCatches aptGetNoRecommends "RUN apt-get -y install python"
+        onBuildRuleCatches aptGetNoRecommends "RUN apt-get -y install python"
+      it "apt-get no install recommends via option" $ do
+        ruleCatchesNot aptGetNoRecommends "RUN apt-get -o APT::Install-Recommends=false install python"
+        onBuildRuleCatchesNot aptGetNoRecommends "RUN apt-get -o APT::Install-Recommends=false install python"
+      it "apt-get version" $ do
+        ruleCatchesNot aptGetVersionPinned "RUN apt-get install -y python=1.2.2"
+        onBuildRuleCatchesNot aptGetVersionPinned "RUN apt-get install -y python=1.2.2"
+      it "apt-get version" $ do
+        ruleCatchesNot aptGetVersionPinned "RUN apt-get install ./wkhtmltox_0.12.5-1.bionic_amd64.deb"
+        onBuildRuleCatchesNot aptGetVersionPinned "RUN apt-get install ./wkhtmltox_0.12.5-1.bionic_amd64.deb"
+      it "apt-get pinned" $ do
+        ruleCatchesNot
+          aptGetVersionPinned
+          "RUN apt-get -y --no-install-recommends install nodejs=0.10"
+        onBuildRuleCatchesNot
+          aptGetVersionPinned
+          "RUN apt-get -y --no-install-recommends install nodejs=0.10"
+      it "apt-get tolerate target-release" $
+        let dockerFile =
+              [ "RUN set -e &&\\",
+                " echo \"deb http://http.debian.net/debian jessie-backports main\" \
+                \> /etc/apt/sources.list.d/jessie-backports.list &&\\",
+                " apt-get update &&\\",
+                " apt-get install -y --no-install-recommends -t jessie-backports \
+                \openjdk-8-jdk=8u131-b11-1~bpo8+1 &&\\",
+                " rm -rf /var/lib/apt/lists/*"
+              ]
+         in do
+              ruleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile
+              onBuildRuleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile
+
+      it "has maintainer" $ ruleCatches hasNoMaintainer "FROM debian\nMAINTAINER Lukas"
+      it "has maintainer first" $ ruleCatches hasNoMaintainer "MAINTAINER Lukas\nFROM DEBIAN"
+      it "has no maintainer" $ ruleCatchesNot hasNoMaintainer "FROM debian"
+      it "using add" $ ruleCatches copyInsteadAdd "ADD file /usr/src/app/"
+
+      it "many cmds" $
+        let dockerFile =
+              [ "FROM debian",
+                "CMD bash",
+                "RUN foo",
+                "CMD another"
+              ]
+         in ruleCatches multipleCmds $ Text.unlines dockerFile
+
+      it "single cmds, different stages" $
+        let dockerFile =
+              [ "FROM debian as distro1",
+                "CMD bash",
+                "RUN foo",
+                "FROM debian as distro2",
+                "CMD another"
+              ]
+         in ruleCatchesNot multipleCmds $ Text.unlines dockerFile
+
+      it "many cmds, different stages" $
+        let dockerFile =
+              [ "FROM debian as distro1",
+                "CMD bash",
+                "RUN foo",
+                "CMD another",
+                "FROM debian as distro2",
+                "CMD another"
+              ]
+         in ruleCatches multipleCmds $ Text.unlines dockerFile
+
+      it "single cmd" $ ruleCatchesNot multipleCmds "CMD /bin/true"
+      it "no cmd" $ ruleCatchesNot multipleEntrypoints "FROM busybox"
+
+      it "many entrypoints" $
+        let dockerFile =
+              [ "FROM debian",
+                "ENTRYPOINT bash",
+                "RUN foo",
+                "ENTRYPOINT another"
+              ]
+         in ruleCatches multipleEntrypoints $ Text.unlines dockerFile
+
+      it "single entrypoint, different stages" $
+        let dockerFile =
+              [ "FROM debian as distro1",
+                "ENTRYPOINT bash",
+                "RUN foo",
+                "FROM debian as distro2",
+                "ENTRYPOINT another"
+              ]
+         in ruleCatchesNot multipleEntrypoints $ Text.unlines dockerFile
+
+      it "many entrypoints, different stages" $
+        let dockerFile =
+              [ "FROM debian as distro1",
+                "ENTRYPOINT bash",
+                "RUN foo",
+                "ENTRYPOINT another",
+                "FROM debian as distro2",
+                "ENTRYPOINT another"
+              ]
+         in ruleCatches multipleEntrypoints $ Text.unlines dockerFile
+      it "single entry" $ ruleCatchesNot multipleEntrypoints "ENTRYPOINT /bin/true"
+      it "no entry" $ ruleCatchesNot multipleEntrypoints "FROM busybox"
+      it "workdir variable" $ ruleCatchesNot absoluteWorkdir "WORKDIR ${work}"
+      it "scratch" $ ruleCatchesNot noUntagged "FROM scratch"
+    --
+    describe "add files and archives" $ do
+      it "add for tar" $ ruleCatchesNot copyInsteadAdd "ADD file.tar /usr/src/app/"
+      it "add for zip" $ ruleCatchesNot copyInsteadAdd "ADD file.zip /usr/src/app/"
+      it "add for gzip" $ ruleCatchesNot copyInsteadAdd "ADD file.gz /usr/src/app/"
+      it "add for bz2" $ ruleCatchesNot copyInsteadAdd "ADD file.bz2 /usr/src/app/"
+      it "add for xz" $ ruleCatchesNot copyInsteadAdd "ADD file.xz /usr/src/app/"
+      it "add for tgz" $ ruleCatchesNot copyInsteadAdd "ADD file.tgz /usr/src/app/"
+      it "add for url" $ ruleCatchesNot copyInsteadAdd "ADD http://file.com /usr/src/app/"
+    --
+    describe "copy last argument" $ do
+      it "no warn on 2 args" $ ruleCatchesNot copyEndingSlash "COPY foo bar"
+      it "warn on 3 args" $ ruleCatches copyEndingSlash "COPY foo bar baz"
+      it "no warn on 3 args" $ ruleCatchesNot copyEndingSlash "COPY foo bar baz/"
+    --
+    describe "copy from existing alias" $ do
+      it "warn on missing alias" $ ruleCatches copyFromExists "COPY --from=foo bar ."
+      it "warn on alias defined after" $
+        let dockerFile =
+              [ "FROM scratch",
+                "COPY --from=build foo .",
+                "FROM node as build",
+                "RUN baz"
+              ]
+         in ruleCatches copyFromExists $ Text.unlines dockerFile
+      it "don't warn on correctly defined aliases" $
+        let dockerFile =
+              [ "FROM scratch as build",
+                "RUN foo",
+                "FROM node",
+                "COPY --from=build foo .",
+                "RUN baz"
+              ]
+         in ruleCatchesNot copyFromExists $ Text.unlines dockerFile
+    --
+    describe "copy from own FROM" $ do
+      it "warn on copying from your the same FROM" $
+        let dockerFile =
+              [ "FROM node as foo",
+                "COPY --from=foo bar ."
+              ]
+         in ruleCatches copyFromAnother $ Text.unlines dockerFile
+      it "don't warn on copying form other sources" $
+        let dockerFile =
+              [ "FROM scratch as build",
+                "RUN foo",
+                "FROM node as run",
+                "COPY --from=build foo .",
+                "RUN baz"
+              ]
+         in ruleCatchesNot copyFromAnother $ Text.unlines dockerFile
+    --
+    describe "Duplicate aliases" $ do
+      it "warn on duplicate aliases" $
+        let dockerFile =
+              [ "FROM node as foo",
+                "RUN something",
+                "FROM scratch as foo",
+                "RUN something"
+              ]
+         in ruleCatches fromAliasUnique $ Text.unlines dockerFile
+      it "don't warn on unique aliases" $
+        let dockerFile =
+              [ "FROM scratch as build",
+                "RUN foo",
+                "FROM node as run",
+                "RUN baz"
+              ]
+         in ruleCatchesNot fromAliasUnique $ Text.unlines dockerFile
+    --
+    describe "format error" $
+      it "display error after line pos" $ do
+        let ast = parseText "FOM debian:jessie"
+            expectedMsg =
+              "<string>:1:1 unexpected 'F' expecting '#', ADD, ARG, CMD, COPY, ENTRYPOINT, "
+                <> "ENV, EXPOSE, FROM, HEALTHCHECK, LABEL, MAINTAINER, ONBUILD, RUN, SHELL, STOPSIGNAL, "
+                <> "USER, VOLUME, WORKDIR, or end of input "
+        case ast of
+          Left err -> assertEqual "Unexpected error msg" expectedMsg (formatError err)
+          Right _ -> assertFailure "AST should fail parsing"
+    --
+    describe "Rules can be ignored with inline comments" $ do
+      it "ignores single rule" $
+        let dockerFile =
+              [ "FROM ubuntu",
+                "# hadolint ignore=DL3002",
+                "USER root"
+              ]
+         in ruleCatchesNot noRootUser $ Text.unlines dockerFile
+      it "ignores only the given rule" $
+        let dockerFile =
+              [ "FROM scratch",
+                "# hadolint ignore=DL3001",
+                "USER root"
+              ]
+         in ruleCatches noRootUser $ Text.unlines dockerFile
+      it "ignores only the given rule, when multiple passed" $
+        let dockerFile =
+              [ "FROM scratch",
+                "# hadolint ignore=DL3001,DL3002",
+                "USER root"
+              ]
+         in ruleCatchesNot noRootUser $ Text.unlines dockerFile
+      it "ignores the rule only if directly above the instruction" $
+        let dockerFile =
+              [ "# hadolint ignore=DL3001,DL3002",
+                "FROM ubuntu",
+                "USER root"
+              ]
+         in ruleCatches noRootUser $ Text.unlines dockerFile
+      it "won't ignore the rule if passed invalid rule names" $
+        let dockerFile =
+              [ "FROM scratch",
+                "# hadolint ignore=crazy,DL3002",
+                "USER root"
+              ]
+         in ruleCatches noRootUser $ Text.unlines dockerFile
+      it "ignores multiple rules correctly, even with some extra whitespace" $
+        let dockerFile =
+              [ "FROM node as foo",
+                "# hadolint ignore=DL3023, DL3021",
+                "COPY --from=foo bar baz ."
+              ]
+         in do
+              ruleCatchesNot copyFromAnother $ Text.unlines dockerFile
+              ruleCatchesNot copyEndingSlash $ Text.unlines dockerFile
+    --
+    describe "JSON notation in ENTRYPOINT and CMD" $ do
+      it "warn on ENTRYPOINT" $
+        let dockerFile =
+              [ "FROM node as foo",
+                "ENTRYPOINT something"
+              ]
+         in ruleCatches useJsonArgs $ Text.unlines dockerFile
+      it "don't warn on ENTRYPOINT json notation" $
+        let dockerFile =
+              [ "FROM scratch as build",
+                "ENTRYPOINT [\"foo\", \"bar\"]"
+              ]
+         in ruleCatchesNot useJsonArgs $ Text.unlines dockerFile
+      it "warn on CMD" $
+        let dockerFile =
+              [ "FROM node as foo",
+                "CMD something"
+              ]
+         in ruleCatches useJsonArgs $ Text.unlines dockerFile
+      it "don't warn on CMD json notation" $
+        let dockerFile =
+              [ "FROM scratch as build",
+                "CMD [\"foo\", \"bar\"]",
+                "CMD [ \"foo\", \"bar\" ]"
+              ]
+         in ruleCatchesNot useJsonArgs $ Text.unlines dockerFile
+
+    --
+    describe "Detects missing pipefail option" $ do
+      it "warn on missing pipefail" $
+        let dockerFile =
+              [ "FROM scratch",
+                "RUN wget -O - https://some.site | wc -l > /number"
+              ]
+         in ruleCatches usePipefail $ Text.unlines dockerFile
+      it "don't warn on commands with no pipes" $
+        let dockerFile =
+              [ "FROM scratch as build",
+                "RUN wget -O - https://some.site && wc -l file > /number"
+              ]
+         in ruleCatchesNot usePipefail $ Text.unlines dockerFile
+      it "don't warn on commands with pipes and the pipefail option" $
+        let dockerFile =
+              [ "FROM scratch as build",
+                "SHELL [\"/bin/bash\", \"-eo\", \"pipefail\", \"-c\"]",
+                "RUN wget -O - https://some.site | wc -l file > /number"
+              ]
+         in ruleCatchesNot usePipefail $ Text.unlines dockerFile
+      it "don't warn on commands with pipes and the pipefail option 2" $
+        let dockerFile =
+              [ "FROM scratch as build",
+                "SHELL [\"/bin/bash\", \"-e\", \"-o\", \"pipefail\", \"-c\"]",
+                "RUN wget -O - https://some.site | wc -l file > /number"
+              ]
+         in ruleCatchesNot usePipefail $ Text.unlines dockerFile
+      it "don't warn on commands with pipes and the pipefail option 3" $
+        let dockerFile =
+              [ "FROM scratch as build",
+                "SHELL [\"/bin/bash\", \"-o\", \"errexit\", \"-o\", \"pipefail\", \"-c\"]",
+                "RUN wget -O - https://some.site | wc -l file > /number"
+              ]
+         in ruleCatchesNot usePipefail $ Text.unlines dockerFile
+      it "don't warn on commands with pipes and the pipefail zsh" $
+        let dockerFile =
+              [ "FROM scratch as build",
+                "SHELL [\"/bin/zsh\", \"-o\", \"pipefail\", \"-c\"]",
+                "RUN wget -O - https://some.site | wc -l file > /number"
+              ]
+         in ruleCatchesNot usePipefail $ Text.unlines dockerFile
+      it "don't warn on powershell" $
+        let dockerFile =
+              [ "FROM scratch as build",
+                "SHELL [\"pwsh\", \"-c\"]",
+                "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"
+              ]
+         in ruleCatchesNot usePipefail $ Text.unlines dockerFile
+      it "warns when using plain sh" $
+        let dockerFile =
+              [ "FROM scratch as build",
+                "SHELL [\"/bin/sh\", \"-o\", \"pipefail\", \"-c\"]",
+                "RUN wget -O - https://some.site | wc -l file > /number"
+              ]
+         in ruleCatches usePipefail $ Text.unlines dockerFile
+      it "warn on missing pipefail in the next image" $
+        let dockerFile =
+              [ "FROM scratch as build",
+                "SHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]",
+                "RUN wget -O - https://some.site | wc -l file > /number",
+                "FROM scratch as build2",
+                "RUN wget -O - https://some.site | wc -l file > /number"
+              ]
+         in ruleCatches usePipefail $ Text.unlines dockerFile
+      it "warn on missing pipefail if next SHELL is not using it" $
+        let dockerFile =
+              [ "FROM scratch as build",
+                "SHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]",
+                "RUN wget -O - https://some.site | wc -l file > /number",
+                "SHELL [\"/bin/sh\", \"-c\"]",
+                "RUN wget -O - https://some.site | wc -l file > /number"
+              ]
+         in ruleCatches usePipefail $ Text.unlines dockerFile
+    --
+    describe "Allowed docker registries" $ do
+      it "warn on non-allowed registry" $
+        let dockerFile =
+              [ "FROM random.com/debian"
+              ]
+         in ruleCatches (registryIsAllowed ["docker.io"]) $ Text.unlines dockerFile
+      it "don't warn on empty allowed registries" $
+        let dockerFile =
+              [ "FROM random.com/debian"
+              ]
+         in ruleCatchesNot (registryIsAllowed []) $ Text.unlines dockerFile
+      it "don't warn on allowed registries" $
+        let dockerFile =
+              [ "FROM random.com/debian"
+              ]
+         in ruleCatchesNot (registryIsAllowed ["x.com", "random.com"]) $ Text.unlines dockerFile
+      it "doesn't warn on scratch image" $
+        let dockerFile =
+              [ "FROM scratch"
+              ]
+         in ruleCatchesNot (registryIsAllowed ["x.com", "random.com"]) $ Text.unlines dockerFile
+      it "allows boths all forms of docker.io" $
+        let dockerFile =
+              [ "FROM ubuntu:18.04 AS builder1",
+                "FROM zemanlx/ubuntu:18.04 AS builder2",
+                "FROM docker.io/zemanlx/ubuntu:18.04 AS builder3"
+              ]
+         in ruleCatchesNot (registryIsAllowed ["docker.io"]) $ Text.unlines dockerFile
+
+      it "allows using previous stages" $
+        let dockerFile =
+              [ "FROM random.com/foo AS builder1",
+                "FROM builder1 AS builder2"
+              ]
+         in ruleCatchesNot (registryIsAllowed ["random.com"]) $ Text.unlines dockerFile
+    --
+    describe "Wget or Curl" $ do
+      it "warns when using both wget and curl" $
+        let dockerFile =
+              [ "FROM node as foo",
+                "RUN wget my.xyz",
+                "RUN curl localhost"
+              ]
+         in ruleCatches wgetOrCurl $ Text.unlines dockerFile
+      it "warns when using both wget and curl in same instruction" $
+        let dockerFile =
+              [ "FROM node as foo",
+                "RUN wget my.xyz && curl localhost"
+              ]
+         in ruleCatches wgetOrCurl $ Text.unlines dockerFile
+      it "does not warn when using only wget" $
+        let dockerFile =
+              [ "FROM node as foo",
+                "RUN wget my.xyz"
+              ]
+         in ruleCatchesNot wgetOrCurl $ Text.unlines dockerFile
+      it "does not warn when using both curl and wget in different stages" $
+        let dockerFile =
+              [ "FROM node as foo",
+                "RUN wget my.xyz",
+                "FROM scratch",
+                "RUN curl localhost"
+              ]
+         in ruleCatchesNot wgetOrCurl $ Text.unlines dockerFile
+      it "does not warns when using both, on a single stage" $
+        let dockerFile =
+              [ "FROM node as foo",
+                "RUN wget my.xyz",
+                "RUN curl localhost",
+                "FROM scratch",
+                "RUN curl localhost"
+              ]
+         in ruleCatches wgetOrCurl $ Text.unlines dockerFile
+      it "only warns on the relevant RUN instruction" $
+        let dockerFile =
+              [ "FROM node as foo",
+                "RUN wget my.xyz",
+                "RUN curl my.xyz",
+                "RUN echo hello"
+              ]
+         in assertChecks
+              wgetOrCurl
+              (Text.unlines dockerFile)
+              ( \checks ->
+                  assertBool
+                    "Expecting warnings only in 1 RUN instruction"
+                    (length checks == 1)
+              )
+      it "only warns on many relevant RUN instructions" $
+        let dockerFile =
+              [ "FROM node as foo",
+                "RUN wget my.xyz",
+                "RUN curl my.xyz",
+                "RUN echo hello",
+                "RUN wget foo.com"
+              ]
+         in assertChecks
+              wgetOrCurl
+              (Text.unlines dockerFile)
+              ( \checks ->
+                  assertBool
+                    "Expecting warnings only in 2 RUN instructions"
+                    (length checks == 2)
+              )
+    --
+    describe "Regression Tests" $
+      it "Comments with backslashes at the end are just comments" $
+        let dockerFile =
+              [ "FROM alpine:3.6",
+                "# The following comment makes hadolint still complain about DL4006",
+                "# \\",
+                "# should solve DL4006",
+                "SHELL [\"/bin/sh\", \"-o\", \"pipefail\", \"-c\"]",
+                "# RUN with pipe. causes DL4006, but should be fixed by above SHELL",
+                "RUN echo \"kaka\" | sed 's/a/o/g' >> /root/afile"
+              ]
+         in ruleCatches usePipefail $ Text.unlines dockerFile
+
+    -- Run tests for the Config module
+    ConfigSpec.tests
+
+assertChecks :: HasCallStack => Rule -> Text.Text -> ([RuleCheck] -> IO a) -> IO a
+assertChecks rule s makeAssertions =
+  case parseText (s <> "\n") of
+    Left err -> assertFailure $ show err
+    Right dockerFile -> makeAssertions $ analyze [rule] dockerFile
+
+assertOnBuildChecks :: HasCallStack => Rule -> Text.Text -> ([RuleCheck] -> IO a) -> IO a
+assertOnBuildChecks rule s makeAssertions =
+  case parseText (s <> "\n") of
+    Left err -> assertFailure $ show err
+    Right dockerFile -> checkOnBuild dockerFile
+  where
+    checkOnBuild dockerFile = makeAssertions $ analyze [rule] (fmap wrapInOnBuild dockerFile)
+    wrapInOnBuild (InstructionPos (Run args) so li) = InstructionPos (OnBuild (Run args)) so li
+    wrapInOnBuild i = i
+
+selectChecksWithLines :: [RuleCheck] -> [RuleCheck]
+selectChecksWithLines checks = [c | c <- checks, linenumber c <= 0]
+
+-- Assert a failed check exists for rule
+ruleCatches :: HasCallStack => Rule -> Text.Text -> Assertion
+ruleCatches rule s = assertChecks rule s f
+  where
+    f checks = do
+      when (null checks) $
+        assertFailure "I was expecting to catch at least one error"
+      assertBool "Incorrect line number for result" $ null $ selectChecksWithLines checks
+
+onBuildRuleCatches :: HasCallStack => Rule -> Text.Text -> Assertion
+onBuildRuleCatches rule s = assertOnBuildChecks rule s f
+  where
+    f checks = do
+      when (length checks /= 1) $
+        assertFailure (Text.unpack . Text.unlines . formatChecks $ checks)
+      assertBool "Incorrect line number for result" $ null $ selectChecksWithLines checks
+
+ruleCatchesNot :: HasCallStack => Rule -> Text.Text -> Assertion
+ruleCatchesNot rule s = assertChecks rule s f
+  where
+    f checks =
+      unless (null checks) $
+        assertFailure $
+          "Not expecting the following errors: \n"
+            ++ (Text.unpack . Text.unlines . formatChecks $ checks)
+
+onBuildRuleCatchesNot :: HasCallStack => Rule -> Text.Text -> Assertion
+onBuildRuleCatchesNot rule s = assertOnBuildChecks rule s f
+  where
+    f checks =
+      unless (null checks) $
+        assertFailure $
+          "Not expecting the following errors: \n"
+            ++ (Text.unpack . Text.unlines . formatChecks $ checks)
