packages feed

hadolint 1.17.1 → 1.17.2

raw patch · 4 files changed

+160/−131 lines, 4 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Hadolint.Formatter.Codacy: instance Data.Aeson.Types.ToJSON.ToJSON Hadolint.Formatter.Codacy.Issue
- Hadolint.Formatter.Codeclimate: instance Data.Aeson.Types.ToJSON.ToJSON Hadolint.Formatter.Codeclimate.Issue
- Hadolint.Formatter.Codeclimate: instance Data.Aeson.Types.ToJSON.ToJSON Hadolint.Formatter.Codeclimate.Location
- Hadolint.Formatter.Codeclimate: instance Data.Aeson.Types.ToJSON.ToJSON Hadolint.Formatter.Codeclimate.Pos
- Hadolint.Formatter.Json: instance (Text.Megaparsec.Stream.Stream s, Text.Megaparsec.Error.ShowErrorComponent e) => Data.Aeson.Types.ToJSON.ToJSON (Hadolint.Formatter.Json.JsonFormat s e)
- Hadolint.Shell: findCommands :: ParsedShell -> [Token]
- Hadolint.Shell: getAllArgs :: Token -> [String]
- Hadolint.Shell: getCommandName :: Token -> Maybe String
+ Hadolint.Formatter.Codacy: instance Data.Aeson.Types.Class.ToJSON Hadolint.Formatter.Codacy.Issue
+ Hadolint.Formatter.Codeclimate: instance Data.Aeson.Types.Class.ToJSON Hadolint.Formatter.Codeclimate.Issue
+ Hadolint.Formatter.Codeclimate: instance Data.Aeson.Types.Class.ToJSON Hadolint.Formatter.Codeclimate.Location
+ Hadolint.Formatter.Codeclimate: instance Data.Aeson.Types.Class.ToJSON Hadolint.Formatter.Codeclimate.Pos
+ Hadolint.Formatter.Json: instance (Text.Megaparsec.Stream.Stream s, Text.Megaparsec.Error.ShowErrorComponent e) => Data.Aeson.Types.Class.ToJSON (Hadolint.Formatter.Json.JsonFormat s e)
+ Hadolint.Shell: CmdPart :: !Text -> !Int -> CmdPart
+ Hadolint.Shell: Command :: !Text -> [CmdPart] -> [CmdPart] -> Command
+ Hadolint.Shell: [arg] :: CmdPart -> !Text
+ Hadolint.Shell: [arguments] :: Command -> [CmdPart]
+ Hadolint.Shell: [flags] :: Command -> [CmdPart]
+ Hadolint.Shell: [name] :: Command -> !Text
+ Hadolint.Shell: [partId] :: CmdPart -> !Int
+ Hadolint.Shell: [presentCommands] :: ParsedShell -> ![Command]
+ Hadolint.Shell: data CmdPart
+ Hadolint.Shell: data Command
+ Hadolint.Shell: extractAllArgs :: Token -> [CmdPart]
+ Hadolint.Shell: findCommandsInResult :: ParseResult -> [Token]
+ Hadolint.Shell: getArgs :: Command -> [Text]
+ Hadolint.Shell: hasAnyFlag :: [Text] -> Command -> Bool
+ Hadolint.Shell: hasArg :: Text -> Command -> Bool
+ Hadolint.Shell: instance GHC.Show.Show Hadolint.Shell.CmdPart
+ Hadolint.Shell: instance GHC.Show.Show Hadolint.Shell.Command
- Hadolint.Rules: apkAddPackages :: ParsedShell -> [String]
+ Hadolint.Rules: apkAddPackages :: ParsedShell -> [Text]
- Hadolint.Rules: aptGetPackages :: ParsedShell -> [String]
+ Hadolint.Rules: aptGetPackages :: ParsedShell -> [Text]
- Hadolint.Rules: gems :: ParsedShell -> [String]
+ Hadolint.Rules: gems :: ParsedShell -> [Text]
- Hadolint.Rules: stripInstallPrefix :: [String] -> [String]
+ Hadolint.Rules: stripInstallPrefix :: [Text] -> [Text]
- Hadolint.Rules: usingProgram :: String -> ParsedShell -> Bool
+ Hadolint.Rules: usingProgram :: Text -> ParsedShell -> Bool
- Hadolint.Shell: ParsedShell :: Text -> ParseResult -> ParsedShell
+ Hadolint.Shell: ParsedShell :: !Text -> !ParseResult -> ![Command] -> ParsedShell
- Hadolint.Shell: [original] :: ParsedShell -> Text
+ Hadolint.Shell: [original] :: ParsedShell -> !Text
- Hadolint.Shell: [parsed] :: ParsedShell -> ParseResult
+ Hadolint.Shell: [parsed] :: ParsedShell -> !ParseResult
- Hadolint.Shell: allCommands :: (Token -> Bool) -> ParsedShell -> Bool
+ Hadolint.Shell: allCommands :: (Command -> Bool) -> ParsedShell -> Bool
- Hadolint.Shell: cmdHasArgs :: String -> [String] -> Token -> Bool
+ Hadolint.Shell: cmdHasArgs :: Text -> [Text] -> Command -> Bool
- Hadolint.Shell: dropFlagArg :: [String] -> Token -> Token
+ Hadolint.Shell: dropFlagArg :: [Text] -> Command -> Command
- Hadolint.Shell: extractTokensWith :: (Token -> Maybe Token) -> ParsedShell -> [Token]
+ Hadolint.Shell: extractTokensWith :: forall a. (Token -> Maybe a) -> ParseResult -> [a]
- Hadolint.Shell: findCommandNames :: ParsedShell -> [String]
+ Hadolint.Shell: findCommandNames :: ParsedShell -> [Text]
- Hadolint.Shell: getAllFlags :: Token -> [(Token, String)]
+ Hadolint.Shell: getAllFlags :: [CmdPart] -> [CmdPart]
- Hadolint.Shell: getArgsNoFlags :: Token -> [String]
+ Hadolint.Shell: getArgsNoFlags :: Command -> [Text]
- Hadolint.Shell: hasFlag :: String -> Token -> Bool
+ Hadolint.Shell: hasFlag :: Text -> Command -> Bool
- Hadolint.Shell: noCommands :: (Token -> Bool) -> ParsedShell -> Bool
+ Hadolint.Shell: noCommands :: (Command -> Bool) -> ParsedShell -> Bool

