packages feed

ShellCheck 0.3.6 → 0.3.7

raw patch · 5 files changed

+188/−71 lines, 5 filesdep +regex-tdfadep −regex-compatdep ~QuickCheckdep ~basedep ~containersPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: regex-tdfa

Dependencies removed: regex-compat

Dependency ranges changed: QuickCheck, base, containers, directory, json, mtl, parsec

API changes (from Hackage documentation)

- ShellCheck.AST: lolHax :: Show a => a -> String
- ShellCheck.Analytics: instance Eq DataSource
- ShellCheck.Analytics: instance Eq DataType
- ShellCheck.Analytics: instance Eq StackData
- ShellCheck.Analytics: instance Eq VariableState
+ ShellCheck.AST: tokenEquals :: Token -> Token -> Bool
+ ShellCheck.Regex: matchAllStrings :: Regex -> String -> [String]
+ ShellCheck.Regex: matchAllSubgroups :: Regex -> String -> [[String]]
+ ShellCheck.Regex: matchRegex :: Regex -> String -> Maybe [String]
+ ShellCheck.Regex: matches :: String -> Regex -> Bool
+ ShellCheck.Regex: mkRegex :: String -> Regex
+ ShellCheck.Regex: subRegex :: Regex -> String -> String -> String

Files

ShellCheck.cabal view
@@ -1,5 +1,5 @@ Name:             ShellCheck-Version:          0.3.6+Version:          0.3.7 Synopsis:         Shell script analysis tool License:          AGPL-3 License-file:     LICENSE@@ -43,7 +43,7 @@       json,       mtl,       parsec,-      regex-compat,+      regex-tdfa,       QuickCheck >= 2.7.4     exposed-modules:       ShellCheck.Analytics@@ -51,6 +51,7 @@       ShellCheck.Data       ShellCheck.Options       ShellCheck.Parser+      ShellCheck.Regex       ShellCheck.Simple     other-modules:       Paths_ShellCheck@@ -64,7 +65,7 @@       json,       mtl,       parsec,-      regex-compat,+      regex-tdfa,       transformers,       QuickCheck >= 2.7.4     main-is: shellcheck.hs@@ -79,7 +80,7 @@       json,       mtl,       parsec,-      regex-compat,+      regex-tdfa,       transformers,       QuickCheck >= 2.7.4     main-is: test/shellcheck.hs
ShellCheck/AST.hs view
@@ -19,7 +19,7 @@  import Control.Monad import Control.Monad.Identity-import qualified Text.Regex as Re+import qualified ShellCheck.Regex as Re  data Id = Id Int deriving (Show, Eq, Ord) @@ -128,11 +128,13 @@ data Annotation = DisableComment Integer deriving (Show, Eq) data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq) --- I apologize for nothing!-lolHax s = Re.subRegex (Re.mkRegex "(Id [0-9]+)") (show s) "(Id 0)"-instance Eq Token where-    (==) a b = lolHax a == lolHax b+-- This is an abomination.+tokenEquals :: Token -> Token -> Bool+tokenEquals a b = kludge a == kludge b+    where kludge s = Re.subRegex (Re.mkRegex "\\(Id [0-9]+\\)") (show s) "(Id 0)" +instance Eq Token where+    (==) = tokenEquals  analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> Token) -> Token -> m Token analyze f g i =
ShellCheck/Analytics.hs view
@@ -15,7 +15,7 @@     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/>. -}-{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TemplateHaskell, FlexibleContexts #-} module ShellCheck.Analytics (AnalysisOptions(..), defaultAnalysisOptions, filterByAnnotation, runAnalytics, shellForExecutable, runTests) where  import Control.Arrow (first)@@ -33,7 +33,7 @@ import ShellCheck.Options import ShellCheck.Data import ShellCheck.Parser hiding (runTests)-import Text.Regex+import ShellCheck.Regex import qualified Data.Map as Map import Test.QuickCheck.All (forAllProperties) import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)@@ -240,12 +240,6 @@  potentially = fromMaybe (return ()) -matchAll re = unfoldr f-  where-    f str = do-        (_, match, rest, _) <- matchRegexAll re str-        return (match, rest)- willSplit x =   case x of     T_DollarBraced {} -> True@@ -280,20 +274,9 @@     suspicious = mkRegex "([A-Za-z1-9])\\*"     contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]" -matches string regex = isJust $ matchRegex regex string- headOrDefault _ (a:_) = a headOrDefault def _ = def -getAllMatches :: Regex -> String -> [[String]]-getAllMatches regex str = fromJust $ f str-  where-    f str = do-        (_, _, rest, groups) <- matchRegexAll regex str-        more <- f rest-        return $ groups : more-      `mappend` return []- isConstant token =     case token of         T_NormalWord _ l   -> all isConstant l@@ -335,16 +318,19 @@ deadSimple _ = []  -- Turn a SimpleCommand foo -avz --bar=baz into args ["a", "v", "z", "bar"]-getFlags (T_SimpleCommand _ _ (_:args)) =-    let textArgs = takeWhile (/= "--") $ map (concat . deadSimple) args in+getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =+    let textArgs = takeWhile (not . stopCondition . snd) $ map (\x -> (x, concat $ deadSimple x)) args in         concatMap flag textArgs   where-    flag ('-':'-':arg) = [ takeWhile (/= '=') arg ]-    flag ('-':args) = map (:[]) args+    flag (x, ('-':'-':arg)) = [ (x, takeWhile (/= '=') arg) ]+    flag (x, ('-':args)) = map (\v -> (x, [v])) args     flag _ = [] -getFlags _ = error "Internal shellcheck error, please report! (getFlags on non-command)"+getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)" +getAllFlags = getFlagsUntil (== "--")+getLeadingFlags = getFlagsUntil (not . ("-" `isPrefixOf`))+ (!!!) list i =     case drop i list of         [] -> Nothing@@ -413,14 +399,19 @@             ["sed", "-e", v] -> checkIn v             _ -> return ()   where-    sedRe = mkRegex "^s(.)(.*)\\1(.*)\\1g?$"+    -- This should have used backreferences, but TDFA doesn't support them+    sedRe = mkRegex "^s(.)([^\n]*)g?$"+    isSimpleSed s = fromMaybe False $ do+        [first,rest] <- matchRegex sedRe s+        let delimiters = filter (== (first !! 0)) rest+        guard $ length delimiters == 2+        return True+     acmd = deadSimple a     bcmd = deadSimple b     checkIn s =-        case matchRegex sedRe s of-                Just _ -> style id 2001-                    "See if you can use ${variable//search/replace} instead."-                _        -> return ()+        when (isSimpleSed s) $+            style id 2001 "See if you can use ${variable//search/replace} instead." checkEchoSed _ _ = return ()  prop_checkPipedAssignment1 = verify checkPipedAssignment "A=ls | grep foo"@@ -642,6 +633,13 @@ prop_checkBashisms17= verify checkBashisms "echo $((RANDOM%6+1))" prop_checkBashisms18= verify checkBashisms "foo &> /dev/null" prop_checkBashisms19= verify checkBashisms "foo > file*.txt"+prop_checkBashisms20= verify checkBashisms "read -ra foo"+prop_checkBashisms21= verify checkBashisms "[ -a foo ]"+prop_checkBashisms22= verifyNot checkBashisms "[ foo -a bar ]"+prop_checkBashisms23= verify checkBashisms "trap mything err int"+prop_checkBashisms24= verifyNot checkBashisms "trap mything int term"+prop_checkBashisms25= verify checkBashisms "cat < /dev/tcp/host/123"+prop_checkBashisms26= verify checkBashisms "trap mything ERR SIGTERM" checkBashisms _ = bashism   where     errMsg id s = err id 2040 $ "In sh, " ++ s ++ " not supported, even when sh is actually bash."@@ -660,24 +658,42 @@     bashism (TC_Binary id SingleBracket op _ _)         | op `elem` [ "-nt", "-ef", "\\<", "\\>", "==" ] =             warnMsg id $ op ++ " is"+    bashism (TC_Unary id _ "-a" _) =+            warnMsg id "unary -a in place of -e is"     bashism (TA_Unary id op _)         | op `elem` [ "|++", "|--", "++|", "--|"] =             warnMsg id $ filter (/= '|') op ++ " is"-    bashism t@(T_SimpleCommand id _ _)-        | t `isCommand` "source" =-            warnMsg id "'source' in place of '.' is"+    bashism (TA_Binary id "**" _ _) = warnMsg id "exponentials are"     bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&> is"-    bashism t@(TA_Expansion id _) | getLiteralString t == Just "RANDOM" =-        warnMsg id "RANDOM is"-    bashism t@(T_DollarBraced id _) | getBracedReference (bracedString t) == "RANDOM" =-        warnMsg id "$RANDOM is"-    bashism (T_DollarBraced id token) =+    bashism (T_IoFile id _ word) | isNetworked =+            warnMsg id "/dev/{tcp,udp} is"+        where+            file = onlyLiteralString word+            isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"]++    bashism t@(TA_Expansion id _) | isBashism =+        warnMsg id $ fromJust str ++ " is"+      where+        str = getLiteralString t+        isBashism = isJust str && fromJust str `elem` bashVars+    bashism t@(T_DollarBraced id token) = do         mapM_ check expansion+        when (var `elem` bashVars) $ warnMsg id $ var ++ " is"       where         str = concat $ deadSimple token+        var = getBracedReference (bracedString token)         check (regex, feature) =             when (isJust $ matchRegex regex str) $ warnMsg id feature +    bashism t@(T_Pipe id "|&") =+        warnMsg id "|& in place of 2>&1 | is"+    bashism (T_Array id _) =+        warnMsg id "arrays are"+    bashism (T_IoFile id _ t) | isGlob t =+        warnMsg id "redirecting to/from globs is"+    bashism (T_CoProc id _ _) =+        warnMsg id "coproc is"+     bashism t@(T_SimpleCommand _ _ (cmd:arg:_))         | t `isCommand` "echo" && "-" `isPrefixOf` argString =             unless ("--" `isPrefixOf` argString) $ -- echo "-------"@@ -688,15 +704,39 @@             warnMsg (getId arg) "exec flags are"     bashism t@(T_SimpleCommand id _ _)         | t `isCommand` "let" = warnMsg id "'let' is"-    bashism t@(T_Pipe id "|&") =-        warnMsg id "|& in place of 2>&1 | is"-    bashism (T_Array id _) =-        warnMsg id "arrays are"-    bashism (T_IoFile id _ t) | isGlob t =-        warnMsg id "redirecting to/from globs is"-    bashism (T_CoProc id _ _) =-        warnMsg id "coproc is" +    bashism t@(T_SimpleCommand id _ (cmd:rest)) =+        let name = fromMaybe "" $ getCommandName t+            flags = getLeadingFlags t+        in do+            when (name `elem` bashCommands) $ warnMsg id $ "'" ++ name ++ "' is"+            potentially $ do+                allowed <- Map.lookup name allowedFlags+                (word, flag) <- listToMaybe $ filter (\x -> snd x `notElem` allowed) flags+                return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is"++            when (name == "source") $ warnMsg id "'source' in place of '.' is"+            when (name == "trap") $+                let+                    check token = potentially $ do+                        word <- liftM (map toLower) $ getLiteralString token+                        guard $ word `elem` ["err", "debug", "return"]+                        return $ warnMsg (getId token) $ "trapping " ++ word ++ " is"+                in+                    mapM_ check (reverse rest)+      where+        bashCommands = [+            "let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",+            "enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend", "type",+            "typeset"+            ]+        allowedFlags = Map.fromList [+            ("read", ["r"]),+            ("ulimit", ["f"]),+            ("echo", []),+            ("exec", [])+            ]+     bashism _ = return ()      varChars="_0-9a-zA-Z"@@ -705,9 +745,12 @@         (re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"),         (re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),         (re $ "^[" ++ varChars ++ "]+:[^-=?+]", "string indexing is"),-        (re $ "^[" ++ varChars ++ "]+(\\[.*\\])?/", "string replacement is"),-        (re "^RANDOM$", "$RANDOM is")+        (re $ "^[" ++ varChars ++ "]+(\\[.*\\])?/", "string replacement is")         ]+    bashVars = [+        "RANDOM", "LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",+        "DIRSTACK", "EUID", "UID", "SECONDS", "SHLVL", "PIPESTATUS", "SHELLOPTS"+        ]  prop_checkForInQuoted = verify checkForInQuoted "for f in \"$(ls)\"; do echo foo; done" prop_checkForInQuoted2 = verifyNot checkForInQuoted "for f in \"$@\"; do echo foo; done"@@ -2017,15 +2060,15 @@     -- (Base expression, specific position, var name, assigned values)     | Assignment (Token, Token, String, DataType)     | Reference (Token, Token, String)-  deriving (Show, Eq)+  deriving (Show)  data DataType = DataString DataSource | DataArray DataSource-  deriving (Show, Eq)+  deriving (Show)  data DataSource = SourceFrom [Token] | SourceExternal | SourceDeclaration-  deriving (Show, Eq)+  deriving (Show) -data VariableState = Dead Token String | Alive deriving (Show, Eq)+data VariableState = Dead Token String | Alive deriving (Show)  dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v] @@ -2111,7 +2154,7 @@     getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]     getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)]     getReference _ = []-    flags = getFlags base+    flags = map snd $ getAllFlags base  getReferencedVariableCommand _ = [] @@ -2144,7 +2187,7 @@          _ -> []   where-    flags = getFlags base+    flags = map snd $ getAllFlags base     stripEquals s = let rest = dropWhile (/= '=') s in         if rest == "" then "" else tail rest     stripEqualsFrom (T_NormalWord id1 (T_Literal id2 s:rs)) =@@ -2220,10 +2263,11 @@     getSpecial _ = fail "empty"  getIndexReferences s = fromMaybe [] $ do-    (_, index, _, _) <- matchRegexAll re s-    return $ matchAll variableNameRegex index+    match <- matchRegex re s+    index <- match !!! 0+    return $ matchAllStrings variableNameRegex index   where-    re = mkRegex "\\[.*\\]"+    re = mkRegex "(\\[.*\\])"  getReferencedVariables t =     case t of@@ -2263,7 +2307,7 @@ prop_getVariablesFromLiteral1 =     getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"] getVariablesFromLiteral string =-    map (!! 0) $ getAllMatches variableRegex string+    map (!! 0) $ matchAllSubgroups variableRegex string   where     variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)" @@ -3107,9 +3151,7 @@     mapM_ checkList $ getCommandSequences t   where     checkList list =-        mapM_ checkGroup groups-      where-        groups = groupWith (liftM fst) $ map getTarget list+        mapM_ checkGroup (groupWith (liftM fst) $ map getTarget list)     checkGroup (f:_:_:_) | isJust f =         style (snd $ fromJust f) 2129             "Consider using { cmd1; cmd2; } >> file instead of individual redirects."@@ -3256,7 +3298,7 @@ checkMaskedReturns _ t@(T_SimpleCommand id _ (cmd:rest)) = potentially $ do     name <- getCommandName t     guard $ name `elem` ["declare", "export"]-        || name == "local" && "r" `notElem` getFlags t+        || name == "local" && "r" `notElem` (map snd $ getAllFlags t)     return $ mapM_ checkArgs rest   where     checkArgs (T_Assignment id _ _ _ word) | any hasReturn $ getWordParts word =
ShellCheck/Parser.hs view
@@ -15,7 +15,7 @@     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/>. -}-{-# LANGUAGE NoMonomorphismRestriction, TemplateHaskell #-}+{-# LANGUAGE NoMonomorphismRestriction, TemplateHaskell, FlexibleContexts #-} module ShellCheck.Parser (Note(..), Severity(..), parseShell, ParseResult(..), ParseNote(..), sortNotes, noteToParseNote, runTests, readScript) where  import ShellCheck.AST@@ -2095,6 +2095,7 @@     badShells = [         "awk",         "csh",+        "expect",         "perl",         "python",         "ruby",
+ ShellCheck/Regex.hs view
@@ -0,0 +1,71 @@+{-+    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/>.+-}+{-# LANGUAGE FlexibleContexts #-}++-- Basically Text.Regex based on regex-tdfa instead of the buggy regex-posix.+module ShellCheck.Regex where++import Data.List+import Data.Maybe+import Control.Monad+import Text.Regex.TDFA++-- Precompile the regex+mkRegex :: String -> Regex+mkRegex str =+    let make :: RegexMaker Regex CompOption ExecOption String => String -> Regex+        make = makeRegex+    in+        make str++-- Does the regex match?+matches :: String -> Regex -> Bool+matches = flip match++-- Get all subgroups of the first match+matchRegex :: Regex -> String -> Maybe [String]+matchRegex re str = do+    (_, _, _, groups) <- matchM re str :: Maybe (String,String,String,[String])+    return groups++-- Get all full matches+matchAllStrings :: Regex -> String -> [String]+matchAllStrings re = unfoldr f+  where+    f :: String -> Maybe (String, String)+    f str = do+        (_, match, rest, _) <- matchM re str :: Maybe (String, String, String, [String])+        return (match, rest)++-- Get all subgroups from all matches+matchAllSubgroups :: Regex -> String -> [[String]]+matchAllSubgroups re = unfoldr f+  where+    f :: String -> Maybe ([String], String)+    f str = do+        (_, _, rest, groups) <- matchM re str :: Maybe (String, String, String, [String])+        return (groups, rest)++-- Replace regex in input with string+subRegex :: Regex -> String -> String -> String+subRegex re input replacement = f input+  where+    f str = fromMaybe str $ do+        (before, match, after) <- matchM re str :: Maybe (String, String, String)+        when (null match) $ error ("Internal error: substituted empty in " ++ str)+        return $ before ++ replacement ++ f after