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 = ""
sourced = False
exclusions = []
parseShell :: Text.Text -> ParsedBash
parseShell txt =
ParsedBash
{ original = txt
, parsed =
runIdentity $
ShellCheck.Parser.parseScript
(mockedSystemInterface [("", "")])
ParseSpec
{ psFilename = ""
, 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