Hello community, here is the log from the commit of package ShellCheck for openSUSE:Factory checked in at 2017-03-14 10:03:51 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/ShellCheck (Old) and /work/SRC/openSUSE:Factory/.ShellCheck.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "ShellCheck" Tue Mar 14 10:03:51 2017 rev:6 rq:461516 version:0.4.5 Changes: -------- --- /work/SRC/openSUSE:Factory/ShellCheck/ShellCheck.changes 2016-07-21 08:01:29.000000000 +0200 +++ /work/SRC/openSUSE:Factory/.ShellCheck.new/ShellCheck.changes 2017-03-14 10:03:55.062715770 +0100 @@ -1,0 +2,5 @@ +Sun Feb 12 14:19:35 UTC 2017 - [email protected] + +- Update to version 0.4.5 with cabal2obs. + +------------------------------------------------------------------- Old: ---- ShellCheck-0.4.4.tar.gz New: ---- ShellCheck-0.4.5.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ ShellCheck.spec ++++++ --- /var/tmp/diff_new_pack.GUoByK/_old 2017-03-14 10:03:55.598639884 +0100 +++ /var/tmp/diff_new_pack.GUoByK/_new 2017-03-14 10:03:55.602639317 +0100 @@ -1,7 +1,7 @@ # # spec file for package ShellCheck # -# Copyright (c) 2016 SUSE LINUX GmbH, Nuernberg, Germany. +# Copyright (c) 2017 SUSE LINUX GmbH, Nuernberg, Germany. # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -19,14 +19,13 @@ %global pkg_name ShellCheck %bcond_with tests Name: %{pkg_name} -Version: 0.4.4 +Version: 0.4.5 Release: 0 Summary: Shell script analysis tool License: GPL-3.0+ Group: Development/Languages/Other Url: https://hackage.haskell.org/package/%{name} Source0: https://hackage.haskell.org/package/%{name}-%{version}/%{name}-%{version}.tar.gz -# Begin cabal-rpm deps: BuildRequires: chrpath BuildRequires: ghc-Cabal-devel BuildRequires: ghc-QuickCheck-devel @@ -39,7 +38,6 @@ BuildRequires: ghc-regex-tdfa-devel BuildRequires: ghc-rpm-macros BuildRoot: %{_tmppath}/%{name}-%{version}-build -# End cabal-rpm deps %description The goals of ShellCheck are: @@ -74,23 +72,16 @@ %prep %setup -q - %build %ghc_lib_build - %install %ghc_lib_install - -%ghc_fix_dynamic_rpath shellcheck - +%ghc_fix_rpath %{pkg_name}-%{version} install -Dpm 0644 shellcheck.1 %{buildroot}%{_mandir}/man1/shellcheck.1 %check -%if %{with tests} -%{cabal} test -%endif - +%cabal_test %post -n ghc-%{name}-devel %ghc_pkg_recache ++++++ ShellCheck-0.4.4.tar.gz -> ShellCheck-0.4.5.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ShellCheck-0.4.4/README.md new/ShellCheck-0.4.5/README.md --- old/ShellCheck-0.4.4/README.md 2016-05-15 04:06:47.000000000 +0200 +++ new/ShellCheck-0.4.5/README.md 2016-10-21 23:19:54.000000000 +0200 @@ -70,6 +70,10 @@ apt-get install shellcheck +On Gentoo based distros: + + emerge --ask shellcheck + On Fedora based distros: dnf install ShellCheck diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ShellCheck-0.4.4/ShellCheck/AST.hs new/ShellCheck-0.4.5/ShellCheck/AST.hs --- old/ShellCheck-0.4.4/ShellCheck/AST.hs 2016-05-15 04:06:47.000000000 +0200 +++ new/ShellCheck-0.4.5/ShellCheck/AST.hs 2016-10-21 23:19:54.000000000 +0200 @@ -21,6 +21,7 @@ import Control.Monad import Control.Monad.Identity +import Text.Parsec import qualified ShellCheck.Regex as Re data Id = Id Int deriving (Show, Eq, Ord) @@ -50,8 +51,10 @@ | T_AndIf Id (Token) (Token) | T_Arithmetic Id Token | T_Array Id [Token] - | T_IndexedElement Id Token Token - | T_Assignment Id AssignmentMode String (Maybe Token) Token + | T_IndexedElement Id [Token] Token + -- Store the index as string, and parse as arithmetic or string later + | T_UnparsedIndex Id SourcePos String + | T_Assignment Id AssignmentMode String [Token] Token | T_Backgrounded Id Token | T_Backticked Id [Token] | T_Bang Id @@ -96,6 +99,7 @@ | T_IfExpression Id [([Token],[Token])] [Token] | T_In Id | T_IoFile Id Token Token + | T_IoDuplicate Id Token String | T_LESSAND Id | T_LESSGREAT Id | T_Lbrace Id @@ -145,7 +149,7 @@ instance Eq Token where (==) = tokenEquals -analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> Token) -> Token -> m Token +analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> m Token) -> Token -> m Token analyze f g i = round where @@ -153,7 +157,7 @@ f t newT <- delve t g t - return . i $ newT + i newT roundAll = mapM round roundMaybe Nothing = return Nothing @@ -186,14 +190,18 @@ delve (T_DollarArithmetic id c) = d1 c $ T_DollarArithmetic id delve (T_DollarBracket id c) = d1 c $ T_DollarBracket id delve (T_IoFile id op file) = d2 op file $ T_IoFile id + delve (T_IoDuplicate id op num) = d1 op $ \x -> T_IoDuplicate id x num delve (T_HereString id word) = d1 word $ T_HereString id delve (T_FdRedirect id v t) = d1 t $ T_FdRedirect id v - delve (T_Assignment id mode var index value) = do - a <- roundMaybe index + delve (T_Assignment id mode var indices value) = do + a <- roundAll indices b <- round value return $ T_Assignment id mode var a b delve (T_Array id t) = dl t $ T_Array id - delve (T_IndexedElement id t1 t2) = d2 t1 t2 $ T_IndexedElement id + delve (T_IndexedElement id indices t) = do + a <- roundAll indices + b <- round t + return $ T_IndexedElement id a b delve (T_Redirecting id redirs cmd) = do a <- roundAll redirs b <- round cmd @@ -312,6 +320,7 @@ T_BraceExpansion id _ -> id T_DollarBraceCommandExpansion id _ -> id T_IoFile id _ _ -> id + T_IoDuplicate id _ _ -> id T_HereDoc id _ _ _ _ -> id T_HereString id _ -> id T_FdRedirect id _ _ -> id @@ -363,10 +372,11 @@ T_CoProc id _ _ -> id T_CoProcBody id _ -> id T_Include id _ _ -> id + T_UnparsedIndex id _ _ -> id blank :: Monad m => Token -> m () blank = const $ return () -doAnalysis f = analyze f blank id -doStackAnalysis startToken endToken = analyze startToken endToken id -doTransform i = runIdentity . analyze blank blank i +doAnalysis f = analyze f blank (return . id) +doStackAnalysis startToken endToken = analyze startToken endToken (return . id) +doTransform i = runIdentity . analyze blank blank (return . i) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ShellCheck-0.4.4/ShellCheck/ASTLib.hs new/ShellCheck-0.4.5/ShellCheck/ASTLib.hs --- old/ShellCheck-0.4.4/ShellCheck/ASTLib.hs 2016-05-15 04:06:47.000000000 +0200 +++ new/ShellCheck-0.4.5/ShellCheck/ASTLib.hs 2016-10-21 23:19:54.000000000 +0200 @@ -21,6 +21,7 @@ import ShellCheck.AST +import Control.Monad.Writer import Control.Monad import Data.List import Data.Maybe @@ -54,6 +55,8 @@ -- Is this shell word a constant? isConstant token = case token of + -- This ignores some cases like ~"foo": + T_NormalWord _ (T_Literal _ ('~':_) : _) -> False T_NormalWord _ l -> all isConstant l T_DoubleQuoted _ l -> all isConstant l T_SingleQuoted _ _ -> True @@ -194,6 +197,8 @@ -- Turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz] getWordParts (T_NormalWord _ l) = concatMap getWordParts l getWordParts (T_DoubleQuoted _ l) = l +-- TA_Expansion is basically T_NormalWord for arithmetic expressions +getWordParts (TA_Expansion _ l) = concatMap getWordParts l getWordParts other = [other] -- Return a list of NormalWords that would result from brace expansion @@ -206,13 +211,31 @@ braceExpand item part x = return x +-- Maybe get a SimpleCommand from immediate wrappers like T_Redirections +getCommand t = + case t of + T_Redirecting _ _ w -> getCommand w + T_SimpleCommand _ _ (w:_) -> return t + T_Annotation _ _ t -> getCommand t + otherwise -> Nothing + -- Maybe get the command name of a token representing a command -getCommandName t = +getCommandName t = do + (T_SimpleCommand _ _ (w:_)) <- getCommand t + getLiteralString w + +-- If a command substitution is a single command, get its name. +-- $(date +%s) = Just "date" +getCommandNameFromExpansion :: Token -> Maybe String +getCommandNameFromExpansion t = case t of - T_Redirecting _ _ w -> getCommandName w - T_SimpleCommand _ _ (w:_) -> getLiteralString w - T_Annotation _ _ t -> getCommandName t + T_DollarExpansion _ [c] -> extract c + T_Backticked _ [c] -> extract c + T_DollarBraceCommandExpansion _ [c] -> extract c otherwise -> Nothing + where + extract (T_Pipeline _ _ [cmd]) = getCommandName cmd + extract _ = Nothing -- Get the basename of a token representing a command getCommandBasename = liftM basename . getCommandName @@ -237,8 +260,8 @@ isFunction t = case t of T_Function {} -> True; _ -> False --- Get the list of commands from tokens that contain them, such as --- the body of while loops and if statements. +-- Get the lists of commands from tokens that contain them, such as +-- the body of while loops or branches of if statements. getCommandSequences t = case t of T_Script _ _ cmds -> [cmds] @@ -251,3 +274,22 @@ T_IfExpression _ thens elses -> map snd thens ++ [elses] otherwise -> [] +-- Get a list of names of associative arrays +getAssociativeArrays t = + nub . execWriter $ doAnalysis f t + where + f :: Token -> Writer [String] () + f t@(T_SimpleCommand {}) = fromMaybe (return ()) $ do + name <- getCommandName t + guard $ name == "declare" + let flags = getAllFlags t + guard $ elem "A" $ map snd flags + let args = map fst . filter ((==) "" . snd) $ flags + let names = mapMaybe (getLiteralStringExt nameAssignments) args + return $ tell names + f _ = return () + + nameAssignments t = + case t of + T_Assignment _ _ name _ _ -> return name + otherwise -> Nothing diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ShellCheck-0.4.4/ShellCheck/Analytics.hs new/ShellCheck-0.4.5/ShellCheck/Analytics.hs --- old/ShellCheck-0.4.4/ShellCheck/Analytics.hs 2016-05-15 04:06:47.000000000 +0200 +++ new/ShellCheck-0.4.5/ShellCheck/Analytics.hs 2016-10-21 23:19:54.000000000 +0200 @@ -85,6 +85,7 @@ ,checkEchoSed ,checkForDecimals ,checkLocalScope + ,checkMultiDimensionalArrays ] runAnalytics :: AnalysisSpec -> [TokenComment] @@ -178,6 +179,7 @@ ,checkReadWithoutR ,checkLoopVariableReassignment ,checkTrailingBracket + ,checkReturnAgainstZero ] @@ -369,6 +371,8 @@ prop_checkPipePitfalls5 = verifyNot checkPipePitfalls "ls -N | foo" prop_checkPipePitfalls6 = verify checkPipePitfalls "find . | xargs foo" prop_checkPipePitfalls7 = verifyNot checkPipePitfalls "find . -printf '%s\\n' | xargs foo" +prop_checkPipePitfalls8 = verify checkPipePitfalls "foo | grep bar | wc -l" +prop_checkPipePitfalls9 = verifyNot checkPipePitfalls "foo | grep -o bar | wc -l" checkPipePitfalls _ (T_Pipeline id _ commands) = do for ["find", "xargs"] $ \(find:xargs:_) -> @@ -388,8 +392,12 @@ for' ["ps", "grep"] $ \x -> info x 2009 "Consider using pgrep instead of grepping ps output." - for' ["grep", "wc"] $ - \x -> style x 2126 "Consider using grep -c instead of grep|wc." + for ["grep", "wc"] $ + \(grep:wc:_) -> + let flags = fromMaybe [] $ map snd <$> getAllFlags <$> getCommand grep + in + unless (any (`elem` ["o", "only-matching"]) flags) $ + style (getId grep) 2126 "Consider using grep -c instead of grep|wc." didLs <- liftM or . sequence $ [ for' ["ls", "grep"] $ @@ -439,10 +447,16 @@ prop_checkShebang1 = verifyNotTree checkShebang "#!/usr/bin/env bash -x\necho cow" prop_checkShebang2 = verifyNotTree checkShebang "#! /bin/sh -l " prop_checkShebang3 = verifyTree checkShebang "ls -l" -checkShebang params (T_Annotation _ _ t) = checkShebang params t +prop_checkShebang4 = verifyNotTree checkShebang "#shellcheck shell=sh\nfoo" +checkShebang params (T_Annotation _ list t) = + if any isOverride list then [] else checkShebang params t + where + isOverride (ShellOverride _) = True + isOverride _ = False checkShebang params (T_Script id sb _) = - [makeComment ErrorC id 2148 "Tips depend on target shell and yours is unknown. Add a shebang." - | not (shellTypeSpecified params) && sb == "" ] + [makeComment ErrorC id 2148 + "Tips depend on target shell and yours is unknown. Add a shebang." + | not (shellTypeSpecified params) && sb == "" ] prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)" prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]" @@ -493,6 +507,9 @@ prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null" prop_checkBashisms48= verifyNot checkBashisms "#!/bin/dash\necho $LINENO" prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE" +prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file" +prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1" +prop_checkBashisms52= verifyNot checkBashisms "#!/bin/sh\ncmd >&2" checkBashisms params = bashism where isDash = shellType params == Dash @@ -524,6 +541,7 @@ warnMsg id $ filter (/= '|') op ++ " is" bashism (TA_Binary id "**" _ _) = warnMsg id "exponentials are" bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&> is" + bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id ">& is" bashism (T_FdRedirect id ('{':_) _) = warnMsg id "named file descriptors are" bashism (T_FdRedirect id num _) | all isDigit num && length num > 1 = warnMsg id "FDs outside 0-9 are" @@ -765,18 +783,23 @@ prop_checkUnquotedExpansions5 = verifyNot checkUnquotedExpansions "for f in $(cmd); do echo $f; done" prop_checkUnquotedExpansions6 = verifyNot checkUnquotedExpansions "$(cmd)" prop_checkUnquotedExpansions7 = verifyNot checkUnquotedExpansions "cat << foo\n$(ls)\nfoo" +prop_checkUnquotedExpansions8 = verifyNot checkUnquotedExpansions "set -- $(seq 1 4)" +prop_checkUnquotedExpansions9 = verifyNot checkUnquotedExpansions "echo foo `# inline comment`" checkUnquotedExpansions params = check where - check t@(T_DollarExpansion _ _) = examine t - check t@(T_Backticked _ _) = examine t - check t@(T_DollarBraceCommandExpansion _ _) = examine t + check t@(T_DollarExpansion _ c) = examine t c + check t@(T_Backticked _ c) = examine t c + check t@(T_DollarBraceCommandExpansion _ c) = examine t c check _ = return () tree = parentMap params - examine t = - unless (isQuoteFree tree t || usedAsCommandName tree t) $ + examine t contents = + unless (null contents || shouldBeSplit t || isQuoteFree tree t || usedAsCommandName tree t) $ warn (getId t) 2046 "Quote this to prevent word splitting." + shouldBeSplit t = + getCommandNameFromExpansion t == Just "seq" + prop_checkRedirectToSame = verify checkRedirectToSame "cat foo > foo" prop_checkRedirectToSame2 = verify checkRedirectToSame "cat lol | sed -e 's/a/b/g' > lol" @@ -933,7 +956,7 @@ "Expanding an array without an index only gives the first element." readF _ _ _ = return [] - writeF _ (T_Assignment id mode name Nothing _) _ (DataString _) = do + writeF _ (T_Assignment id mode name [] _) _ (DataString _) = do isArray <- gets (isJust . Map.lookup name) return $ if not isArray then [] else case mode of @@ -951,7 +974,7 @@ isIndexed expr = case expr of - T_Assignment _ _ _ (Just _) _ -> True + T_Assignment _ _ _ (_:_) _ -> True _ -> False prop_checkStderrRedirect = verify checkStderrRedirect "test 2>&1 > cow" @@ -961,7 +984,7 @@ prop_checkStderrRedirect5 = verifyNot checkStderrRedirect "read < <(test 2>&1 > file)" prop_checkStderrRedirect6 = verify checkStderrRedirect "foo | bar 2>&1 > /dev/null" checkStderrRedirect params redir@(T_Redirecting _ [ - T_FdRedirect id "2" (T_IoFile _ (T_GREATAND _) (T_NormalWord _ [T_Literal _ "1"])), + T_FdRedirect id "2" (T_IoDuplicate _ (T_GREATAND _) "1"), T_FdRedirect _ _ (T_IoFile _ op _) ] _) = case op of T_Greater _ -> error @@ -1030,6 +1053,7 @@ ,"alias" ,"sudo" -- covering "sudo sh" and such ,"dpkg-query" + ,"jq" -- could also check that user provides --arg ] || "awk" `isSuffixOf` commandName || "perl" `isPrefixOf` commandName @@ -1154,6 +1178,7 @@ prop_checkSingleBracketOperators2 = verify checkSingleBracketOperators "[ $foo > $bar ]" prop_checkSingleBracketOperators3 = verifyNot checkSingleBracketOperators "[[ foo < bar ]]" prop_checkSingleBracketOperators5 = verify checkSingleBracketOperators "until [ $n <= $z ]; do echo foo; done" +prop_checkSingleBracketOperators6 = verifyNot checkSingleBracketOperators "[ $foo '>' $bar ]" checkSingleBracketOperators _ (TC_Binary id typ op lhs rhs) | typ == SingleBracket && op `elem` ["<", ">", "<=", ">="] = err id 2073 $ "Can't use " ++ op ++" in [ ]. Escape it or use [[..]]." @@ -1234,12 +1259,11 @@ prop_checkConstantIfs5 = verifyNot checkConstantIfs "[[ $n -le $n ]]" prop_checkConstantIfs6 = verifyNot checkConstantIfs "[[ a -ot b ]]" prop_checkConstantIfs7 = verifyNot checkConstantIfs "[ a -nt b ]" +prop_checkConstantIfs8 = verifyNot checkConstantIfs "[[ ~foo == '~foo' ]]" checkConstantIfs _ (TC_Binary id typ op lhs rhs) | not isDynamic = - when (isJust lLit && isJust rLit) $ + when (isConstant lhs && isConstant rhs) $ warn id 2050 "This expression is constant. Did you forget the $ on a variable?" where - lLit = getLiteralString lhs - rLit = getLiteralString rhs isDynamic = op `elem` [ "-lt", "-gt", "-le", "-ge", "-eq", "-ne" ] && typ == DoubleBracket @@ -1322,7 +1346,9 @@ (`isUnqualifiedCommand` "eval") <$> getClosestCommand (parentMap params) t checkBraceExpansionVars _ _ = return () -prop_checkForDecimals = verify checkForDecimals "((3.14*c))" +prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))" +prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar" +prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar" checkForDecimals params t@(TA_Expansion id _) = potentially $ do guard $ not (hasFloatingPoint params) str <- getLiteralString t @@ -1356,7 +1382,7 @@ unless (isException $ bracedString b) getWarning where isException [] = True - isException s = any (`elem` "/.:#%?*@$") s || isDigit (head s) + isException s = any (`elem` "/.:#%?*@$-") s || isDigit (head s) getWarning = fromMaybe noWarning . msum . map warningFor $ parents params t warningFor t = case t of @@ -2071,6 +2097,8 @@ prop_checkUnused31= verifyTree checkUnusedAssignments "let 'a=1'" prop_checkUnused32= verifyTree checkUnusedAssignments "let a=b=c; echo $a" prop_checkUnused33= verifyNotTree checkUnusedAssignments "a=foo; [[ foo =~ ^{$a}$ ]]" +prop_checkUnused34= verifyNotTree checkUnusedAssignments "foo=1; (( t = foo )); echo $t" +prop_checkUnused35= verifyNotTree checkUnusedAssignments "a=foo; b=2; echo ${a:b}" checkUnusedAssignments params t = execWriter (mapM_ warnFor unused) where flow = variableFlow params @@ -2115,6 +2143,11 @@ prop_checkUnassignedReferences20= verifyNotTree checkUnassignedReferences "printf -v foo bar; echo $foo" prop_checkUnassignedReferences21= verifyTree checkUnassignedReferences "echo ${#foo}" prop_checkUnassignedReferences22= verifyNotTree checkUnassignedReferences "echo ${!os*}" +prop_checkUnassignedReferences23= verifyTree checkUnassignedReferences "declare -a foo; foo[bar]=42;" +prop_checkUnassignedReferences24= verifyNotTree checkUnassignedReferences "declare -A foo; foo[bar]=42;" +prop_checkUnassignedReferences25= verifyNotTree checkUnassignedReferences "declare -A foo=(); foo[bar]=42;" +prop_checkUnassignedReferences26= verifyNotTree checkUnassignedReferences "a::b() { foo; }; readonly -f a::b" +prop_checkUnassignedReferences27= verifyNotTree checkUnassignedReferences ": ${foo:=bar}" checkUnassignedReferences params t = warnings where (readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty) @@ -2262,7 +2295,7 @@ case t of T_SimpleCommand _ vars (_:_) -> mapM_ checkVar vars otherwise -> check rest - checkVar (T_Assignment aId mode aName Nothing value) | + checkVar (T_Assignment aId mode aName [] value) | aName == name && (aId `notElem` idPath) = do warn aId 2097 "This assignment is only seen by the forked process." warn id 2098 "This expansion will not see the mentioned assignment." @@ -2542,7 +2575,7 @@ checkOverridingPath _ (T_SimpleCommand _ vars []) = mapM_ checkVar vars where - checkVar (T_Assignment id Assign "PATH" Nothing word) = + checkVar (T_Assignment id Assign "PATH" [] word) = let string = concat $ oversimplify word in unless (any (`isInfixOf` string) ["/bin", "/sbin" ]) $ do when ('/' `elem` string && ':' `notElem` string) $ notify id @@ -2557,7 +2590,7 @@ checkTildeInPath _ (T_SimpleCommand _ vars _) = mapM_ checkVar vars where - checkVar (T_Assignment id Assign "PATH" Nothing (T_NormalWord _ parts)) = + checkVar (T_Assignment id Assign "PATH" [] (T_NormalWord _ parts)) = when (any (\x -> isQuoted x && hasTilde x) parts) $ warn id 2147 "Literal tilde in PATH works poorly across programs." checkVar _ = return () @@ -2618,7 +2651,7 @@ prop_checkSuspiciousIFS1 = verify checkSuspiciousIFS "IFS=\"\\n\"" prop_checkSuspiciousIFS2 = verifyNot checkSuspiciousIFS "IFS=$'\\t'" -checkSuspiciousIFS params (T_Assignment id Assign "IFS" Nothing value) = +checkSuspiciousIFS params (T_Assignment id Assign "IFS" [] value) = potentially $ do str <- getLiteralString value return $ check str @@ -2790,5 +2823,54 @@ "]" -> "[" x -> x +prop_checkMultiDimensionalArrays1 = verify checkMultiDimensionalArrays "foo[a][b]=3" +prop_checkMultiDimensionalArrays2 = verifyNot checkMultiDimensionalArrays "foo[a]=3" +prop_checkMultiDimensionalArrays3 = verify checkMultiDimensionalArrays "foo=( [a][b]=c )" +prop_checkMultiDimensionalArrays4 = verifyNot checkMultiDimensionalArrays "foo=( [a]=c )" +prop_checkMultiDimensionalArrays5 = verify checkMultiDimensionalArrays "echo ${foo[bar][baz]}" +prop_checkMultiDimensionalArrays6 = verifyNot checkMultiDimensionalArrays "echo ${foo[bar]}" +checkMultiDimensionalArrays _ token = + case token of + T_Assignment _ _ name (first:second:_) _ -> about second + T_IndexedElement _ (first:second:_) _ -> about second + T_DollarBraced {} -> + when (isMultiDim token) $ about token + _ -> return () + where + about t = warn (getId t) 2180 "Bash does not support multidimensional arrays. Use 1D or associative arrays." + + re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well + isMultiDim t = getBracedModifier (bracedString t) `matches` re + +prop_checkReturnAgainstZero1 = verify checkReturnAgainstZero "[ $? -eq 0 ]" +prop_checkReturnAgainstZero2 = verify checkReturnAgainstZero "[[ \"$?\" -gt 0 ]]" +prop_checkReturnAgainstZero3 = verify checkReturnAgainstZero "[[ 0 -ne $? ]]" +prop_checkReturnAgainstZero4 = verifyNot checkReturnAgainstZero "[[ $? -eq 4 ]]" +prop_checkReturnAgainstZero5 = verify checkReturnAgainstZero "[[ 0 -eq $? ]]" +prop_checkReturnAgainstZero6 = verifyNot checkReturnAgainstZero "[[ $R -eq 0 ]]" +prop_checkReturnAgainstZero7 = verify checkReturnAgainstZero "(( $? == 0 ))" +prop_checkReturnAgainstZero8 = verify checkReturnAgainstZero "(( $? ))" +prop_checkReturnAgainstZero9 = verify checkReturnAgainstZero "(( ! $? ))" +checkReturnAgainstZero _ token = + case token of + TC_Binary id _ _ lhs rhs -> check lhs rhs + TA_Binary id _ lhs rhs -> check lhs rhs + TA_Unary id _ exp -> + when (isExitCode exp) $ message (getId exp) + TA_Sequence _ [exp] -> + when (isExitCode exp) $ message (getId exp) + otherwise -> return () + where + check lhs rhs = + if isZero rhs && isExitCode lhs + then message (getId lhs) + else when (isZero lhs && isExitCode rhs) $ message (getId rhs) + isZero t = getLiteralString t == Just "0" + isExitCode t = + case getWordParts t of + [exp@(T_DollarBraced {})] -> bracedString exp == "?" + otherwise -> False + message id = style id 2181 "Check exit code directly with e.g. 'if mycmd;', not indirectly with $?." + return [] runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ShellCheck-0.4.4/ShellCheck/AnalyzerLib.hs new/ShellCheck-0.4.5/ShellCheck/AnalyzerLib.hs --- old/ShellCheck-0.4.4/ShellCheck/AnalyzerLib.hs 2016-05-15 04:06:47.000000000 +0200 +++ new/ShellCheck-0.4.5/ShellCheck/AnalyzerLib.hs 2016-10-21 23:19:54.000000000 +0200 @@ -334,6 +334,12 @@ name <- getLiteralString lhs return (t, t, name, DataString $ SourceFrom [rhs]) + T_DollarBraced _ l -> maybeToList $ do + let string = bracedString t + let modifier = getBracedModifier string + guard $ ":=" `isPrefixOf` modifier + return (t, t, getBracedReference string, DataString $ SourceFrom [l]) + t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo [(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op] @@ -361,7 +367,10 @@ "declare" -> if any (`elem` flags) ["x", "p"] then concatMap getReference rest else [] - "readonly" -> concatMap getReference rest + "readonly" -> + if any (`elem` flags) ["f", "p"] + then [] + else concatMap getReference rest "trap" -> case rest of head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head @@ -395,7 +404,10 @@ "typeset" -> declaredVars "local" -> concatMap getModifierParamString rest - "readonly" -> concatMap getModifierParamString rest + "readonly" -> + if any (`elem` flags) ["f", "p"] + then [] + else concatMap getModifierParamString rest "set" -> maybeToList $ do params <- getSetParams rest return (base, base, "@", DataString $ SourceFrom params) @@ -474,11 +486,20 @@ where re = mkRegex "(\\[.*\\])" +getOffsetReferences mods = fromMaybe [] $ do + match <- matchRegex re mods + offsets <- match !!! 0 + return $ matchAllStrings variableNameRegex offsets + where + re = mkRegex "^ *:(.*)" + getReferencedVariables parents t = case t of T_DollarBraced id l -> let str = bracedString t in (t, t, getBracedReference str) : - map (\x -> (l, l, x)) (getIndexReferences str) + map (\x -> (l, l, x)) ( + getIndexReferences str + ++ getOffsetReferences (getBracedModifier str)) TA_Expansion id _ -> if isArithmeticAssignment t then [] @@ -520,7 +541,7 @@ isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"]) isArithmeticAssignment t = case getPath parents t of - this: TA_Assignment _ "=" _ _ :_ -> True + this: TA_Assignment _ "=" lhs _ :_ -> lhs == t _ -> False dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v] @@ -573,6 +594,7 @@ prop_getBracedReference10= getBracedReference "foo: -1" == "foo" prop_getBracedReference11= getBracedReference "!os*" == "" prop_getBracedReference12= getBracedReference "!os?bar**" == "" +prop_getBracedReference13= getBracedReference "foo[bar]" == "foo" getBracedReference s = fromMaybe s $ nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s where @@ -595,6 +617,20 @@ return "" nameExpansion _ = Nothing +prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz" +prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo" +prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]" +getBracedModifier s = fromMaybe "" . listToMaybe $ do + let var = getBracedReference s + a <- dropModifier s + dropPrefix var a + where + dropPrefix [] t = return t + dropPrefix (a:b) (c:d) | a == c = dropPrefix b d + dropPrefix _ _ = [] + + dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest] + dropModifier x = [x] -- Useful generic functions potentially :: Monad m => Maybe (m ()) -> m () @@ -628,5 +664,5 @@ getCode (TokenComment _ (Comment _ c _)) = c -return [] +return [] runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ShellCheck-0.4.4/ShellCheck/Checker.hs new/ShellCheck-0.4.5/ShellCheck/Checker.hs --- old/ShellCheck-0.4.4/ShellCheck/Checker.hs 2016-05-15 04:06:47.000000000 +0200 +++ new/ShellCheck-0.4.5/ShellCheck/Checker.hs 2016-10-21 23:19:54.000000000 +0200 @@ -39,7 +39,7 @@ tokenToPosition map (TokenComment id c) = fromMaybe fail $ do position <- Map.lookup id map - return $ PositionedComment position c + return $ PositionedComment position position c where fail = error "Internal shellcheck error: id doesn't exist. Please report!" @@ -65,13 +65,13 @@ return . nub . sortMessages . filter shouldInclude $ (parseMessages ++ map translator analysisMessages) - shouldInclude (PositionedComment _ (Comment _ code _)) = + shouldInclude (PositionedComment _ _ (Comment _ code _)) = code `notElem` csExcludedWarnings spec sortMessages = sortBy (comparing order) - order (PositionedComment pos (Comment severity code message)) = + order (PositionedComment pos _ (Comment severity code message)) = (posFile pos, posLine pos, posColumn pos, severity, code, message) - getPosition (PositionedComment pos _) = pos + getPosition (PositionedComment pos _ _) = pos analysisSpec root = AnalysisSpec { @@ -84,7 +84,7 @@ sort . map getCode . crComments $ runIdentity (checkScript sys spec) where - getCode (PositionedComment _ (Comment _ code _)) = code + getCode (PositionedComment _ _ (Comment _ code _)) = code check = checkWithIncludes [] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ShellCheck-0.4.4/ShellCheck/Checks/Commands.hs new/ShellCheck-0.4.5/ShellCheck/Checks/Commands.hs --- old/ShellCheck-0.4.4/ShellCheck/Checks/Commands.hs 2016-05-15 04:06:47.000000000 +0200 +++ new/ShellCheck-0.4.5/ShellCheck/Checks/Commands.hs 2016-10-21 23:19:54.000000000 +0200 @@ -90,6 +90,8 @@ ,checkExportedExpansions ,checkAliasesUsesArgs ,checkAliasesExpandEarly + ,checkUnsetGlobs + ,checkFindWithoutPath ] buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis) @@ -150,7 +152,7 @@ info (getId word) 2020 "tr replaces sets of chars, not words (mentioned due to duplicates)." unless ("[:" `isPrefixOf` s) $ when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && ('*' `notElem` s)) $ - info (getId word) 2021 "Don't use [] around ranges in tr, it replaces literal square brackets." + info (getId word) 2021 "Don't use [] around classes in tr, it replaces literal square brackets." Nothing -> return () duplicated s = @@ -470,16 +472,49 @@ prop_checkPrintfVar2 = verifyNot checkPrintfVar "printf 'Lol: $s'" prop_checkPrintfVar3 = verify checkPrintfVar "printf -v cow $(cmd)" prop_checkPrintfVar4 = verifyNot checkPrintfVar "printf \"%${count}s\" var" +prop_checkPrintfVar5 = verify checkPrintfVar "printf '%s %s %s' foo bar" +prop_checkPrintfVar6 = verify checkPrintfVar "printf foo bar baz" +prop_checkPrintfVar7 = verify checkPrintfVar "printf -- foo bar baz" +prop_checkPrintfVar8 = verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\"" +prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png" +prop_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz" checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where + f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest - f (format:params) = check format + f (format:params) = check format params f _ = return () - check format = + + countFormats string = + case string of + '%':'%':rest -> countFormats rest + '%':rest -> 1 + countFormats rest + _:rest -> countFormats rest + [] -> 0 + + check format more = do + fromMaybe (return ()) $ do + string <- getLiteralString format + let vars = countFormats string + + return $ do + when (vars == 0 && more /= []) $ + err (getId format) 2182 + "This printf format string has no variables. Other arguments are ignored." + + when (vars > 0 + && length more < vars + && all (not . mayBecomeMultipleArgs) more) $ + warn (getId format) 2183 $ + "This format string has " ++ show vars ++ " variables, but is passed " ++ show (length more) ++ " arguments." + + unless ('%' `elem` concat (oversimplify format) || isLiteral format) $ - warn (getId format) 2059 + info (getId format) 2059 "Don't use variables in the printf format string. Use printf \"..%s..\" \"$foo\"." + + prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)" prop_checkUuoeCmd2 = verify checkUuoeCmd "echo `date`" prop_checkUuoeCmd3 = verify checkUuoeCmd "echo \"$(date)\"" @@ -556,5 +591,33 @@ checkArg _ = return () +prop_checkUnsetGlobs1 = verify checkUnsetGlobs "unset foo[1]" +prop_checkUnsetGlobs2 = verifyNot checkUnsetGlobs "unset foo" +checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments) + where + check arg = + when (isGlob arg) $ + warn (getId arg) 2184 "Quote arguments to unset so they're not glob expanded." + + +prop_checkFindWithoutPath1 = verify checkFindWithoutPath "find -type f" +prop_checkFindWithoutPath2 = verify checkFindWithoutPath "find" +prop_checkFindWithoutPath3 = verifyNot checkFindWithoutPath "find . -type f" +prop_checkFindWithoutPath4 = verifyNot checkFindWithoutPath "find -H -L \"$path\" -print" +checkFindWithoutPath = CommandCheck (Basename "find") f + where + f (T_SimpleCommand _ _ (cmd:args)) = + unless (hasPath args) $ + info (getId cmd) 2185 "Some finds don't have a default path. Specify '.' explicitly." + + -- This is a bit of a kludge. find supports flag arguments both before and after the path, + -- as well as multiple non-flag arguments that are not the path. We assume that all the + -- pre-path flags are single characters, which is generally the case. + hasPath (first:rest) = + let flag = fromJust $ getLiteralStringExt (const $ return "___") first in + not ("-" `isPrefixOf` flag) || length flag <= 2 && hasPath rest + hasPath [] = False + + return [] runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ShellCheck-0.4.4/ShellCheck/Formatter/Format.hs new/ShellCheck-0.4.5/ShellCheck/Formatter/Format.hs --- old/ShellCheck-0.4.4/ShellCheck/Formatter/Format.hs 2016-05-15 04:06:47.000000000 +0200 +++ new/ShellCheck-0.4.5/ShellCheck/Formatter/Format.hs 2016-10-21 23:19:54.000000000 +0200 @@ -30,13 +30,15 @@ footer :: IO () } -lineNo (PositionedComment pos _) = posLine pos -colNo (PositionedComment pos _) = posColumn pos -codeNo (PositionedComment _ (Comment _ code _)) = code -messageText (PositionedComment _ (Comment _ _ t)) = t +lineNo (PositionedComment pos _ _) = posLine pos +endLineNo (PositionedComment _ end _) = posLine end +colNo (PositionedComment pos _ _) = posColumn pos +endColNo (PositionedComment _ end _) = posColumn end +codeNo (PositionedComment _ _ (Comment _ code _)) = code +messageText (PositionedComment _ _ (Comment _ _ t)) = t severityText :: PositionedComment -> String -severityText (PositionedComment _ (Comment c _ _)) = +severityText (PositionedComment _ _ (Comment c _ _)) = case c of ErrorC -> "error" WarningC -> "warning" @@ -48,12 +50,15 @@ map fix comments where ls = lines contents - fix c@(PositionedComment pos comment) = PositionedComment pos { - posColumn = - if lineNo c > 0 && lineNo c <= fromIntegral (length ls) - then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c) - else colNo c + fix c@(PositionedComment start end comment) = PositionedComment start { + posColumn = realignColumn lineNo colNo c + } end { + posColumn = realignColumn endLineNo endColNo c } comment + realignColumn lineNo colNo c = + if lineNo c > 0 && lineNo c <= fromIntegral (length ls) + then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c) + else colNo c real _ r v target | target <= v = r real [] r v _ = r -- should never happen real ('\t':rest) r v target = diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ShellCheck-0.4.4/ShellCheck/Formatter/JSON.hs new/ShellCheck-0.4.5/ShellCheck/Formatter/JSON.hs --- old/ShellCheck-0.4.4/ShellCheck/Formatter/JSON.hs 2016-05-15 04:06:47.000000000 +0200 +++ new/ShellCheck-0.4.5/ShellCheck/Formatter/JSON.hs 2016-10-21 23:19:54.000000000 +0200 @@ -37,10 +37,12 @@ } instance JSON (PositionedComment) where - showJSON comment@(PositionedComment pos (Comment level code string)) = makeObj [ - ("file", showJSON $ posFile pos), - ("line", showJSON $ posLine pos), - ("column", showJSON $ posColumn pos), + showJSON comment@(PositionedComment start end (Comment level code string)) = makeObj [ + ("file", showJSON $ posFile start), + ("line", showJSON $ posLine start), + ("endLine", showJSON $ posLine end), + ("column", showJSON $ posColumn start), + ("endColumn", showJSON $ posColumn end), ("level", showJSON $ severityText comment), ("code", showJSON code), ("message", showJSON string) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ShellCheck-0.4.4/ShellCheck/Interface.hs new/ShellCheck-0.4.5/ShellCheck/Interface.hs --- old/ShellCheck-0.4.4/ShellCheck/Interface.hs 2016-05-15 04:06:47.000000000 +0200 +++ new/ShellCheck-0.4.5/ShellCheck/Interface.hs 2016-10-21 23:19:54.000000000 +0200 @@ -94,7 +94,7 @@ } deriving (Show, Eq) data Comment = Comment Severity Code String deriving (Show, Eq) -data PositionedComment = PositionedComment Position Comment deriving (Show, Eq) +data PositionedComment = PositionedComment Position Position Comment deriving (Show, Eq) data TokenComment = TokenComment Id Comment deriving (Show, Eq) data ColorOption = diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ShellCheck-0.4.4/ShellCheck/Parser.hs new/ShellCheck-0.4.5/ShellCheck/Parser.hs --- old/ShellCheck-0.4.4/ShellCheck/Parser.hs 2016-05-15 04:06:47.000000000 +0200 +++ new/ShellCheck-0.4.5/ShellCheck/Parser.hs 2016-10-21 23:19:54.000000000 +0200 @@ -135,7 +135,7 @@ --------- Message/position annotation on top of user state data Note = Note Id Severity Code String deriving (Show, Eq) -data ParseNote = ParseNote SourcePos Severity Code String deriving (Show, Eq) +data ParseNote = ParseNote SourcePos SourcePos Severity Code String deriving (Show, Eq) data Context = ContextName SourcePos String | ContextAnnotation [Annotation] @@ -162,9 +162,9 @@ pendingHereDocs = [] } -codeForParseNote (ParseNote _ _ code _) = code +codeForParseNote (ParseNote _ _ _ code _) = code noteToParseNote map (Note id severity code message) = - ParseNote pos severity code message + ParseNote pos pos severity code message where pos = fromJust $ Map.lookup id map @@ -181,6 +181,7 @@ return newId where incId (Id n) = Id $ n+1 +getNextId :: Monad m => SCParser m Id getNextId = do pos <- getPosition getNextIdAt pos @@ -320,14 +321,16 @@ v <- getCurrentContexts setCurrentContexts (c:v) -parseProblemAt pos level code msg = do +parseProblemAtWithEnd start end level code msg = do irrelevant <- shouldIgnoreCode code unless irrelevant $ Ms.modify (\state -> state { parseProblems = note:parseProblems state }) where - note = ParseNote pos level code msg + note = ParseNote start end level code msg + +parseProblemAt pos = parseProblemAtWithEnd pos pos -- Store non-parse problems inside @@ -335,7 +338,9 @@ pos <- getPosition parseNoteAt pos c l a -parseNoteAt pos c l a = addParseNote $ ParseNote pos c l a +parseNoteAt pos c l a = addParseNote $ ParseNote pos pos c l a + +parseNoteAtWithEnd start end c l a = addParseNote $ ParseNote start end c l a --------- Convenient combinators thenSkip main follow = do @@ -406,7 +411,7 @@ pos <- getPosition s <- many1 letter when (s `elem` commonCommands) $ - parseProblemAt pos WarningC 1009 "Use 'if cmd; then ..' to check exit code, or 'if [[ $(cmd) == .. ]]' to check output.") + parseProblemAt pos WarningC 1014 "Use 'if cmd; then ..' to check exit code, or 'if [[ $(cmd) == .. ]]' to check output.") where spacingOrLf = condSpacing True @@ -442,7 +447,7 @@ c <- oneOf "'\"" s <- anyOp char c - return s + return $ escaped s anyOp = flagOp <|> flaglessOp <|> fail "Expected comparison operator (don't wrap commands in []/[[]])" @@ -645,6 +650,7 @@ prop_a20= isOk readArithmeticContents "a ? b ? c : d : e" prop_a21= isOk readArithmeticContents "a ? b : c ? d : e" prop_a22= isOk readArithmeticContents "!!a" +readArithmeticContents :: Monad m => SCParser m Token readArithmeticContents = readSequence where @@ -658,12 +664,40 @@ id <- getNextId op <- choice (map (\x -> try $ do s <- string x - notFollowedBy2 $ oneOf "&|<>=" + failIfIncompleteOp return s ) op) spacing return $ token id op + failIfIncompleteOp = notFollowedBy2 $ oneOf "&|<>=" + + -- Read binary minus, but also check for -lt, -gt and friends: + readMinusOp = do + id <- getNextId + pos <- getPosition + try $ do + char '-' + failIfIncompleteOp + optional $ do + (str, alt) <- lookAhead . choice $ map tryOp [ + ("lt", "<"), + ("gt", ">"), + ("le", "<="), + ("ge", ">="), + ("eq", "=="), + ("ne", "!=") + ] + parseProblemAt pos ErrorC 1106 $ "In arithmetic contexts, use " ++ alt ++ " instead of -" ++ str + spacing + return $ TA_Binary id "-" + where + tryOp (str, alt) = try $ do + string str + spacing1 + return (str, alt) + + readArrayIndex = do id <- getNextId char '[' @@ -733,7 +767,7 @@ readEquated = readCompared `splitBy` ["==", "!="] readCompared = readShift `splitBy` ["<=", ">=", "<", ">"] readShift = readAddition `splitBy` ["<<", ">>"] - readAddition = readMultiplication `splitBy` ["+", "-"] + readAddition = chainl1 readMultiplication (readBinary ["+"] <|> readMinusOp) readMultiplication = readExponential `splitBy` ["*", "/", "%"] readExponential = readAnyNegated `splitBy` ["**"] @@ -810,7 +844,7 @@ pos <- getPosition space <- allspacing when (null space) $ - parseProblemAt pos ErrorC 1035 $ "You need a space after the " ++ + parseProblemAtWithEnd opos pos ErrorC 1035 $ "You need a space after the " ++ if single then "[ and before the ]." else "[[ and before the ]]." @@ -835,10 +869,11 @@ prop_readAnnotation1 = isOk readAnnotation "# shellcheck disable=1234,5678\n" prop_readAnnotation2 = isOk readAnnotation "# shellcheck disable=SC1234 disable=SC5678\n" prop_readAnnotation3 = isOk readAnnotation "# shellcheck disable=SC1234 source=/dev/null disable=SC5678\n" +prop_readAnnotation4 = isWarning readAnnotation "# shellcheck cats=dogs disable=SC1234\n" readAnnotation = called "shellcheck annotation" $ do try readAnnotationPrefix many1 linewhitespace - values <- many1 (readDisable <|> readSourceOverride <|> readShellOverride) + values <- many1 (readDisable <|> readSourceOverride <|> readShellOverride <|> anyKey) linefeed many linewhitespace return $ concat values @@ -870,6 +905,13 @@ many linewhitespace return value + anyKey = do + pos <- getPosition + anyChar `reluctantlyTill1` whitespace + many linewhitespace + parseNoteAt pos WarningC 1107 "This directive is unknown. It will be ignored." + return [] + readAnnotations = do annotations <- many (readAnnotation `thenSkip` allspacing) return $ concat annotations @@ -897,6 +939,20 @@ checkPossibleTermination pos x return $ T_NormalWord id x +readIndexSpan = do + id <- getNextId + x <- many (readNormalWordPart "]" <|> someSpace <|> otherLiteral) + return $ T_NormalWord id x + where + someSpace = do + id <- getNextId + str <- spacing1 + return $ T_Literal id str + otherLiteral = do + id <- getNextId + str <- many1 $ oneOf quotableChars + return $ T_Literal id str + checkPossibleTermination pos [T_Literal _ x] = when (x `elem` ["do", "done", "then", "fi", "esac"]) $ parseProblemAt pos WarningC 1010 $ "Use semicolon or linefeed before '" ++ x ++ "' (or quote to make it literal)." @@ -1060,13 +1116,18 @@ setPosition lastPosition return result -inSeparateContext parser = do +-- Parse something, but forget all parseProblems +inSeparateContext = parseForgettingContext True +-- Parse something, but forget all parseProblems on failure +forgetOnFailure = parseForgettingContext False + +parseForgettingContext alsoOnSuccess parser = do context <- Ms.get success context <|> failure context where success c = do res <- try parser - Ms.put c + when alsoOnSuccess $ Ms.put c return res failure c = do Ms.put c @@ -1303,7 +1364,17 @@ prop_readDollarExpression1 = isOk readDollarExpression "$(((1) && 3))" prop_readDollarExpression2 = isWarning readDollarExpression "$(((1)) && 3)" -readDollarExpression = readTripleParenthesis "$" readDollarArithmetic readDollarExpansion <|> readDollarArithmetic <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarExpansion <|> readDollarVariable +prop_readDollarExpression3 = isWarning readDollarExpression "$((\"$@\" &); foo;)" +readDollarExpression :: Monad m => SCParser m Token +readDollarExpression = do + -- The grammar should have been designed along the lines of readDollarExpr = char '$' >> stuff, but + -- instead, each subunit parses its own $. This results in ~7 1-3 char lookaheads instead of one 1-char. + -- Instead of optimizing the grammar, here's a green cut that decreases shellcheck runtime by 10%: + lookAhead $ char '$' + arithmetic <|> readDollarExpansion <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarVariable + where + arithmetic = readAmbiguous "$((" readDollarArithmetic readDollarExpansion (\pos -> + parseNoteAt pos WarningC 1102 "Shells disambiguate $(( differently or not at all. For $(command substition), add space after $( . For $((arithmetics)), fix parsing errors.") prop_readDollarSingleQuote = isOk readDollarSingleQuote "$'foo\\\'lol'" readDollarSingleQuote = called "$'..' expression" $ do @@ -1349,25 +1420,20 @@ string "))" return (T_Arithmetic id c) --- Check if maybe ((( was intended as ( (( rather than (( ( -readTripleParenthesis prefix expected alternative = do - pos <- try . lookAhead $ do - string prefix - p <- getPosition - string "(((" -- should optimally be "((" but it's noisy and rarely helpful - return p - +-- If the next characters match prefix, try two different parsers and warn if the alternate parser had to be used +readAmbiguous :: Monad m => String -> SCParser m p -> SCParser m p -> (SourcePos -> SCParser m ()) -> SCParser m p +readAmbiguous prefix expected alternative warner = do + pos <- getPosition + try . lookAhead $ string prefix -- If the expected parser fails, try the alt. -- If the alt fails, run the expected one again for the errors. - try expected <|> tryAlt pos <|> expected + try expected <|> try (withAlt pos) <|> expected where - tryAlt pos = do - t <- try alternative - parseNoteAt pos WarningC 1102 $ - "Shells differ in parsing ambiguous " ++ prefix ++ "(((. Use spaces: " ++ prefix ++ "( (( ." + withAlt pos = do + t <- forgetOnFailure alternative + warner pos return t - prop_readDollarBraceCommandExpansion1 = isOk readDollarBraceCommandExpansion "${ ls; }" prop_readDollarBraceCommandExpansion2 = isOk readDollarBraceCommandExpansion "${\nls\n}" readDollarBraceCommandExpansion = called "ksh ${ ..; } command expansion" $ do @@ -1481,7 +1547,7 @@ -- add empty tokens for now, read the rest in readPendingHereDocs let doc = T_HereDoc hid dashed quoted endToken [] addPendingHereDoc doc - return $ T_FdRedirect fid "" doc + return doc where stripLiteral (T_Literal _ x) = x stripLiteral (T_SingleQuoted _ x) = x @@ -1552,7 +1618,13 @@ readFilename = readNormalWord -readIoFileOp = choice [g_LESSAND, g_GREATAND, g_DGREAT, g_LESSGREAT, g_CLOBBER, redirToken '<' T_Less, redirToken '>' T_Greater ] +readIoFileOp = choice [g_DGREAT, g_LESSGREAT, g_GREATAND, g_LESSAND, g_CLOBBER, redirToken '<' T_Less, redirToken '>' T_Greater ] + +readIoDuplicate = try $ do + id <- getNextId + op <- g_GREATAND <|> g_LESSAND + target <- readIoVariable <|> many1 digit <|> string "-" + return $ T_IoDuplicate id op target prop_readIoFile = isOk readIoFile ">> \"$(date +%YYmmDD)\"" readIoFile = called "redirection" $ do @@ -1560,35 +1632,31 @@ op <- readIoFileOp spacing file <- readFilename - return $ T_FdRedirect id "" $ T_IoFile id op file + return $ T_IoFile id op file readIoVariable = try $ do char '{' x <- readVariableName char '}' - lookAhead readIoFileOp return $ "{" ++ x ++ "}" -readIoNumber = try $ do - x <- many1 digit <|> string "&" - lookAhead readIoFileOp +readIoSource = try $ do + x <- string "&" <|> readIoVariable <|> many digit + lookAhead $ void readIoFileOp <|> void (string "<<") return x -prop_readIoNumberRedirect = isOk readIoNumberRedirect "3>&2" -prop_readIoNumberRedirect2 = isOk readIoNumberRedirect "2> lol" -prop_readIoNumberRedirect3 = isOk readIoNumberRedirect "4>&-" -prop_readIoNumberRedirect4 = isOk readIoNumberRedirect "&> lol" -prop_readIoNumberRedirect5 = isOk readIoNumberRedirect "{foo}>&2" -prop_readIoNumberRedirect6 = isOk readIoNumberRedirect "{foo}<&-" -readIoNumberRedirect = do - id <- getNextId - n <- readIoVariable <|> readIoNumber - op <- readHereString <|> readHereDoc <|> readIoFile - let actualOp = case op of T_FdRedirect _ "" x -> x +prop_readIoRedirect = isOk readIoRedirect "3>&2" +prop_readIoRedirect2 = isOk readIoRedirect "2> lol" +prop_readIoRedirect3 = isOk readIoRedirect "4>&-" +prop_readIoRedirect4 = isOk readIoRedirect "&> lol" +prop_readIoRedirect5 = isOk readIoRedirect "{foo}>&2" +prop_readIoRedirect6 = isOk readIoRedirect "{foo}<&-" +readIoRedirect = do + id <- getNextId + n <- readIoSource + redir <- readHereString <|> readHereDoc <|> readIoDuplicate <|> readIoFile spacing - return $ T_FdRedirect id n actualOp - -readIoRedirect = choice [ readIoNumberRedirect, readHereString, readHereDoc, readIoFile ] `thenSkip` spacing + return $ T_FdRedirect id n redir readRedirectList = many1 readIoRedirect @@ -1599,7 +1667,7 @@ spacing id2 <- getNextId word <- readNormalWord - return $ T_FdRedirect id "" $ T_HereString id2 word + return $ T_HereString id2 word readNewlineList = many1 ((linefeed <|> carriageReturn) `thenSkip` spacing) readLineBreak = optional readNewlineList @@ -1878,7 +1946,7 @@ pos <- getPosition correctElif <- elif unless correctElif $ - parseProblemAt pos ErrorC 1075 "Use 'elif' instead of 'else if'." + parseProblemAt pos ErrorC 1075 "Use 'elif' instead of 'else if' (or put 'if' on new line if nesting)." allspacing condition <- readTerm @@ -2176,8 +2244,8 @@ id <- getNextId cmd <- choice [ readBraceGroup, - readTripleParenthesis "" readArithmeticExpression readSubshell, - readArithmeticExpression, + readAmbiguous "((" readArithmeticExpression readSubshell (\pos -> + parseNoteAt pos WarningC 1105 "Shells disambiguate (( differently or not at all. For subshell, add spaces around ( . For ((, fix parsing errors."), readSubshell, readCondition, readWhileClause, @@ -2251,7 +2319,7 @@ -- Get whatever a parser would parse as a string readStringForParser parser = do - pos <- lookAhead (parser >> getPosition) + pos <- inSeparateContext $ lookAhead (parser >> getPosition) readUntil pos where readUntil endPos = anyChar `reluctantlyTill` (getPosition >>= guard . (== endPos)) @@ -2278,7 +2346,7 @@ variable <- readVariableName optional (readNormalDollar >> parseNoteAt pos ErrorC 1067 "For indirection, use (associative) arrays or 'read \"var$n\" <<< \"value\"'") - index <- optionMaybe readArrayIndex + indices <- many readArrayIndex hasLeftSpace <- liftM (not . null) spacing pos <- getPosition op <- readAssignmentOp @@ -2290,13 +2358,13 @@ parseNoteAt pos WarningC 1007 "Remove space after = if trying to assign a value (for empty string, use var='' ... )." value <- readEmptyLiteral - return $ T_Assignment id op variable index value + return $ T_Assignment id op variable indices value else do when (hasLeftSpace || hasRightSpace) $ parseNoteAt pos ErrorC 1068 "Don't put spaces around the = in assignments." value <- readArray <|> readNormalWord spacing - return $ T_Assignment id op variable index value + return $ T_Assignment id op variable indices value where readAssignmentOp = do pos <- getPosition @@ -2316,12 +2384,14 @@ return $ T_Literal id "" readArrayIndex = do + id <- getNextId char '[' - optional space - x <- readArithmeticContents + pos <- getPosition + str <- readStringForParser readIndexSpan char ']' - return x + return $ T_UnparsedIndex id pos str +readArray :: Monad m => SCParser m Token readArray = called "array assignment" $ do id <- getNextId char '(' @@ -2334,7 +2404,7 @@ readIndexed = do id <- getNextId index <- try $ do - x <- readArrayIndex + x <- many1 readArrayIndex char '=' return x value <- readNormalWord <|> nothing @@ -2477,12 +2547,12 @@ try (lookAhead p) action -prop_readScript1 = isOk readScript "#!/bin/bash\necho hello world\n" -prop_readScript2 = isWarning readScript "#!/bin/bash\r\necho hello world\n" -prop_readScript3 = isWarning readScript "#!/bin/bash\necho hello\xA0world" -prop_readScript4 = isWarning readScript "#!/usr/bin/perl\nfoo=(" -prop_readScript5 = isOk readScript "#!/bin/bash\n#This is an empty script\n\n" -readScript = do +prop_readScript1 = isOk readScriptFile "#!/bin/bash\necho hello world\n" +prop_readScript2 = isWarning readScriptFile "#!/bin/bash\r\necho hello world\n" +prop_readScript3 = isWarning readScriptFile "#!/bin/bash\necho hello\xA0world" +prop_readScript4 = isWarning readScriptFile "#!/usr/bin/perl\nfoo=(" +prop_readScript5 = isOk readScriptFile "#!/bin/bash\n#This is an empty script\n\n" +readScriptFile = do id <- getNextId pos <- getPosition optional $ do @@ -2497,7 +2567,8 @@ annotations <- readAnnotations commands <- withAnnotations annotations readCompoundListOrEmpty verifyEof - return $ T_Annotation annotationId annotations $ T_Script id sb commands + let script = T_Annotation annotationId annotations $ T_Script id sb commands + reparseIndices script else do many anyChar return $ T_Script id sb [] @@ -2549,6 +2620,9 @@ readUtf8Bom = called "Byte Order Mark" $ string "\xFEFF" +readScript = do + script <- readScriptFile + reparseIndices script isWarning p s = parsesCleanly p s == Just False isOk p s = parsesCleanly p s == Just True @@ -2571,13 +2645,15 @@ state <- getState return (item, state) -compareNotes (ParseNote pos1 level1 _ s1) (ParseNote pos2 level2 _ s2) = compare (pos1, level1) (pos2, level2) +compareNotes (ParseNote pos1 pos1' level1 _ s1) (ParseNote pos2 pos2' level2 _ s2) = compare (pos1, pos1', level1) (pos2, pos2', level2) sortNotes = sortBy compareNotes makeErrorFor parsecError = - ParseNote (errorPos parsecError) ErrorC 1072 $ + ParseNote pos pos ErrorC 1072 $ getStringFromParsec $ errorMessages parsecError + where + pos = errorPos parsecError getStringFromParsec errors = case map f errors of @@ -2630,11 +2706,46 @@ isName (ContextName _ _) = True isName _ = False notesForContext list = zipWith ($) [first, second] $ filter isName list - first (ContextName pos str) = ParseNote pos ErrorC 1073 $ + first (ContextName pos str) = ParseNote pos pos ErrorC 1073 $ "Couldn't parse this " ++ str ++ "." - second (ContextName pos str) = ParseNote pos InfoC 1009 $ + second (ContextName pos str) = ParseNote pos pos InfoC 1009 $ "The mentioned parser error was in this " ++ str ++ "." +-- Go over all T_UnparsedIndex and reparse them as either arithmetic or text +-- depending on declare -A statements. +reparseIndices root = + analyze blank blank f root + where + associative = getAssociativeArrays root + isAssociative s = s `elem` associative + f (T_Assignment id mode name indices value) = do + newIndices <- mapM (fixAssignmentIndex name) indices + newValue <- case value of + (T_Array id2 words) -> do + newWords <- mapM (fixIndexElement name) words + return $ T_Array id2 newWords + x -> return x + return $ T_Assignment id mode name newIndices newValue + f t = return t + + fixIndexElement name word = + case word of + T_IndexedElement id indices value -> do + new <- mapM (fixAssignmentIndex name) indices + return $ T_IndexedElement id new value + otherwise -> return word + + fixAssignmentIndex name word = + case word of + T_UnparsedIndex id pos src -> do + parsed name pos src + otherwise -> return word + + parsed name pos src = + if isAssociative name + then subParse pos (called "associative array index" $ readIndexSpan) src + else subParse pos (called "arithmetic array index expression" $ optional space >> readArithmeticContents) src + reattachHereDocs root map = doTransform f root where @@ -2644,8 +2755,8 @@ f t = t toPositionedComment :: ParseNote -> PositionedComment -toPositionedComment (ParseNote pos severity code message) = - PositionedComment (posToPos pos) $ Comment severity code message +toPositionedComment (ParseNote start end severity code message) = + PositionedComment (posToPos start) (posToPos end) $ Comment severity code message posToPos :: SourcePos -> Position posToPos sp = Position { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ShellCheck-0.4.4/ShellCheck.cabal new/ShellCheck-0.4.5/ShellCheck.cabal --- old/ShellCheck-0.4.4/ShellCheck.cabal 2016-05-15 04:06:47.000000000 +0200 +++ new/ShellCheck-0.4.5/ShellCheck.cabal 2016-10-21 23:19:54.000000000 +0200 @@ -1,5 +1,5 @@ Name: ShellCheck -Version: 0.4.4 +Version: 0.4.5 Synopsis: Shell script analysis tool License: GPL-3 License-file: LICENSE
