packages feed

hadolint 1.18.2 → 1.19.0

raw patch · 10 files changed

+337/−45 lines, 10 filesdep ~megaparsec

Dependency ranges changed: megaparsec

Files

README.md view
@@ -203,6 +203,14 @@ | [DL3031](https://github.com/hadolint/hadolint/wiki/DL3031)   | Do not use `yum update`                                                                                                                             | | [DL3032](https://github.com/hadolint/hadolint/wiki/DL3032)   | `yum clean all` missing after yum command.                                                                                                          | | [DL3033](https://github.com/hadolint/hadolint/wiki/DL3033)   | Specify version with `yum install -y <package>-<version>`                                                                                           |+| [DL3034](https://github.com/hadolint/hadolint/wiki/DL3034)   | Non-interactive switch missing from `zypper` command: `zypper install -y`                                                                           |+| [DL3035](https://github.com/hadolint/hadolint/wiki/DL3035)   | Do not use `zypper update`.                                                                                                                         |+| [DL3036](https://github.com/hadolint/hadolint/wiki/DL3036)   | `zypper clean` missing after zypper use.                                                                                                            |+| [DL3037](https://github.com/hadolint/hadolint/wiki/DL3037)   | Specify version with `zypper install -y <package>[=]<version>`.                                                                                     |+| [DL3038](https://github.com/hadolint/hadolint/wiki/DL3038)   | Use the `-y` switch to avoid manual input `dnf install -y <package>`                                                                                |+| [DL3039](https://github.com/hadolint/hadolint/wiki/DL3039)   | Do not use `dnf update`                                                                                                                             |+| [DL3040](https://github.com/hadolint/hadolint/wiki/DL3040)   | `dnf clean all` missing after dnf command.                                                                                                          |+| [DL3041](https://github.com/hadolint/hadolint/wiki/DL3041)   | Specify version with `dnf install -y <package>-<version>`                                                                                           | | [DL4000](https://github.com/hadolint/hadolint/wiki/DL4000)   | MAINTAINER is deprecated.                                                                                                                           | | [DL4001](https://github.com/hadolint/hadolint/wiki/DL4001)   | Either use Wget or Curl but not both.                                                                                                               | | [DL4003](https://github.com/hadolint/hadolint/wiki/DL4003)   | Multiple `CMD` instructions found.                                                                                                                  |
hadolint.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 418683858197c94a7d90196b639708548b3c5966bc20506c43b2383a836a281b+-- hash: afee9524ce1aad80f4f3a651eef3a68c327005e1f97e965b5a82d3cd36afa09e  name:           hadolint-version:        1.18.2+version:        1.19.0 synopsis:       Dockerfile Linter JavaScript API description:    A smarter Dockerfile linter that helps you build best practice Docker images. category:       Development@@ -61,7 +61,7 @@     , directory >=1.3.0     , filepath     , language-docker >=9.1.2 && <10-    , megaparsec >=7.0+    , megaparsec >=9.0.0     , mtl     , parallel     , split >=0.2@@ -82,7 +82,7 @@     , gitrev >=1.3.1     , hadolint     , language-docker >=9.1.2 && <10-    , megaparsec >=7.0+    , megaparsec >=9.0.0     , optparse-applicative >=0.14.0     , text   if flag(static) && !(os(osx))@@ -108,7 +108,7 @@     , hadolint     , hspec     , language-docker >=9.1.2 && <10-    , megaparsec >=7.0+    , megaparsec >=9.0.0     , split >=0.2     , text   default-language: Haskell2010
src/Hadolint/Formatter/Checkstyle.hs view
@@ -17,9 +17,10 @@ import Hadolint.Formatter.Format import Hadolint.Rules (Metadata (..), RuleCheck (..)) import ShellCheck.Interface-import Text.Megaparsec (Stream)+import Text.Megaparsec (TraversableStream) import Text.Megaparsec.Error import Text.Megaparsec.Pos (sourceColumn, sourceLine, sourceName, unPos)+import Text.Megaparsec.Stream (VisualStream)  data CheckStyle = CheckStyle   { file :: String,@@ -30,7 +31,7 @@     source :: String   } -errorToCheckStyle :: (Stream s, ShowErrorComponent e) => ParseErrorBundle s e -> CheckStyle+errorToCheckStyle :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => ParseErrorBundle s e -> CheckStyle errorToCheckStyle err =   CheckStyle     { file = sourceName pos,@@ -83,7 +84,7 @@         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 :: (VisualStream s, TraversableStream 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>"   where@@ -94,5 +95,5 @@     checkstyleChecks = fmap ruleToCheckStyle checks     sameFileName CheckStyle {file = f1} CheckStyle {file = f2} = f1 == f2 -printResult :: (Stream s, ShowErrorComponent e) => Result s e -> IO ()+printResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> IO () printResult result = B.putStr (Builder.toLazyByteString (formatResult result))
src/Hadolint/Formatter/Codacy.hs view
@@ -14,9 +14,10 @@ import qualified Data.Text as Text import Hadolint.Formatter.Format (Result (..), errorPosition) import Hadolint.Rules (Metadata (..), RuleCheck (..))-import Text.Megaparsec (Stream)+import Text.Megaparsec (TraversableStream) import Text.Megaparsec.Error import Text.Megaparsec.Pos (sourceLine, sourceName, unPos)+import Text.Megaparsec.Stream (VisualStream)  data Issue = Issue   { filename :: String,@@ -29,7 +30,7 @@   toJSON Issue {..} =     object ["filename" .= filename, "patternId" .= patternId, "message" .= msg, "line" .= line] -errorToIssue :: (Stream s, ShowErrorComponent e) => ParseErrorBundle s e -> Issue+errorToIssue :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => ParseErrorBundle s e -> Issue errorToIssue err =   Issue     { filename = sourceName pos,@@ -50,14 +51,14 @@       line = linenumber     } -formatResult :: (Stream s, ShowErrorComponent e) => Result s e -> Seq Issue+formatResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> Seq Issue formatResult (Result errors checks) = allIssues   where     allIssues = errorMessages <> checkMessages     errorMessages = fmap errorToIssue errors     checkMessages = fmap checkToIssue checks -printResult :: (Stream s, ShowErrorComponent e) => Result s e -> IO ()+printResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> IO () printResult result = mapM_ output (formatResult result)   where     output value = B.putStrLn (encode value)
src/Hadolint/Formatter/Codeclimate.hs view
@@ -17,9 +17,10 @@ import Hadolint.Formatter.Format (Result (..), errorPosition) import Hadolint.Rules (Metadata (..), RuleCheck (..)) import ShellCheck.Interface-import Text.Megaparsec (Stream)+import Text.Megaparsec (TraversableStream) import Text.Megaparsec.Error import Text.Megaparsec.Pos (sourceColumn, sourceLine, sourceName, unPos)+import Text.Megaparsec.Stream (VisualStream)  data Issue = Issue   { checkName :: String,@@ -60,7 +61,7 @@         "severity" .= impact       ] -errorToIssue :: (Stream s, ShowErrorComponent e) => ParseErrorBundle s e -> Issue+errorToIssue :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => ParseErrorBundle s e -> Issue errorToIssue err =   Issue     { checkName = "DL1000",@@ -90,14 +91,14 @@     InfoC -> "info"     StyleC -> "minor" -formatResult :: (Stream s, ShowErrorComponent e) => Result s e -> Seq Issue+formatResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> Seq Issue formatResult (Result errors checks) = allIssues   where     allIssues = errorMessages <> checkMessages     errorMessages = fmap errorToIssue errors     checkMessages = fmap checkToIssue checks -printResult :: (Stream s, ShowErrorComponent e) => Result s e -> IO ()+printResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> IO () printResult result = mapM_ output (formatResult result)   where     output value = do
src/Hadolint/Formatter/Format.hs view
@@ -19,9 +19,10 @@ import qualified Data.Sequence as Seq import Hadolint.Rules import ShellCheck.Interface-import Text.Megaparsec (Stream (..), pstateSourcePos)+import Text.Megaparsec (TraversableStream (..), pstateSourcePos) import Text.Megaparsec.Error import Text.Megaparsec.Pos (SourcePos, sourcePosPretty)+import Text.Megaparsec.Stream (VisualStream)  data Result s e = Result   { errors :: !(Seq (ParseErrorBundle s e)),@@ -62,14 +63,14 @@           else c     ) -errorMessageLine :: (Stream s, ShowErrorComponent e) => ParseErrorBundle s e -> String+errorMessageLine :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => ParseErrorBundle s e -> String errorMessageLine err@(ParseErrorBundle e _) =   errorPositionPretty err ++ " " ++ parseErrorTextPretty (NE.head e) -errorPositionPretty :: Stream s => ParseErrorBundle s e -> String+errorPositionPretty :: TraversableStream s => ParseErrorBundle s e -> String errorPositionPretty err = sourcePosPretty (errorPosition err) -errorPosition :: Stream s => ParseErrorBundle s e -> Text.Megaparsec.Pos.SourcePos+errorPosition :: TraversableStream s => ParseErrorBundle s e -> Text.Megaparsec.Pos.SourcePos errorPosition (ParseErrorBundle e s) =   let (_, posState) = reachOffset (errorOffset (NE.head e)) s    in pstateSourcePos posState
src/Hadolint/Formatter/Json.hs view
@@ -13,15 +13,16 @@ import Hadolint.Formatter.Format (Result (..), errorPosition, severityText) import Hadolint.Rules (Metadata (..), RuleCheck (..)) import ShellCheck.Interface-import Text.Megaparsec (Stream)+import Text.Megaparsec (TraversableStream) import Text.Megaparsec.Error import Text.Megaparsec.Pos (sourceColumn, sourceLine, sourceName, unPos)+import Text.Megaparsec.Stream (VisualStream)  data JsonFormat s e   = JsonCheck RuleCheck   | JsonParseError (ParseErrorBundle s e) -instance (Stream s, ShowErrorComponent e) => ToJSON (JsonFormat s e) where+instance (VisualStream s, TraversableStream s, ShowErrorComponent e) => ToJSON (JsonFormat s e) where   toJSON (JsonCheck RuleCheck {..}) =     object       [ "file" .= filename,@@ -43,12 +44,12 @@     where       pos = errorPosition err -formatResult :: (Stream s, ShowErrorComponent e) => Result s e -> Value+formatResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> Value formatResult (Result errors checks) = toJSON allMessages   where     allMessages = errorMessages <> checkMessages     errorMessages = fmap JsonParseError errors     checkMessages = fmap JsonCheck checks -printResult :: (Stream s, ShowErrorComponent e) => Result s e -> IO ()+printResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> IO () printResult result = B.putStrLn (encode (formatResult result))
src/Hadolint/Formatter/TTY.hs view
@@ -13,13 +13,14 @@ import Hadolint.Formatter.Format import Hadolint.Rules import Language.Docker.Syntax-import Text.Megaparsec (Stream (..))+import Text.Megaparsec (TraversableStream) import Text.Megaparsec.Error+import Text.Megaparsec.Stream (VisualStream) -formatErrors :: (Stream s, ShowErrorComponent e, Functor f) => f (ParseErrorBundle s e) -> f String+formatErrors :: (VisualStream s, TraversableStream s, ShowErrorComponent e, Functor f) => f (ParseErrorBundle s e) -> f String formatErrors = fmap formatError -formatError :: (Stream s, ShowErrorComponent e) => ParseErrorBundle s e -> String+formatError :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => ParseErrorBundle s e -> String formatError err = stripNewlines (errorMessageLine err)  formatChecks :: Functor f => f RuleCheck -> f Text.Text@@ -31,7 +32,7 @@ formatPos :: Filename -> Linenumber -> Text.Text formatPos source line = source <> ":" <> Text.pack (show line) <> " " -printResult :: (Stream s, ShowErrorComponent e) => Result s e -> IO ()+printResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> IO () printResult Result {errors, checks} = printErrors >> printChecks   where     printErrors = mapM_ putStrLn (formatErrors errors)
src/Hadolint/Rules.hs view
@@ -210,7 +210,15 @@     yumYes,     noYumUpdate,     yumCleanup,-    yumVersionPinned+    yumVersionPinned,+    zypperYes,+    noZypperUpdate,+    zypperCleanup,+    zypperVersionPinned,+    dnfYes,+    noDnfUpdate,+    dnfCleanup,+    dnfVersionPinned   ]  optionalRules :: RulesConfig -> [Rule]@@ -284,10 +292,14 @@     severity = ErrorC     message = "Use absolute WORKDIR"     check (Workdir loc)-      | "$" `Text.isPrefixOf` loc = True-      | "/" `Text.isPrefixOf` loc = True+      | "$" `Text.isPrefixOf` Text.dropAround dropQuotes loc = True+      | "/" `Text.isPrefixOf` Text.dropAround dropQuotes loc = True       | otherwise = False     check _ = True+    dropQuotes chr+      | chr == '\"' = True+      | chr == '\'' = True+      | otherwise = False  hasNoMaintainer :: Rule hasNoMaintainer = instructionRule code severity message check@@ -635,10 +647,12 @@               "upgrade-strategy"             ]             cmd-    versionFixed package = hasVersionSymbol package || isVersionedGit package+    versionFixed package = hasVersionSymbol package || isVersionedGit package || isLocalPackage package     isVersionedGit package = "git+http" `Text.isInfixOf` package && "@" `Text.isInfixOf` package     versionSymbols = ["==", ">=", "<=", ">", "<", "!=", "~=", "==="]     hasVersionSymbol package = or [s `Text.isInfixOf` package | s <- versionSymbols]+    localPackageFileExtensions = [".whl", ".tar.gz"]+    isLocalPackage package = or [s `Text.isSuffixOf` package | s <- localPackageFileExtensions]  stripInstallPrefix :: [Text.Text] -> [Text.Text] stripInstallPrefix cmd = dropWhile (== "install") (dropWhile (/= "install") cmd)@@ -950,6 +964,148 @@ yumPackages :: Shell.ParsedShell -> [Text.Text] yumPackages args =   [ arg | cmd <- Shell.presentCommands args, Shell.cmdHasArgs "yum" ["install"] cmd, arg <- Shell.getArgsNoFlags cmd, arg /= "install"+  ]++zypperYes :: Rule+zypperYes = instructionRule code severity message check+  where+    code = "DL3034"+    severity = WarningC+    message = "Non-interactive switch missing from `zypper` command: `zypper install -y`"+    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotZypperYesOption) args+    check _ = True+    forgotZypperYesOption cmd = isZypperInstall cmd && not (hasYesOption cmd)+    isZypperInstall =+      Shell.cmdHasArgs+        "zypper"+        [ "install",+          "in",+          "remove",+          "rm",+          "source-install",+          "si",+          "patch"+        ]+    hasYesOption = Shell.hasAnyFlag ["no-confirm", "y"]++noZypperUpdate :: Rule+noZypperUpdate = instructionRule code severity message check+  where+    code = "DL3035"+    severity = WarningC+    message = "Do not use `zypper update`."+    check (Run (RunArgs args _)) =+      argumentsRule+        ( Shell.noCommands+            ( Shell.cmdHasArgs+                "zypper"+                [ "update",+                  "up",+                  "dist-upgrade",+                  "dup"+                ]+            )+        )+        args+    check _ = True++zypperCleanup :: Rule+zypperCleanup = instructionRule code severity message check+  where+    code = "DL3036"+    severity = WarningC+    message = "`zypper clean` missing after zypper use."+    check (Run (RunArgs args _)) =+      argumentsRule (Shell.noCommands zypperInstall) args+        || ( argumentsRule (Shell.anyCommands zypperInstall) args+               && argumentsRule (Shell.anyCommands zypperClean) args+           )+    check _ = True+    zypperInstall = Shell.cmdHasArgs "zypper" ["install", "in"]+    zypperClean = Shell.cmdHasArgs "zypper" ["clean", "cc"]++zypperVersionPinned :: Rule+zypperVersionPinned = instructionRule code severity message check+  where+    code = "DL3037"+    severity = WarningC+    message = "Specify version with `zypper install -y <package>=<version>`."+    check (Run (RunArgs args _)) = argumentsRule (all versionFixed . zypperPackages) args+    check _ = True+    versionFixed package =+      "=" `Text.isInfixOf` package+        || ">=" `Text.isInfixOf` package+        || ">" `Text.isInfixOf` package+        || "<=" `Text.isInfixOf` package+        || "<" `Text.isInfixOf` package+        || ".rpm" `Text.isSuffixOf` package++zypperPackages :: Shell.ParsedShell -> [Text.Text]+zypperPackages args =+  [ arg | cmd <- Shell.presentCommands args, Shell.cmdHasArgs "zypper" ["install", "in"] cmd, arg <- Shell.getArgsNoFlags cmd, arg /= "install", arg /= "in"+  ]++dnfYes :: Rule+dnfYes = instructionRule code severity message check+  where+    code = "DL3038"+    severity = WarningC+    message = "Use the -y switch to avoid manual input `dnf install -y <package`"+    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotDnfYesOption) args+    check _ = True+    forgotDnfYesOption cmd = isDnfInstall cmd && not (hasYesOption cmd)+    isDnfInstall = Shell.cmdHasArgs "dnf" ["install", "groupinstall", "localinstall"]+    hasYesOption = Shell.hasAnyFlag ["y", "assumeyes"]++noDnfUpdate :: Rule+noDnfUpdate = instructionRule code severity message check+  where+    code = "DL3039"+    severity = ErrorC+    message = "Do not use dnf update."+    check (Run (RunArgs args _)) =+      argumentsRule+        ( Shell.noCommands+            ( Shell.cmdHasArgs+                "dnf"+                [ "upgrade",+                  "upgrade-minimal"+                ]+            )+        )+        args+    check _ = True++dnfCleanup :: Rule+dnfCleanup = instructionRule code severity message check+  where+    code = "DL3040"+    severity = WarningC+    message = "`dnf clean all` missing after dnf command."+    check (Run (RunArgs args _)) =+      argumentsRule (Shell.noCommands dnfInstall) args+        || ( argumentsRule (Shell.anyCommands dnfInstall) args+              && argumentsRule (Shell.anyCommands dnfClean) args+           )+    check _ = True+    dnfInstall = Shell.cmdHasArgs "dnf" ["install"]+    dnfClean = Shell.cmdHasArgs "dnf" ["clean", "all"]++dnfVersionPinned :: Rule+dnfVersionPinned = instructionRule code severity message check+  where+    code = "DL3041"+    severity = WarningC+    message = "Specify version with `dnf install -y <package>-<version>`."+    check (Run (RunArgs args _)) = argumentsRule (all versionFixed . dnfPackages) args+    check _ = True+    versionFixed package =+      "-" `Text.isInfixOf` package+        || ".rpm" `Text.isSuffixOf` package++dnfPackages :: Shell.ParsedShell -> [Text.Text]+dnfPackages args =+  [ arg | cmd <- Shell.presentCommands args, Shell.cmdHasArgs "dnf" ["install"] cmd, arg <- Shell.getArgsNoFlags cmd, arg /= "install"   ]  gems :: Shell.ParsedShell -> [Text.Text]
test/Spec.hs view
@@ -190,19 +190,127 @@             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"+        it "yum update" $ do+            ruleCatches noYumUpdate "RUN yum update"+            ruleCatchesNot noYumUpdate "RUN yum install -y httpd-2.4.42 && yum clean all"+            ruleCatchesNot noYumUpdate "RUN bash -c `# not even a yum command`"+            onBuildRuleCatches noYumUpdate "RUN yum update"+            onBuildRuleCatchesNot noYumUpdate "RUN yum install -y httpd-2.4.42 && yum clean all"+            onBuildRuleCatchesNot noYumUpdate "RUN bash -c `# not even a yum command`"+        it "yum version pinning" $ do+            ruleCatches yumVersionPinned "RUN yum install -y tomcat && yum clean all"+            ruleCatchesNot yumVersionPinned "RUN yum install -y tomcat-9.2 && yum clean all"+            ruleCatchesNot yumVersionPinned "RUN bash -c `# not even a yum command`"+            onBuildRuleCatches yumVersionPinned "RUN yum install -y tomcat && yum clean all"+            onBuildRuleCatchesNot yumVersionPinned "RUN yum install -y tomcat-9.2 && yum clean all"+            onBuildRuleCatchesNot yumVersionPinned "RUN bash -c `# not even a yum command`"+        it "yum no clean all" $ do+            ruleCatches yumCleanup "RUN yum install -y mariadb-10.4"+            ruleCatchesNot yumCleanup "RUN yum install -y mariadb-10.4 && yum clean all"+            ruleCatchesNot yumCleanup "RUN bash -c `# not even a yum command`"+            onBuildRuleCatches yumCleanup "RUN yum install -y mariadb-10.4"+            onBuildRuleCatchesNot yumCleanup "RUN yum install -y mariadb-10.4 && yum clean all"+            onBuildRuleCatchesNot yumCleanup "RUN bash -c `# not even a yum command`"+        it "yum non-interactive" $ do+            ruleCatches yumYes "RUN yum install httpd-2.4.24 && yum clean all"+            ruleCatchesNot yumYes "RUN yum install -y httpd-2.4.24 && yum clean all"+            ruleCatchesNot yumYes "RUN bash -c `# not even a yum command`"+            onBuildRuleCatches yumYes "RUN yum install httpd-2.4.24 && yum clean all"+            onBuildRuleCatchesNot yumYes "RUN yum install -y httpd-2.4.24 && yum clean all"+            onBuildRuleCatchesNot yumYes "RUN bash -c `# not even a yum command`"     --+    describe "zypper rules" $ do+        it "zupper update" $ do+            ruleCatches noZypperUpdate "RUN zypper update"+            ruleCatches noZypperUpdate "RUN zypper up"+            ruleCatches noZypperUpdate "RUN zypper dist-upgrade"+            ruleCatches noZypperUpdate "RUN zypper dup"+            onBuildRuleCatches noZypperUpdate "RUN zypper update"+            onBuildRuleCatches noZypperUpdate "RUN zypper up"+            onBuildRuleCatches noZypperUpdate "RUN zypper dist-upgrade"+            onBuildRuleCatches noZypperUpdate "RUN zypper dup"+        it "zypper version pinning" $ do+-- NOTE: In Haskell strings, '\' has to be escaped. And in shell commands, '>'+-- and '<' have to be escaped. Hence the double escaping.+            ruleCatches zypperVersionPinned "RUN zypper install -y tomcat && zypper clean"+            ruleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat=9.0.39 && zypper clean"+            ruleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\>=9.0 && zypper clean"+            ruleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\>9.0 && zypper clean"+            ruleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\<=9.0 && zypper clean"+            ruleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\<9.0 && zypper clean"+            ruleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat-9.0.39-1.rpm && zypper clean"+            onBuildRuleCatches zypperVersionPinned "RUN zypper install -y tomcat && zypper clean"+            onBuildRuleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat=9.0.39 && zypper clean"+            onBuildRuleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\>=9.0 && zypper clean"+            onBuildRuleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\>9.0 && zypper clean"+            onBuildRuleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\<=9.0 && zypper clean"+            onBuildRuleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\<9.0 && zypper clean"+            onBuildRuleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat-9.0.39-1.rpm && zypper clean"+        it "zypper no clean all" $ do+            ruleCatches zypperCleanup "RUN zypper install -y mariadb=10.4"+            ruleCatchesNot zypperCleanup "RUN zypper install -y mariadb=10.4 && zypper clean"+            ruleCatchesNot zypperCleanup "RUN zypper install -y mariadb=10.4 && zypper cc"+            onBuildRuleCatches zypperCleanup "RUN zypper install -y mariadb=10.4"+            onBuildRuleCatchesNot zypperCleanup "RUN zypper install -y mariadb=10.4 && zypper clean"+            onBuildRuleCatchesNot zypperCleanup "RUN zypper install -y mariadb=10.4 && zypper cc"+        it "zypper non-interactive" $ do+            ruleCatches zypperYes "RUN zypper install httpd=2.4.24 && zypper clean"+            ruleCatchesNot zypperYes "RUN zypper install -y httpd=2.4.24 && zypper clean"+            ruleCatchesNot zypperYes "RUN zypper install --no-confirm httpd=2.4.24 && zypper clean"+            onBuildRuleCatches zypperYes "RUN zypper install httpd=2.4.24 && zypper clean"+            onBuildRuleCatchesNot zypperYes "RUN zypper install -y httpd=2.4.24 && zypper clean"+            onBuildRuleCatchesNot zypperYes "RUN zypper install --no-confirm httpd=2.4.24 && zypper clean"+    --+    describe "dnf rules" $ do+      it "dnf update" $ do+        ruleCatches noDnfUpdate "RUN dnf upgrade"+        onBuildRuleCatches noDnfUpdate "RUN dnf upgrade"+      it "dnf version pinning" $ do+        ruleCatches dnfVersionPinned "RUN dnf install -y tomcat && dnf clean all"+        ruleCatchesNot dnfVersionPinned "RUN dnf install -y tomcat-9.0.1 && dnf clean all"+        onBuildRuleCatches dnfVersionPinned "RUN dnf install -y tomcat && dnf clean all"+        onBuildRuleCatchesNot dnfVersionPinned "RUN dnf install -y tomcat-9.0.1 && dnf clean all"+      it "dnf no clean all" $ do+        ruleCatches dnfCleanup "RUN dnf install -y mariadb-10.4"+        ruleCatchesNot dnfCleanup "RUN dnf install -y mariadb-10.4 && dnf clean all"+        onBuildRuleCatches dnfCleanup "RUN dnf install -y mariadb-10.4"+        onBuildRuleCatchesNot dnfCleanup "RUN dnf install -y mariadb-10.4 && dnf clean all"+      it "dnf non-interactive" $ do+        ruleCatches dnfYes "RUN dnf install httpd-2.4.24 && dnf clean all"+        ruleCatchesNot dnfYes "RUN dnf install -y httpd-2.4.24 && dnf clean all"+        onBuildRuleCatches dnfYes "RUN dnf install httpd-2.4.24 && dnf clean all"+        onBuildRuleCatchesNot dnfYes "RUN dnf install -y httpd-2.4.24 && dnf clean all"+    --+    describe "dnf rules" $ do+      it "dnf update" $ do+        ruleCatches noDnfUpdate "RUN dnf upgrade"+        ruleCatches noDnfUpdate "RUN dnf upgrade-minimal"+        ruleCatchesNot noDnfUpdate "RUN notdnf upgrade"+        onBuildRuleCatches noDnfUpdate "RUN dnf upgrade"+        onBuildRuleCatches noDnfUpdate "RUN dnf upgrade-minimal"+        onBuildRuleCatchesNot noDnfUpdate "RUN notdnf upgrade"+      it "dnf version pinning" $ do+        ruleCatches dnfVersionPinned "RUN dnf install -y tomcat && dnf clean all"+        ruleCatchesNot dnfVersionPinned "RUN dnf install -y tomcat-9.0.1 && dnf clean all"+        ruleCatchesNot dnfVersionPinned "RUN notdnf install tomcat"+        onBuildRuleCatches dnfVersionPinned "RUN dnf install -y tomcat && dnf clean all"+        onBuildRuleCatchesNot dnfVersionPinned "RUN dnf install -y tomcat-9.0.1 && dnf clean all"+        onBuildRuleCatchesNot dnfVersionPinned "RUN notdnf install tomcat"+      it "dnf no clean all" $ do+        ruleCatches dnfCleanup "RUN dnf install -y mariadb-10.4"+        ruleCatchesNot dnfCleanup "RUN dnf install -y mariadb-10.4 && dnf clean all"+        ruleCatchesNot dnfCleanup "RUN notdnf install mariadb"+        onBuildRuleCatches dnfCleanup "RUN dnf install -y mariadb-10.4"+        onBuildRuleCatchesNot dnfCleanup "RUN dnf install -y mariadb-10.4 && dnf clean all"+        onBuildRuleCatchesNot dnfCleanup "RUN notdnf install mariadb"+      it "dnf non-interactive" $ do+        ruleCatches dnfYes "RUN dnf install httpd-2.4.24 && dnf clean all"+        ruleCatchesNot dnfYes "RUN dnf install -y httpd-2.4.24 && dnf clean all"+        ruleCatchesNot dnfYes "RUN notdnf install httpd"+        onBuildRuleCatches dnfYes "RUN dnf install httpd-2.4.24 && dnf clean all"+        onBuildRuleCatchesNot dnfYes "RUN dnf install -y httpd-2.4.24 && dnf clean all"+        onBuildRuleCatchesNot dnfYes "RUN notdnf install httpd"+    --     describe "apt-get rules" $ do       it "apt" $         let dockerFile =@@ -410,6 +518,11 @@       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 "pip3 install from local package" $ do+        ruleCatchesNot pipVersionPinned "RUN pip3 install mypkg.whl"+        ruleCatchesNot pipVersionPinned "RUN pip3 install mypkg.tar.gz"+        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install mypkg.whl"+        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install mypkg.tar.gz"       it "pip install requirements" $ do         ruleCatchesNot pipVersionPinned "RUN pip install -r requirements.txt"         onBuildRuleCatchesNot pipVersionPinned "RUN pip install -r requirements.txt"@@ -898,9 +1011,18 @@                 "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 relative" $ ruleCatches absoluteWorkdir "WORKDIR relative/dir"+      it "workdir absolute" $ ruleCatchesNot absoluteWorkdir "WORKDIR /usr/local"       it "workdir variable" $ ruleCatchesNot absoluteWorkdir "WORKDIR ${work}"+      it "workdir relative single quotes" $ ruleCatches absoluteWorkdir "WORKDIR \'relative/dir\'"+      it "workdir absolute single quotes" $ ruleCatchesNot absoluteWorkdir "WORKDIR \'/usr/local\'"+      -- no test for variable/single quotes since the variable would not expand.+      it "workdir relative double quotes" $ ruleCatches absoluteWorkdir "WORKDIR \"relative/dir\""+      it "workdir absolute double quotes" $ ruleCatchesNot absoluteWorkdir "WORKDIR \"/usr/local\""+      it "workdir variable double quotes" $ ruleCatchesNot absoluteWorkdir "WORKDIR \"${dir}\""       it "scratch" $ ruleCatchesNot noUntagged "FROM scratch"     --     describe "add files and archives" $ do