47 Commits

Author SHA1 Message Date
Vidar Holen
5efb724a3e Stable version 0.4.5
This release is dedicated to Google Inc for four great years of
employment and being good sports about hobby projects like this.
2016-10-21 14:00:50 -07:00
Vidar Holen
619b6c42f3 Improve parsing of fd close/duplicate redirections. 2016-10-21 11:31:58 -07:00
Vidar Holen
88c56ecd53 Allow unrecognized directives with warnings. 2016-10-14 12:14:20 -07:00
Vidar Holen
6b62b5bf7e Don't warn about [ a '>' b ] needing escapes. 2016-10-01 14:54:28 -07:00
Vidar Holen
8672af29ef Split duplicate SC1009 into SC1014 for if [ grep foo bar ] 2016-10-01 13:34:14 -07:00
Vidar Holen
1a8e34bfea Don't suggest grep -c when used with -o 2016-10-01 13:26:53 -07:00
Vidar Holen
b0dae063bf Add info when using 'find' without path 2016-09-25 11:56:32 -07:00
Vidar Holen
4fc4899803 Consider ${foo:=bar} an assignment. 2016-09-24 19:01:13 -07:00
Vidar Holen
cd4896192c Don't consider ~foo constant. 2016-09-24 15:32:44 -07:00
Vidar Holen
868d53af95 Warn about passing globs to unset. 2016-09-24 14:49:52 -07:00
Vidar Holen
6a4b86cbea Fix warning for >& 2016-09-24 14:08:00 -07:00
Vidar Holen
fe2398edc9 Warn about >& in sh 2016-09-24 14:03:54 -07:00
Vidar Holen
3a7dc86de1 Don't warn about unused vars with readonly -f 2016-09-24 13:42:20 -07:00
koalaman
1c0ec9c6f6 Merge pull request #734 from NLKNguyen/add_CI_CD_solution_with_Docker
Add CI/CD solution with Docker
2016-09-11 23:42:06 -07:00
NLKNguyen
84110dbef4 Change DOCKER_REPO value and add test runner 2016-09-08 23:57:46 -07:00
NLKNguyen
1a7e98beaf Use cleaner escaping method 2016-09-08 21:58:51 -07:00
NLKNguyen
a5d5831a2e Fix syntax error with traditional if-clause instead of escaping bracket 2016-09-08 21:41:06 -07:00
NLKNguyen
47201822f9 Change syntax style for readability 2016-09-08 21:31:23 -07:00
NLKNguyen
32689ef5eb Test Dockerfiles and Travis CI on downstream repos 2016-09-08 21:05:54 -07:00
Vidar Holen
87481dce25 Warn about printf hello world and printf "%s %s" foo 2016-09-06 21:16:59 -07:00
Vidar Holen
a90b6d14b3 Count b as a reference in ${a:b} 2016-09-05 14:01:53 -07:00
Vidar Holen
5a46eeb09a Allow #inline comments without SC2046. 2016-09-05 12:38:35 -07:00
Vidar Holen
47a7065a7a Add style note for 'mycmd; if [ $? -eq 0 ]'. 2016-08-28 20:54:08 -07:00
koalaman
dbafbb3b3b Merge pull request #722 from kpankonen/docker
add Dockerfile that will build shellcheck
2016-08-07 11:43:37 -07:00
Vidar Holen
13a2070a32 Support multidimensional KSH arrays and warn in Bash. 2016-08-06 18:40:08 -07:00
Kevin Pankonen
fa4874c044 add Dockerfile that will build shellcheck 2016-08-05 16:42:21 -07:00
Vidar Holen
6a71ff6f46 Don't suggest removing $ in (( ${COLUMNS-80} )) 2016-07-30 10:42:33 -07:00
Vidar Holen
36263fb3f5 s/range/class/ when warning about tr '[abc]' 2016-07-05 08:51:40 -07:00
Vidar Holen
6dc419bbf5 Improve warning for 'else if'. 2016-07-02 15:40:29 -07:00
Vidar Holen
7af3470a91 Improve parser errors when reparsing array indices. 2016-07-01 22:06:50 -07:00
Vidar Holen
42f7479fb8 Don't warn about missing shebang when using directives. 2016-07-01 22:02:06 -07:00
Vidar Holen
50084c06c5 Don't warn when $(seq) is used unquoted. 2016-07-01 21:26:46 -07:00
Vidar Holen
e3bef9dc97 Warn about (( 1 -lt 2 )) 2016-07-01 20:33:07 -07:00
Vidar Holen
6c1abb2dee Performance: make readDollarExpr fail early if no $ 2016-06-30 10:01:03 -07:00
Vidar Holen
43c26061b9 Improve parsing for ambiguous $((foo) ) and ((foo) ). 2016-06-26 22:13:48 -07:00
Vidar Holen
07fd5724b8 Recognize declare -A statements when value is inlined. 2016-06-26 14:57:52 -07:00
Vidar Holen
eb2472ada8 Merge branch 'master' of github.com:koalaman/shellcheck 2016-06-26 14:40:43 -07:00
Vidar Holen
3e5ecaa262 Parse indices of associative arrays properly 2016-06-26 14:39:49 -07:00
koalaman
e1cec6c5d3 Merge pull request #694 from eatnumber1/end_column
Emit the end line in the JSON
2016-06-18 15:09:58 -07:00
Russell Harmon
eaa319ec57 Emit the end line in the JSON.
This handles the case where the end line is not on the same line as the
start line when using the new end column feature.
2016-06-18 15:06:28 -07:00
koalaman
717b5e91f5 Merge pull request #693 from eatnumber1/end_column
Make SC1035 emit an end column
2016-06-18 15:05:44 -07:00
Russell Harmon
7f5f5b7fb5 Make SC1035 emit a proper end column
Example JSON output:
```
$ shellcheck -s bash -f json /dev/stdin <<< "[[0 -eq 1 ]]"
[{"file":"/tmp/zshNCNwPz","line":1,"column":1,"endColumn":3,"level":"error","code":1035,"message":"You need a space after the [[ and before the ]]."}]
```
2016-06-18 14:59:47 -07:00
Russell Harmon
856d57f7d8 PositionedComment and ParseNote contains end cols.
This change makes PositionedComment and ParseNote contain end columns.
It additionally modifies the JSON formatter to show the end column in an
"endColumn" property. No modifications to the messages shown by any
other formatter have been made.

