shellcheck.hs 9.5 KB
Newer Older
V
Vidar Holen 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
{-
    This file is part of ShellCheck.
    http://www.vidarholen.net/contents/shellcheck

    ShellCheck is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    ShellCheck is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-}
18
import Control.Exception
19
import Control.Monad
20
import Data.Char
21
import Data.Maybe
22 23
import GHC.Exts
import GHC.IO.Device
24
import Prelude hiding (catch)
V
Vidar Holen 已提交
25
import ShellCheck.Data
26
import ShellCheck.Simple
27
import ShellCheck.Analytics
V
Vidar Holen 已提交
28
import System.Console.GetOpt
29 30 31 32
import System.Directory
import System.Environment
import System.Exit
import System.IO
33
import Text.JSON
V
Vidar Holen 已提交
34
import qualified Data.Map as Map
35

V
Vidar Holen 已提交
36 37 38 39 40
data Flag = Flag String String

header = "Usage: shellcheck [OPTIONS...] FILES..."
options = [
    Option ['f'] ["format"]
41 42
        (ReqArg (Flag "format") "FORMAT") "output format",
    Option ['e'] ["exclude"]
43 44
        (ReqArg (Flag "exclude") "CODE1,CODE2..") "exclude types of warnings",
    Option ['s'] ["shell"]
V
Vidar Holen 已提交
45 46 47
        (ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (bash,sh,ksh,zsh)",
    Option ['V'] ["version"]
        (NoArg $ Flag "version" "true") "Print version information"
V
Vidar Holen 已提交
48 49 50 51
    ]

printErr = hPutStrLn stderr

V
Vidar Holen 已提交
52 53 54
syntaxFailure = ExitFailure 3
supportFailure = ExitFailure 4

55 56 57 58 59 60 61 62 63 64
instance JSON ShellCheckComment where
  showJSON c = makeObj [
      ("line", showJSON $ scLine c),
      ("column", showJSON $ scColumn c),
      ("level", showJSON $ scSeverity c),
      ("code", showJSON $ scCode c),
      ("message", showJSON $ scMessage c)
      ]
  readJSON = undefined

V
Vidar Holen 已提交
65 66
parseArguments argv =
    case getOpt Permute options argv of
V
Vidar Holen 已提交
67 68 69
        (opts, files, []) -> do
            verifyOptions opts files
            return $ Just (opts, files)
V
Vidar Holen 已提交
70 71

        (_, _, errors) -> do
72
            printErr $ (concat errors) ++ "\n" ++ usageInfo header options
V
Vidar Holen 已提交
73
            exitWith syntaxFailure
V
Vidar Holen 已提交
74 75

formats = Map.fromList [
76
    ("json", forJson),
77
    ("gcc", forGcc),
78
    ("checkstyle", forCheckstyle),
V
Vidar Holen 已提交
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
    ("tty", forTty)
    ]

forTty options files = do
    output <- mapM doFile files
    return $ and output
  where
    clear = ansi 0
    ansi n = "\x1B[" ++ (show n) ++ "m"

    colorForLevel "error" = 31 -- red
    colorForLevel "warning" = 33 -- yellow
    colorForLevel "info" = 32 -- green
    colorForLevel "style" = 32 -- green
    colorForLevel "message" = 1 -- bold
    colorForLevel "source" = 0 -- none
    colorForLevel _ = 0 -- none

    colorComment level comment = (ansi $ colorForLevel level) ++ comment ++ clear

    doFile path = do
100 101
        contents <- readContents path
        doInput path contents
V
Vidar Holen 已提交
102 103 104 105

    doInput filename contents = do
        let fileLines = lines contents
        let lineCount = length fileLines
106
        let comments = getComments options contents
V
Vidar Holen 已提交
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
        let groups = groupWith scLine comments
        colorFunc <- getColorFunc
        mapM_ (\x -> do
            let lineNum = scLine (head x)
            let line = if lineNum < 1 || lineNum > lineCount
                            then ""
                            else fileLines !! (lineNum - 1)
            putStrLn ""
            putStrLn $ colorFunc "message" ("In " ++ filename ++" line " ++ (show $ lineNum) ++ ":")
            putStrLn (colorFunc "source" line)
            mapM (\c -> putStrLn (colorFunc (scSeverity c) $ cuteIndent c)) x
            putStrLn ""
          ) groups
        return $ null comments

    cuteIndent comment =
        (replicate ((scColumn comment) - 1) ' ') ++ "^-- " ++ (code $ scCode comment) ++ ": " ++ (scMessage comment)

    code code = "SC" ++ (show code)

    getColorFunc = do
        term <- hIsTerminalDevice stdout
        return $ if term then colorComment else const id

131 132
-- This totally ignores the filenames. Fixme?
forJson options files = do
133
    comments <- liftM concat $ mapM (commentsFor options) files
134 135
    putStrLn $ encodeStrict $ comments
    return . null $ comments
136

137
-- Mimic GCC "file:line:col: (error|warning|note): message" format
138 139 140
forGcc options files = do
    files <- mapM process files
    return $ and files
141 142
  where
    process file = do
143
        contents <- readContents file
144
        let comments = makeNonVirtual (getComments options contents) contents
145 146 147 148 149 150 151
        mapM_ (putStrLn . format file) comments
        return $ null comments

    format filename c = concat [
            filename, ":",
            show $ scLine c, ":",
            show $ scColumn c, ": ",
152
            case scSeverity c of
153 154 155 156 157 158 159
                "error" -> "error"
                "warning" -> "warning"
                _ -> "note",
            ": ",
            concat . lines $ scMessage c,
            " [SC", show $ scCode c, "]"
      ]
160 161 162 163 164 165 166 167 168 169

-- Checkstyle compatible output. A bit of a hack to avoid XML dependencies
forCheckstyle options files = do
    putStrLn "<?xml version='1.0' encoding='UTF-8'?>"
    putStrLn "<checkstyle version='4.3'>"
    statuses <- mapM (\x -> process x `catch` report) files
    putStrLn "</checkstyle>"
    return $ and statuses
  where
    process file = do
170
        comments <- commentsFor options file
171 172 173
        putStrLn (formatFile file comments)
        return $ null comments
    report error = do
174
        printErr $ show (error :: SomeException)
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
        return False

    severity "error" = "error"
    severity "warning" = "warning"
    severity _ = "info"
    attr s v = concat [ s, "='", escape v, "' " ]
    escape msg = concatMap escape' msg
    escape' c = if isOk c then [c] else "&#" ++ (show $ ord c) ++ ";"
    isOk x = any ($x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` " ./")]

    formatFile name comments = concat [
        "<file ", attr "name" name, ">\n",
            concatMap format comments,
        "</file>"
        ]

    format c = concat [
        "<error ",
        attr "line" $ show . scLine $ c,
        attr "column" $ show . scColumn $ c,
        attr "severity" $ severity . scSeverity $ c,
        attr "message" $ scMessage c,
        attr "source" $ "ShellCheck.SC" ++ (show $ scCode c),
        "/>\n"
        ]

201 202 203 204
commentsFor options file =
    liftM (getComments options) $ readContents file

getComments options contents =
205 206 207 208 209 210 211 212
    excludeCodes (getExclusions options) $ shellCheck contents analysisOptions
  where
    analysisOptions = catMaybes [ shellOption ]
    shellOption = do
        option <- getOption options "shell"
        sh <- shellForExecutable option
        return $ ForceShell sh

213

214
readContents file = if file == "-" then getContents else readFile file
215

216 217 218 219 220 221 222 223 224 225 226 227
-- Realign comments from a tabstop of 8 to 1
makeNonVirtual comments contents =
    map fix comments
  where
    ls = lines contents
    fix c = c { scColumn = real (ls !! (scLine c - 1)) 0 0 (scColumn c) }
    real _ r v target | target <= v = r
    real [] r v _ = r -- should never happen
    real ('\t':rest) r v target =
        real rest (r+1) (v + 8 - (v `mod` 8)) target
    real (_:rest) r v target = real rest (r+1) (v+1) target

228 229 230
getOption [] _ = Nothing
getOption ((Flag var val):_) name | name == var = return val
getOption (_:rest) flag = getOption rest flag
231

232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
getOptions options name =
    map (\(Flag _ val) -> val) . filter (\(Flag var _) -> var == name) $ options

split char str =
    split' str []
  where
    split' (a:rest) element =
        if a == char
        then (reverse element) : split' rest []
        else split' rest (a:element)
    split' [] element = [reverse element]

getExclusions options =
    let elements = concatMap (split ',') $ getOptions options "exclude"
        clean = dropWhile (not . isDigit)
    in
        map (Prelude.read . clean) elements :: [Int]

excludeCodes codes comments =
    filter (not . hasCode) comments
  where
    hasCode c = scCode c `elem` codes

255 256
main = do
    args <- getArgs
V
Vidar Holen 已提交
257
    parsedArgs <- parseArguments args
258
    code <- do
259
        status <- process parsedArgs
260 261 262 263 264 265
        return $ if status then ExitSuccess else ExitFailure 1
     `catch` return
     `catch` \err -> do
        printErr $ show (err :: SomeException)
        return $ ExitFailure 2
    exitWith code
V
Vidar Holen 已提交
266 267

process Nothing = return False
268 269
process (Just (options, files)) = do
  let format = fromMaybe "tty" $ getOption options "format" in
V
Vidar Holen 已提交
270 271 272 273 274
    case Map.lookup format formats of
        Nothing -> do
            printErr $ "Unknown format " ++ format
            printErr $ "Supported formats:"
            mapM_ (printErr . write) $ Map.keys formats
V
Vidar Holen 已提交
275
            exitWith supportFailure
V
Vidar Holen 已提交
276 277 278
          where write s = "  " ++ s
        Just f -> do
            f options files
279

V
Vidar Holen 已提交
280 281 282 283
verifyOptions opts files = do
    when (isJust $ getOption opts "version") printVersionAndExit

    let shell = getOption opts "shell" in
284 285 286 287 288
        if isNothing shell
        then return ()
        else when (isNothing $ shell >>= shellForExecutable) $ do
                printErr $ "Unknown shell: " ++ (fromJust shell)
                exitWith supportFailure
V
Vidar Holen 已提交
289 290 291 292 293 294 295 296 297 298 299 300

    when (null files) $ do
        printErr "No files specified.\n"
        printErr $ usageInfo header options
        exitWith syntaxFailure

printVersionAndExit = do
    putStrLn $ "ShellCheck - shell script analysis tool"
    putStrLn $ "version: " ++ shellcheckVersion
    putStrLn $ "license: GNU Affero General Public License, version 3"
    putStrLn $ "website: http://www.shellcheck.net"
    exitWith ExitSuccess