shellcheck.hs 11.4 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 21
import Control.Monad.Trans
import Control.Monad.Trans.Error
V
Vidar Holen 已提交
22
import Control.Monad.Trans.List
23
import Data.Char
24
import Data.List
25
import Data.Maybe
26
import Data.Monoid
27 28
import GHC.Exts
import GHC.IO.Device
29
import Prelude hiding (catch)
V
Vidar Holen 已提交
30
import ShellCheck.Data
V
Vidar Holen 已提交
31
import ShellCheck.Options
32
import ShellCheck.Simple
33
import ShellCheck.Analytics
V
Vidar Holen 已提交
34
import System.Console.GetOpt
35 36 37
import System.Directory
import System.Environment
import System.Exit
38
import System.Info
39
import System.IO
40
import Text.JSON
V
Vidar Holen 已提交
41
import qualified Data.Map as Map
42

V
Vidar Holen 已提交
43
data Flag = Flag String String
44 45
data Status = NoProblems | SomeProblems | BadInput | SupportFailure | SyntaxFailure | RuntimeException deriving (Ord, Eq)

V
Vidar Holen 已提交
46 47
data JsonComment = JsonComment FilePath ShellCheckComment

48 49 50 51 52 53
instance Error Status where
    noMsg = RuntimeException

instance Monoid Status where
    mempty = NoProblems
    mappend = max
V
Vidar Holen 已提交
54 55 56

header = "Usage: shellcheck [OPTIONS...] FILES..."
options = [
57
    Option "e" ["exclude"]
58
        (ReqArg (Flag "exclude") "CODE1,CODE2..") "exclude types of warnings",
V
Vidar Holen 已提交
59 60
    Option "f" ["format"]
        (ReqArg (Flag "format") "FORMAT") "output format",
61
    Option "s" ["shell"]
V
Vidar Holen 已提交
62
        (ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (bash,sh,ksh)",
63
    Option "V" ["version"]
V
Vidar Holen 已提交
64
        (NoArg $ Flag "version" "true") "Print version information"
V
Vidar Holen 已提交
65 66 67 68
    ]

printErr = hPutStrLn stderr

V
Vidar Holen 已提交
69

V
Vidar Holen 已提交
70 71 72
instance JSON (JsonComment) where
  showJSON (JsonComment filename c) = makeObj [
      ("file", showJSON $ filename),
73 74 75 76 77 78 79 80
      ("line", showJSON $ scLine c),
      ("column", showJSON $ scColumn c),
      ("level", showJSON $ scSeverity c),
      ("code", showJSON $ scCode c),
      ("message", showJSON $ scMessage c)
      ]
  readJSON = undefined

81
parseArguments :: [String] -> ErrorT Status IO ([Flag], [FilePath])
V
Vidar Holen 已提交
82 83
parseArguments argv =
    case getOpt Permute options argv of
V
Vidar Holen 已提交
84
        (opts, files, []) -> return (opts, files)
V
Vidar Holen 已提交
85
        (_, _, errors) -> do
86 87
            liftIO . printErr $ concat errors ++ "\n" ++ usageInfo header options
            throwError SyntaxFailure
V
Vidar Holen 已提交
88

V
Vidar Holen 已提交
89
formats :: Map.Map String (AnalysisOptions -> [FilePath] -> IO Status)
V
Vidar Holen 已提交
90
formats = Map.fromList [
91
    ("json", forJson),
92
    ("gcc", forGcc),
93
    ("checkstyle", forCheckstyle),
V
Vidar Holen 已提交
94 95 96
    ("tty", forTty)
    ]

97 98 99
toStatus = liftM (either id (const NoProblems)) . runErrorT

catchExceptions :: IO Status -> IO Status
V
Vidar Holen 已提交
100
catchExceptions action = action -- action `catch` handler
101 102 103 104 105 106 107
  where
    handler err = do
        printErr $ show (err :: SomeException)
        return RuntimeException

checkComments comments = if null comments then NoProblems else SomeProblems

V
Vidar Holen 已提交
108
forTty :: AnalysisOptions -> [FilePath] -> IO Status
V
Vidar Holen 已提交
109 110
forTty options files = do
    output <- mapM doFile files
111
    return $ mconcat output
V
Vidar Holen 已提交
112 113
  where
    clear = ansi 0
V
Vidar Holen 已提交
114
    ansi n = "\x1B[" ++ show n ++ "m"
V
Vidar Holen 已提交
115 116 117 118 119 120 121 122 123

    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

V
Vidar Holen 已提交
124 125
    colorComment level comment =
        ansi (colorForLevel level) ++ comment ++ clear
V
Vidar Holen 已提交
126

127
    doFile path = catchExceptions $ do
128 129
        contents <- readContents path
        doInput path contents
V
Vidar Holen 已提交
130 131 132 133

    doInput filename contents = do
        let fileLines = lines contents
        let lineCount = length fileLines
134
        let comments = getComments options contents
V
Vidar Holen 已提交
135 136 137 138 139 140 141 142
        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 ""
V
Vidar Holen 已提交
143 144
            putStrLn $ colorFunc "message"
                ("In " ++ filename ++" line " ++ show lineNum ++ ":")
V
Vidar Holen 已提交
145
            putStrLn (colorFunc "source" line)
V
Vidar Holen 已提交
146
            mapM_ (\c -> putStrLn (colorFunc (scSeverity c) $ cuteIndent c)) x
V
Vidar Holen 已提交
147 148
            putStrLn ""
          ) groups
