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
}
}
findCommands :: ParsedBash -> [Token]
findCommands (ParsedBash _ ast) =
case prRoot ast of
Nothing -> []
Just script -> nub . execWriter $ ShellCheck.AST.doAnalysis extract script
where
extract :: Token -> Writer [Token] ()
extract t =
case ShellCheck.ASTLib.getCommand t of
Nothing -> return ()
Just cmd -> tell [cmd]
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