module Hadolint.Bash where import Control.Monad.Writer (Writer, execWriter, tell) import Data.Functor.Identity (runIdentity) import Data.List (nub) import Data.Maybe (mapMaybe) import qualified Data.Text as Text import qualified ShellCheck.AST import ShellCheck.AST (Id(..), Token(..)) import qualified ShellCheck.ASTLib import ShellCheck.Checker import ShellCheck.Interface import qualified ShellCheck.Parser data ParsedBash = ParsedBash { original :: Text.Text , parsed :: ParseResult } shellcheck :: ParsedBash -> [Comment] shellcheck (ParsedBash txt _) = map comment $ crComments $ runIdentity $ checkScript si spec where comment (PositionedComment _ _ c) = c si = mockedSystemInterface [("", "")] spec = CheckSpec filename script sourced exclusions (Just Bash) script = "#!/bin/bash\n" ++ Text.unpack txt filename = "" -- filename can be ommited because we only want the parse results back sourced = False exclusions = [] parseShell :: Text.Text -> ParsedBash parseShell txt = ParsedBash { original = txt , parsed = runIdentity $ ShellCheck.Parser.parseScript (mockedSystemInterface [("", "")]) ParseSpec { psFilename = "" -- There is no filename , psScript = "#!/bin/bash\n" ++ Text.unpack txt , psCheckSourced = False } } extractTokensWith :: (Token -> Maybe Token) -> ParsedBash -> [Token] extractTokensWith extractor (ParsedBash _ ast) = case prRoot ast of Nothing -> [] Just script -> nub . execWriter $ ShellCheck.AST.doAnalysis extract script where extract :: Token -> Writer [Token] () extract token = case extractor token of Nothing -> return () Just t -> tell [t] findPipes :: ParsedBash -> [Token] findPipes = extractTokensWith pipesExtractor where pipesExtractor pipe@T_Pipe{} = Just pipe pipesExtractor _ = Nothing hasPipes :: ParsedBash -> Bool hasPipes = not . null . findPipes findCommands :: ParsedBash -> [Token] findCommands = extractTokensWith commandsExtractor where commandsExtractor = ShellCheck.ASTLib.getCommand allCommands :: (Token -> Bool) -> ParsedBash -> Bool allCommands check script = all check (findCommands script) noCommands :: (Token -> Bool) -> ParsedBash -> Bool noCommands check = allCommands (not . check) getCommandName :: Token -> Maybe String getCommandName = ShellCheck.ASTLib.getCommandName findCommandNames :: ParsedBash -> [String] findCommandNames = mapMaybe getCommandName . findCommands 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 getAllArgs :: Token -> [String] getAllArgs (T_SimpleCommand _ _ (_:allArgs)) = concatMap ShellCheck.ASTLib.oversimplify allArgs getAllArgs _ = [] getArgsNoFlags :: Token -> [String] getArgsNoFlags cmd@(T_SimpleCommand _ _ (_:allArgs)) = concatMap ShellCheck.ASTLib.oversimplify args where flags = [t | (t, _) <- getAllFlags cmd] args = [a | a <- allArgs, a `notElem` flags] getArgsNoFlags _ = [] getAllFlags :: Token -> [(Token, String)] getAllFlags cmd@T_SimpleCommand {} = [(t, f) | (t, f) <- ShellCheck.ASTLib.getAllFlags cmd, f /= ""] getAllFlags _ = [] hasFlag :: String -> Token -> Bool hasFlag flag = any (\(_, f) -> f == flag) . getAllFlags dropFlagArg :: [String] -> Token -> Token dropFlagArg flags cmd@(T_SimpleCommand cid b allArgs) = T_SimpleCommand cid b filterdArgs 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