diff --git a/ShellCheck/Analytics.hs b/ShellCheck/Analytics.hs index fd821bd494e6b8f6da0e545c39b08303eee58b81..96193373af124c8aa3e242d1d65bf539aeddd5d7 100644 --- a/ShellCheck/Analytics.hs +++ b/ShellCheck/Analytics.hs @@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . -} -module ShellCheck.Analytics (AnalysisOption(..), filterByAnnotation, runAnalytics) where +module ShellCheck.Analytics (AnalysisOption(..), filterByAnnotation, runAnalytics, shellForExecutable) where import ShellCheck.AST import ShellCheck.Data @@ -41,7 +41,7 @@ data Parameters = Parameters { shellType :: Shell } -data AnalysisOption = NotImplemented +data AnalysisOption = ForceShell Shell -- Checks that are run on the AST root treeChecks :: [Parameters -> Token -> [Note]] @@ -75,36 +75,41 @@ checksFor Bash = [ ] runAnalytics :: [AnalysisOption] -> Token -> [Note] -runAnalytics options root = runList root treeChecks +runAnalytics options root = runList options root treeChecks -runList root list = notes +runList options root list = notes where params = Parameters { - shellType = determineShell root, + shellType = getShellOption, parentMap = getParentTree root, variableFlow = getVariableFlow (shellType params) (parentMap params) root } notes = concatMap (\f -> f params root) list + getShellOption = + fromMaybe (determineShell root) . msum $ + map ((\option -> + case option of + ForceShell x -> return x + )) options + checkList l t = concatMap (\f -> f t) l prop_determineShell0 = determineShell (T_Script (Id 0) "#!/bin/sh" []) == Sh prop_determineShell1 = determineShell (T_Script (Id 0) "#!/usr/bin/env ksh" []) == Ksh prop_determineShell2 = determineShell (T_Script (Id 0) "" []) == Bash -determineShell (T_Script _ shebang _) = normalize $ shellFor shebang +determineShell (T_Script _ shebang _) = fromMaybe Bash . shellForExecutable $ shellFor shebang where shellFor s | "/env " `isInfixOf` s = head ((drop 1 $ words s)++[""]) shellFor s = reverse . takeWhile (/= '/') . reverse $ s - normalize "sh" = Sh - normalize "ash" = Sh - normalize "dash" = Sh - - normalize "ksh" = Ksh - normalize "ksh93" = Ksh - - normalize "zsh" = Zsh - normalize "bash" = Bash - normalize _ = Bash +shellForExecutable "sh" = return Sh +shellForExecutable "ash" = return Sh +shellForExecutable "dash" = return Sh +shellForExecutable "ksh" = return Ksh +shellForExecutable "ksh93" = return Ksh +shellForExecutable "zsh" = return Zsh +shellForExecutable "bash" = return Bash +shellForExecutable _ = Nothing -- Checks that are run on each node in the AST runNodeAnalysis f p t = execWriter (doAnalysis (f p) t) @@ -288,7 +293,7 @@ verifyNotTree f s = checkTree f s == Just False checkNode f s = checkTree (runNodeAnalysis f) s checkTree f s = case parseShell "-" s of - (ParseResult (Just (t, m)) _) -> Just . not . null $ runList t [f] + (ParseResult (Just (t, m)) _) -> Just . not . null $ runList [] t [f] _ -> Nothing diff --git a/ShellCheck/Simple.hs b/ShellCheck/Simple.hs index 04f48e11a2036776024870a96213b423d3df5eb3..766fcf11d745807cdaf8ceda60d52a54b281bf1d 100644 --- a/ShellCheck/Simple.hs +++ b/ShellCheck/Simple.hs @@ -25,27 +25,27 @@ import Data.List prop_findsParseIssue = - let comments = shellCheck "echo \"$12\"" in + let comments = shellCheck "echo \"$12\"" [] in (length comments) == 1 && (scCode $ head comments) == 1037 prop_commentDisablesParseIssue1 = - null $ shellCheck "#shellcheck disable=SC1037\necho \"$12\"" + null $ shellCheck "#shellcheck disable=SC1037\necho \"$12\"" [] prop_commentDisablesParseIssue2 = - null $ shellCheck "#shellcheck disable=SC1037\n#lol\necho \"$12\"" + null $ shellCheck "#shellcheck disable=SC1037\n#lol\necho \"$12\"" [] prop_findsAnalysisIssue = - let comments = shellCheck "echo $1" in + let comments = shellCheck "echo $1" [] in (length comments) == 1 && (scCode $ head comments) == 2086 prop_commentDisablesAnalysisIssue1 = - null $ shellCheck "#shellcheck disable=SC2086\necho $1" + null $ shellCheck "#shellcheck disable=SC2086\necho $1" [] prop_commentDisablesAnalysisIssue2 = - null $ shellCheck "#shellcheck disable=SC2086\n#lol\necho $1" + null $ shellCheck "#shellcheck disable=SC2086\n#lol\necho $1" [] -shellCheck :: String -> [ShellCheckComment] -shellCheck script = +shellCheck :: String -> [AnalysisOption] -> [ShellCheckComment] +shellCheck script options = let (ParseResult result notes) = parseShell "-" script in let allNotes = notes ++ (concat $ maybeToList $ do (tree, posMap) <- result - let list = runAnalytics [] tree + let list = runAnalytics options tree return $ map (noteToParseNote posMap) $ filterByAnnotation tree list ) in diff --git a/shellcheck.hs b/shellcheck.hs index 62042b6f0d9b6c11c147b32bb5b325ebeb26e434..673970b6d8ad2365cd73c30351ce9e967dbb7f12 100644 --- a/shellcheck.hs +++ b/shellcheck.hs @@ -18,10 +18,12 @@ import Control.Exception import Control.Monad import Data.Char +import Data.Maybe import GHC.Exts import GHC.IO.Device import Prelude hiding (catch) import ShellCheck.Simple +import ShellCheck.Analytics import System.Console.GetOpt import System.Directory import System.Environment @@ -37,7 +39,9 @@ options = [ Option ['f'] ["format"] (ReqArg (Flag "format") "FORMAT") "output format", Option ['e'] ["exclude"] - (ReqArg (Flag "exclude") "CODE1,CODE2..") "exclude types of warnings" + (ReqArg (Flag "exclude") "CODE1,CODE2..") "exclude types of warnings", + Option ['s'] ["shell"] + (ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (bash,sh,ksh,zsh)" ] printErr = hPutStrLn stderr @@ -200,7 +204,14 @@ commentsFor options file = liftM (getComments options) $ readContents file getComments options contents = - excludeCodes (getExclusions options) $ shellCheck contents + excludeCodes (getExclusions options) $ shellCheck contents analysisOptions + where + analysisOptions = catMaybes [ shellOption ] + shellOption = do + option <- getOption options "shell" + sh <- shellForExecutable option + return $ ForceShell sh + readContents file = if file == "-" then getContents else readFile file @@ -216,9 +227,9 @@ makeNonVirtual comments contents = real rest (r+1) (v + 8 - (v `mod` 8)) target real (_:rest) r v target = real rest (r+1) (v+1) target -getOption [] _ def = def -getOption ((Flag var val):_) name _ | name == var = val -getOption (_:rest) flag def = getOption rest flag def +getOption [] _ = Nothing +getOption ((Flag var val):_) name | name == var = return val +getOption (_:rest) flag = getOption rest flag getOptions options name = map (\(Flag _ val) -> val) . filter (\(Flag var _) -> var == name) $ options @@ -256,8 +267,9 @@ main = do exitWith code process Nothing = return False -process (Just (options, files)) = - let format = getOption options "format" "tty" in +process (Just (options, files)) = do + verifyShellOption options + let format = fromMaybe "tty" $ getOption options "format" in case Map.lookup format formats of Nothing -> do printErr $ "Unknown format " ++ format @@ -268,3 +280,10 @@ process (Just (options, files)) = Just f -> do f options files +verifyShellOption options = + let shell = getOption options "shell" in + if isNothing shell + then return () + else when (isNothing $ shell >>= shellForExecutable) $ do + printErr $ "Unknown shell: " ++ (fromJust shell) + exitWith supportFailure