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 +5/−4
- ShellCheck/AST.hs +7/−5
- ShellCheck/Analytics.hs +103/−61
- ShellCheck/Parser.hs +2/−1
- ShellCheck/Regex.hs +71/−0
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