Currently, all checks set the end column to the start column. It should
now be possible however to start setting the end column in the parser.
Additional work is needed to set the end column during AST analysis.
2016-06-18 14:58:00 -07:00
koalaman
c45e9d4878 Merge pull request #677 from Maffblaster/patch-1
Add Gentoo to supported distribution list.
2016-05-26 12:11:02 -07:00
Matthew Marchese
89c6f6c800 Add Gentoo to supported distribution list.
It was missing from the list. I thought it was best to add it. :)
2016-05-26 09:56:36 -07:00
Vidar Holen
85e69f86eb In (( x = y )), logic to not reference x also grabbed y 2016-05-24 09:12:47 -07:00
Vidar Holen
47fd16b8e8 Stable version 0.4.4
This release is dedicated to AlphaGo.
The second golden age of AI is upon us!
2016-05-15 13:53:37 -07:00
15 changed files with 584 additions and 143 deletions

21
.travis.yml Normal file
View File

@@ -0,0 +1,21 @@
sudo: required
language: sh
services:
- docker
before_install:
- export DOCKER_REPO=koalaman/shellcheck
- |-
export TAG=$([ "$TRAVIS_BRANCH" == "master" ] && echo "latest" || echo $TRAVIS_BRANCH)
script:
- docker build -t builder -f Dockerfile_builder .
- docker run --rm -it -v $(pwd):/mnt builder
- docker build -t $DOCKER_REPO:$TAG .
after_success:
- docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
- |-
[ "$TRAVIS_BRANCH" == "master" ] && docker push $DOCKER_REPO:$TAG

11
Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM alpine:latest
MAINTAINER Nikyle Nguyen <NLKNguyen@MSN.com>
COPY package/bin/shellcheck /usr/local/bin/
COPY package/lib/ /usr/local/lib/
RUN ldconfig /usr/local/lib
WORKDIR /mnt
ENTRYPOINT ["shellcheck"]

54
Dockerfile_builder Normal file
View File