Files

README.md view
@@ -4,7 +4,7 @@ [![GitHub release][release-img]][release] [![Github downloads][downloads-img]]() <img align="right" alt="pipecat" width="150"-src="http://hadolint.lukasmartinelli.ch/img/cat_container.png" />+src="https://hadolint.github.io/hadolint/img/cat_container.png" />  # Haskell Dockerfile Linter @@ -13,9 +13,9 @@ top of the AST. It is standing on the shoulders of [ShellCheck][] to lint the Bash code inside `RUN` instructions. -<!-- [:globe_with_meridians: **Check the online version on- hadolint.lukasmartinelli.ch**](http://hadolint.lukasmartinelli.ch/.)-[![Screenshot](screenshot.png)](http://hadolint.lukasmartinelli.ch/) -->+[:globe_with_meridians: **Check the online version on+ hadolint.github.io/hadolint**](https://hadolint.github.io/hadolint)+[![Screenshot](screenshot.png)](https://hadolint.github.io/hadolint)  ## How to use 
hadolint.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 099ae897b9dd6e8bd846e847751883f65ef076c560a6157945fbeb7be74a5859+-- hash: 0f8b34c24db80480b579a95b0f4a3ff72ce38bb93a16e92f0578975efa60cbcc  name:           hadolint-version:        1.17.1+version:        1.17.2 synopsis:       Dockerfile Linter JavaScript API description:    A smarter Dockerfile linter that helps you build best practice Docker images. category:       Development
src/Hadolint/Rules.hs view
@@ -263,7 +263,8 @@  -- | 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))+commentMetadata 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@@ -291,7 +292,7 @@     check _ = True  -- Check if a command contains a program call in the Run instruction-usingProgram :: String -> Shell.ParsedShell -> Bool+usingProgram :: Text.Text -> Shell.ParsedShell -> Bool usingProgram prog args = not $ null [cmd | cmd <- Shell.findCommandNames args, cmd == prog]  multipleCmds :: Rule@@ -444,14 +445,14 @@         \install <package>=<version>`"     check (Run args) = argumentsRule (all versionFixed . aptGetPackages) args     check _ = True-    versionFixed package = "=" `isInfixOf` package+    versionFixed package = "=" `Text.isInfixOf` package -aptGetPackages :: Shell.ParsedShell -> [String]+aptGetPackages :: Shell.ParsedShell -> [Text.Text] aptGetPackages args =     [ arg-    | cmd <- dropTarget <$> Shell.findCommands args+    | cmd <- Shell.presentCommands args     , Shell.cmdHasArgs "apt-get" ["install"] cmd-    , arg <- Shell.getArgsNoFlags cmd+    , arg <- Shell.getArgsNoFlags (dropTarget cmd)     , arg /= "install"     ]   where@@ -478,8 +479,8 @@         | not (hasUpdate args) || not (imageIsUsed line baseimage) = True         | otherwise = hasCleanup args     hasCleanup args =-        any (Shell.cmdHasArgs "rm" ["-rf", "/var/lib/apt/lists/*"]) (Shell.findCommands args)-    hasUpdate args = any (Shell.cmdHasArgs "apt-get" ["update"]) (Shell.findCommands 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@@ -509,14 +510,14 @@         "Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`"     check (Run args) = argumentsRule (\as -> and [versionFixed p | p <- apkAddPackages as]) args     check _ = True-    versionFixed package = "=" `isInfixOf` package+    versionFixed package = "=" `Text.isInfixOf` package -apkAddPackages :: Shell.ParsedShell -> [String]+apkAddPackages :: Shell.ParsedShell -> [Text.Text] apkAddPackages args =     [ arg-    | cmd <- dropTarget <$> Shell.findCommands args+    | cmd <- Shell.presentCommands args     , Shell.cmdHasArgs "apk" ["add"] cmd-    , arg <- Shell.getArgsNoFlags cmd+    , arg <- Shell.getArgsNoFlags (dropTarget cmd)     , arg /= "add"     ]   where@@ -548,22 +549,22 @@             ]     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@@ -589,23 +590,22 @@     forgotToPinVersion cmd =         isPipInstall cmd && not (hasBuildConstraint cmd) && not (all versionFixed (packages cmd))     -- Check if the command is a pip* install command, and that specific pacakges are being listed-    isPipInstall cmd =-        case Shell.getCommandName cmd of-            Just ('p':'i':'p':_) -> relevantInstall cmd-            _ -> False+    isPipInstall cmd@(Shell.Command name _ _) = "pip" `Text.isPrefixOf` name && relevantInstall cmd     -- If the user is installing requirements from a file or just the local module, then we are not interested     -- in running this rule     relevantInstall cmd =-        ["install"] `isInfixOf` Shell.getAllArgs cmd &&-        not (["-r"] `isInfixOf` Shell.getAllArgs cmd || ["."] `isInfixOf` Shell.getAllArgs cmd)+        ["install"] `isInfixOf` Shell.getArgs cmd &&+        not (["-r"] `isInfixOf` Shell.getArgs cmd || ["."] `isInfixOf` Shell.getArgs cmd)     hasBuildConstraint = Shell.hasFlag "constraint"-    packages cmd = stripInstallPrefix $ Shell.getArgsNoFlags $ Shell.dropFlagArg ["i", "index-url", "extra-index-url"] cmd+    packages cmd =+        stripInstallPrefix $+        Shell.getArgsNoFlags $ Shell.dropFlagArg ["i", "index-url", "extra-index-url"] cmd     versionFixed package = hasVersionSymbol package || isVersionedGit package-    isVersionedGit package = "git+http" `isInfixOf` package && "@" `isInfixOf` package+    isVersionedGit package = "git+http" `Text.isInfixOf` package && "@" `Text.isInfixOf` package     versionSymbols = ["==", ">=", "<=", ">", "<", "!=", "~=", "==="]-    hasVersionSymbol package = or [s `isInfixOf` package | s <- versionSymbols]+    hasVersionSymbol package = or [s `Text.isInfixOf` package | s <- versionSymbols] -stripInstallPrefix :: [String] -> [String]+stripInstallPrefix :: [Text.Text] -> [Text.Text] stripInstallPrefix = dropWhile (== "install")  {-|@@ -638,13 +638,13 @@             then isVersionedGit package             else hasVersionSymbol package     gitPrefixes = ["git://", "git+ssh://", "git+http://", "git+https://"]-    hasGitPrefix package = or [p `isPrefixOf` package | p <- gitPrefixes]-    isVersionedGit package = "#" `isInfixOf` package-    hasVersionSymbol package = "@" `isInfixOf` dropScope package+    hasGitPrefix package = or [p `Text.isPrefixOf` package | p <- gitPrefixes]+    isVersionedGit package = "#" `Text.isInfixOf` package+    hasVersionSymbol package = "@" `Text.isInfixOf` dropScope package       where         dropScope pkg =-            if "@" `isPrefixOf` pkg-                then dropWhile ('/' <) pkg+            if "@" `Text.isPrefixOf` pkg+                then Text.dropWhile ('/' <) pkg                 else pkg  aptGetYes :: Rule@@ -657,11 +657,7 @@     check _ = True     forgotAptYesOption cmd = isAptGetInstall cmd && not (hasYesOption cmd)     isAptGetInstall = Shell.cmdHasArgs "apt-get" ["install"]-    hasYesOption cmd =-        "y" `elem` allFlags cmd ||-        "yes" `elem` allFlags cmd ||-        length (filter (== "q") (allFlags cmd)) > 1 || "assume-yes" `elem` allFlags cmd-    allFlags cmd = snd <$> Shell.getAllFlags cmd+    hasYesOption = Shell.hasAnyFlag ["y", "yes", "q", "assume-yes"]  aptGetNoRecommends :: Rule aptGetNoRecommends = instructionRule code severity message check@@ -675,7 +671,7 @@     isAptGetInstall = Shell.cmdHasArgs "apt-get" ["install"]     disablesRecommendOption cmd =         Shell.hasFlag "no-install-recommends" cmd ||-        "APT::Install-Recommends=false" `elem` Shell.getAllArgs cmd+        Shell.hasArg "APT::Install-Recommends=false" cmd  isArchive :: Text.Text -> Bool isArchive path =@@ -798,17 +794,17 @@         | otherwise = (argumentsRule hasPipefailOption args, True)     check False _ (Run args) = (False, argumentsRule notHasPipes args)     check st _ _ = (st, True)-    isPowerShell (Shell.ParsedShell orig _) = "pwsh" `Text.isPrefixOf` orig+    isPowerShell (Shell.ParsedShell orig _ _) = "pwsh" `Text.isPrefixOf` orig     notHasPipes script = not (Shell.hasPipes script)     hasPipefailOption script =         not $         null             [ True-            | cmd <- Shell.findCommands script+            | cmd@(Shell.Command name arguments _) <- Shell.presentCommands script             , validShell <- ["/bin/bash", "/bin/zsh", "/bin/ash", "bash", "zsh", "ash"]-            , Shell.getCommandName cmd == Just validShell+            , name == validShell             , Shell.hasFlag "o" cmd-            , arg <- Shell.getAllArgs cmd+            , arg <- Shell.arg <$> arguments             , arg == "pipefail"             ] @@ -840,16 +836,16 @@         \install <gem>:<version>`"     check (Run args) = argumentsRule (all versionFixed . gems) args     check _ = True-    versionFixed package = ":" `isInfixOf` package+    versionFixed package = ":" `Text.isInfixOf` package -gems :: Shell.ParsedShell -> [String]-gems args =+gems :: Shell.ParsedShell -> [Text.Text]+gems shell =     [ arg-    | cmd <- Shell.findCommands args+    | cmd <- Shell.presentCommands shell     , Shell.cmdHasArgs "gem" ["install", "i"] cmd     , not (Shell.cmdHasArgs "gem" ["-v"] cmd)     , arg <- Shell.getArgsNoFlags cmd     , arg /= "install"     , arg /= "i"-    , not ("--" `isPrefixOf` arg)+    , arg /= "--"     ]
src/Hadolint/Shell.hs view
@@ -1,14 +1,16 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE ScopedTypeVariables #-}  module Hadolint.Shell where  import Control.Monad.Writer (Writer, execWriter, tell) import Data.Functor.Identity (runIdentity)-import Data.List (nub) 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 ShellCheck.AST import ShellCheck.AST (Id(..), Token(..)) import qualified ShellCheck.ASTLib@@ -16,9 +18,21 @@ import ShellCheck.Interface import qualified ShellCheck.Parser +data CmdPart = CmdPart+    { arg :: !Text+    , partId :: !Int+    } deriving (Show)++data Command = Command+    { name :: !Text.Text+    , arguments :: [CmdPart]+    , flags :: [CmdPart]+    } deriving (Show)+ data ParsedShell = ParsedShell-    { original :: Text.Text-    , parsed :: ParseResult+    { original :: !Text.Text+    , parsed :: !ParseResult+    , presentCommands :: ![Command]     }  data ShellOpts = ShellOpts@@ -48,24 +62,26 @@ setShell s (ShellOpts _ v) = ShellOpts s v  shellcheck :: ShellOpts -> ParsedShell -> [PositionedComment]-shellcheck (ShellOpts sh env) (ParsedShell txt _) =+shellcheck (ShellOpts sh env) (ParsedShell txt _ _) =     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-    }+    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+            }     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     extractShell s =@@ -76,34 +92,45 @@     printVars = Text.unpack . Text.unlines . Set.toList $ Set.map (\v -> "export " <> v <> "=1") env  parseShell :: Text.Text -> ParsedShell-parseShell txt =-    ParsedShell-    { original = txt-    , parsed =-          runIdentity $-          ShellCheck.Parser.parseScript-              (mockedSystemInterface [("", "")])-              newParseSpec-              { psFilename = "" -- There is no filename-              , psScript = "#!/bin/bash\n" ++ Text.unpack txt-              , psCheckSourced = False-              }-    }+parseShell txt = ParsedShell {original = txt, parsed = parsedResult, presentCommands = commands}+  where+    parsedResult =+        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+    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)+      where+        allArgs = extractAllArgs token -extractTokensWith :: (Token -> Maybe Token) -> ParsedShell -> [Token]-extractTokensWith extractor (ParsedShell _ ast) =+findCommandsInResult :: ParseResult -> [Token]+findCommandsInResult = extractTokensWith commandsExtractor+  where+    commandsExtractor = ShellCheck.ASTLib.getCommand++extractTokensWith :: forall a. (Token -> Maybe a) -> ParseResult -> [a]+extractTokensWith extractor ast =     case prRoot ast of         Nothing -> []-        Just script -> nub . execWriter $ ShellCheck.AST.doAnalysis extract script+        Just script -> execWriter $ ShellCheck.AST.doAnalysis extract script   where-    extract :: Token -> Writer [Token] ()+    extract :: Token -> Writer [a] ()     extract token =         case extractor token of             Nothing -> return ()             Just t -> tell [t]  findPipes :: ParsedShell -> [Token]-findPipes = extractTokensWith pipesExtractor+findPipes (ParsedShell _ ast _) = extractTokensWith pipesExtractor ast   where     pipesExtractor pipe@T_Pipe {} = Just pipe     pipesExtractor _ = Nothing@@ -111,52 +138,58 @@ hasPipes :: ParsedShell -> Bool hasPipes = not . null . findPipes -findCommands :: ParsedShell -> [Token]-findCommands = extractTokensWith commandsExtractor-  where-    commandsExtractor = ShellCheck.ASTLib.getCommand--allCommands :: (Token -> Bool) -> ParsedShell -> Bool-allCommands check script = all check (findCommands script)+allCommands :: (Command -> Bool) -> ParsedShell -> Bool+allCommands check script = all check (presentCommands script) -noCommands :: (Token -> Bool) -> ParsedShell -> Bool+noCommands :: (Command -> Bool) -> ParsedShell -> Bool noCommands check = allCommands (not . check) -getCommandName :: Token -> Maybe String-getCommandName = ShellCheck.ASTLib.getCommandName+findCommandNames :: ParsedShell -> [Text]+findCommandNames script = map name (presentCommands script) -findCommandNames :: ParsedShell -> [String]-findCommandNames = mapMaybe getCommandName . findCommands+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] -cmdHasArgs :: String -> [String] -> Token -> Bool-cmdHasArgs command arguments token@T_SimpleCommand {}-    | ShellCheck.ASTLib.getCommandName token /= Just command = False-    | otherwise = not $ null [arg | arg <- getAllArgs token, arg `elem` arguments]-cmdHasArgs _ _ _ = False+extractAllArgs :: Token -> [CmdPart]+extractAllArgs (T_SimpleCommand _ _ (_:allArgs)) = map mkPart allArgs+  where+    mkPart token =+        CmdPart+            (Text.pack . concat $ ShellCheck.ASTLib.oversimplify token)+            (mkId (ShellCheck.AST.getId token))+    mkId (Id i) = i+extractAllArgs _ = [] -getAllArgs :: Token -> [String]-getAllArgs (T_SimpleCommand _ _ (_:allArgs)) = concatMap ShellCheck.ASTLib.oversimplify allArgs-getAllArgs _ = []+getArgs :: Command -> [Text.Text]+getArgs cmd = map arg (arguments cmd) -getArgsNoFlags :: Token -> [String]-getArgsNoFlags cmd@(T_SimpleCommand _ _ (_:allArgs)) = concatMap ShellCheck.ASTLib.oversimplify args+getAllFlags :: [CmdPart] -> [CmdPart]+getAllFlags = concatMap flag   where-    flags = [t | (t, _) <- getAllFlags cmd]-    args = [a | a <- allArgs, a `notElem` flags]-getArgsNoFlags _ = []+    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 = [] -getAllFlags :: Token -> [(Token, String)]-getAllFlags cmd@T_SimpleCommand {} = [(t, f) | (t, f) <- ShellCheck.ASTLib.getAllFlags cmd, f /= ""]-getAllFlags _ = []+getArgsNoFlags :: Command -> [Text.Text]+getArgsNoFlags args = map arg $ filter (notAFlagId . partId) (arguments args)+  where+    notAFlagId pId = pId `notElem` map partId (flags args) -hasFlag :: String -> Token -> Bool-hasFlag flag = any (\(_, f) -> f == flag) . getAllFlags+hasFlag :: Text.Text -> Command -> Bool+hasFlag flag Command {flags} = not $ null [f | CmdPart f _ <- flags, f == flag] -dropFlagArg :: [String] -> Token -> Token-dropFlagArg flags cmd@(T_SimpleCommand cid b allArgs) = T_SimpleCommand cid b filterdArgs+hasAnyFlag :: [Text.Text] -> Command -> Bool+hasAnyFlag fs Command {flags} = not $ null [f | CmdPart f _ <- flags, f `elem` fs]++hasArg :: Text.Text -> Command -> Bool+hasArg arg Command {arguments} = not $ null [a | CmdPart a _ <- arguments, a == arg]++dropFlagArg :: [Text.Text] -> Command -> Command+dropFlagArg flagsToDrop Command {name, arguments, flags} = Command name filterdArgs flags   where-    filterdArgs = [arg | arg <- allArgs, isNotNextToken arg]-    isNotNextToken arg = ShellCheck.AST.getId arg `notElem` findTokensToDrop-    findTokensToDrop = [next (ShellCheck.AST.getId t) | (t, f) <- getAllFlags cmd, f `elem` flags]-    next (Id i) = Id (i + 2)-dropFlagArg _ token = token+    idsToDrop = Set.fromList [fId + 2 | CmdPart f fId <- flags, f `elem` flagsToDrop]+    filterdArgs = [arg | arg@(CmdPart _ aId) <- arguments, not (aId `Set.member` idsToDrop)]