149
        return . checkComments $ comments
V
Vidar Holen 已提交
150 151

    cuteIndent comment =
V
Vidar Holen 已提交
152 153
        replicate (scColumn comment - 1) ' ' ++
            "^-- " ++ code (scCode comment) ++ ": " ++ scMessage comment
V
Vidar Holen 已提交
154

155
    code code = "SC" ++ show code
V
Vidar Holen 已提交
156 157 158

    getColorFunc = do
        term <- hIsTerminalDevice stdout
159 160
        let windows = "mingw" `isPrefixOf` os
        return $ if term && not windows then colorComment else const id
V
Vidar Holen 已提交
161

V
Vidar Holen 已提交
162
forJson :: AnalysisOptions -> [FilePath] -> IO Status
163
forJson options files = catchExceptions $ do
V
Vidar Holen 已提交
164 165 166 167
    comments <- runListT $ do
        file <- ListT $ return files
        comment <- ListT $ commentsFor options file
        return $ JsonComment file comment
V
Vidar Holen 已提交
168
    putStrLn $ encodeStrict comments
169
    return $ checkComments comments
170

171
-- Mimic GCC "file:line:col: (error|warning|note): message" format
V
Vidar Holen 已提交
172
forGcc :: AnalysisOptions -> [FilePath] -> IO Status
173 174
forGcc options files = do
    files <- mapM process files
175
    return $ mconcat files
176
  where
177
    process file = catchExceptions $ do
178
        contents <- readContents file
179
        let comments = makeNonVirtual (getComments options contents) contents
180
        mapM_ (putStrLn . format file) comments
181
        return $ checkComments comments
182 183 184 185 186

    format filename c = concat [
            filename, ":",
            show $ scLine c, ":",
            show $ scColumn c, ": ",
187
            case scSeverity c of
188 189 190 191 192 193 194
                "error" -> "error"
                "warning" -> "warning"
                _ -> "note",
            ": ",
            concat . lines $ scMessage c,
            " [SC", show $ scCode c, "]"
      ]
195 196

-- Checkstyle compatible output. A bit of a hack to avoid XML dependencies
V
Vidar Holen 已提交
197
forCheckstyle :: AnalysisOptions -> [FilePath] -> IO Status
198 199 200
forCheckstyle options files = do
    putStrLn "<?xml version='1.0' encoding='UTF-8'?>"
    putStrLn "<checkstyle version='4.3'>"
201
    statuses <- mapM process files
202
    putStrLn "</checkstyle>"
203
    return $ mconcat statuses
204
  where
205
    process file = catchExceptions $ do
206
        comments <- commentsFor options file
207
        putStrLn (formatFile file comments)
208
        return $ checkComments comments
209 210 211 212 213

    severity "error" = "error"
    severity "warning" = "warning"
    severity _ = "info"
    attr s v = concat [ s, "='", escape v, "' " ]
V
Vidar Holen 已提交
214 215
    escape = concatMap escape'
    escape' c = if isOk c then [c] else "&#" ++ show (ord c) ++ ";"
