diff --git a/ShellCheck/Analytics.hs b/ShellCheck/Analytics.hs index 5476eea..8aa4deb 100644 --- a/ShellCheck/Analytics.hs +++ b/ShellCheck/Analytics.hs @@ -31,6 +31,8 @@ basicChecks = [ ,checkStderrRedirect ,checkMissingPositionalQuotes ,checkSingleQuotedVariables + ,checkUnquotedZN + ,checkNumberComparisons ] modifyMap = modify @@ -197,6 +199,32 @@ checkSingleQuotedVariables _ = return () checkSingleQuotedVariablesRe = mkRegex "(\\$[0-9a-zA-Z_]+)" +prop_checkUnquotedZN = verify checkUnquotedZN "if [ -z $foo ]; then echo cow; fi" +prop_checkUnquotedZN2 = verify checkUnquotedZN "[ -n $cow ]" +prop_checkUnquotedZN3 = verifyNot checkUnquotedZN "[[ -z $foo ]] && echo cow" +checkUnquotedZN (T_Condition _ SingleBracket (TC_Unary _ SingleBracket op (T_NormalWord id [t]))) | ( op == "-z" || op == "-n" ) && willSplit t = + addNoteFor id $ Note ErrorC "Always true because you failed to quote. Use [[ ]] instead." +checkUnquotedZN _ = return () + +prop_checkNumberComparisons1 = verify checkNumberComparisons "[[ $foo < 3 ]]" +prop_checkNumberComparisons2 = verify checkNumberComparisons "[[ 0 >= $(cmd) ]]" +prop_checkNumberComparisons3 = verifyNot checkNumberComparisons "[[ $foo ]] > 3" +prop_checkNumberComparisons4 = verify checkNumberComparisons "[ $foo > $bar ]" +prop_checkNumberComparisons5 = verify checkNumberComparisons "until [ $n <= $z ]; do echo foo; done" +checkNumberComparisons (TC_Binary id typ op lhs rhs) + | op `elem` ["<", ">", "<=", ">="] = do + when (isNum lhs || isNum rhs) $ addNoteFor id $ Note ErrorC $ "\"" ++ op ++ "\" is for string comparisons. Use " ++ (eqv op) + when (typ == SingleBracket) $ addNoteFor id $ Note ErrorC $ "Can't use " ++ op ++" in [ ]. Use [[ ]]." + where + isNum t = case deadSimple t of [v] -> all isDigit v + _ -> False + eqv "<" = "-lt" + eqv ">" = "-gt" + eqv "<=" = "-le" + eqv ">=" = "-ge" + eqv _ = "the numerical equivalent" +checkNumberComparisons _ = return () + allModifiedVariables t = snd $ runState (doAnalysis (\x -> modify $ (++) (getModifiedVariables t)) t) [] --- Subshell detection diff --git a/ShellCheck/Parser.hs b/ShellCheck/Parser.hs index 53a46d2..b2fb2e4 100644 --- a/ShellCheck/Parser.hs +++ b/ShellCheck/Parser.hs @@ -1,6 +1,6 @@ {-# LANGUAGE NoMonomorphismRestriction #-} -module ShellCheck.Parser (Token(..), Id(..), Note(..), Severity(..), parseShell, ParseResult(..), ParseNote(..), notesFromMap, Metadata(..), doAnalysis, doStackAnalysis, doTransform, sortNotes) where +module ShellCheck.Parser (Token(..), ConditionType(..), Id(..), Note(..), Severity(..), parseShell, ParseResult(..), ParseNote(..), notesFromMap, Metadata(..), doAnalysis, doStackAnalysis, doTransform, sortNotes) where import Text.Parsec import Debug.Trace @@ -153,14 +153,15 @@ readConditionContents single = do parseProblemAt pos WarningC "To check a command, skip [] and just do 'if foo | grep bar; then'.") where + typ = if single then SingleBracket else DoubleBracket readCondBinaryOp = try $ do - op <- choice $ (map tryOp ["-nt", "-ot", "-ef", "=", "==", "!=", "<", ">", "-eq", "-ne", "-lt", "-le", "-gt", "-ge", "=~"]) + op <- choice $ (map tryOp ["-nt", "-ot", "-ef", "=", "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le", "-gt", "-ge", "=~", ">", "<"]) hardCondSpacing return op where tryOp s = try $ do id <- getNextId string s - return $ TC_Binary id s + return $ TC_Binary id typ s readCondUnaryExp = do op <- readCondUnaryOp @@ -181,7 +182,7 @@ readConditionContents single = do where tryOp s = try $ do id <- getNextId string s - return $ TC_Unary id s + return $ TC_Unary id typ s readCondWord = do notFollowedBy (try (spacing >> (string "]"))) @@ -205,7 +206,8 @@ readConditionContents single = do when (single && x == "&&") $ addNoteFor id $ Note ErrorC "You can't use && inside [..]. Use [[..]] instead." when (not single && x == "-a") $ addNoteFor id $ Note ErrorC "In [[..]], use && instead of -a" softCondSpacing - return $ TC_And id x + return $ TC_And id typ x + readCondOrOp = do id <- getNextId @@ -213,7 +215,7 @@ readConditionContents single = do when (single && x == "||") $ addNoteFor id $ Note ErrorC "You can't use || inside [..]. Use [[..]] instead." when (not single && x == "-o") $ addNoteFor id $ Note ErrorC "In [[..]], use && instead of -o" softCondSpacing - return $ TC_Or id x + return $ TC_Or id typ x readCondNoaryOrBinary = do id <- getNextId @@ -229,7 +231,7 @@ readConditionContents single = do op <- readCondBinaryOp y <- readCondWord <|> ( (parseProblemAt pos ErrorC $ "Expected another argument for this operator") >> mzero) return (x `op` y) - ) <|> (return $ TC_Noary id x) + ) <|> (return $ TC_Noary id typ x) readCondGroup = do id <- getNextId @@ -242,7 +244,7 @@ readConditionContents single = do rparen <- string ")" <|> string "\\)" when (single && rparen == ")") $ parseProblemAt pos ErrorC "In [..] you have to escape (). Use [[..]] instead." when (isEscaped lparen `xor` isEscaped rparen) $ parseProblemAt pos ErrorC "Did you just escape one half of () but not the other?" - return $ TC_Group id x + return $ TC_Group id typ x where isEscaped ('\\':_) = True isEscaped _ = False @@ -254,7 +256,7 @@ readConditionContents single = do char '!' softCondSpacing expr <- readCondExpr - return $ TC_Not id expr + return $ TC_Not id typ expr readCondExpr = readCondGroup <|> readCondUnaryExp <|> readCondNoaryOrBinary @@ -278,7 +280,7 @@ readCondition = do close <- (try $ string "]]") <|> (string "]") when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC "Did you mean ]] ?" when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC "Did you mean [[ ?" - return $ T_Condition id condition + return $ T_Condition id (if single then SingleBracket else DoubleBracket) condition hardCondSpacing = condSpacingMsg False "You need a space here" @@ -290,9 +292,11 @@ condSpacingMsg soft msg = do -- Horrifying AST data Token = T_AND_IF Id | T_OR_IF Id | T_DSEMI Id | T_Semi Id | T_DLESS Id | T_DGREAT Id | T_LESSAND Id | T_GREATAND Id | T_LESSGREAT Id | T_DLESSDASH Id | T_CLOBBER Id | T_If Id | T_Then Id | T_Else Id | T_Elif Id | T_Fi Id | T_Do Id | T_Done Id | T_Case Id | T_Esac Id | T_While Id | T_Until Id | T_For Id | T_Lbrace Id | T_Rbrace Id | T_Lparen Id | T_Rparen Id | T_Bang Id | T_In Id | T_NEWLINE Id | T_EOF Id | T_Less Id | T_Greater Id | T_SingleQuoted Id String | T_Literal Id String | T_NormalWord Id [Token] | T_DoubleQuoted Id [Token] | T_DollarExpansion Id [Token] | T_DollarBraced Id String | T_DollarArithmetic Id String | T_BraceExpansion Id String | T_IoFile Id Token Token | T_HereDoc Id Bool Bool String | T_HereString Id Token | T_FdRedirect Id String Token | T_Assignment Id String Token | T_Array Id [Token] | T_Redirecting Id [Token] Token | T_SimpleCommand Id [Token] [Token] | T_Pipeline Id [Token] | T_Banged Id Token | T_AndIf Id (Token) (Token) | T_OrIf Id (Token) (Token) | T_Backgrounded Id Token | T_IfExpression Id [([Token],[Token])] [Token] | T_Subshell Id [Token] | T_BraceGroup Id [Token] | T_WhileExpression Id [Token] [Token] | T_UntilExpression Id [Token] [Token] | T_ForIn Id String [Token] [Token] | T_CaseExpression Id Token [([Token],[Token])] | T_Function Id String Token | T_Arithmetic Id String | T_Script Id [Token] | - T_Condition Id Token | TC_And Id String Token Token | TC_Or Id String Token Token | TC_Not Id Token | TC_Group Id Token | TC_Binary Id String Token Token | TC_Unary Id String Token | TC_Noary Id Token + T_Condition Id ConditionType Token | TC_And Id ConditionType String Token Token | TC_Or Id ConditionType String Token Token | TC_Not Id ConditionType Token | TC_Group Id ConditionType Token | TC_Binary Id ConditionType String Token Token | TC_Unary Id ConditionType String Token | TC_Noary Id ConditionType Token deriving (Show) +data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq) + analyzeScopes f g i = mapM (analyze f g i) analyze f g i s@(T_NormalWord id list) = do @@ -457,50 +461,50 @@ analyze f g i s@(T_Function id name body) = do g s return . i $ T_Function id name a -analyze f g i s@(T_Condition id token) = do +analyze f g i s@(T_Condition id typ token) = do f s a <- analyze f g i token g s - return . i $ T_Condition id a + return . i $ T_Condition id typ a -analyze f g i s@(TC_And id str t1 t2) = do +analyze f g i s@(TC_And id typ str t1 t2) = do f s a <- analyze f g i t1 b <- analyze f g i t2 g s - return . i $ TC_And id str a b + return . i $ TC_And id typ str a b -analyze f g i s@(TC_Or id str t1 t2) = do +analyze f g i s@(TC_Or id typ str t1 t2) = do f s a <- analyze f g i t1 b <- analyze f g i t2 g s - return . i $ TC_Or id str a b + return . i $ TC_Or id typ str a b -analyze f g i s@(TC_Group id token) = do +analyze f g i s@(TC_Group id typ token) = do f s a <- analyze f g i token g s - return . i $ TC_Group id a + return . i $ TC_Group id typ a -analyze f g i s@(TC_Binary id op lhs rhs) = do +analyze f g i s@(TC_Binary id typ op lhs rhs) = do f s a <- analyze f g i lhs b <- analyze f g i rhs g s - return . i $ TC_Binary id op a b + return . i $ TC_Binary id typ op a b -analyze f g i s@(TC_Unary id op token) = do +analyze f g i s@(TC_Unary id typ op token) = do f s a <- analyze f g i token g s - return . i $ TC_Unary id op a + return . i $ TC_Unary id typ op a -analyze f g i s@(TC_Noary id token) = do +analyze f g i s@(TC_Noary id typ token) = do f s a <- analyze f g i token g s - return . i $ TC_Noary id a + return . i $ TC_Noary id typ a analyze f g i t = do f t diff --git a/badcase/singlebracketcompare b/badcase/singlebracketcompare new file mode 100644 index 0000000..7f98c9c --- /dev/null +++ b/badcase/singlebracketcompare @@ -0,0 +1 @@ +until [ $var > $foo ]; do var=$(cow); done diff --git a/shellcheck.hs b/shellcheck.hs index 0b3fe2b..d73feba 100644 --- a/shellcheck.hs +++ b/shellcheck.hs @@ -13,7 +13,7 @@ ansi n = "\x1B[" ++ (show n) ++ "m" colorForLevel "error" = 31 -- red colorForLevel "warning" = 33 -- yellow -colorForLevel "info" = 33 -- yellow +colorForLevel "info" = 32 -- green colorForLevel "style" = 32 -- green colorForLevel "message" = 1 -- bold colorForLevel "source" = 0 -- none