{-# LANGUAGE OverloadedStrings #-}
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 qualified ShellCheck.AST
import ShellCheck.AST (Id(..), Token(..))
import qualified ShellCheck.ASTLib
import ShellCheck.Checker
import ShellCheck.Interface
import qualified ShellCheck.Parser
data ParsedShell = ParsedShell
{ original :: Text.Text
, parsed :: ParseResult
}
data ShellOpts = ShellOpts
{ shellName :: Text.Text
, envVars :: Set.Set Text.Text
}
defaultShellOpts :: ShellOpts
defaultShellOpts = ShellOpts "/bin/sh -c" defaultVars
where
defaultVars =
Set.fromList
[ "HTTP_PROXY"
, "http_proxy"
, "HTTPS_PROXY"
, "https_proxy"
, "FTP_PROXY"
, "ftp_proxy"
, "NO_PROXY"
, "no_proxy"
]
addVars :: [Text.Text] -> ShellOpts -> ShellOpts
addVars vars (ShellOpts n v) = ShellOpts n (v <> Set.fromList vars)
setShell :: Text.Text -> ShellOpts -> ShellOpts
setShell s (ShellOpts _ v) = ShellOpts s v
shellcheck :: ShellOpts -> ParsedShell -> [PositionedComment]
shellcheck (ShellOpts sh env) (ParsedShell txt _) =
if "pwsh" `Text.isPrefixOf` sh
then []
else runShellCheck
where
runShellCheck = crComments $ runIdentity $ checkScript si spec
si = mockedSystemInterface [("", "")]
spec = emptyCheckSpec {
csFilename = "",
csScript = script,
csCheckSourced = False,
csExcludedWarnings = exclusions,
csShellTypeOverride = Nothing,
csMinSeverity = StyleC
}
script = "#!" ++ extractShell sh ++ "\n" ++ printVars ++ Text.unpack txt
exclusions =
[ 2187
]
extractShell s =
case listToMaybe . Text.words $ s of
Nothing -> ""
Just shell -> Text.unpack shell
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 = ""
, psScript = "#!/bin/bash\n" ++ Text.unpack txt
, psCheckSourced = False
}
}
extractTokensWith :: (Token -> Maybe Token) -> ParsedShell -> [Token]
extractTokensWith extractor (ParsedShell _ 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 :: ParsedShell -> [Token]
findPipes = extractTokensWith pipesExtractor
where
pipesExtractor pipe@T_Pipe {} = Just pipe
pipesExtractor _ = Nothing
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)
noCommands :: (Token -> Bool) -> ParsedShell -> Bool
noCommands check = allCommands (not . check)
getCommandName :: Token -> Maybe String
getCommandName = ShellCheck.ASTLib.getCommandName
findCommandNames :: ParsedShell -> [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