216 217 218 219 220 221 222 223 224 225 226 227 228 229
    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,
230
        attr "source" $ "ShellCheck.SC" ++ show (scCode c),
231 232 233
        "/>\n"
        ]

234
commentsFor options file = liftM (getComments options) $ readContents file
235

V
Vidar Holen 已提交
236
getComments = shellCheck
237

238 239 240 241 242
readContents :: FilePath -> IO String
readContents file =
    if file == "-"
    then getContents
    else readFile file
243

244 245 246 247 248
-- Realign comments from a tabstop of 8 to 1
makeNonVirtual comments contents =
    map fix comments
  where
    ls = lines contents
249 250 251 252 253 254
    fix c = c {
        scColumn =
            if scLine c > 0 && scLine c <= length ls
            then real (ls !! (scLine c - 1)) 0 0 (scColumn c)
            else scColumn c
    }
255 256 257 258 259 260
    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

261
getOption [] _ = Nothing
V
Vidar Holen 已提交
262
getOption (Flag var val:_) name | name == var = return val
263
getOption (_:rest) flag = getOption rest flag
264

265 266 267 268 269 270 271 272
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
273
        then reverse element : split' rest []
274 275 276 277 278 279 280 281 282
        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]

V
Vidar Holen 已提交
283 284
excludeCodes codes =
    filter (not . hasCode)
285 286 287
  where
    hasCode c = scCode c `elem` codes

288 289
main = do
    args <- getArgs
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
    status <- toStatus $ do
        (flags, files) <- parseArguments args
        process flags files
    exitWith $ statusToCode status

statusToCode status =
    case status of
        NoProblems -> ExitSuccess
        SomeProblems -> ExitFailure 1
        BadInput -> ExitFailure 5
        SyntaxFailure -> ExitFailure 3
        SupportFailure -> ExitFailure 4
        RuntimeException -> ExitFailure 2

process :: [Flag] -> [FilePath] -> ErrorT Status IO ()
V
Vidar Holen 已提交
305 306 307 308
process flags files = do
    options <- foldM (flip parseOption) defaultAnalysisOptions flags
    verifyFiles files
    let format = fromMaybe "tty" $ getOption flags "format"
V
Vidar Holen 已提交
309 310
    case Map.lookup format formats of
        Nothing -> do
311 312 313 314 315
            liftIO $ do
                printErr $ "Unknown format " ++ format
                printErr "Supported formats:"
                mapM_ (printErr . write) $ Map.keys formats
            throwError SupportFailure
V
Vidar Holen 已提交
316
          where write s = "  " ++ s
317
        Just f -> ErrorT $ liftM Left $ f options files
318

V
Vidar Holen 已提交
319 320 321 322 323 324
parseOption flag options =
    case flag of
        Flag "shell" str ->
                fromMaybe (die $ "Unknown shell: " ++ str) $ do
                    shell <- shellForExecutable str
                    return $ return options { optionShellType = Just shell }
V
Vidar Holen 已提交
325

V
Vidar Holen 已提交
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345
        Flag "exclude" str -> do
            new <- mapM parseNum $ split ',' str
            let old = optionExcludes options
            return options { optionExcludes = new ++ old }

        Flag "version" _ -> do
            liftIO printVersion
            throwError NoProblems

        _ -> return options
  where
    die s = do
        liftIO $ printErr s
        throwError SupportFailure
    parseNum ('S':'C':str) = parseNum str
    parseNum num = do
        unless (all isDigit num) $ do
            liftIO . printErr $ "Bad exclusion: " ++ num
            throwError SyntaxFailure
        return (Prelude.read num :: Integer)
V
Vidar Holen 已提交
346

V
Vidar Holen 已提交
347
verifyFiles files =
V
Vidar Holen 已提交
348
    when (null files) $ do
349 350 351
        liftIO $ printErr "No files specified.\n"
        liftIO $ printErr $ usageInfo header options
        throwError SyntaxFailure
V
Vidar Holen 已提交
352

353 354
printVersion = do
    putStrLn   "ShellCheck - shell script analysis tool"
V
Vidar Holen 已提交
355
    putStrLn $ "version: " ++ shellcheckVersion
356 357
    putStrLn   "license: GNU Affero General Public License, version 3"
    putStrLn   "website: http://www.shellcheck.net"