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 +4/−4
- hadolint.cabal +2/−2
- src/Hadolint/Rules.hs +54/−58
- src/Hadolint/Shell.hs +100/−67
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/.)-[](http://hadolint.lukasmartinelli.ch/) -->+[:globe_with_meridians: **Check the online version on+ hadolint.github.io/hadolint**](https://hadolint.github.io/hadolint)+[](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)]