@@ -0,0 +1,54 @@
FROM mitchty/alpine-ghc:latest
MAINTAINER Nikyle Nguyen <NLKNguyen@MSN.com>
RUN apk add --no-cache build-base
RUN mkdir -p /usr/src/shellcheck
WORKDIR /usr/src/shellcheck
# # ------------------------------------------------------------
# # Build & Test
# # ------------------------------------------------------------
# Obtain the dependencies first, which are less likely to change, in order to reduce
# subsequent build time by leveraging image cache. This benefits developers when they
# build their code with this image locally. In case of Travis CI, this doesn't help
# reduce building time because Travis CI doesn't use cache.
COPY ShellCheck.cabal .
RUN cabal update && cabal install --only-dependencies
# Copy the rest of the source files, including ShellCheck.cabal again but doesn't matter
COPY . .
# Build
RUN cabal install
# Test
RUN cabal test
# # ------------------------------------------------------------
# # Set PATH
# # ------------------------------------------------------------
# Add runtime path to easily reach the executable file. This only exists during build.
ENV PATH "/root/.cabal/bin:$PATH"
# Make it permanent for someone who login to the container of this image
RUN echo "export PATH=${PATH}" >> /etc/profile
# # ------------------------------------------------------------
# # Extract Binaries
# # ------------------------------------------------------------
# Get shellcheck binary
RUN mkdir -p /package/bin/
RUN cp $(which shellcheck) /package/bin/
# Get shared libraries using magic
RUN mkdir -p /package/lib/
RUN ldd $(which shellcheck) | grep "=> /" | awk '{print $3}' | xargs -I '{}' cp -v '{}' /package/lib/
# Copy shellcheck package out to mounted directory
CMD ["cp", "-avr", "/package", "/mnt/"]

View File

@@ -70,6 +70,10 @@ On Debian based distros:
apt-get install shellcheck
On Gentoo based distros:
emerge --ask shellcheck
On Fedora based distros:
dnf install ShellCheck

View File

@@ -1,5 +1,5 @@
Name: ShellCheck
Version: 0.4.3
Version: 0.4.5
Synopsis: Shell script analysis tool
License: GPL-3
License-file: LICENSE

View File

@@ -21,6 +21,7 @@ module ShellCheck.AST where
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 @@ data Token =
| 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 @@ data Token =
| 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 @@ tokenEquals a b = kludge a == kludge b
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 @@ analyze f g i =
f t
newT <- delve t
g t
return . i $ newT
i newT
roundAll = mapM round
roundMaybe Nothing = return Nothing
@@ -186,14 +190,18 @@ analyze f g i =
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 @@ getId t = case t of
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 @@ getId t = case t of
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)

View File

@@ -21,6 +21,7 @@ module ShellCheck.ASTLib where
import ShellCheck.AST
import Control.Monad.Writer
import Control.Monad
import Data.List
import Data.Maybe
@@ -54,6 +55,8 @@ isGlob _ = False
-- 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 @@ isLiteral t = isJust $ getLiteralString t
-- 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,14 +211,32 @@ braceExpand (T_NormalWord id list) = take 1000 $ do
braceExpand item
part x = return x
-- Maybe get the command name of a token representing a command
getCommandName t =
-- Maybe get a SimpleCommand from immediate wrappers like T_Redirections
getCommand t =
case t of
T_Redirecting _ _ w -> getCommandName w
T_SimpleCommand _ _ (w:_) -> getLiteralString w
T_Annotation _ _ t -> getCommandName t
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 = 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_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
where
@@ -237,8 +260,8 @@ isOnlyRedirection t =
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 @@ getCommandSequences t =
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

View File

@@ -85,6 +85,7 @@ checksFor Bash = [
,checkEchoSed
,checkForDecimals
,checkLocalScope
,checkMultiDimensionalArrays
]
runAnalytics :: AnalysisSpec -> [TokenComment]
@@ -178,6 +179,7 @@ nodeChecks = [
,checkReadWithoutR
,checkLoopVariableReassignment
,checkTrailingBracket
,checkReturnAgainstZero
]
@@ -369,6 +371,8 @@ prop_checkPipePitfalls4 = verifyNot checkPipePitfalls "find . -print0 | xargs -0
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 @@ checkPipePitfalls _ (T_Pipeline id _ commands) = do
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 @@ checkShebangParameters _ (T_Script id sb _) =
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_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
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 @@ checkBashisms params = bashism
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_checkUnquotedExpansions4 = verifyNot checkUnquotedExpansions "[[ $(foo) ==
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 @@ checkArrayWithoutIndex params _ =
"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 @@ checkArrayWithoutIndex params _ =
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_checkStderrRedirect4 = verifyNot checkStderrRedirect "errors=$(test 2>&1 >
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 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
,"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_checkSingleBracketOperators1 = verify checkSingleBracketOperators "[ test =
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_checkConstantIfs4 = verifyNot checkConstantIfs "[[ $n -le 3 ]]"
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 @@ checkBraceExpansionVars params t@(T_BraceExpansion id list) = mapM_ check list
(`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 @@ checkArithmeticDeref params t@(TA_Expansion _ [b@(T_DollarBraced id _)]) =
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_checkUnused30= verifyTree checkUnusedAssignments "let a=1"
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_checkUnassignedReferences19= verifyNotTree checkUnassignedReferences "reado
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 @@ checkPrefixAssignmentReference params t@(T_DollarBraced id value) =
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 @@ prop_checkOverridingPath8 = verifyNot checkOverridingPath "PATH=$PATH:/stuff"
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 @@ prop_checkTildeInPath3 = verifyNot checkTildeInPath "PATH=~/bin"
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 @@ checkMultipleAppends params t =
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 @@ checkTrailingBracket _ token =
"]" -> "["
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 }) ) |])

View File

@@ -334,6 +334,12 @@ getModifiedVariables t =
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 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
"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 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
"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 @@ getIndexReferences s = fromMaybe [] $ do
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 @@ getReferencedVariables parents t =
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_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
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 @@ getBracedReference s = fromMaybe s $
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 @@ filterByAnnotation token =
getCode (TokenComment _ (Comment _ c _)) = c
return []
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -39,7 +39,7 @@ import Test.QuickCheck.All
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 @@ checkScript sys spec = do
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 @@ getErrors sys spec =
sort . map getCode . crComments $
runIdentity (checkScript sys spec)
where
getCode (PositionedComment _ (Comment _ code _)) = code
getCode (PositionedComment _ _ (Comment _ code _)) = code
check = checkWithIncludes []

View File

@@ -90,6 +90,8 @@ commandChecks = [
,checkExportedExpansions
,checkAliasesUsesArgs
,checkAliasesExpandEarly
,checkUnsetGlobs
,checkFindWithoutPath
]
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
@@ -150,7 +152,7 @@ checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
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_checkPrintfVar1 = verify checkPrintfVar "printf \"Lol: $s\""
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 @@ checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
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 }) ) |])

View File

@@ -30,13 +30,15 @@ data Formatter = Formatter {
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 @@ makeNonVirtual comments contents =
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 =

View File

@@ -37,10 +37,12 @@ format = do
}
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)

View File

@@ -94,7 +94,7 @@ data Position = Position {
} 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 =

View File

@@ -135,7 +135,7 @@ almostSpace =
--------- 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 @@ initialUserState = UserState {
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 @@ getNextIdAt sourcepos = do
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 @@ pushContext c = do
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 @@ parseNote c l a = do
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 @@ readConditionContents single =
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 @@ readConditionContents single =
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_a19= isOk readArithmeticContents "\\\n3 +\\\n 2"
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 @@ readArithmeticContents =
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 @@ readArithmeticContents =
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 @@ readCondition = called "test expression" $ do
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 @@ readAnnotationPrefix = do
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 @@ readAnnotation = called "shellcheck annotation" $ do
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 @@ readNormalishWord end = do
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 @@ subParse pos parser input = do
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 @@ readDoubleQuotedDollar = readDollarExpression <|> readDollarLonely
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 @@ readArithmeticExpression = called "((..)) command" $ do
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 @@ readHereDoc = called "here document" $ do
-- 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 @@ readPendingHereDocs = do
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 @@ readIoFile = called "redirection" $ do
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
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 <- readIoVariable <|> readIoNumber
op <- readHereString <|> readHereDoc <|> readIoFile
let actualOp = case op of T_FdRedirect _ "" x -> x
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 @@ readHereString = called "here string" $ do
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 @@ readElifPart = called "elif clause" $ do
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 @@ readCompoundCommand = do
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 @@ readEvalSuffix = many1 (readIoRedirect <|> readCmdWord <|> evalFallback)
-- 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 @@ readAssignmentWord = try $ do
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 @@ readAssignmentWord = try $ do
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 @@ readAssignmentWord = try $ do
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 @@ readArray = called "array assignment" $ do
readIndexed = do
id <- getNextId
index <- try $ do
x <- readArrayIndex
x <- many1 readArrayIndex
char '='
return x
value <- readNormalWord <|> nothing
@@ -2477,12 +2547,12 @@ verifyEof = eof <|> choice [
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 @@ readScript = do
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 @@ readScript = do
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 @@ parseWithNotes parser = do
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 @@ parseShell sys name contents = do
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 @@ reattachHereDocs root map =
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 {